From 97ec31e1167e18639b0dde74163fdc0c437435dd Mon Sep 17 00:00:00 2001 From: Gustavo Santos Ferreira Date: Tue, 30 Oct 2018 17:33:26 -0300 Subject: [PATCH 01/42] convert to address and then string when solidity type address is received --- .../templates/smart_contract/_function_response.html.eex | 2 +- .../lib/block_scout_web/views/smart_contract_view.ex | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/templates/smart_contract/_function_response.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/smart_contract/_function_response.html.eex index 31e629f771..c2f47607c3 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/smart_contract/_function_response.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/smart_contract/_function_response.html.eex @@ -5,6 +5,6 @@ [<%= for item <- @outputs do %> <%= if named_argument?(item) do %><%= item["name"] %> <% end %> -(<%= item["type"] %>) : <%= values(item["value"]) %><% end %>] +(<%= item["type"] %>) : <%= values(item["value"], item["type"]) %><% end %>] diff --git a/apps/block_scout_web/lib/block_scout_web/views/smart_contract_view.ex b/apps/block_scout_web/lib/block_scout_web/views/smart_contract_view.ex index a63d0ef0c9..f8c9fa1236 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/smart_contract_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/smart_contract_view.ex @@ -10,6 +10,11 @@ defmodule BlockScoutWeb.SmartContractView do def named_argument?(%{"name" => _}), do: true def named_argument?(_), do: false - def values(values) when is_list(values), do: Enum.join(values, ",") - def values(value), do: value + def values(value, "address") do + {:ok, address} = Explorer.Chain.Hash.Address.cast(value) + to_string(address) + end + + def values(values, _) when is_list(values), do: Enum.join(values, ",") + def values(value, _), do: value end From 1bbca2604ddab7fcd09d06be13fa19430de0329e Mon Sep 17 00:00:00 2001 From: Gustavo Santos Ferreira Date: Tue, 30 Oct 2018 17:36:27 -0300 Subject: [PATCH 02/42] also support address payable type --- .../lib/block_scout_web/views/smart_contract_view.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/views/smart_contract_view.ex b/apps/block_scout_web/lib/block_scout_web/views/smart_contract_view.ex index f8c9fa1236..0d499104be 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/smart_contract_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/smart_contract_view.ex @@ -3,14 +3,14 @@ defmodule BlockScoutWeb.SmartContractView do def queryable?(inputs), do: Enum.any?(inputs) - def address?(type), do: type == "address" + def address?(type), do: type in ["address", "address payable"] def named_argument?(%{"name" => ""}), do: false def named_argument?(%{"name" => nil}), do: false def named_argument?(%{"name" => _}), do: true def named_argument?(_), do: false - def values(value, "address") do + def values(value, type) when type in ["address", "address payable"] do {:ok, address} = Explorer.Chain.Hash.Address.cast(value) to_string(address) end From e5c9aaa2df8bff5aea99cbe1246eabd92006ca9a Mon Sep 17 00:00:00 2001 From: Gustavo Santos Ferreira Date: Tue, 30 Oct 2018 18:03:53 -0300 Subject: [PATCH 03/42] add tests --- .../views/tokens/smart_contract_view_test.exs | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/apps/block_scout_web/test/block_scout_web/views/tokens/smart_contract_view_test.exs b/apps/block_scout_web/test/block_scout_web/views/tokens/smart_contract_view_test.exs index 281ab779dc..503a206c25 100644 --- a/apps/block_scout_web/test/block_scout_web/views/tokens/smart_contract_view_test.exs +++ b/apps/block_scout_web/test/block_scout_web/views/tokens/smart_contract_view_test.exs @@ -24,6 +24,12 @@ defmodule BlockScoutWeb.SmartContractViewTest do assert SmartContractView.address?(type) end + test "returns true when the type is equal to the string 'address payable'" do + type = "address payable" + + assert SmartContractView.address?(type) + end + test "returns false when the type is not equal the string 'address'" do type = "name" @@ -57,17 +63,27 @@ defmodule BlockScoutWeb.SmartContractViewTest do end end - describe "values/1" do - test "joins the values when it is a list" do + describe "values/2" do + test "joins the values when it is a list of a given type" do values = [8, 6, 9, 2, 2, 37] - assert SmartContractView.values(values) == "8,6,9,2,2,37" + assert SmartContractView.values(values, "type") == "8,6,9,2,2,37" + end + + test "convert the value to string receiving a value and the 'address' type" do + value = <<95, 38, 9, 115, 52, 182, 163, 43, 121, 81, 223, 97, 253, 12, 88, 3, 236, 93, 131, 84>> + assert SmartContractView.values(value, "address") == "0x5f26097334b6a32b7951df61fd0c5803ec5d8354" + end + + test "convert the value to string receiving a value and the 'address payable' type" do + value = <<95, 38, 9, 115, 52, 182, 163, 43, 121, 81, 223, 97, 253, 12, 88, 3, 236, 93, 131, 84>> + assert SmartContractView.values(value, "address payable") == "0x5f26097334b6a32b7951df61fd0c5803ec5d8354" end - test "returns the value" do + test "returns the value when the type is neither 'address' nor 'address payable'" do value = "POA" - assert SmartContractView.values(value) == "POA" + assert SmartContractView.values(value, "not address") == "POA" end end end From c0ffdd2f0c0dce2a47883468544b27b3b9edcf63 Mon Sep 17 00:00:00 2001 From: Alex Garibay Date: Mon, 29 Oct 2018 16:34:23 -0500 Subject: [PATCH 04/42] Add function for recovering signer address for Clique --- apps/indexer/lib/indexer/block/util.ex | 75 +++++++++++++++++++ apps/indexer/mix.exs | 4 + apps/indexer/test/indexer/block/util_test.exs | 40 ++++++++++ mix.lock | 4 +- 4 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 apps/indexer/lib/indexer/block/util.ex create mode 100644 apps/indexer/test/indexer/block/util_test.exs diff --git a/apps/indexer/lib/indexer/block/util.ex b/apps/indexer/lib/indexer/block/util.ex new file mode 100644 index 0000000000..ad603ea10c --- /dev/null +++ b/apps/indexer/lib/indexer/block/util.ex @@ -0,0 +1,75 @@ +defmodule Indexer.Block.Util do + @moduledoc """ + Helper functions for parsing block information. + """ + + @doc """ + Calculates the signer's address by recovering the ECDSA public key. + + https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm + """ + def signer(block) when is_map(block) do + # Last 65 bytes is the signature. Multiply by two since we haven't transformed to raw bytes + {extra_data, signature} = String.split_at(trim_prefix(block.extra_data), -130) + + block = %{block | extra_data: extra_data} + + signature_hash = signature_hash(block) + + recover_pub_key(signature_hash, decode(signature)) + end + + # Signature hash calculated from the block header. + # Needed for PoA-based chains + defp signature_hash(block) do + header_data = [ + decode(block.parent_hash), + decode(block.sha3_uncles), + decode(block.miner), + decode(block.state_root), + decode(block.transactions_root), + decode(block.receipts_root), + decode(block.logs_bloom), + block.difficulty, + block.number, + block.gas_limit, + block.gas_used, + block.timestamp, + decode(block.extra_data), + decode(block.mix_hash), + decode(block.nonce) + ] + + :keccakf1600.hash(:sha3_256, ExRLP.encode(header_data)) + end + + defp trim_prefix("0x" <> rest), do: rest + + defp decode("0x" <> rest) do + decode(rest) + end + + defp decode(data) do + Base.decode16!(data, case: :mixed) + end + + # Recovers the key from the signature hash and signature + defp recover_pub_key(signature_hash, signature) do + << + r::bytes-size(32), + s::bytes-size(32), + v::integer-size(8) + >> = signature + + # First byte represents compression which can be ignored + # Private key is the last 64 bytes + {:ok, <<_compression::bytes-size(1), private_key::binary>>} = + :libsecp256k1.ecdsa_recover_compact(signature_hash, r <> s, :uncompressed, v) + + # Public key comes from the last 20 bytes + <<_::bytes-size(12), public_key::binary>> = :keccakf1600.hash(:sha3_256, private_key) + + miner_address = Base.encode16(public_key, case: :lower) + "0x" <> miner_address + end +end diff --git a/apps/indexer/mix.exs b/apps/indexer/mix.exs index f8b26a7f47..44b5d3dea5 100644 --- a/apps/indexer/mix.exs +++ b/apps/indexer/mix.exs @@ -46,10 +46,14 @@ defmodule Indexer.MixProject do [ # JSONRPC access to Parity for `Explorer.Indexer` {:ethereum_jsonrpc, in_umbrella: true}, + # RLP encoding + {:ex_rlp, "~> 0.3"}, # Code coverage {:excoveralls, "~> 0.10.0", only: [:test], github: "KronicDeth/excoveralls", branch: "circle-workflows"}, # Importing to database {:explorer, in_umbrella: true}, + # libsecp2561k1 crypto functions + {:libsecp256k1, "~> 0.1.10"}, # Log errors and application output to separate files {:logger_file_backend, "~> 0.0.10"}, # Mocking `EthereumJSONRPC.Transport`, so we avoid hitting real chains for local testing diff --git a/apps/indexer/test/indexer/block/util_test.exs b/apps/indexer/test/indexer/block/util_test.exs new file mode 100644 index 0000000000..c75875d038 --- /dev/null +++ b/apps/indexer/test/indexer/block/util_test.exs @@ -0,0 +1,40 @@ +defmodule Indexer.Block.UtilTest do + use ExUnit.Case + + alias Indexer.Block.Util + + test "signer/1" do + data = %{ + difficulty: 1, + extra_data: + "0xd68301080d846765746886676f312e3130856c696e7578000000000000000000773ab2ca8f47904a14739ad80a75b71d9d29b9fff8b7ecdcb73efffa6f74122f17d304b5dc8e6e5f256c9474dd115c8d4dae31b7a3d409e5c3270f8fde41cd8c00", + gas_limit: 7_753_377, + gas_used: 1_810_195, + hash: "0x7004c895e812c55b0c2be8a46d72ca300a683dc27d1d7917ee7742d4d0359c1f", + logs_bloom: + "0x00000000000000020000000000002000000400000000000000000000000000000000000000000000040000080004000020000010000000000000000000000000000000000000000008000008000000000000000000200000000000000000000000000000020000000000000000000800000000000000804000000010080000000800000000000000000000000000000000000000000000800000000000080000000008000400000000404000000000000000000000000200000000000000000000000002000000000000001002000000000000002000000008000000000020000000000000000000000000000000000000000000000000400000800000000000", + miner: "0x0000000000000000000000000000000000000000", + mix_hash: "0x0000000000000000000000000000000000000000000000000000000000000000", + nonce: "0x0000000000000000", + number: 2_848_394, + parent_hash: "0x20350fc367e19d3865be1ea7da72ab81f8f9941c43ac6bb24a34a0a7caa2f3df", + receipts_root: "0x6ade4ac1079ea50cfadcce2b75ffbe4f9b14bf69b4607bbf1739463076ca6246", + sha3_uncles: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + size: 6437, + state_root: "0x23f63347851bcd109059d007d71e19c4f5e73b7f0862bebcd04458333a004d92", + timestamp: 1_534_796_040, + total_difficulty: 5_353_647, + transactions: [ + "0x7e3bb851fc74a436826d2af6b96e4db9484431811ef0d9c9e78370488d33d4e5", + "0x3976fd1e3d2a715c3cfcfde9bd3210798c26c017b8edb841d319227ecb3322fb", + "0xd8db124005bb8b6fda7b71fd56ac782552a66af58fe843ba3c4930423b87d1d2", + "0x10c1a1ca4d9f4b2bd5b89f7bbcbbc2d69e166fe23662b8db4f6beae0f50ac9fd", + "0xaa58a6545677c796a56b8bc874174c8cfd31a6c6e6ca3a87e086d4f66d52858a" + ], + transactions_root: "0xde8d25c0b9b54310128a21601331094b43f910f9f96102869c2e2dca94884bf4", + uncles: [] + } + + assert Util.signer(data) == "0xfc18cbc391de84dbd87db83b20935d3e89f5dd91" + end +end diff --git a/mix.lock b/mix.lock index 8c75e5a4c7..4a62ae5e91 100644 --- a/mix.lock +++ b/mix.lock @@ -32,6 +32,7 @@ "ex_cldr_units": {:hex, :ex_cldr_units, "1.1.1", "b3c7256709bdeb3740a5f64ce2bce659eb9cf4cc1afb4cf94aba033b4a18bc5f", [:mix], [{:ex_cldr, "~> 1.0", [hex: :ex_cldr, optional: false]}, {:ex_cldr_numbers, "~> 1.0", [hex: :ex_cldr_numbers, optional: false]}]}, "ex_doc": {:hex, :ex_doc, "0.19.1", "519bb9c19526ca51d326c060cb1778d4a9056b190086a8c6c115828eaccea6cf", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.7", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"}, "ex_machina": {:hex, :ex_machina, "2.2.1", "df84d0b23487aaa8570c35e586d7f9f197a7787e1121344a41d8832a7ea41edf", [:mix], [{:ecto, "~> 2.1", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"}, + "ex_rlp": {:hex, :ex_rlp, "0.3.1", "190554f7b26f79734fc5a772241eec14a71b2e83576e43f451479feb017013e9", [:mix], [], "hexpm"}, "exactor": {:hex, :exactor, "2.2.4", "5efb4ddeb2c48d9a1d7c9b465a6fffdd82300eb9618ece5d34c3334d5d7245b1", [:mix], []}, "excoveralls": {:git, "https://github.com/KronicDeth/excoveralls.git", "0a859b68851eeba9b43eba59fbc8f9098299cfe1", [branch: "circle-workflows"]}, "exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, optional: false]}]}, @@ -50,7 +51,7 @@ "jsx": {:hex, :jsx, "2.8.3", "a05252d381885240744d955fbe3cf810504eb2567164824e19303ea59eef62cf", [:mix, :rebar3], []}, "junit_formatter": {:hex, :junit_formatter, "2.2.0", "da6093f0740c58a824f9585ebb7cb1b960efaecf48d1fa969e95d9c47c6b19dd", [:mix], [], "hexpm"}, "keccakf1600": {:hex, :keccakf1600_orig, "2.0.0", "0a7217ddb3ee8220d449bbf7575ec39d4e967099f220a91e3dfca4dbaef91963", [:rebar3], []}, - "libsecp256k1": {:hex, :libsecp256k1, "0.1.4", "42b7f76d8e32f85f578ccda0abfdb1afa0c5c231d1fd8aeab9cda352731a2d83", [:rebar3], []}, + "libsecp256k1": {:hex, :libsecp256k1, "0.1.10", "d27495e2b9851c7765129b76c53b60f5e275bd6ff68292c50536bf6b8d091a4d", [:make, :mix], [{:mix_erlang_tasks, "0.1.0", [hex: :mix_erlang_tasks, repo: "hexpm", optional: false]}], "hexpm"}, "logger_file_backend": {:hex, :logger_file_backend, "0.0.10", "876f9f84ae110781207c54321ffbb62bebe02946fe3c13f0d7c5f5d8ad4fa910", [:mix], [], "hexpm"}, "makeup": {:hex, :makeup, "0.5.5", "9e08dfc45280c5684d771ad58159f718a7b5788596099bdfb0284597d368a882", [:mix], [{:nimble_parsec, "~> 0.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, "makeup_elixir": {:hex, :makeup_elixir, "0.10.0", "0f09c2ddf352887a956d84f8f7e702111122ca32fbbc84c2f0569b8b65cbf7fa", [:mix], [{:makeup, "~> 0.5.5", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"}, @@ -59,6 +60,7 @@ "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], []}, "mime": {:hex, :mime, "1.3.0", "5e8d45a39e95c650900d03f897fbf99ae04f60ab1daa4a34c7a20a5151b7a5fe", [:mix], [], "hexpm"}, "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], []}, + "mix_erlang_tasks": {:hex, :mix_erlang_tasks, "0.1.0", "36819fec60b80689eb1380938675af215565a89320a9e29c72c70d97512e4649", [:mix], [], "hexpm"}, "mochiweb": {:hex, :mochiweb, "2.18.0", "eb55f1db3e6e960fac4e6db4e2db9ec3602cc9f30b86cd1481d56545c3145d2e", [:rebar3], [], "hexpm"}, "mock": {:hex, :mock, "0.3.2", "e98e998fd76c191c7e1a9557c8617912c53df3d4a6132f561eb762b699ef59fa", [:mix], [{:meck, "~> 0.8.8", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm"}, "mox": {:hex, :mox, "0.4.0", "7f120840f7d626184a3d65de36189ca6f37d432e5d63acd80045198e4c5f7e6e", [:mix], [], "hexpm"}, From e23ebab069edb9fdcf9b21fb82a70224ab3caa9d Mon Sep 17 00:00:00 2001 From: Alex Garibay Date: Tue, 30 Oct 2018 18:08:28 -0500 Subject: [PATCH 05/42] Add transform functions for block indexing --- .../lib/ethereum_jsonrpc/block.ex | 34 +++++++++++ .../lib/ethereum_jsonrpc/blocks.ex | 7 +++ .../test/ethereum_jsonrpc_test.exs | 12 ++++ apps/indexer/config/config.exs | 5 +- apps/indexer/lib/indexer/block/fetcher.ex | 2 + apps/indexer/lib/indexer/block/transform.ex | 31 ++++++++++ .../lib/indexer/block/transform/base.ex | 14 +++++ .../lib/indexer/block/transform/clique.ex | 16 ++++++ apps/indexer/lib/indexer/block/util.ex | 4 +- .../bound_interval_supervisor_test.exs | 6 ++ .../indexer/block/transform/base_test.exs | 42 ++++++++++++++ .../indexer/block/transform/clique_test.exs | 43 ++++++++++++++ .../test/indexer/block/transform_test.exs | 56 +++++++++++++++++++ .../test/indexer/block/uncle/fetcher_test.exs | 4 ++ apps/indexer/test/indexer/block/util_test.exs | 4 +- 15 files changed, 274 insertions(+), 6 deletions(-) create mode 100644 apps/indexer/lib/indexer/block/transform.ex create mode 100644 apps/indexer/lib/indexer/block/transform/base.ex create mode 100644 apps/indexer/lib/indexer/block/transform/clique.ex create mode 100644 apps/indexer/test/indexer/block/transform/base_test.exs create mode 100644 apps/indexer/test/indexer/block/transform/clique_test.exs create mode 100644 apps/indexer/test/indexer/block/transform_test.exs diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/block.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/block.ex index ebade1d4da..b202d8a64d 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/block.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/block.ex @@ -11,16 +11,23 @@ defmodule EthereumJSONRPC.Block do @type elixir :: %{String.t() => non_neg_integer | DateTime.t() | String.t() | nil} @type params :: %{ difficulty: pos_integer(), + extra_data: EthereumJSONRPC.hash(), gas_limit: non_neg_integer(), gas_used: non_neg_integer(), hash: EthereumJSONRPC.hash(), + logs_bloom: EthereumJSONRPC.hash(), miner_hash: EthereumJSONRPC.hash(), + mix_hash: EthereumJSONRPC.hash(), nonce: EthereumJSONRPC.hash(), number: non_neg_integer(), parent_hash: EthereumJSONRPC.hash(), + receipts_root: EthereumJSONRPC.hash(), + sha3_uncles: EthereumJSONRPC.hash(), size: non_neg_integer(), + state_root: EthereumJSONRPC.hash(), timestamp: DateTime.t(), total_difficulty: non_neg_integer(), + transactions_root: EthereumJSONRPC.hash(), uncles: [EthereumJSONRPC.hash()] } @@ -95,16 +102,23 @@ defmodule EthereumJSONRPC.Block do ...> ) %{ difficulty: 340282366920938463463374607431465537093, + extra_data: "0xd5830108048650617269747986312e32322e31826c69", gas_limit: 6706541, gas_used: 0, hash: "0x52c867bc0a91e573dc39300143c3bead7408d09d45bdb686749f02684ece72f3", + logs_bloom: "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", miner_hash: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca", + mix_hash: "0x0", nonce: 0, number: 1, parent_hash: "0x5b28c1bfd3a15230c9a46b399cd0f9a6920d432e85381cc6a140b06e8410112f", + receipts_root: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + sha3_uncles: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", size: 576, + state_root: "0xc196ad59d867542ef20b29df5f418d07dc7234f4bc3d25260526620b7958a8fb", timestamp: Timex.parse!("2017-12-15T21:03:30Z", "{ISO:Extended:Z}"), total_difficulty: 340282366920938463463374607431465668165, + transactions_root: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", uncles: [] } @@ -136,16 +150,23 @@ defmodule EthereumJSONRPC.Block do ...> ) %{ difficulty: 17561410778, + extra_data: "0x476574682f4c5649562f76312e302e302f6c696e75782f676f312e342e32", gas_limit: 5000, gas_used: 0, hash: "0x4d9423080290a650eaf6db19c87c76dff83d1b4ab64aefe6e5c5aa2d1f4b6623", + logs_bloom: "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + mix_hash: "0xbbb93d610b2b0296a59f18474ac3d6086a9902aa7ca4b9a306692f7c3d496fdf", miner_hash: "0xbb7b8287f3f0a933474a79eae42cbca977791171", nonce: 5539500215739777653, number: 59, parent_hash: "0xcd5b5c4cecd7f18a13fe974255badffd58e737dc67596d56bc01f063dd282e9e", + receipts_root: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + sha3_uncles: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", size: 542, + state_root: "0x6fd0a5d82ca77d9f38c3ebbde11b11d304a5fcf3854f291df64395ab38ed43ba", timestamp: Timex.parse!("2015-07-30T15:32:07Z", "{ISO:Extended:Z}"), total_difficulty: 1039309006117, + transactions_root: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", uncles: [] } @@ -154,30 +175,43 @@ defmodule EthereumJSONRPC.Block do def elixir_to_params( %{ "difficulty" => difficulty, + "extraData" => extra_data, "gasLimit" => gas_limit, "gasUsed" => gas_used, "hash" => hash, + "logsBloom" => logs_bloom, "miner" => miner_hash, "number" => number, "parentHash" => parent_hash, + "receiptsRoot" => receipts_root, + "sha3Uncles" => sha3_uncles, "size" => size, + "stateRoot" => state_root, "timestamp" => timestamp, "totalDifficulty" => total_difficulty, + "transactionsRoot" => transactions_root, "uncles" => uncles } = elixir ) do %{ difficulty: difficulty, + extra_data: extra_data, gas_limit: gas_limit, gas_used: gas_used, hash: hash, + logs_bloom: logs_bloom, miner_hash: miner_hash, + mix_hash: Map.get(elixir, "mixHash", "0x0"), nonce: Map.get(elixir, "nonce", 0), number: number, parent_hash: parent_hash, + receipts_root: receipts_root, + sha3_uncles: sha3_uncles, size: size, + state_root: state_root, timestamp: timestamp, total_difficulty: total_difficulty, + transactions_root: transactions_root, uncles: uncles } end diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/blocks.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/blocks.ex index a6964e17db..1eef103513 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/blocks.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/blocks.ex @@ -45,16 +45,23 @@ defmodule EthereumJSONRPC.Blocks do [ %{ difficulty: 131072, + extra_data: "0x", gas_limit: 6700000, gas_used: 0, hash: "0x5b28c1bfd3a15230c9a46b399cd0f9a6920d432e85381cc6a140b06e8410112f", + logs_bloom: "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", miner_hash: "0x0000000000000000000000000000000000000000", + mix_hash: "0x0", nonce: 0, number: 0, parent_hash: "0x0000000000000000000000000000000000000000000000000000000000000000", + receipts_root: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + sha3_uncles: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", size: 533, + state_root: "0xfad4af258fd11939fae0c6c6eec9d340b1caac0b0196fd9a1bc3f489c5bf00b3", timestamp: Timex.parse!("1970-01-01T00:00:00Z", "{ISO:Extended:Z}"), total_difficulty: 131072, + transactions_root: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", uncles: ["0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d15273311"] } ] diff --git a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc_test.exs b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc_test.exs index fd3dc3766a..1c19a1fa97 100644 --- a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc_test.exs +++ b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc_test.exs @@ -207,10 +207,15 @@ defmodule EthereumJSONRPCTest do "gasLimit" => "0x0", "gasUsed" => "0x0", "hash" => block_hash, + "extraData" => "0x0", + "logsBloom" => "0x0", "miner" => "0x0", "number" => block_number, "parentHash" => "0x0", + "receiptsRoot" => "0x0", "size" => "0x0", + "sha3Uncles" => "0x0", + "stateRoot" => "0x0", "timestamp" => "0x0", "totalDifficulty" => "0x0", "transactions" => [ @@ -231,6 +236,7 @@ defmodule EthereumJSONRPCTest do "value" => "0x0" } ], + "transactionsRoot" => "0x0", "uncles" => [] } } @@ -364,16 +370,22 @@ defmodule EthereumJSONRPCTest do id: 0, result: %{ "difficulty" => "0x0", + "extraData" => "0x0", "gasLimit" => "0x0", "gasUsed" => "0x0", "hash" => "0x0", + "logsBloom" => "0x0", "miner" => "0x0", "number" => "0x0", "parentHash" => "0x0", + "receiptsRoot" => "0x0", + "sha3Uncles" => "0x0", "size" => "0x0", + "stateRoot" => "0x0", "timestamp" => "0x0", "totalDifficulty" => "0x0", "transactions" => [], + "transactionsRoot" => "0x0", "uncles" => [] }, jsonrpc: "2.0" diff --git a/apps/indexer/config/config.exs b/apps/indexer/config/config.exs index ac8db25c30..43ff7c3d18 100644 --- a/apps/indexer/config/config.exs +++ b/apps/indexer/config/config.exs @@ -5,9 +5,10 @@ use Mix.Config import Bitwise config :indexer, + block_transformer: Indexer.Block.Transform.Base, + ecto_repos: [Explorer.Repo], # bytes - memory_limit: 1 <<< 30, - ecto_repos: [Explorer.Repo] + memory_limit: 1 <<< 30 config :logger, :indexer, # keep synced with `config/config.exs` diff --git a/apps/indexer/lib/indexer/block/fetcher.ex b/apps/indexer/lib/indexer/block/fetcher.ex index 07f35ee5c0..41a705a869 100644 --- a/apps/indexer/lib/indexer/block/fetcher.ex +++ b/apps/indexer/lib/indexer/block/fetcher.ex @@ -9,6 +9,7 @@ defmodule Indexer.Block.Fetcher do alias Indexer.{AddressExtraction, CoinBalance, MintTransfer, Token, TokenTransfers} alias Indexer.Address.{CoinBalances, TokenBalances} alias Indexer.Block.Fetcher.Receipts + alias Indexer.Block.Transform @type address_hash_to_fetched_balance_block_number :: %{String.t() => Block.block_number()} @type transaction_hash_to_block_number :: %{String.t() => Block.block_number()} @@ -96,6 +97,7 @@ defmodule Indexer.Block.Fetcher do transactions: transactions_without_receipts, block_second_degree_relations: block_second_degree_relations } = result, + blocks = Transform.transform_blocks(blocks), {:receipts, {:ok, receipt_params}} <- {:receipts, Receipts.fetch(state, transactions_without_receipts)}, %{logs: logs, receipts: receipts} = receipt_params, transactions_with_receipts = Receipts.put(transactions_without_receipts, receipts), diff --git a/apps/indexer/lib/indexer/block/transform.ex b/apps/indexer/lib/indexer/block/transform.ex new file mode 100644 index 0000000000..72f7f46f58 --- /dev/null +++ b/apps/indexer/lib/indexer/block/transform.ex @@ -0,0 +1,31 @@ +defmodule Indexer.Block.Transform do + @moduledoc """ + Protocol for transforming blocks. + """ + + @type block :: map() + + @doc """ + Transforms a block. + """ + @callback transform(block :: block()) :: block() + + @doc """ + Runs a list of blocks through the configured block transformer. + """ + def transform_blocks(blocks) when is_list(blocks) do + transformer = Application.get_env(:indexer, :block_transformer) + + unless transformer do + raise ArgumentError, + """ + No block transformer defined. Set a blocker transformer." + + config :indexer, + block_transformer: Indexer.Block.Transform.Base + """ + end + + Enum.map(blocks, &transformer.transform/1) + end +end diff --git a/apps/indexer/lib/indexer/block/transform/base.ex b/apps/indexer/lib/indexer/block/transform/base.ex new file mode 100644 index 0000000000..c094f9b1bc --- /dev/null +++ b/apps/indexer/lib/indexer/block/transform/base.ex @@ -0,0 +1,14 @@ +defmodule Indexer.Block.Transform.Base do + @moduledoc """ + Default block transformer to be used. + """ + + alias Indexer.Block.Transform + + @behaviour Transform + + @impl Transform + def transform(block) when is_map(block) do + block + end +end diff --git a/apps/indexer/lib/indexer/block/transform/clique.ex b/apps/indexer/lib/indexer/block/transform/clique.ex new file mode 100644 index 0000000000..bafab1e509 --- /dev/null +++ b/apps/indexer/lib/indexer/block/transform/clique.ex @@ -0,0 +1,16 @@ +defmodule Indexer.Block.Transform.Clique do + @moduledoc """ + Handles block transforms for Clique chain. + """ + + alias Indexer.Block.{Transform, Util} + + @behaviour Transform + + @impl Transform + def transform(block) when is_map(block) do + miner_address = Util.signer(block) + + %{block | miner_hash: miner_address} + end +end diff --git a/apps/indexer/lib/indexer/block/util.ex b/apps/indexer/lib/indexer/block/util.ex index ad603ea10c..fbf0d77997 100644 --- a/apps/indexer/lib/indexer/block/util.ex +++ b/apps/indexer/lib/indexer/block/util.ex @@ -25,7 +25,7 @@ defmodule Indexer.Block.Util do header_data = [ decode(block.parent_hash), decode(block.sha3_uncles), - decode(block.miner), + decode(block.miner_hash), decode(block.state_root), decode(block.transactions_root), decode(block.receipts_root), @@ -34,7 +34,7 @@ defmodule Indexer.Block.Util do block.number, block.gas_limit, block.gas_used, - block.timestamp, + DateTime.to_unix(block.timestamp), decode(block.extra_data), decode(block.mix_hash), decode(block.nonce) diff --git a/apps/indexer/test/indexer/block/catchup/bound_interval_supervisor_test.exs b/apps/indexer/test/indexer/block/catchup/bound_interval_supervisor_test.exs index 1e1bf69492..07d97fc87b 100644 --- a/apps/indexer/test/indexer/block/catchup/bound_interval_supervisor_test.exs +++ b/apps/indexer/test/indexer/block/catchup/bound_interval_supervisor_test.exs @@ -448,20 +448,26 @@ defmodule Indexer.Block.Catchup.BoundIntervalSupervisorTest do jsonrpc: "2.0", result: %{ "difficulty" => "0x0", + "extraData" => "0x0", "gasLimit" => "0x0", "gasUsed" => "0x0", "hash" => Explorer.Factory.block_hash() |> to_string(), + "logsBloom" => "0x0", "miner" => "0xb2930b35844a230f00e51431acae96fe543a0347", "number" => "0x0", "parentHash" => Explorer.Factory.block_hash() |> to_string(), + "receiptsRoot" => "0x0", + "sha3Uncles" => "0x0", "size" => "0x0", + "stateRoot" => "0x0", "timestamp" => "0x0", "totalDifficulty" => "0x0", "transactions" => [], + "transactionsRoot" => "0x0", "uncles" => [] } } diff --git a/apps/indexer/test/indexer/block/transform/base_test.exs b/apps/indexer/test/indexer/block/transform/base_test.exs new file mode 100644 index 0000000000..b55adebc57 --- /dev/null +++ b/apps/indexer/test/indexer/block/transform/base_test.exs @@ -0,0 +1,42 @@ +defmodule Indexer.Block.Transform.BaseTest do + use ExUnit.Case + + alias Indexer.Block.Transform.Base + + @block %{ + difficulty: 1, + extra_data: + "0xd68301080d846765746886676f312e3130856c696e7578000000000000000000773ab2ca8f47904a14739ad80a75b71d9d29b9fff8b7ecdcb73efffa6f74122f17d304b5dc8e6e5f256c9474dd115c8d4dae31b7a3d409e5c3270f8fde41cd8c00", + gas_limit: 7_753_377, + gas_used: 1_810_195, + hash: "0x7004c895e812c55b0c2be8a46d72ca300a683dc27d1d7917ee7742d4d0359c1f", + logs_bloom: + "0x00000000000000020000000000002000000400000000000000000000000000000000000000000000040000080004000020000010000000000000000000000000000000000000000008000008000000000000000000200000000000000000000000000000020000000000000000000800000000000000804000000010080000000800000000000000000000000000000000000000000000800000000000080000000008000400000000404000000000000000000000000200000000000000000000000002000000000000001002000000000000002000000008000000000020000000000000000000000000000000000000000000000000400000800000000000", + miner: "0x0000000000000000000000000000000000000000", + mix_hash: "0x0000000000000000000000000000000000000000000000000000000000000000", + nonce: "0x0000000000000000", + number: 2_848_394, + parent_hash: "0x20350fc367e19d3865be1ea7da72ab81f8f9941c43ac6bb24a34a0a7caa2f3df", + receipts_root: "0x6ade4ac1079ea50cfadcce2b75ffbe4f9b14bf69b4607bbf1739463076ca6246", + sha3_uncles: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + size: 6437, + state_root: "0x23f63347851bcd109059d007d71e19c4f5e73b7f0862bebcd04458333a004d92", + timestamp: DateTime.from_unix!(1_534_796_040), + total_difficulty: 5_353_647, + transactions: [ + "0x7e3bb851fc74a436826d2af6b96e4db9484431811ef0d9c9e78370488d33d4e5", + "0x3976fd1e3d2a715c3cfcfde9bd3210798c26c017b8edb841d319227ecb3322fb", + "0xd8db124005bb8b6fda7b71fd56ac782552a66af58fe843ba3c4930423b87d1d2", + "0x10c1a1ca4d9f4b2bd5b89f7bbcbbc2d69e166fe23662b8db4f6beae0f50ac9fd", + "0xaa58a6545677c796a56b8bc874174c8cfd31a6c6e6ca3a87e086d4f66d52858a" + ], + transactions_root: "0xde8d25c0b9b54310128a21601331094b43f910f9f96102869c2e2dca94884bf4", + uncles: [] + } + + describe "transform/1" do + test "passes the block through unchanged" do + assert Base.transform(@block) == @block + end + end +end diff --git a/apps/indexer/test/indexer/block/transform/clique_test.exs b/apps/indexer/test/indexer/block/transform/clique_test.exs new file mode 100644 index 0000000000..4af5d05895 --- /dev/null +++ b/apps/indexer/test/indexer/block/transform/clique_test.exs @@ -0,0 +1,43 @@ +defmodule Indexer.Block.Transform.CliqueTest do + use ExUnit.Case + + alias Indexer.Block.Transform.Clique + + @block %{ + difficulty: 1, + extra_data: + "0xd68301080d846765746886676f312e3130856c696e7578000000000000000000773ab2ca8f47904a14739ad80a75b71d9d29b9fff8b7ecdcb73efffa6f74122f17d304b5dc8e6e5f256c9474dd115c8d4dae31b7a3d409e5c3270f8fde41cd8c00", + gas_limit: 7_753_377, + gas_used: 1_810_195, + hash: "0x7004c895e812c55b0c2be8a46d72ca300a683dc27d1d7917ee7742d4d0359c1f", + logs_bloom: + "0x00000000000000020000000000002000000400000000000000000000000000000000000000000000040000080004000020000010000000000000000000000000000000000000000008000008000000000000000000200000000000000000000000000000020000000000000000000800000000000000804000000010080000000800000000000000000000000000000000000000000000800000000000080000000008000400000000404000000000000000000000000200000000000000000000000002000000000000001002000000000000002000000008000000000020000000000000000000000000000000000000000000000000400000800000000000", + miner_hash: "0x0000000000000000000000000000000000000000", + mix_hash: "0x0000000000000000000000000000000000000000000000000000000000000000", + nonce: "0x0000000000000000", + number: 2_848_394, + parent_hash: "0x20350fc367e19d3865be1ea7da72ab81f8f9941c43ac6bb24a34a0a7caa2f3df", + receipts_root: "0x6ade4ac1079ea50cfadcce2b75ffbe4f9b14bf69b4607bbf1739463076ca6246", + sha3_uncles: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + size: 6437, + state_root: "0x23f63347851bcd109059d007d71e19c4f5e73b7f0862bebcd04458333a004d92", + timestamp: DateTime.from_unix!(1_534_796_040), + total_difficulty: 5_353_647, + transactions: [ + "0x7e3bb851fc74a436826d2af6b96e4db9484431811ef0d9c9e78370488d33d4e5", + "0x3976fd1e3d2a715c3cfcfde9bd3210798c26c017b8edb841d319227ecb3322fb", + "0xd8db124005bb8b6fda7b71fd56ac782552a66af58fe843ba3c4930423b87d1d2", + "0x10c1a1ca4d9f4b2bd5b89f7bbcbbc2d69e166fe23662b8db4f6beae0f50ac9fd", + "0xaa58a6545677c796a56b8bc874174c8cfd31a6c6e6ca3a87e086d4f66d52858a" + ], + transactions_root: "0xde8d25c0b9b54310128a21601331094b43f910f9f96102869c2e2dca94884bf4", + uncles: [] + } + + describe "transform/1" do + test "updates the miner hash with signer address" do + expected = %{@block | miner_hash: "0xfc18cbc391de84dbd87db83b20935d3e89f5dd91"} + assert Clique.transform(@block) == expected + end + end +end diff --git a/apps/indexer/test/indexer/block/transform_test.exs b/apps/indexer/test/indexer/block/transform_test.exs new file mode 100644 index 0000000000..dd6bb0b84e --- /dev/null +++ b/apps/indexer/test/indexer/block/transform_test.exs @@ -0,0 +1,56 @@ +defmodule Indexer.Block.TransformTest do + use ExUnit.Case + + alias Indexer.Block.Transform + + @block %{ + difficulty: 1, + extra_data: + "0xd68301080d846765746886676f312e3130856c696e7578000000000000000000773ab2ca8f47904a14739ad80a75b71d9d29b9fff8b7ecdcb73efffa6f74122f17d304b5dc8e6e5f256c9474dd115c8d4dae31b7a3d409e5c3270f8fde41cd8c00", + gas_limit: 7_753_377, + gas_used: 1_810_195, + hash: "0x7004c895e812c55b0c2be8a46d72ca300a683dc27d1d7917ee7742d4d0359c1f", + logs_bloom: + "0x00000000000000020000000000002000000400000000000000000000000000000000000000000000040000080004000020000010000000000000000000000000000000000000000008000008000000000000000000200000000000000000000000000000020000000000000000000800000000000000804000000010080000000800000000000000000000000000000000000000000000800000000000080000000008000400000000404000000000000000000000000200000000000000000000000002000000000000001002000000000000002000000008000000000020000000000000000000000000000000000000000000000000400000800000000000", + miner_hash: "0x0000000000000000000000000000000000000000", + mix_hash: "0x0000000000000000000000000000000000000000000000000000000000000000", + nonce: "0x0000000000000000", + number: 2_848_394, + parent_hash: "0x20350fc367e19d3865be1ea7da72ab81f8f9941c43ac6bb24a34a0a7caa2f3df", + receipts_root: "0x6ade4ac1079ea50cfadcce2b75ffbe4f9b14bf69b4607bbf1739463076ca6246", + sha3_uncles: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + size: 6437, + state_root: "0x23f63347851bcd109059d007d71e19c4f5e73b7f0862bebcd04458333a004d92", + timestamp: DateTime.from_unix!(1_534_796_040), + total_difficulty: 5_353_647, + transactions: [ + "0x7e3bb851fc74a436826d2af6b96e4db9484431811ef0d9c9e78370488d33d4e5", + "0x3976fd1e3d2a715c3cfcfde9bd3210798c26c017b8edb841d319227ecb3322fb", + "0xd8db124005bb8b6fda7b71fd56ac782552a66af58fe843ba3c4930423b87d1d2", + "0x10c1a1ca4d9f4b2bd5b89f7bbcbbc2d69e166fe23662b8db4f6beae0f50ac9fd", + "0xaa58a6545677c796a56b8bc874174c8cfd31a6c6e6ca3a87e086d4f66d52858a" + ], + transactions_root: "0xde8d25c0b9b54310128a21601331094b43f910f9f96102869c2e2dca94884bf4", + uncles: [] + } + + @blocks [@block, @block] + + describe "transform_blocks/1" do + setup do + original = Application.get_env(:indexer, :block_transformer) + + on_exit(fn -> Application.put_env(:indexer, :block_transformer, original) end) + end + + test "transforms a list of blocks" do + assert Transform.transform_blocks(@blocks) + end + + test "raises when no transformer is configured" do + Application.put_env(:indexer, :block_transformer, nil) + + assert_raise ArgumentError, fn -> Transform.transform_blocks(@blocks) end + end + end +end diff --git a/apps/indexer/test/indexer/block/uncle/fetcher_test.exs b/apps/indexer/test/indexer/block/uncle/fetcher_test.exs index 970f1700a0..5c70683aa7 100644 --- a/apps/indexer/test/indexer/block/uncle/fetcher_test.exs +++ b/apps/indexer/test/indexer/block/uncle/fetcher_test.exs @@ -69,6 +69,9 @@ defmodule Indexer.Block.Uncle.FetcherTest do "number" => number_quantity, "parentHash" => "0x006edcaa1e6fde822908783bc4ef1ad3675532d542fce53537557391cfe34c3c", "size" => "0x243", + "receiptsRoot" => "0x0", + "sha3Uncles" => "0x0", + "stateRoot" => "0x0", "timestamp" => "0x5b437f41", "totalDifficulty" => "0x342337ffffffffffffffffffffffffed8d29bb", "transactions" => [ @@ -93,6 +96,7 @@ defmodule Indexer.Block.Uncle.FetcherTest do "value" => "0x0" } ], + "transactionsRoot" => "0x0", "uncles" => [uncle_uncle_hash_data] } } diff --git a/apps/indexer/test/indexer/block/util_test.exs b/apps/indexer/test/indexer/block/util_test.exs index c75875d038..f0228cb1be 100644 --- a/apps/indexer/test/indexer/block/util_test.exs +++ b/apps/indexer/test/indexer/block/util_test.exs @@ -13,7 +13,7 @@ defmodule Indexer.Block.UtilTest do hash: "0x7004c895e812c55b0c2be8a46d72ca300a683dc27d1d7917ee7742d4d0359c1f", logs_bloom: "0x00000000000000020000000000002000000400000000000000000000000000000000000000000000040000080004000020000010000000000000000000000000000000000000000008000008000000000000000000200000000000000000000000000000020000000000000000000800000000000000804000000010080000000800000000000000000000000000000000000000000000800000000000080000000008000400000000404000000000000000000000000200000000000000000000000002000000000000001002000000000000002000000008000000000020000000000000000000000000000000000000000000000000400000800000000000", - miner: "0x0000000000000000000000000000000000000000", + miner_hash: "0x0000000000000000000000000000000000000000", mix_hash: "0x0000000000000000000000000000000000000000000000000000000000000000", nonce: "0x0000000000000000", number: 2_848_394, @@ -22,7 +22,7 @@ defmodule Indexer.Block.UtilTest do sha3_uncles: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", size: 6437, state_root: "0x23f63347851bcd109059d007d71e19c4f5e73b7f0862bebcd04458333a004d92", - timestamp: 1_534_796_040, + timestamp: DateTime.from_unix!(1_534_796_040), total_difficulty: 5_353_647, transactions: [ "0x7e3bb851fc74a436826d2af6b96e4db9484431811ef0d9c9e78370488d33d4e5", From bd8b5d68647d953d60088f3d9050e35cdb86b9a6 Mon Sep 17 00:00:00 2001 From: Alex Garibay Date: Wed, 31 Oct 2018 12:36:11 -0500 Subject: [PATCH 06/42] Remove unused test functions --- .../ethereum_jsonrpc/request_coordinator_test.exs | 9 --------- .../web_socket/web_socket_client_test.exs | 12 ------------ 2 files changed, 21 deletions(-) diff --git a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/request_coordinator_test.exs b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/request_coordinator_test.exs index 4eedbe93dd..d299491eca 100644 --- a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/request_coordinator_test.exs +++ b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/request_coordinator_test.exs @@ -10,15 +10,6 @@ defmodule EthereumJSONRPC.RequestCoordinatorTest do setup :set_mox_global setup :verify_on_exit! - defp sleep_time(timeouts) do - wait_per_timeout = - :ethereum_jsonrpc - |> Application.get_env(RequestCoordinator) - |> Keyword.fetch!(:wait_per_timeout) - - timeouts * wait_per_timeout - end - setup do table = Application.get_env(:ethereum_jsonrpc, EthereumJSONRPC.RequestCoordinator)[:rolling_window_opts][:table] diff --git a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/web_socket/web_socket_client_test.exs b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/web_socket/web_socket_client_test.exs index 11629769cb..986eeb362a 100644 --- a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/web_socket/web_socket_client_test.exs +++ b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/web_socket/web_socket_client_test.exs @@ -95,18 +95,6 @@ defmodule EthereumJSONRPC.WebSocket.WebSocketClientTest do end end - defp cowboy(0) do - dispatch = :cowboy_router.compile([{:_, [{"/websocket", EthereumJSONRPC.WebSocket.Cowboy.WebSocketHandler, []}]}]) - {:ok, _} = :cowboy.start_http(EthereumJSONRPC.WebSocket.Cowboy, 100, [], env: [dispatch: dispatch]) - :ranch.get_port(EthereumJSONRPC.WebSocket.Cowboy) - end - - defp cowboy(port) do - dispatch = :cowboy_router.compile([{:_, [{"/websocket", EthereumJSONRPC.WebSocket.Cowboy.WebSocketHandler, []}]}]) - {:ok, _} = :cowboy.start_http(EthereumJSONRPC.WebSocket.Cowboy, 100, [port: port], env: [dispatch: dispatch]) - port - end - defp example_state(_) do %{state: %WebSocketClient{url: "ws://example.com"}} end From 7b469665063bb74516d72683202efbae72af7343 Mon Sep 17 00:00:00 2001 From: Alex Garibay Date: Wed, 31 Oct 2018 15:47:11 -0500 Subject: [PATCH 07/42] Change manager for libsecp256k1 --- apps/indexer/mix.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/indexer/mix.exs b/apps/indexer/mix.exs index 44b5d3dea5..64f2635196 100644 --- a/apps/indexer/mix.exs +++ b/apps/indexer/mix.exs @@ -53,7 +53,7 @@ defmodule Indexer.MixProject do # Importing to database {:explorer, in_umbrella: true}, # libsecp2561k1 crypto functions - {:libsecp256k1, "~> 0.1.10"}, + {:libsecp256k1, "~> 0.1.10", manager: :mix, override: true}, # Log errors and application output to separate files {:logger_file_backend, "~> 0.0.10"}, # Mocking `EthereumJSONRPC.Transport`, so we avoid hitting real chains for local testing From de97c6b8f97fe10095ceb2ba9e8162d9d4989250 Mon Sep 17 00:00:00 2001 From: robertoschneiders Date: Wed, 31 Oct 2018 18:41:23 -0300 Subject: [PATCH 08/42] Add gettxinfo API endpoint --- .../api/rpc/transaction_controller.ex | 34 ++++++ .../lib/block_scout_web/etherscan.ex | 113 ++++++++++++++++++ .../views/api/rpc/transaction_view.ex | 43 +++++++ .../api/rpc/transaction_controller_test.exs | 101 ++++++++++++++++ 4 files changed, 291 insertions(+) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/transaction_controller.ex index a167d92638..7b5104a458 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/transaction_controller.ex @@ -3,6 +3,26 @@ defmodule BlockScoutWeb.API.RPC.TransactionController do alias Explorer.Chain + def gettxinfo(conn, params) do + with {:txhash_param, {:ok, txhash_param}} <- fetch_txhash(params), + {:format, {:ok, transaction_hash}} <- to_transaction_hash(txhash_param), + {:transaction, {:ok, transaction}} <- transaction_from_hash(transaction_hash) do + max_block_number = max_block_number() + + logs = Chain.transaction_to_logs(transaction) + render(conn, :gettxinfo, %{transaction: transaction, max_block_number: max_block_number, logs: logs}) + else + {:transaction, :error} -> + render(conn, :error, error: "Transaction not found") + + {:txhash_param, :error} -> + render(conn, :error, error: "Query parameter txhash is required") + + {:format, :error} -> + render(conn, :error, error: "Invalid txhash format") + end + end + def gettxreceiptstatus(conn, params) do with {:txhash_param, {:ok, txhash_param}} <- fetch_txhash(params), {:format, {:ok, transaction_hash}} <- to_transaction_hash(txhash_param) do @@ -35,6 +55,13 @@ defmodule BlockScoutWeb.API.RPC.TransactionController do {:txhash_param, Map.fetch(params, "txhash")} end + defp transaction_from_hash(transaction_hash) do + case Chain.hash_to_transaction(transaction_hash, necessity_by_association: %{block: :required}) do + {:error, :not_found} -> {:transaction, :error} + {:ok, transaction} -> {:transaction, {:ok, transaction}} + end + end + defp to_transaction_hash(transaction_hash_string) do {:format, Chain.string_to_transaction_hash(transaction_hash_string)} end @@ -54,4 +81,11 @@ defmodule BlockScoutWeb.API.RPC.TransactionController do _ -> "" end end + + defp max_block_number do + case Chain.max_block_number() do + {:ok, number} -> number + {:error, :not_found} -> 0 + end + end end diff --git a/apps/block_scout_web/lib/block_scout_web/etherscan.ex b/apps/block_scout_web/lib/block_scout_web/etherscan.ex index 033be85e52..4bb6a624d2 100644 --- a/apps/block_scout_web/lib/block_scout_web/etherscan.ex +++ b/apps/block_scout_web/lib/block_scout_web/etherscan.ex @@ -321,6 +321,30 @@ defmodule BlockScoutWeb.Etherscan do "result" => nil } + @transaction_gettxinfo_example_value %{ + "status" => "1", + "result" => %{ + "blockNumber" => "3", + "confirmations" => "0", + "from" => "0x000000000000000000000000000000000000000c", + "gasLimit" => "91966", + "gasUsed" => "95123", + "hash" => "0x0000000000000000000000000000000000000000000000000000000000000004", + "input" => "0x04", + "logs" => [ + %{ + "address" => "0x000000000000000000000000000000000000000e", + "data" => "0x00", + "topics" => ["First Topic", "Second Topic", "Third Topic", "Fourth Topic"] + } + ], + "success" => true, + "timeStamp" => "1541018182", + "to" => "0x000000000000000000000000000000000000000d", + "value" => "67612" + } + } + @transaction_gettxreceiptstatus_example_value %{ "status" => "1", "message" => "OK", @@ -428,6 +452,28 @@ defmodule BlockScoutWeb.Etherscan do example: ~s("18") } + @logs_details %{ + name: "Log Detail", + fields: %{ + address: @address_hash_type, + topics: %{ + type: "topics", + definition: "An array including the topics for the log.", + example: ~s(["0xf63780e752c6a54a94fc52715dbc5518a3b4c3c2833d301a204226548a2a8545"]) + }, + data: %{ + type: "data", + definition: "Non-indexed log parameters.", + example: ~s("0x") + }, + blockNumber: %{ + type: "block number", + definition: "A nonnegative number used to identify blocks.", + example: ~s("0x5c958") + } + } + } + @address_balance %{ name: "AddressBalance", fields: %{ @@ -735,6 +781,35 @@ defmodule BlockScoutWeb.Etherscan do } } + @transaction_info_model %{ + name: "TransactionInfo", + fields: %{ + hash: @transaction_hash_type, + timeStamp: %{ + type: "timestamp", + definition: "The transaction's block-timestamp.", + example: ~s("1439232889") + }, + blockNumber: @block_number_type, + confirmations: @confirmation_type, + success: %{ + type: "boolean", + definition: "Flag for success during tx execution", + example: ~s(true) + }, + from: @address_hash_type, + to: @address_hash_type, + value: @wei_type, + input: @input_type, + gasLimit: @wei_type, + gasUsed: @gas_type, + logs: %{ + type: "array", + array_type: @logs_details + } + } + } + @transaction_status_model %{ name: "TransactionStatus", fields: %{ @@ -1569,6 +1644,43 @@ defmodule BlockScoutWeb.Etherscan do ] } + @transaction_gettxinfo_action %{ + name: "gettxinfo", + description: "Get transaction info.", + required_params: [ + %{ + key: "txhash", + placeholder: "transactionHash", + type: "string", + description: "Transaction hash. Hash of contents of the transaction." + } + ], + optional_params: [], + responses: [ + %{ + code: "200", + description: "successful operation", + example_value: Jason.encode!(@transaction_gettxinfo_example_value), + model: %{ + name: "Result", + fields: %{ + status: @status_type, + message: @message_type, + result: %{ + type: "model", + model: @transaction_info_model + } + } + } + }, + %{ + code: "200", + description: "error", + example_value: Jason.encode!(@transaction_gettxreceiptstatus_example_value_error) + } + ] + } + @transaction_gettxreceiptstatus_action %{ name: "gettxreceiptstatus", description: "Get transaction receipt status.", @@ -1692,6 +1804,7 @@ defmodule BlockScoutWeb.Etherscan do @transaction_module %{ name: "transaction", actions: [ + @transaction_gettxinfo_action, @transaction_gettxreceiptstatus_action, @transaction_getstatus_action ] diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/transaction_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/transaction_view.ex index 6c19ac24c0..7dd88efe1b 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/transaction_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/transaction_view.ex @@ -3,6 +3,18 @@ defmodule BlockScoutWeb.API.RPC.TransactionView do alias BlockScoutWeb.API.RPC.RPCView + def render("gettxinfo.json", %{transaction: transaction, max_block_number: max_block_number, logs: logs}) do + try do + data = prepare_transaction(transaction, max_block_number, logs) + IO.puts "after prepare" + IO.inspect data + RPCView.render("show.json", data: data) + catch + x -> "Got #{x}" + :exit, _ -> "not really" + end + end + def render("gettxreceiptstatus.json", %{status: status}) do prepared_status = prepare_tx_receipt_status(status) RPCView.render("show.json", data: %{"status" => prepared_status}) @@ -44,4 +56,35 @@ defmodule BlockScoutWeb.API.RPC.TransactionView do "errDescription" => error |> Atom.to_string() |> String.replace("_", " ") } end + + defp prepare_transaction(transaction, max_block_number, logs) do + %{ + "hash" => "#{transaction.hash}", + "timeStamp" => "#{DateTime.to_unix(transaction.block.timestamp)}", + "blockNumber" => "#{transaction.block_number}", + "confirmations" => "#{(max_block_number - transaction.block_number)}", + "success" => if(transaction.status == :ok, do: true, else: false), + "from" => "#{transaction.from_address_hash}", + "to" => "#{transaction.to_address_hash}", + "value" => "#{transaction.value.value}", + "input" => "#{transaction.input}", + "gasLimit" => "#{transaction.gas}", + "gasUsed" => "#{transaction.gas_used}", + "logs" => Enum.map(logs, &prepare_log/1) + } + end + + defp prepare_log(log) do + %{ + "address" => "#{log.address_hash}", + "topics" => get_topics(log), + "data" => "#{log.data}" + } + end + + defp get_topics(log) do + log + |> Map.take([:first_topic, :second_topic, :third_topic, :fourth_topic]) + |> Map.values() + end end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/transaction_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/transaction_controller_test.exs index 008bb64300..026eb43890 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/transaction_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/transaction_controller_test.exs @@ -310,4 +310,105 @@ defmodule BlockScoutWeb.API.RPC.TransactionControllerTest do assert response["message"] == "OK" end end + + describe "gettxinfo" do + test "with missing txhash", %{conn: conn} do + params = %{ + "module" => "transaction", + "action" => "gettxinfo" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] =~ "txhash is required" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + end + + test "with an invalid txhash", %{conn: conn} do + params = %{ + "module" => "transaction", + "action" => "gettxinfo", + "txhash" => "badhash" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] =~ "Invalid txhash format" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + end + + test "with a txhash that doesn't exist", %{conn: conn} do + params = %{ + "module" => "transaction", + "action" => "gettxinfo", + "txhash" => "0x40eb908387324f2b575b4879cd9d7188f69c8fc9d87c901b9e2daaea4b442170" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] =~ "Transaction not found" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + end + + test "with a txhash with ok status", %{conn: conn} do + block = insert(:block) + + transaction = + :transaction + |> insert() + |> with_block(block, status: :ok) + + address = insert(:address) + insert(:log, address: address, transaction: transaction) + + params = %{ + "module" => "transaction", + "action" => "gettxinfo", + "txhash" => "#{transaction.hash}" + } + + expected_result = %{ + "hash" => "#{transaction.hash}", + "timeStamp" => "#{DateTime.to_unix(transaction.block.timestamp)}", + "blockNumber" => "#{transaction.block_number}", + "confirmations" => "0", + "success" => true, + "from" => "#{transaction.from_address_hash}", + "to" => "#{transaction.to_address_hash}", + "value" => "#{transaction.value.value}", + "input" => "#{transaction.input}", + "gasLimit" => "#{transaction.gas}", + "gasUsed" => "#{transaction.gas_used}", + "logs" => [%{ + "address" => "#{address}", + "data" => "0x00", + "topics" => [nil, nil, nil, nil] + }] + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"] == expected_result + assert response["status"] == "1" + assert response["message"] == "OK" + end + end end From 6a3b3b3c430f1e54dad9b0c6b3d8dd6910864655 Mon Sep 17 00:00:00 2001 From: robertoschneiders Date: Wed, 31 Oct 2018 18:45:32 -0300 Subject: [PATCH 09/42] Removes debug code --- .../block_scout_web/views/api/rpc/transaction_view.ex | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/transaction_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/transaction_view.ex index 7dd88efe1b..b12081ec86 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/transaction_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/transaction_view.ex @@ -4,15 +4,8 @@ defmodule BlockScoutWeb.API.RPC.TransactionView do alias BlockScoutWeb.API.RPC.RPCView def render("gettxinfo.json", %{transaction: transaction, max_block_number: max_block_number, logs: logs}) do - try do - data = prepare_transaction(transaction, max_block_number, logs) - IO.puts "after prepare" - IO.inspect data - RPCView.render("show.json", data: data) - catch - x -> "Got #{x}" - :exit, _ -> "not really" - end + data = prepare_transaction(transaction, max_block_number, logs) + RPCView.render("show.json", data: data) end def render("gettxreceiptstatus.json", %{status: status}) do From 31a369e37143415a7d3b01ef70813c032e30ec5d Mon Sep 17 00:00:00 2001 From: Alex Garibay Date: Wed, 31 Oct 2018 16:51:19 -0500 Subject: [PATCH 10/42] Add custom build step for libsecp256k1 --- .circleci/config.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index f9db91d227..cc733b35e0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -14,6 +14,8 @@ jobs: working_directory: ~/app steps: + - run: apt-get update; apt-get -y install autoconf build-essential libgmp3-dev libtool + - checkout - run: mix local.hex --force @@ -70,6 +72,11 @@ jobs: - run: mix compile + # Ensure NIF is compiled for libsecp256k1 + - run: + command: make + working_directory: "deps/libsecp256k1" + # `deps` needs to be cached with `_build` because `_build` will symlink into `deps` - save_cache: From b50b80dd1e2853ea4949a096948d32a8c2390689 Mon Sep 17 00:00:00 2001 From: robertoschneiders Date: Wed, 31 Oct 2018 19:39:38 -0300 Subject: [PATCH 11/42] Fixes broken test --- .../views/api/rpc/transaction_view.ex | 2 +- .../api/rpc/transaction_controller_test.exs | 16 +++++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/transaction_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/transaction_view.ex index b12081ec86..2820163235 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/transaction_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/transaction_view.ex @@ -55,7 +55,7 @@ defmodule BlockScoutWeb.API.RPC.TransactionView do "hash" => "#{transaction.hash}", "timeStamp" => "#{DateTime.to_unix(transaction.block.timestamp)}", "blockNumber" => "#{transaction.block_number}", - "confirmations" => "#{(max_block_number - transaction.block_number)}", + "confirmations" => "#{max_block_number - transaction.block_number}", "success" => if(transaction.status == :ok, do: true, else: false), "from" => "#{transaction.from_address_hash}", "to" => "#{transaction.to_address_hash}", diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/transaction_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/transaction_controller_test.exs index 026eb43890..7d28864e5f 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/transaction_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/transaction_controller_test.exs @@ -374,7 +374,7 @@ defmodule BlockScoutWeb.API.RPC.TransactionControllerTest do |> with_block(block, status: :ok) address = insert(:address) - insert(:log, address: address, transaction: transaction) + log = insert(:log, address: address, transaction: transaction) params = %{ "module" => "transaction", @@ -382,7 +382,7 @@ defmodule BlockScoutWeb.API.RPC.TransactionControllerTest do "txhash" => "#{transaction.hash}" } - expected_result = %{ + expected_result = %{ "hash" => "#{transaction.hash}", "timeStamp" => "#{DateTime.to_unix(transaction.block.timestamp)}", "blockNumber" => "#{transaction.block_number}", @@ -394,11 +394,13 @@ defmodule BlockScoutWeb.API.RPC.TransactionControllerTest do "input" => "#{transaction.input}", "gasLimit" => "#{transaction.gas}", "gasUsed" => "#{transaction.gas_used}", - "logs" => [%{ - "address" => "#{address}", - "data" => "0x00", - "topics" => [nil, nil, nil, nil] - }] + "logs" => [ + %{ + "address" => "#{address}", + "data" => "#{log.data}", + "topics" => [nil, nil, nil, nil] + } + ] } assert response = From 505568657889b24a5b89d2e9aaabe8c243eee885 Mon Sep 17 00:00:00 2001 From: robertoschneiders Date: Thu, 1 Nov 2018 09:55:13 -0300 Subject: [PATCH 12/42] Fixes the topics order in the gettxinfo endpoint --- .../block_scout_web/views/api/rpc/transaction_view.ex | 4 +--- .../api/rpc/transaction_controller_test.exs | 11 +++++++++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/transaction_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/transaction_view.ex index 2820163235..73dae614e6 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/transaction_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/transaction_view.ex @@ -76,8 +76,6 @@ defmodule BlockScoutWeb.API.RPC.TransactionView do end defp get_topics(log) do - log - |> Map.take([:first_topic, :second_topic, :third_topic, :fourth_topic]) - |> Map.values() + [log.first_topic, log.second_topic, log.third_topic, log.fourth_topic] end end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/transaction_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/transaction_controller_test.exs index 7d28864e5f..bc0cfcca96 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/transaction_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/transaction_controller_test.exs @@ -374,7 +374,14 @@ defmodule BlockScoutWeb.API.RPC.TransactionControllerTest do |> with_block(block, status: :ok) address = insert(:address) - log = insert(:log, address: address, transaction: transaction) + + log = + insert(:log, + address: address, + transaction: transaction, + first_topic: "first topic", + second_topic: "second topic" + ) params = %{ "module" => "transaction", @@ -398,7 +405,7 @@ defmodule BlockScoutWeb.API.RPC.TransactionControllerTest do %{ "address" => "#{address}", "data" => "#{log.data}", - "topics" => [nil, nil, nil, nil] + "topics" => ["first topic", "second topic", nil, nil] } ] } From 52fb43ae4a42732b6156ba196743b52fe8ba7bb4 Mon Sep 17 00:00:00 2001 From: jimmay5469 Date: Thu, 25 Oct 2018 10:07:30 -0400 Subject: [PATCH 13/42] One JS page file per route --- .../__tests__/pages/{block.js => blocks.js} | 2 +- .../__tests__/pages/pending_transactions.js | 232 +++++++++++ .../assets/__tests__/pages/transaction.js | 382 ------------------ .../assets/__tests__/pages/transactions.js | 160 ++++++++ apps/block_scout_web/assets/js/app.js | 12 +- .../assets/js/pages/{block.js => blocks.js} | 0 .../assets/js/pages/pending_transactions.js | 131 ++++++ .../assets/js/pages/transaction.js | 186 +-------- .../assets/js/pages/transactions.js | 100 +++++ 9 files changed, 634 insertions(+), 571 deletions(-) rename apps/block_scout_web/assets/__tests__/pages/{block.js => blocks.js} (98%) create mode 100644 apps/block_scout_web/assets/__tests__/pages/pending_transactions.js create mode 100644 apps/block_scout_web/assets/__tests__/pages/transactions.js rename apps/block_scout_web/assets/js/pages/{block.js => blocks.js} (100%) create mode 100644 apps/block_scout_web/assets/js/pages/pending_transactions.js create mode 100644 apps/block_scout_web/assets/js/pages/transactions.js diff --git a/apps/block_scout_web/assets/__tests__/pages/block.js b/apps/block_scout_web/assets/__tests__/pages/blocks.js similarity index 98% rename from apps/block_scout_web/assets/__tests__/pages/block.js rename to apps/block_scout_web/assets/__tests__/pages/blocks.js index d854fb9746..2ddad6a5a3 100644 --- a/apps/block_scout_web/assets/__tests__/pages/block.js +++ b/apps/block_scout_web/assets/__tests__/pages/blocks.js @@ -1,4 +1,4 @@ -import { reducer, initialState } from '../../js/pages/block' +import { reducer, initialState } from '../../js/pages/blocks' test('CHANNEL_DISCONNECTED', () => { const state = initialState diff --git a/apps/block_scout_web/assets/__tests__/pages/pending_transactions.js b/apps/block_scout_web/assets/__tests__/pages/pending_transactions.js new file mode 100644 index 0000000000..25d03aed61 --- /dev/null +++ b/apps/block_scout_web/assets/__tests__/pages/pending_transactions.js @@ -0,0 +1,232 @@ +import { reducer, initialState } from '../../js/pages/pending_transactions' + +test('CHANNEL_DISCONNECTED', () => { + const state = initialState + const action = { + type: 'CHANNEL_DISCONNECTED' + } + const output = reducer(state, action) + + expect(output.channelDisconnected).toBe(true) +}) + +describe('RECEIVED_NEW_PENDING_TRANSACTION_BATCH', () => { + test('single transaction', () => { + const state = initialState + const action = { + type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH', + msgs: [{ + transactionHash: '0x00', + transactionHtml: 'test' + }] + } + const output = reducer(state, action) + + expect(output.newPendingTransactions).toEqual(['test']) + expect(output.newPendingTransactionHashesBatch.length).toEqual(0) + expect(output.pendingTransactionCount).toEqual(1) + }) + test('large batch of transactions', () => { + const state = initialState + const action = { + type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH', + msgs: [{ + transactionHash: '0x01', + transactionHtml: 'test 1' + },{ + transactionHash: '0x02', + transactionHtml: 'test 2' + },{ + transactionHash: '0x03', + transactionHtml: 'test 3' + },{ + transactionHash: '0x04', + transactionHtml: 'test 4' + },{ + transactionHash: '0x05', + transactionHtml: 'test 5' + },{ + transactionHash: '0x06', + transactionHtml: 'test 6' + },{ + transactionHash: '0x07', + transactionHtml: 'test 7' + },{ + transactionHash: '0x08', + transactionHtml: 'test 8' + },{ + transactionHash: '0x09', + transactionHtml: 'test 9' + },{ + transactionHash: '0x10', + transactionHtml: 'test 10' + },{ + transactionHash: '0x11', + transactionHtml: 'test 11' + }] + } + const output = reducer(state, action) + + expect(output.newPendingTransactions).toEqual([]) + expect(output.newPendingTransactionHashesBatch.length).toEqual(11) + expect(output.pendingTransactionCount).toEqual(11) + }) + test('single transaction after single transaction', () => { + const state = Object.assign({}, initialState, { + newPendingTransactions: ['test 1'], + pendingTransactionCount: 1 + }) + const action = { + type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH', + msgs: [{ + transactionHash: '0x02', + transactionHtml: 'test 2' + }] + } + const output = reducer(state, action) + + expect(output.newPendingTransactions).toEqual(['test 1', 'test 2']) + expect(output.newPendingTransactionHashesBatch.length).toEqual(0) + expect(output.pendingTransactionCount).toEqual(2) + }) + test('single transaction after large batch of transactions', () => { + const state = Object.assign({}, initialState, { + newPendingTransactionHashesBatch: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11'] + }) + const action = { + type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH', + msgs: [{ + transactionHash: '0x12', + transactionHtml: 'test 12' + }] + } + const output = reducer(state, action) + + expect(output.newPendingTransactions).toEqual([]) + expect(output.newPendingTransactionHashesBatch.length).toEqual(12) + }) + test('large batch of transactions after large batch of transactions', () => { + const state = Object.assign({}, initialState, { + newPendingTransactionHashesBatch: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11'] + }) + const action = { + type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH', + msgs: [{ + transactionHash: '0x12', + transactionHtml: 'test 12' + },{ + transactionHash: '0x13', + transactionHtml: 'test 13' + },{ + transactionHash: '0x14', + transactionHtml: 'test 14' + },{ + transactionHash: '0x15', + transactionHtml: 'test 15' + },{ + transactionHash: '0x16', + transactionHtml: 'test 16' + },{ + transactionHash: '0x17', + transactionHtml: 'test 17' + },{ + transactionHash: '0x18', + transactionHtml: 'test 18' + },{ + transactionHash: '0x19', + transactionHtml: 'test 19' + },{ + transactionHash: '0x20', + transactionHtml: 'test 20' + },{ + transactionHash: '0x21', + transactionHtml: 'test 21' + },{ + transactionHash: '0x22', + transactionHtml: 'test 22' + }] + } + const output = reducer(state, action) + + expect(output.newPendingTransactions).toEqual([]) + expect(output.newPendingTransactionHashesBatch.length).toEqual(22) + }) + test('after disconnection', () => { + const state = Object.assign({}, initialState, { + channelDisconnected: true + }) + const action = { + type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH', + msgs: [{ + transactionHash: '0x00', + transactionHtml: 'test' + }] + } + const output = reducer(state, action) + + expect(output.newPendingTransactions).toEqual([]) + }) + test('on page 2+', () => { + const state = Object.assign({}, initialState, { + beyondPageOne: true, + pendingTransactionCount: 1 + }) + const action = { + type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH', + msgs: [{ + transactionHash: '0x00', + transactionHtml: 'test' + }] + } + const output = reducer(state, action) + + expect(output.newPendingTransactions).toEqual([]) + expect(output.pendingTransactionCount).toEqual(2) + }) +}) + +describe('RECEIVED_NEW_TRANSACTION', () => { + test('single transaction collated', () => { + const state = { ...initialState, pendingTransactionCount: 2 } + const action = { + type: 'RECEIVED_NEW_TRANSACTION', + msg: { + transactionHash: '0x00' + } + } + const output = reducer(state, action) + + expect(output.pendingTransactionCount).toBe(1) + expect(output.newTransactionHashes).toEqual(['0x00']) + }) + test('single transaction collated after batch', () => { + const state = Object.assign({}, initialState, { + newPendingTransactionHashesBatch: ['0x01', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11'] + }) + const action = { + type: 'RECEIVED_NEW_TRANSACTION', + msg: { + transactionHash: '0x01' + } + } + const output = reducer(state, action) + + expect(output.newPendingTransactionHashesBatch.length).toEqual(10) + expect(output.newPendingTransactionHashesBatch).not.toContain('0x01') + }) + test('on page 2+', () => { + const state = Object.assign({}, initialState, { + beyondPageOne: true, + pendingTransactionCount: 2 + }) + const action = { + type: 'RECEIVED_NEW_TRANSACTION', + msg: { + transactionHash: '0x01' + } + } + const output = reducer(state, action) + + expect(output.pendingTransactionCount).toEqual(1) + }) +}) diff --git a/apps/block_scout_web/assets/__tests__/pages/transaction.js b/apps/block_scout_web/assets/__tests__/pages/transaction.js index ea4eb3595f..55aa68f4b4 100644 --- a/apps/block_scout_web/assets/__tests__/pages/transaction.js +++ b/apps/block_scout_web/assets/__tests__/pages/transaction.js @@ -1,16 +1,5 @@ import { reducer, initialState } from '../../js/pages/transaction' -test('CHANNEL_DISCONNECTED', () => { - const state = initialState - const action = { - type: 'CHANNEL_DISCONNECTED' - } - const output = reducer(state, action) - - expect(output.channelDisconnected).toBe(true) - expect(output.batchCountAccumulator).toBe(0) -}) - test('RECEIVED_NEW_BLOCK', () => { const state = { ...initialState, blockNumber: 1 } const action = { @@ -23,374 +12,3 @@ test('RECEIVED_NEW_BLOCK', () => { expect(output.confirmations).toBe(4) }) - -describe('RECEIVED_NEW_PENDING_TRANSACTION_BATCH', () => { - test('single transaction', () => { - const state = initialState - const action = { - type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH', - msgs: [{ - transactionHash: '0x00', - transactionHtml: 'test' - }] - } - const output = reducer(state, action) - - expect(output.newPendingTransactions).toEqual(['test']) - expect(output.newPendingTransactionHashesBatch.length).toEqual(0) - expect(output.pendingTransactionCount).toEqual(1) - }) - test('large batch of transactions', () => { - const state = initialState - const action = { - type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH', - msgs: [{ - transactionHash: '0x01', - transactionHtml: 'test 1' - },{ - transactionHash: '0x02', - transactionHtml: 'test 2' - },{ - transactionHash: '0x03', - transactionHtml: 'test 3' - },{ - transactionHash: '0x04', - transactionHtml: 'test 4' - },{ - transactionHash: '0x05', - transactionHtml: 'test 5' - },{ - transactionHash: '0x06', - transactionHtml: 'test 6' - },{ - transactionHash: '0x07', - transactionHtml: 'test 7' - },{ - transactionHash: '0x08', - transactionHtml: 'test 8' - },{ - transactionHash: '0x09', - transactionHtml: 'test 9' - },{ - transactionHash: '0x10', - transactionHtml: 'test 10' - },{ - transactionHash: '0x11', - transactionHtml: 'test 11' - }] - } - const output = reducer(state, action) - - expect(output.newPendingTransactions).toEqual([]) - expect(output.newPendingTransactionHashesBatch.length).toEqual(11) - expect(output.pendingTransactionCount).toEqual(11) - }) - test('single transaction after single transaction', () => { - const state = Object.assign({}, initialState, { - newPendingTransactions: ['test 1'], - pendingTransactionCount: 1 - }) - const action = { - type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH', - msgs: [{ - transactionHash: '0x02', - transactionHtml: 'test 2' - }] - } - const output = reducer(state, action) - - expect(output.newPendingTransactions).toEqual(['test 1', 'test 2']) - expect(output.newPendingTransactionHashesBatch.length).toEqual(0) - expect(output.pendingTransactionCount).toEqual(2) - }) - test('single transaction after large batch of transactions', () => { - const state = Object.assign({}, initialState, { - newPendingTransactionHashesBatch: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11'] - }) - const action = { - type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH', - msgs: [{ - transactionHash: '0x12', - transactionHtml: 'test 12' - }] - } - const output = reducer(state, action) - - expect(output.newPendingTransactions).toEqual([]) - expect(output.newPendingTransactionHashesBatch.length).toEqual(12) - }) - test('large batch of transactions after large batch of transactions', () => { - const state = Object.assign({}, initialState, { - newPendingTransactionHashesBatch: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11'] - }) - const action = { - type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH', - msgs: [{ - transactionHash: '0x12', - transactionHtml: 'test 12' - },{ - transactionHash: '0x13', - transactionHtml: 'test 13' - },{ - transactionHash: '0x14', - transactionHtml: 'test 14' - },{ - transactionHash: '0x15', - transactionHtml: 'test 15' - },{ - transactionHash: '0x16', - transactionHtml: 'test 16' - },{ - transactionHash: '0x17', - transactionHtml: 'test 17' - },{ - transactionHash: '0x18', - transactionHtml: 'test 18' - },{ - transactionHash: '0x19', - transactionHtml: 'test 19' - },{ - transactionHash: '0x20', - transactionHtml: 'test 20' - },{ - transactionHash: '0x21', - transactionHtml: 'test 21' - },{ - transactionHash: '0x22', - transactionHtml: 'test 22' - }] - } - const output = reducer(state, action) - - expect(output.newPendingTransactions).toEqual([]) - expect(output.newPendingTransactionHashesBatch.length).toEqual(22) - }) - test('after disconnection', () => { - const state = Object.assign({}, initialState, { - channelDisconnected: true - }) - const action = { - type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH', - msgs: [{ - transactionHash: '0x00', - transactionHtml: 'test' - }] - } - const output = reducer(state, action) - - expect(output.newPendingTransactions).toEqual([]) - expect(output.batchCountAccumulator).toEqual(0) - }) - test('on page 2+', () => { - const state = Object.assign({}, initialState, { - beyondPageOne: true, - pendingTransactionCount: 1 - }) - const action = { - type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH', - msgs: [{ - transactionHash: '0x00', - transactionHtml: 'test' - }] - } - const output = reducer(state, action) - - expect(output.newPendingTransactions).toEqual([]) - expect(output.batchCountAccumulator).toEqual(0) - expect(output.pendingTransactionCount).toEqual(2) - }) -}) - -describe('RECEIVED_NEW_TRANSACTION', () => { - test('single transaction collated', () => { - const state = { ...initialState, pendingTransactionCount: 2 } - const action = { - type: 'RECEIVED_NEW_TRANSACTION', - msg: { - transactionHash: '0x00' - } - } - const output = reducer(state, action) - - expect(output.pendingTransactionCount).toBe(1) - expect(output.newTransactionHashes).toEqual(['0x00']) - }) - test('single transaction collated after batch', () => { - const state = Object.assign({}, initialState, { - newPendingTransactionHashesBatch: ['0x01', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11'] - }) - const action = { - type: 'RECEIVED_NEW_TRANSACTION', - msg: { - transactionHash: '0x01' - } - } - const output = reducer(state, action) - - expect(output.newPendingTransactionHashesBatch.length).toEqual(10) - expect(output.newPendingTransactionHashesBatch).not.toContain('0x01') - }) - test('on page 2+', () => { - const state = Object.assign({}, initialState, { - beyondPageOne: true, - pendingTransactionCount: 2 - }) - const action = { - type: 'RECEIVED_NEW_TRANSACTION', - msg: { - transactionHash: '0x01' - } - } - const output = reducer(state, action) - - expect(output.pendingTransactionCount).toEqual(1) - }) -}) - -describe('RECEIVED_NEW_TRANSACTION_BATCH', () => { - test('single transaction', () => { - const state = initialState - const action = { - type: 'RECEIVED_NEW_TRANSACTION_BATCH', - msgs: [{ - transactionHtml: 'test' - }] - } - const output = reducer(state, action) - - expect(output.newTransactions).toEqual(['test']) - expect(output.batchCountAccumulator).toEqual(0) - expect(output.transactionCount).toEqual(1) - }) - test('large batch of transactions', () => { - const state = initialState - const action = { - type: 'RECEIVED_NEW_TRANSACTION_BATCH', - msgs: [{ - transactionHtml: 'test 1' - },{ - transactionHtml: 'test 2' - },{ - transactionHtml: 'test 3' - },{ - transactionHtml: 'test 4' - },{ - transactionHtml: 'test 5' - },{ - transactionHtml: 'test 6' - },{ - transactionHtml: 'test 7' - },{ - transactionHtml: 'test 8' - },{ - transactionHtml: 'test 9' - },{ - transactionHtml: 'test 10' - },{ - transactionHtml: 'test 11' - }] - } - const output = reducer(state, action) - - expect(output.newTransactions).toEqual([]) - expect(output.batchCountAccumulator).toEqual(11) - expect(output.transactionCount).toEqual(11) - }) - test('single transaction after single transaction', () => { - const state = Object.assign({}, initialState, { - newTransactions: ['test 1'] - }) - const action = { - type: 'RECEIVED_NEW_TRANSACTION_BATCH', - msgs: [{ - transactionHtml: 'test 2' - }] - } - const output = reducer(state, action) - - expect(output.newTransactions).toEqual(['test 1', 'test 2']) - expect(output.batchCountAccumulator).toEqual(0) - }) - test('single transaction after large batch of transactions', () => { - const state = Object.assign({}, initialState, { - batchCountAccumulator: 11 - }) - const action = { - type: 'RECEIVED_NEW_TRANSACTION_BATCH', - msgs: [{ - transactionHtml: 'test 12' - }] - } - const output = reducer(state, action) - - expect(output.newTransactions).toEqual([]) - expect(output.batchCountAccumulator).toEqual(12) - }) - test('large batch of transactions after large batch of transactions', () => { - const state = Object.assign({}, initialState, { - batchCountAccumulator: 11 - }) - const action = { - type: 'RECEIVED_NEW_TRANSACTION_BATCH', - msgs: [{ - transactionHtml: 'test 12' - },{ - transactionHtml: 'test 13' - },{ - transactionHtml: 'test 14' - },{ - transactionHtml: 'test 15' - },{ - transactionHtml: 'test 16' - },{ - transactionHtml: 'test 17' - },{ - transactionHtml: 'test 18' - },{ - transactionHtml: 'test 19' - },{ - transactionHtml: 'test 20' - },{ - transactionHtml: 'test 21' - },{ - transactionHtml: 'test 22' - }] - } - const output = reducer(state, action) - - expect(output.newTransactions).toEqual([]) - expect(output.batchCountAccumulator).toEqual(22) - }) - test('after disconnection', () => { - const state = Object.assign({}, initialState, { - channelDisconnected: true - }) - const action = { - type: 'RECEIVED_NEW_TRANSACTION_BATCH', - msgs: [{ - transactionHtml: 'test' - }] - } - const output = reducer(state, action) - - expect(output.newTransactions).toEqual([]) - expect(output.batchCountAccumulator).toEqual(0) - }) - test('on page 2+', () => { - const state = Object.assign({}, initialState, { - beyondPageOne: true, - transactionCount: 1 - }) - const action = { - type: 'RECEIVED_NEW_TRANSACTION_BATCH', - msgs: [{ - transactionHtml: 'test' - }] - } - const output = reducer(state, action) - - expect(output.newTransactions).toEqual([]) - expect(output.batchCountAccumulator).toEqual(0) - expect(output.transactionCount).toEqual(2) - }) -}) diff --git a/apps/block_scout_web/assets/__tests__/pages/transactions.js b/apps/block_scout_web/assets/__tests__/pages/transactions.js new file mode 100644 index 0000000000..f45a4960d4 --- /dev/null +++ b/apps/block_scout_web/assets/__tests__/pages/transactions.js @@ -0,0 +1,160 @@ +import { reducer, initialState } from '../../js/pages/transactions' + +test('CHANNEL_DISCONNECTED', () => { + const state = initialState + const action = { + type: 'CHANNEL_DISCONNECTED' + } + const output = reducer(state, action) + + expect(output.channelDisconnected).toBe(true) + expect(output.batchCountAccumulator).toBe(0) +}) + +describe('RECEIVED_NEW_TRANSACTION_BATCH', () => { + test('single transaction', () => { + const state = initialState + const action = { + type: 'RECEIVED_NEW_TRANSACTION_BATCH', + msgs: [{ + transactionHtml: 'test' + }] + } + const output = reducer(state, action) + + expect(output.newTransactions).toEqual(['test']) + expect(output.batchCountAccumulator).toEqual(0) + expect(output.transactionCount).toEqual(1) + }) + test('large batch of transactions', () => { + const state = initialState + const action = { + type: 'RECEIVED_NEW_TRANSACTION_BATCH', + msgs: [{ + transactionHtml: 'test 1' + },{ + transactionHtml: 'test 2' + },{ + transactionHtml: 'test 3' + },{ + transactionHtml: 'test 4' + },{ + transactionHtml: 'test 5' + },{ + transactionHtml: 'test 6' + },{ + transactionHtml: 'test 7' + },{ + transactionHtml: 'test 8' + },{ + transactionHtml: 'test 9' + },{ + transactionHtml: 'test 10' + },{ + transactionHtml: 'test 11' + }] + } + const output = reducer(state, action) + + expect(output.newTransactions).toEqual([]) + expect(output.batchCountAccumulator).toEqual(11) + expect(output.transactionCount).toEqual(11) + }) + test('single transaction after single transaction', () => { + const state = Object.assign({}, initialState, { + newTransactions: ['test 1'] + }) + const action = { + type: 'RECEIVED_NEW_TRANSACTION_BATCH', + msgs: [{ + transactionHtml: 'test 2' + }] + } + const output = reducer(state, action) + + expect(output.newTransactions).toEqual(['test 1', 'test 2']) + expect(output.batchCountAccumulator).toEqual(0) + }) + test('single transaction after large batch of transactions', () => { + const state = Object.assign({}, initialState, { + batchCountAccumulator: 11 + }) + const action = { + type: 'RECEIVED_NEW_TRANSACTION_BATCH', + msgs: [{ + transactionHtml: 'test 12' + }] + } + const output = reducer(state, action) + + expect(output.newTransactions).toEqual([]) + expect(output.batchCountAccumulator).toEqual(12) + }) + test('large batch of transactions after large batch of transactions', () => { + const state = Object.assign({}, initialState, { + batchCountAccumulator: 11 + }) + const action = { + type: 'RECEIVED_NEW_TRANSACTION_BATCH', + msgs: [{ + transactionHtml: 'test 12' + },{ + transactionHtml: 'test 13' + },{ + transactionHtml: 'test 14' + },{ + transactionHtml: 'test 15' + },{ + transactionHtml: 'test 16' + },{ + transactionHtml: 'test 17' + },{ + transactionHtml: 'test 18' + },{ + transactionHtml: 'test 19' + },{ + transactionHtml: 'test 20' + },{ + transactionHtml: 'test 21' + },{ + transactionHtml: 'test 22' + }] + } + const output = reducer(state, action) + + expect(output.newTransactions).toEqual([]) + expect(output.batchCountAccumulator).toEqual(22) + }) + test('after disconnection', () => { + const state = Object.assign({}, initialState, { + channelDisconnected: true + }) + const action = { + type: 'RECEIVED_NEW_TRANSACTION_BATCH', + msgs: [{ + transactionHtml: 'test' + }] + } + const output = reducer(state, action) + + expect(output.newTransactions).toEqual([]) + expect(output.batchCountAccumulator).toEqual(0) + }) + test('on page 2+', () => { + const state = Object.assign({}, initialState, { + beyondPageOne: true, + transactionCount: 1 + }) + const action = { + type: 'RECEIVED_NEW_TRANSACTION_BATCH', + msgs: [{ + transactionHtml: 'test' + }] + } + const output = reducer(state, action) + + expect(output.newTransactions).toEqual([]) + expect(output.batchCountAccumulator).toEqual(0) + expect(output.transactionCount).toEqual(2) + }) +}) diff --git a/apps/block_scout_web/assets/js/app.js b/apps/block_scout_web/assets/js/app.js index 935a325d04..83e3080c5c 100644 --- a/apps/block_scout_web/assets/js/app.js +++ b/apps/block_scout_web/assets/js/app.js @@ -20,6 +20,13 @@ import 'bootstrap' import './locale' +import './pages/address' +import './pages/blocks' +import './pages/chain' +import './pages/pending_transactions' +import './pages/transaction' +import './pages/transactions' + import './lib/clipboard_buttons' import './lib/currency' import './lib/from_now' @@ -37,8 +44,3 @@ import './lib/token_balance_dropdown_search' import './lib/token_transfers_toggle' import './lib/tooltip' import './lib/try_api' - -import './pages/address' -import './pages/block' -import './pages/chain' -import './pages/transaction' diff --git a/apps/block_scout_web/assets/js/pages/block.js b/apps/block_scout_web/assets/js/pages/blocks.js similarity index 100% rename from apps/block_scout_web/assets/js/pages/block.js rename to apps/block_scout_web/assets/js/pages/blocks.js diff --git a/apps/block_scout_web/assets/js/pages/pending_transactions.js b/apps/block_scout_web/assets/js/pages/pending_transactions.js new file mode 100644 index 0000000000..75c3ef8d0f --- /dev/null +++ b/apps/block_scout_web/assets/js/pages/pending_transactions.js @@ -0,0 +1,131 @@ +import $ from 'jquery' +import _ from 'lodash' +import URI from 'urijs' +import humps from 'humps' +import numeral from 'numeral' +import socket from '../socket' +import { updateAllAges } from '../lib/from_now' +import { batchChannel, initRedux, slideDownPrepend, slideUpRemove } from '../utils' + +const BATCH_THRESHOLD = 10 + +export const initialState = { + newPendingTransactionHashesBatch: [], + beyondPageOne: null, + channelDisconnected: false, + newPendingTransactions: [], + newTransactionHashes: [], + pendingTransactionCount: null +} + +export function reducer (state = initialState, action) { + switch (action.type) { + case 'PAGE_LOAD': { + return Object.assign({}, state, { + beyondPageOne: action.beyondPageOne, + pendingTransactionCount: numeral(action.pendingTransactionCount).value() + }) + } + case 'CHANNEL_DISCONNECTED': { + return Object.assign({}, state, { + channelDisconnected: true + }) + } + case 'RECEIVED_NEW_TRANSACTION': { + if (state.channelDisconnected) return state + + return Object.assign({}, state, { + newPendingTransactionHashesBatch: _.without(state.newPendingTransactionHashesBatch, action.msg.transactionHash), + pendingTransactionCount: state.pendingTransactionCount - 1, + newTransactionHashes: [action.msg.transactionHash] + }) + } + case 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH': { + if (state.channelDisconnected) return state + + const pendingTransactionCount = state.pendingTransactionCount + action.msgs.length + + if (state.beyondPageOne) return Object.assign({}, state, { pendingTransactionCount }) + + if (!state.newPendingTransactionHashesBatch.length && action.msgs.length < BATCH_THRESHOLD) { + return Object.assign({}, state, { + newPendingTransactions: [ + ...state.newPendingTransactions, + ..._.map(action.msgs, 'transactionHtml') + ], + pendingTransactionCount + }) + } else { + return Object.assign({}, state, { + newPendingTransactionHashesBatch: [ + ...state.newPendingTransactionHashesBatch, + ..._.map(action.msgs, 'transactionHash') + ], + pendingTransactionCount + }) + } + } + default: + return state + } +} + +const $transactionPendingListPage = $('[data-page="transaction-pending-list"]') +if ($transactionPendingListPage.length) { + initRedux(reducer, { + main (store) { + store.dispatch({ + type: 'PAGE_LOAD', + pendingTransactionCount: $('[data-selector="transaction-pending-count"]').text(), + beyondPageOne: !!humps.camelizeKeys(URI(window.location).query(true)).insertedAt + }) + const transactionsChannel = socket.channel(`transactions:new_transaction`) + transactionsChannel.join() + transactionsChannel.onError(() => store.dispatch({ type: 'CHANNEL_DISCONNECTED' })) + transactionsChannel.on('transaction', (msg) => + store.dispatch({ type: 'RECEIVED_NEW_TRANSACTION', msg: humps.camelizeKeys(msg) }) + ) + const pendingTransactionsChannel = socket.channel(`transactions:new_pending_transaction`) + pendingTransactionsChannel.join() + pendingTransactionsChannel.onError(() => store.dispatch({ type: 'CHANNEL_DISCONNECTED' })) + pendingTransactionsChannel.on('pending_transaction', batchChannel((msgs) => + store.dispatch({ type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH', msgs: humps.camelizeKeys(msgs) })) + ) + }, + render (state, oldState) { + const $channelBatching = $('[data-selector="channel-batching-message"]') + const $channelBatchingCount = $('[data-selector="channel-batching-count"]') + const $channelDisconnected = $('[data-selector="channel-disconnected-message"]') + const $pendingTransactionsList = $('[data-selector="transactions-pending-list"]') + const $pendingTransactionsCount = $('[data-selector="transaction-pending-count"]') + + if (state.channelDisconnected) $channelDisconnected.show() + if (oldState.pendingTransactionCount !== state.pendingTransactionCount) { + $pendingTransactionsCount.empty().append(numeral(state.pendingTransactionCount).format()) + } + if (oldState.newTransactionHashes !== state.newTransactionHashes && state.newTransactionHashes.length > 0) { + const $transaction = $(`[data-transaction-hash="${state.newTransactionHashes[0]}"]`) + $transaction.addClass('shrink-out') + setTimeout(() => { + if ($transaction.length === 1 && $transaction.siblings().length === 0 && state.pendingTransactionCount > 0) { + window.location.href = URI(window.location).removeQuery('inserted_at').removeQuery('hash').toString() + } else { + slideUpRemove($transaction) + } + }, 400) + } + if (state.newPendingTransactionHashesBatch.length) { + $channelBatching.show() + $channelBatchingCount[0].innerHTML = numeral(state.newPendingTransactionHashesBatch.length).format() + } else { + $channelBatching.hide() + } + if (oldState.newPendingTransactions !== state.newPendingTransactions) { + const newTransactionsToInsert = state.newPendingTransactions.slice(oldState.newPendingTransactions.length) + slideDownPrepend($pendingTransactionsList, newTransactionsToInsert.reverse().join('')) + + updateAllAges() + } + } + }) +} diff --git a/apps/block_scout_web/assets/js/pages/transaction.js b/apps/block_scout_web/assets/js/pages/transaction.js index beda3482c4..335a59e974 100644 --- a/apps/block_scout_web/assets/js/pages/transaction.js +++ b/apps/block_scout_web/assets/js/pages/transaction.js @@ -1,42 +1,19 @@ import $ from 'jquery' -import _ from 'lodash' -import URI from 'urijs' import humps from 'humps' import numeral from 'numeral' import socket from '../socket' -import { updateAllAges } from '../lib/from_now' -import { batchChannel, initRedux, slideDownPrepend, slideUpRemove } from '../utils' - -const BATCH_THRESHOLD = 10 +import { initRedux } from '../utils' export const initialState = { - batchCountAccumulator: 0, - newPendingTransactionHashesBatch: [], - beyondPageOne: null, blockNumber: null, - channelDisconnected: false, - confirmations: null, - newPendingTransactions: [], - newTransactions: [], - newTransactionHashes: [], - transactionCount: null, - pendingTransactionCount: null + confirmations: null } export function reducer (state = initialState, action) { switch (action.type) { case 'PAGE_LOAD': { return Object.assign({}, state, { - beyondPageOne: action.beyondPageOne, - blockNumber: parseInt(action.blockNumber, 10), - transactionCount: numeral(action.transactionCount).value(), - pendingTransactionCount: numeral(action.pendingTransactionCount).value() - }) - } - case 'CHANNEL_DISCONNECTED': { - return Object.assign({}, state, { - channelDisconnected: true, - batchCountAccumulator: 0 + blockNumber: parseInt(action.blockNumber, 10) }) } case 'RECEIVED_NEW_BLOCK': { @@ -46,62 +23,6 @@ export function reducer (state = initialState, action) { }) } else return state } - case 'RECEIVED_NEW_TRANSACTION': { - if (state.channelDisconnected) return state - - return Object.assign({}, state, { - newPendingTransactionHashesBatch: _.without(state.newPendingTransactionHashesBatch, action.msg.transactionHash), - pendingTransactionCount: state.pendingTransactionCount - 1, - newTransactionHashes: [action.msg.transactionHash] - }) - } - case 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH': { - if (state.channelDisconnected) return state - - const pendingTransactionCount = state.pendingTransactionCount + action.msgs.length - - if (state.beyondPageOne) return Object.assign({}, state, { pendingTransactionCount }) - - if (!state.newPendingTransactionHashesBatch.length && action.msgs.length < BATCH_THRESHOLD) { - return Object.assign({}, state, { - newPendingTransactions: [ - ...state.newPendingTransactions, - ..._.map(action.msgs, 'transactionHtml') - ], - pendingTransactionCount - }) - } else { - return Object.assign({}, state, { - newPendingTransactionHashesBatch: [ - ...state.newPendingTransactionHashesBatch, - ..._.map(action.msgs, 'transactionHash') - ], - pendingTransactionCount - }) - } - } - case 'RECEIVED_NEW_TRANSACTION_BATCH': { - if (state.channelDisconnected) return state - - const transactionCount = state.transactionCount + action.msgs.length - - if (state.beyondPageOne) return Object.assign({}, state, { transactionCount }) - - if (!state.batchCountAccumulator && action.msgs.length < BATCH_THRESHOLD) { - return Object.assign({}, state, { - newTransactions: [ - ...state.newTransactions, - ..._.map(action.msgs, 'transactionHtml') - ], - transactionCount - }) - } else { - return Object.assign({}, state, { - batchCountAccumulator: state.batchCountAccumulator + action.msgs.length, - transactionCount - }) - } - } default: return state } @@ -134,104 +55,3 @@ if ($transactionDetailsPage.length) { } }) } - -const $transactionPendingListPage = $('[data-page="transaction-pending-list"]') -if ($transactionPendingListPage.length) { - initRedux(reducer, { - main (store) { - store.dispatch({ - type: 'PAGE_LOAD', - pendingTransactionCount: $('[data-selector="transaction-pending-count"]').text(), - beyondPageOne: !!humps.camelizeKeys(URI(window.location).query(true)).insertedAt - }) - const transactionsChannel = socket.channel(`transactions:new_transaction`) - transactionsChannel.join() - transactionsChannel.onError(() => store.dispatch({ type: 'CHANNEL_DISCONNECTED' })) - transactionsChannel.on('transaction', (msg) => - store.dispatch({ type: 'RECEIVED_NEW_TRANSACTION', msg: humps.camelizeKeys(msg) }) - ) - const pendingTransactionsChannel = socket.channel(`transactions:new_pending_transaction`) - pendingTransactionsChannel.join() - pendingTransactionsChannel.onError(() => store.dispatch({ type: 'CHANNEL_DISCONNECTED' })) - pendingTransactionsChannel.on('pending_transaction', batchChannel((msgs) => - store.dispatch({ type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH', msgs: humps.camelizeKeys(msgs) })) - ) - }, - render (state, oldState) { - const $channelBatching = $('[data-selector="channel-batching-message"]') - const $channelBatchingCount = $('[data-selector="channel-batching-count"]') - const $channelDisconnected = $('[data-selector="channel-disconnected-message"]') - const $pendingTransactionsList = $('[data-selector="transactions-pending-list"]') - const $pendingTransactionsCount = $('[data-selector="transaction-pending-count"]') - - if (state.channelDisconnected) $channelDisconnected.show() - if (oldState.pendingTransactionCount !== state.pendingTransactionCount) { - $pendingTransactionsCount.empty().append(numeral(state.pendingTransactionCount).format()) - } - if (oldState.newTransactionHashes !== state.newTransactionHashes && state.newTransactionHashes.length > 0) { - const $transaction = $(`[data-transaction-hash="${state.newTransactionHashes[0]}"]`) - $transaction.addClass('shrink-out') - setTimeout(() => { - if ($transaction.length === 1 && $transaction.siblings().length === 0 && state.pendingTransactionCount > 0) { - window.location.href = URI(window.location).removeQuery('inserted_at').removeQuery('hash').toString() - } else { - slideUpRemove($transaction) - } - }, 400) - } - if (state.newPendingTransactionHashesBatch.length) { - $channelBatching.show() - $channelBatchingCount[0].innerHTML = numeral(state.newPendingTransactionHashesBatch.length).format() - } else { - $channelBatching.hide() - } - if (oldState.newPendingTransactions !== state.newPendingTransactions) { - const newTransactionsToInsert = state.newPendingTransactions.slice(oldState.newPendingTransactions.length) - slideDownPrepend($pendingTransactionsList, newTransactionsToInsert.reverse().join('')) - - updateAllAges() - } - } - }) -} - -const $transactionListPage = $('[data-page="transaction-list"]') -if ($transactionListPage.length) { - initRedux(reducer, { - main (store) { - store.dispatch({ - type: 'PAGE_LOAD', - transactionCount: $('[data-selector="transaction-count"]').text(), - beyondPageOne: !!humps.camelizeKeys(URI(window.location).query(true)).index - }) - const transactionsChannel = socket.channel(`transactions:new_transaction`) - transactionsChannel.join() - transactionsChannel.onError(() => store.dispatch({ type: 'CHANNEL_DISCONNECTED' })) - transactionsChannel.on('transaction', batchChannel((msgs) => - store.dispatch({ type: 'RECEIVED_NEW_TRANSACTION_BATCH', msgs: humps.camelizeKeys(msgs) })) - ) - }, - render (state, oldState) { - const $channelBatching = $('[data-selector="channel-batching-message"]') - const $channelBatchingCount = $('[data-selector="channel-batching-count"]') - const $channelDisconnected = $('[data-selector="channel-disconnected-message"]') - const $transactionsList = $('[data-selector="transactions-list"]') - const $transactionCount = $('[data-selector="transaction-count"]') - - if (state.channelDisconnected) $channelDisconnected.show() - if (oldState.transactionCount !== state.transactionCount) $transactionCount.empty().append(numeral(state.transactionCount).format()) - if (state.batchCountAccumulator) { - $channelBatching.show() - $channelBatchingCount[0].innerHTML = numeral(state.batchCountAccumulator).format() - } else { - $channelBatching.hide() - } - if (oldState.newTransactions !== state.newTransactions) { - const newTransactionsToInsert = state.newTransactions.slice(oldState.newTransactions.length) - slideDownPrepend($transactionsList, newTransactionsToInsert.reverse().join('')) - - updateAllAges() - } - } - }) -} diff --git a/apps/block_scout_web/assets/js/pages/transactions.js b/apps/block_scout_web/assets/js/pages/transactions.js new file mode 100644 index 0000000000..04544e83b4 --- /dev/null +++ b/apps/block_scout_web/assets/js/pages/transactions.js @@ -0,0 +1,100 @@ +import $ from 'jquery' +import _ from 'lodash' +import URI from 'urijs' +import humps from 'humps' +import numeral from 'numeral' +import socket from '../socket' +import { updateAllAges } from '../lib/from_now' +import { batchChannel, initRedux, slideDownPrepend } from '../utils' + +const BATCH_THRESHOLD = 10 + +export const initialState = { + batchCountAccumulator: 0, + beyondPageOne: null, + channelDisconnected: false, + newTransactions: [], + transactionCount: null +} + +export function reducer (state = initialState, action) { + switch (action.type) { + case 'PAGE_LOAD': { + return Object.assign({}, state, { + beyondPageOne: action.beyondPageOne, + transactionCount: numeral(action.transactionCount).value() + }) + } + case 'CHANNEL_DISCONNECTED': { + return Object.assign({}, state, { + channelDisconnected: true, + batchCountAccumulator: 0 + }) + } + case 'RECEIVED_NEW_TRANSACTION_BATCH': { + if (state.channelDisconnected) return state + + const transactionCount = state.transactionCount + action.msgs.length + + if (state.beyondPageOne) return Object.assign({}, state, { transactionCount }) + + if (!state.batchCountAccumulator && action.msgs.length < BATCH_THRESHOLD) { + return Object.assign({}, state, { + newTransactions: [ + ...state.newTransactions, + ..._.map(action.msgs, 'transactionHtml') + ], + transactionCount + }) + } else { + return Object.assign({}, state, { + batchCountAccumulator: state.batchCountAccumulator + action.msgs.length, + transactionCount + }) + } + } + default: + return state + } +} + +const $transactionListPage = $('[data-page="transaction-list"]') +if ($transactionListPage.length) { + initRedux(reducer, { + main (store) { + store.dispatch({ + type: 'PAGE_LOAD', + transactionCount: $('[data-selector="transaction-count"]').text(), + beyondPageOne: !!humps.camelizeKeys(URI(window.location).query(true)).index + }) + const transactionsChannel = socket.channel(`transactions:new_transaction`) + transactionsChannel.join() + transactionsChannel.onError(() => store.dispatch({ type: 'CHANNEL_DISCONNECTED' })) + transactionsChannel.on('transaction', batchChannel((msgs) => + store.dispatch({ type: 'RECEIVED_NEW_TRANSACTION_BATCH', msgs: humps.camelizeKeys(msgs) })) + ) + }, + render (state, oldState) { + const $channelBatching = $('[data-selector="channel-batching-message"]') + const $channelBatchingCount = $('[data-selector="channel-batching-count"]') + const $channelDisconnected = $('[data-selector="channel-disconnected-message"]') + const $transactionsList = $('[data-selector="transactions-list"]') + const $transactionCount = $('[data-selector="transaction-count"]') + + if (state.channelDisconnected) $channelDisconnected.show() + if (oldState.transactionCount !== state.transactionCount) $transactionCount.empty().append(numeral(state.transactionCount).format()) + if (state.batchCountAccumulator) { + $channelBatching.show() + $channelBatchingCount[0].innerHTML = numeral(state.batchCountAccumulator).format() + } else { + $channelBatching.hide() + } + if (oldState.newTransactions !== state.newTransactions) { + const newTransactionsToInsert = state.newTransactions.slice(oldState.newTransactions.length) + slideDownPrepend($transactionsList, newTransactionsToInsert.reverse().join('')) + + updateAllAges() + } + } + }) +} From 98899b57764a162855dee08c63d6079b2581d387 Mon Sep 17 00:00:00 2001 From: jimmay5469 Date: Thu, 25 Oct 2018 10:34:13 -0400 Subject: [PATCH 14/42] Install nanomorph for dom diffing --- apps/block_scout_web/assets/package-lock.json | 54 +++++++++---------- apps/block_scout_web/assets/package.json | 1 + 2 files changed, 25 insertions(+), 30 deletions(-) diff --git a/apps/block_scout_web/assets/package-lock.json b/apps/block_scout_web/assets/package-lock.json index c9c461305c..a9a49fb7c1 100644 --- a/apps/block_scout_web/assets/package-lock.json +++ b/apps/block_scout_web/assets/package-lock.json @@ -3789,8 +3789,7 @@ "version": "2.1.1", "resolved": false, "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -3814,15 +3813,13 @@ "version": "1.0.0", "resolved": false, "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "resolved": false, "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3839,22 +3836,19 @@ "version": "1.1.0", "resolved": false, "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "resolved": false, "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "resolved": false, "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -3985,8 +3979,7 @@ "version": "2.0.3", "resolved": false, "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -4000,7 +3993,6 @@ "resolved": false, "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -4017,7 +4009,6 @@ "resolved": false, "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -4026,15 +4017,13 @@ "version": "0.0.8", "resolved": false, "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.2.4", "resolved": false, "integrity": "sha512-hzXIWWet/BzWhYs2b+u7dRHlruXhwdgvlTMDKC6Cb1U7ps6Ac6yQlR39xsbjWJE377YTCtKwIXIpJ5oP+j5y8g==", "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -4055,7 +4044,6 @@ "resolved": false, "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -4144,8 +4132,7 @@ "version": "1.0.1", "resolved": false, "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -4159,7 +4146,6 @@ "resolved": false, "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -4255,8 +4241,7 @@ "version": "5.1.1", "resolved": false, "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -4298,7 +4283,6 @@ "resolved": false, "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -4320,7 +4304,6 @@ "resolved": false, "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -4369,15 +4352,13 @@ "version": "1.0.2", "resolved": false, "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.2", "resolved": false, "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=", - "dev": true, - "optional": true + "dev": true } } }, @@ -6428,6 +6409,11 @@ "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==", "dev": true }, + "nanoassert": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/nanoassert/-/nanoassert-1.1.0.tgz", + "integrity": "sha1-TzFS4JVA/eKMdvRLGbvNHVpCR40=" + }, "nanomatch": { "version": "1.2.13", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", @@ -6455,6 +6441,14 @@ } } }, + "nanomorph": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/nanomorph/-/nanomorph-5.1.3.tgz", + "integrity": "sha512-VydkKjFWU/DAO0R10awFASRNXQKHrZUMdMIiNcdmWm+IhuifuPOw/dDtpiQ1cNROF8f3ATPrcKRVarEayQJOqA==", + "requires": { + "nanoassert": "^1.1.0" + } + }, "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", diff --git a/apps/block_scout_web/assets/package.json b/apps/block_scout_web/assets/package.json index a3c05cba5a..be66f75088 100644 --- a/apps/block_scout_web/assets/package.json +++ b/apps/block_scout_web/assets/package.json @@ -28,6 +28,7 @@ "jquery": "^3.3.1", "lodash": "^4.17.10", "moment": "^2.22.1", + "nanomorph": "^5.1.3", "numeral": "^2.0.6", "path-parser": "^4.1.1", "phoenix": "file:../../../deps/phoenix", From 1fdd7ed19c9f8df879d34efecff1f7fa87afe375 Mon Sep 17 00:00:00 2001 From: jimmay5469 Date: Thu, 25 Oct 2018 17:08:18 -0400 Subject: [PATCH 15/42] Write listMorph for list dom diffing --- apps/block_scout_web/assets/js/utils.js | 46 ++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/apps/block_scout_web/assets/js/utils.js b/apps/block_scout_web/assets/js/utils.js index ed69186d1c..74f80f3f30 100644 --- a/apps/block_scout_web/assets/js/utils.js +++ b/apps/block_scout_web/assets/js/utils.js @@ -1,6 +1,8 @@ import $ from 'jquery' import _ from 'lodash' import { createStore } from 'redux' +import morph from 'nanomorph' +import { updateAllAges } from './lib/from_now' export function batchChannel (func) { let msgs = [] @@ -27,7 +29,7 @@ export function initRedux (reducer, { main, render, debug } = {}) { } if (!render) console.warn('initRedux: You have not passed a render function.') - const store = createStore(reducer) + const store = createStore(reducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()) if (debug) store.subscribe(() => { console.log(store.getState()) }) let oldState = store.getState() if (render) { @@ -52,6 +54,13 @@ export function slideDownPrepend ($container, content) { } }) } +export function slideDownAppend ($container, content) { + smarterSlideDown($(content), { + insert ($el) { + $container.append($el) + } + }) +} export function slideDownBefore ($container, content) { smarterSlideDown($(content), { insert ($el) { @@ -105,3 +114,38 @@ function smarterSlideUp ($el, { complete = _.noop } = {}) { $el.slideUp({ complete, easing: 'linear' }) } } + +export function listMorph (container, newElements, { key, horizontal }) { + const oldElements = $(container).children().get() + let currentList = _.map(oldElements, (el) => ({ id: _.get(el, key), el })) + const newList = _.map(newElements, (el) => ({ id: _.get(el, key), el })) + const overlap = _.intersectionBy(newList, currentList, 'id') + + // remove old items + const removals = _.differenceBy(currentList, newList, 'id') + removals.forEach(({ el }) => { + if (horizontal) return el.remove() + const $el = $(el) + $el.addClass('shrink-out') + setTimeout(() => { slideUpRemove($el) }, 400) + }) + currentList = _.differenceBy(currentList, removals, 'id') + + // update kept items + currentList = currentList.map(({ el }, i) => ({ + id: overlap[i].id, + el: morph(el, overlap[i].el) + })) + + // add new items + const finalList = newList.map(({ id, el }) => _.get(_.find(currentList, { id }), 'el', el)).reverse() + finalList.forEach((el, i) => { + if (el.parentElement) return + if (horizontal) return container.insertBefore(el, _.get(finalList, `[${i - 1}]`)) + if (!_.get(finalList, `[${i - 1}]`)) return slideDownAppend($(container), el) + slideDownBefore($(_.get(finalList, `[${i - 1}]`)), el) + }) + + // update ages + updateAllAges() +} From 2e7e5d19321d2e47bfc00d3ed67cdc6d7deed4b7 Mon Sep 17 00:00:00 2001 From: jimmay5469 Date: Fri, 26 Oct 2018 11:23:21 -0400 Subject: [PATCH 16/42] Use listMorph on address page --- .../assets/js/pages/address.js | 210 ++++++++++-------- .../channels/address_channel.ex | 1 + .../address_transaction/index.html.eex | 12 +- 3 files changed, 122 insertions(+), 101 deletions(-) diff --git a/apps/block_scout_web/assets/js/pages/address.js b/apps/block_scout_web/assets/js/pages/address.js index c3ed5a9ba7..63726d5ad1 100644 --- a/apps/block_scout_web/assets/js/pages/address.js +++ b/apps/block_scout_web/assets/js/pages/address.js @@ -4,27 +4,30 @@ import URI from 'urijs' import humps from 'humps' import numeral from 'numeral' import socket from '../socket' -import { batchChannel, initRedux, slideDownPrepend, slideUpRemove } from '../utils' -import { updateAllAges } from '../lib/from_now' +import { batchChannel, initRedux, listMorph } from '../utils' import { updateAllCalculatedUsdValues } from '../lib/currency.js' import { loadTokenBalanceDropdown } from '../lib/token_balance_dropdown' const BATCH_THRESHOLD = 10 +const TRANSACTION_VALIDATED_MOVE_DELAY = 1000 export const initialState = { - addressHash: null, - balance: null, - batchCountAccumulator: 0, - beyondPageOne: null, channelDisconnected: false, + + addressHash: null, filter: null, - newBlock: null, - newInternalTransactions: [], - newPendingTransactions: [], - newTransactions: [], - pendingTransactionHashes: [], + + balance: null, transactionCount: null, - validationCount: null + validationCount: null, + + pendingTransactions: [], + transactions: [], + internalTransactions: [], + internalTransactionsBatch: [], + validatedBlocks: [], + + beyondPageOne: null } export function reducer (state = initialState, action) { @@ -32,11 +35,18 @@ export function reducer (state = initialState, action) { case 'PAGE_LOAD': { return Object.assign({}, state, { addressHash: action.addressHash, - beyondPageOne: action.beyondPageOne, filter: action.filter, - pendingTransactionHashes: action.pendingTransactionHashes, + + balance: action.balance, transactionCount: numeral(action.transactionCount).value(), - validationCount: action.validationCount ? numeral(action.validationCount).value() : null + validationCount: action.validationCount ? numeral(action.validationCount).value() : null, + + pendingTransactions: action.pendingTransactions, + transactions: action.transactions, + internalTransactions: action.internalTransactions, + validatedBlocks: action.validatedBlocks, + + beyondPageOne: action.beyondPageOne }) } case 'CHANNEL_DISCONNECTED': { @@ -44,7 +54,7 @@ export function reducer (state = initialState, action) { return Object.assign({}, state, { channelDisconnected: true, - batchCountAccumulator: 0 + internalTransactionsBatch: [] }) } case 'RECEIVED_NEW_BLOCK': { @@ -54,7 +64,10 @@ export function reducer (state = initialState, action) { if (state.beyondPageOne) return Object.assign({}, state, { validationCount }) return Object.assign({}, state, { - newBlock: action.msg.blockHtml, + validatedBlocks: [ + action.msg, + ...state.validatedBlocks + ], validationCount }) } @@ -68,16 +81,19 @@ export function reducer (state = initialState, action) { (state.filter === 'from' && fromAddressHash === state.addressHash) )) - if (!state.batchCountAccumulator && incomingInternalTransactions.length < BATCH_THRESHOLD) { + if (!state.internalTransactionsBatch.length && incomingInternalTransactions.length < BATCH_THRESHOLD) { return Object.assign({}, state, { - newInternalTransactions: [ - ...state.newInternalTransactions, - ..._.map(incomingInternalTransactions, 'internalTransactionHtml') + internalTransactions: [ + ...incomingInternalTransactions.reverse(), + ...state.internalTransactions ] }) } else { return Object.assign({}, state, { - batchCountAccumulator: state.batchCountAccumulator + incomingInternalTransactions.length + internalTransactionsBatch: [ + ...incomingInternalTransactions.reverse(), + ...state.internalTransactionsBatch + ] }) } } @@ -90,16 +106,19 @@ export function reducer (state = initialState, action) { } return Object.assign({}, state, { - newPendingTransactions: [ - ...state.newPendingTransactions, - action.msg.transactionHtml - ], - pendingTransactionHashes: [ - ...state.pendingTransactionHashes, - action.msg.transactionHash + pendingTransactions: [ + action.msg, + ...state.pendingTransactions ] }) } + case 'REMOVE_PENDING_TRANSACTION': { + if (state.channelDisconnected) return state + + return Object.assign({}, state, { + pendingTransactions: state.pendingTransactions.filter((transaction) => action.msg.transactionHash !== transaction.transactionHash) + }) + } case 'RECEIVED_NEW_TRANSACTION': { if (state.channelDisconnected) return state @@ -111,15 +130,12 @@ export function reducer (state = initialState, action) { return Object.assign({}, state, { transactionCount }) } - const updatedPendingTransactionHashes = - _.without(state.pendingTransactionHashes, action.msg.transactionHash) - return Object.assign({}, state, { - newTransactions: [ - ...state.newTransactions, - action.msg + pendingTransactions: state.pendingTransactions.map((transaction) => action.msg.transactionHash === transaction.transactionHash ? Object.assign({}, action.msg, { validated: true }) : transaction), + transactions: [ + action.msg, + ...state.transactions ], - pendingTransactionHashes: updatedPendingTransactionHashes, transactionCount: transactionCount }) } @@ -142,13 +158,32 @@ if ($addressDetailsPage.length) { const { filter, blockNumber } = humps.camelizeKeys(URI(window.location).query(true)) store.dispatch({ type: 'PAGE_LOAD', + addressHash, - beyondPageOne: !!blockNumber, filter, - pendingTransactionHashes: $('[data-selector="pending-transactions-list"]').children() - .map((index, el) => el.dataset.transactionHash).toArray(), + + balance: $('[data-selector="balance-card"]').html(), transactionCount: $('[data-selector="transaction-count"]').text(), - validationCount: $('[data-selector="validation-count"]') ? $('[data-selector="validation-count"]').text() : null + validationCount: $('[data-selector="validation-count"]') ? $('[data-selector="validation-count"]').text() : null, + + pendingTransactions: $('[data-selector="pending-transactions-list"]').children().map((index, el) => ({ + transactionHash: el.dataset.transactionHash, + transactionHtml: el.outerHTML + })).toArray(), + transactions: $('[data-selector="transactions-list"]').children().map((index, el) => ({ + transactionHash: el.dataset.transactionHash, + transactionHtml: el.outerHTML + })).toArray(), + internalTransactions: $('[data-selector="internal-transactions-list"]').children().map((index, el) => ({ + internalTransactionId: el.dataset.internalTransactionId, + internalTransactionHtml: el.outerHTML + })).toArray(), + validatedBlocks: $('[data-selector="validations-list"]').children().map((index, el) => ({ + blockNumber: parseInt(el.dataset.blockNumber), + blockHtml: el.outerHTML + })).toArray(), + + beyondPageOne: !!blockNumber }) addressChannel.join() addressChannel.onError(() => store.dispatch({ type: 'CHANNEL_DISCONNECTED' })) @@ -159,77 +194,60 @@ if ($addressDetailsPage.length) { store.dispatch({ type: 'RECEIVED_NEW_INTERNAL_TRANSACTION_BATCH', msgs: humps.camelizeKeys(msgs) }) )) addressChannel.on('pending_transaction', (msg) => store.dispatch({ type: 'RECEIVED_NEW_PENDING_TRANSACTION', msg: humps.camelizeKeys(msg) })) - addressChannel.on('transaction', (msg) => store.dispatch({ type: 'RECEIVED_NEW_TRANSACTION', msg: humps.camelizeKeys(msg) })) + addressChannel.on('transaction', (msg) => { + store.dispatch({ type: 'RECEIVED_NEW_TRANSACTION', msg: humps.camelizeKeys(msg) }) + setTimeout(() => store.dispatch({ type: 'REMOVE_PENDING_TRANSACTION', msg: humps.camelizeKeys(msg) }), TRANSACTION_VALIDATED_MOVE_DELAY) + }) const blocksChannel = socket.channel(`blocks:${addressHash}`, {}) blocksChannel.join() blocksChannel.onError(() => store.dispatch({ type: 'CHANNEL_DISCONNECTED' })) blocksChannel.on('new_block', (msg) => store.dispatch({ type: 'RECEIVED_NEW_BLOCK', msg: humps.camelizeKeys(msg) })) }, render (state, oldState) { - const $balance = $('[data-selector="balance-card"]') - const $channelBatching = $('[data-selector="channel-batching-message"]') - const $channelBatchingCount = $('[data-selector="channel-batching-count"]') - const $channelDisconnected = $('[data-selector="channel-disconnected-message"]') - const $emptyInternalTransactionsList = $('[data-selector="empty-internal-transactions-list"]') - const $emptyTransactionsList = $('[data-selector="empty-transactions-list"]') - const $internalTransactionsList = $('[data-selector="internal-transactions-list"]') - const $pendingTransactionsCount = $('[data-selector="pending-transactions-count"]') - const $pendingTransactionsList = $('[data-selector="pending-transactions-list"]') - const $transactionCount = $('[data-selector="transaction-count"]') - const $transactionsList = $('[data-selector="transactions-list"]') - const $validationCount = $('[data-selector="validation-count"]') - const $validationsList = $('[data-selector="validations-list"]') - - if ($emptyInternalTransactionsList.length && state.newInternalTransactions.length) window.location.reload() - if ($emptyTransactionsList.length && state.newTransactions.length) window.location.reload() - if (state.channelDisconnected) $channelDisconnected.show() + if (state.channelDisconnected) $('[data-selector="channel-disconnected-message"]').show() + if (oldState.balance !== state.balance) { - $balance.empty().append(state.balance) + $('[data-selector="balance-card"]').empty().append(state.balance) loadTokenBalanceDropdown() updateAllCalculatedUsdValues() } - if (oldState.transactionCount !== state.transactionCount) $transactionCount.empty().append(numeral(state.transactionCount).format()) - if (oldState.validationCount !== state.validationCount) $validationCount.empty().append(numeral(state.validationCount).format()) - if (state.batchCountAccumulator) { - $channelBatching.show() - $channelBatchingCount[0].innerHTML = numeral(state.batchCountAccumulator).format() - } else { - $channelBatching.hide() + if (oldState.transactionCount !== state.transactionCount) $('[data-selector="transaction-count"]').empty().append(numeral(state.transactionCount).format()) + if (oldState.validationCount !== state.validationCount) $('[data-selector="validation-count"]').empty().append(numeral(state.validationCount).format()) + + if (oldState.pendingTransactions !== state.pendingTransactions) { + const container = $('[data-selector="pending-transactions-list"]')[0] + const newElements = _.map(state.pendingTransactions, ({ transactionHtml }) => $(transactionHtml)[0]) + listMorph(container, newElements, { key: 'dataset.transactionHash' }) + if($('[data-selector="pending-transactions-count"]').length) $('[data-selector="pending-transactions-count"]')[0].innerHTML = numeral(state.pendingTransactions.filter(({ validated }) => !validated).length).format() } - if (oldState.newInternalTransactions !== state.newInternalTransactions && $internalTransactionsList.length) { - slideDownPrepend($internalTransactionsList, state.newInternalTransactions.slice(oldState.newInternalTransactions.length).reverse().join('')) - updateAllAges() + function updateTransactions () { + const container = $('[data-selector="transactions-list"]')[0] + const newElements = _.map(state.transactions, ({ transactionHtml }) => $(transactionHtml)[0]) + listMorph(container, newElements, { key: 'dataset.transactionHash' }) } - if (oldState.pendingTransactionHashes.length !== state.pendingTransactionHashes.length && $pendingTransactionsCount.length) { - $pendingTransactionsCount[0].innerHTML = numeral(state.pendingTransactionHashes.length).format() + if (oldState.transactions !== state.transactions) { + if ($('[data-selector="pending-transactions-list"]').is(':visible')) { + setTimeout(updateTransactions, TRANSACTION_VALIDATED_MOVE_DELAY + 400) + } else { + updateTransactions() + } } - if (oldState.newPendingTransactions !== state.newPendingTransactions && $pendingTransactionsList.length) { - slideDownPrepend($pendingTransactionsList, state.newPendingTransactions.slice(oldState.newPendingTransactions.length).reverse().join('')) - updateAllAges() + if (oldState.internalTransactions !== state.internalTransactions) { + const container = $('[data-selector="internal-transactions-list"]')[0] + const newElements = _.map(state.internalTransactions, ({ internalTransactionHtml }) => $(internalTransactionHtml)[0]) + listMorph(container, newElements, { key: 'dataset.internalTransactionId' }) } - if (oldState.newTransactions !== state.newTransactions && $transactionsList.length) { - const newlyValidatedTransactions = state.newTransactions.slice(oldState.newTransactions.length).reverse() - newlyValidatedTransactions.forEach(({ transactionHash, transactionHtml }) => { - let $transaction = $(`[data-selector="pending-transactions-list"] [data-transaction-hash="${transactionHash}"]`) - $transaction.html($(transactionHtml).html()) - if ($transaction.is(':visible')) { - setTimeout(() => { - $transaction.addClass('shrink-out') - setTimeout(() => { - slideUpRemove($transaction) - slideDownPrepend($transactionsList, transactionHtml) - }, 400) - }, 1000) - } else { - $transaction.remove() - slideDownPrepend($transactionsList, transactionHtml) - } - }) - updateAllAges() + const $channelBatching = $('[data-selector="channel-batching-message"]') + if (state.internalTransactionsBatch.length) { + $channelBatching.show() + $('[data-selector="channel-batching-count"]')[0].innerHTML = numeral(state.internalTransactionsBatch.length).format() + } else { + $channelBatching.hide() } - if (oldState.newBlock !== state.newBlock) { - slideDownPrepend($validationsList, state.newBlock) - updateAllAges() + if (oldState.validatedBlocks !== state.validatedBlocks) { + const container = $('[data-selector="validations-list"]')[0] + const newElements = _.map(state.validatedBlocks, ({ blockHtml }) => $(blockHtml)[0]) + listMorph(container, newElements, { key: 'dataset.blockNumber' }) } } }) diff --git a/apps/block_scout_web/lib/block_scout_web/channels/address_channel.ex b/apps/block_scout_web/lib/block_scout_web/channels/address_channel.ex index 6213b6aaf5..383dcf1897 100644 --- a/apps/block_scout_web/lib/block_scout_web/channels/address_channel.ex +++ b/apps/block_scout_web/lib/block_scout_web/channels/address_channel.ex @@ -55,6 +55,7 @@ defmodule BlockScoutWeb.AddressChannel do push(socket, "internal_transaction", %{ to_address_hash: to_string(internal_transaction.to_address_hash), from_address_hash: to_string(internal_transaction.from_address_hash), + internal_transaction_id: to_string(internal_transaction.id), internal_transaction_html: rendered_internal_transaction }) diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_transaction/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_transaction/index.html.eex index 9dda749f2c..ac1c69eee6 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address_transaction/index.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_transaction/index.html.eex @@ -52,17 +52,19 @@

<%= gettext "Transactions" %>

- <%= link to: "#pending-transactions", class: "d-inline-block mb-3", "data-toggle": "collapse" do %> + <%= link to: "#pending-transactions-container", class: "d-inline-block mb-3", "data-toggle": "collapse" do %> <%= gettext("Show") %> <%= gettext("Hide") %> <%= length(@pending_transactions) %> <%= gettext("Pending Transactions") %> <% end %> -
- <%= for pending_transaction <- @pending_transactions do %> - <%= render(BlockScoutWeb.TransactionView, "_tile.html", current_address: @address, transaction: pending_transaction) %> - <% end %> +
+
+ <%= for pending_transaction <- @pending_transactions do %> + <%= render(BlockScoutWeb.TransactionView, "_tile.html", current_address: @address, transaction: pending_transaction) %> + <% end %> +

From 4a682a7134803924e90d02fbe111e7ab27c09b17 Mon Sep 17 00:00:00 2001 From: jimmay5469 Date: Fri, 26 Oct 2018 12:15:02 -0400 Subject: [PATCH 17/42] Infinite scroll spike --- .../assets/js/pages/address.js | 24 +++++++- apps/block_scout_web/assets/js/utils.js | 11 ++++ .../address_transaction_controller.ex | 60 +++++++++++++++++-- .../address_transaction/index.html.eex | 3 +- 4 files changed, 90 insertions(+), 8 deletions(-) diff --git a/apps/block_scout_web/assets/js/pages/address.js b/apps/block_scout_web/assets/js/pages/address.js index 63726d5ad1..bed4421aa4 100644 --- a/apps/block_scout_web/assets/js/pages/address.js +++ b/apps/block_scout_web/assets/js/pages/address.js @@ -4,7 +4,7 @@ import URI from 'urijs' import humps from 'humps' import numeral from 'numeral' import socket from '../socket' -import { batchChannel, initRedux, listMorph } from '../utils' +import { batchChannel, initRedux, listMorph, atBottom } from '../utils' import { updateAllCalculatedUsdValues } from '../lib/currency.js' import { loadTokenBalanceDropdown } from '../lib/token_balance_dropdown' @@ -27,6 +27,8 @@ export const initialState = { internalTransactionsBatch: [], validatedBlocks: [], + nextPage: null, + beyondPageOne: null } @@ -46,6 +48,8 @@ export function reducer (state = initialState, action) { internalTransactions: action.internalTransactions, validatedBlocks: action.validatedBlocks, + nextPage: action.nextPage, + beyondPageOne: action.beyondPageOne }) } @@ -144,6 +148,15 @@ export function reducer (state = initialState, action) { balance: action.msg.balance }) } + case 'NEXT_TRANSACTIONS_PAGE': { + return Object.assign({}, state, { + nextPage: action.msg.nextPage, + transactions: [ + ...state.transactions, + ...action.msg.transactions + ] + }) + } default: return state } @@ -183,6 +196,8 @@ if ($addressDetailsPage.length) { blockHtml: el.outerHTML })).toArray(), + nextPage: $('[data-selector="next-page-button"]').length ? `${$('[data-selector="next-page-button"]').hide().attr("href")}&type=JSON` : null, + beyondPageOne: !!blockNumber }) addressChannel.join() @@ -202,6 +217,13 @@ if ($addressDetailsPage.length) { blocksChannel.join() blocksChannel.onError(() => store.dispatch({ type: 'CHANNEL_DISCONNECTED' })) blocksChannel.on('new_block', (msg) => store.dispatch({ type: 'RECEIVED_NEW_BLOCK', msg: humps.camelizeKeys(msg) })) + + $('[data-selector="transactions-list"]').length && atBottom(function loadMoreTransactions() { + $.get(store.getState().nextPage).done(msg => { + store.dispatch({ type: 'NEXT_TRANSACTIONS_PAGE', msg: humps.camelizeKeys(msg) }) + setTimeout(() => atBottom(loadMoreTransactions), 1000) + }) + }) }, render (state, oldState) { if (state.channelDisconnected) $('[data-selector="channel-disconnected-message"]').show() diff --git a/apps/block_scout_web/assets/js/utils.js b/apps/block_scout_web/assets/js/utils.js index 74f80f3f30..afb70b6740 100644 --- a/apps/block_scout_web/assets/js/utils.js +++ b/apps/block_scout_web/assets/js/utils.js @@ -149,3 +149,14 @@ export function listMorph (container, newElements, { key, horizontal }) { // update ages updateAllAges() } + +export function atBottom(callback) { + $(window).on("scroll", function infiniteScrollChecker () { + var scrollHeight = $(document).height(); + var scrollPosition = $(window).height() + $(window).scrollTop(); + if ((scrollHeight - scrollPosition) / scrollHeight === 0) { + $(window).off("scroll", infiniteScrollChecker) + callback() + } + }); +} diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex index 3a797d2974..06eff87dd9 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex @@ -10,6 +10,59 @@ defmodule BlockScoutWeb.AddressTransactionController do alias Explorer.{Chain, Market} alias Explorer.ExchangeRates.Token + alias Explorer.Chain.Hash + alias BlockScoutWeb.{InternalTransactionView, AddressView, TransactionView} + alias Phoenix.View + + def index(conn, %{"address_id" => address_hash_string, "type" => "JSON"} = params) do + with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), + {:ok, address} <- Chain.hash_to_address(address_hash) do + full_options = + [ + necessity_by_association: %{ + :block => :required, + [created_contract_address: :names] => :optional, + [from_address: :names] => :optional, + [to_address: :names] => :optional, + :token_transfers => :optional + } + ] + |> Keyword.merge(paging_options(params)) + |> Keyword.merge(current_filter(params)) + + transactions_plus_one = Chain.address_to_transactions(address, full_options) + {transactions, next_page} = split_list_by_page(transactions_plus_one) + + json( + conn, + %{ + transactions: Enum.map(transactions, fn transaction -> + %{ + transaction_hash: Hash.to_string(transaction.hash), + transaction_html: View.render_to_string( + TransactionView, + "_tile.html", + current_address: address, + transaction: transaction + ) + } + end), + next_page: address_transaction_path( + conn, + :index, + address, + next_page_params(next_page, transactions, params) + ) + } + ) + else + :error -> + unprocessable_entity(conn) + + {:error, :not_found} -> + not_found(conn) + end + end def index(conn, %{"address_id" => address_hash_string} = params) do with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), @@ -23,7 +76,6 @@ defmodule BlockScoutWeb.AddressTransactionController do :token_transfers => :optional } ] - |> Keyword.merge(paging_options(params)) |> Keyword.merge(current_filter(params)) full_options = put_in(pending_options, [:necessity_by_association, :block], :required) @@ -31,11 +83,7 @@ defmodule BlockScoutWeb.AddressTransactionController do transactions_plus_one = Chain.address_to_transactions(address, full_options) {transactions, next_page} = split_list_by_page(transactions_plus_one) - pending_transactions = - case Map.has_key?(params, "block_number") do - true -> [] - false -> Chain.address_to_pending_transactions(address, pending_options) - end + pending_transactions = Chain.address_to_pending_transactions(address, pending_options) render( conn, diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_transaction/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_transaction/index.html.eex index ac1c69eee6..1708e41bb3 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address_transaction/index.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_transaction/index.html.eex @@ -9,7 +9,7 @@
-
+
@@ -84,6 +84,7 @@ <%= link( gettext("Older"), class: "button button-secondary button-sm float-right mt-3", + "data-selector": "next-page-button", to: address_transaction_path( @conn, :index, From 95ef4b9718eae89ac7528810740661cc75e560ee Mon Sep 17 00:00:00 2001 From: jimmay5469 Date: Mon, 29 Oct 2018 09:57:47 -0400 Subject: [PATCH 18/42] Optimize listMorph and infinite scroll --- apps/block_scout_web/assets/js/lib/from_now.js | 5 +++-- apps/block_scout_web/assets/js/utils.js | 14 +++++++------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/apps/block_scout_web/assets/js/lib/from_now.js b/apps/block_scout_web/assets/js/lib/from_now.js index ce00071d21..d0f1d57f63 100644 --- a/apps/block_scout_web/assets/js/lib/from_now.js +++ b/apps/block_scout_web/assets/js/lib/from_now.js @@ -8,8 +8,9 @@ moment.relativeTimeThreshold('m', 60) moment.relativeTimeThreshold('s', 60) moment.relativeTimeThreshold('ss', 1) -export function updateAllAges () { - $('[data-from-now]').each((i, el) => tryUpdateAge(el)) +export function updateAllAges ($container = $(document)) { + $container.find('[data-from-now]').each((i, el) => tryUpdateAge(el)) + return $container } function tryUpdateAge (el) { if (!el.dataset.fromNow) return diff --git a/apps/block_scout_web/assets/js/utils.js b/apps/block_scout_web/assets/js/utils.js index afb70b6740..b7996827bc 100644 --- a/apps/block_scout_web/assets/js/utils.js +++ b/apps/block_scout_web/assets/js/utils.js @@ -119,12 +119,13 @@ export function listMorph (container, newElements, { key, horizontal }) { const oldElements = $(container).children().get() let currentList = _.map(oldElements, (el) => ({ id: _.get(el, key), el })) const newList = _.map(newElements, (el) => ({ id: _.get(el, key), el })) - const overlap = _.intersectionBy(newList, currentList, 'id') + const overlap = _.intersectionBy(newList, currentList, 'id').map(({ id, el }) => ({ id, el: updateAllAges($(el))[0] })) // remove old items const removals = _.differenceBy(currentList, newList, 'id') + let canAnimate = !horizontal && removals.length <= 1 removals.forEach(({ el }) => { - if (horizontal) return el.remove() + if (!canAnimate) return el.remove() const $el = $(el) $el.addClass('shrink-out') setTimeout(() => { slideUpRemove($el) }, 400) @@ -134,20 +135,19 @@ export function listMorph (container, newElements, { key, horizontal }) { // update kept items currentList = currentList.map(({ el }, i) => ({ id: overlap[i].id, - el: morph(el, overlap[i].el) + el: el.outerHTML === overlap[i].el.outerHTML ? el : morph(el, overlap[i].el) })) // add new items const finalList = newList.map(({ id, el }) => _.get(_.find(currentList, { id }), 'el', el)).reverse() + canAnimate = !horizontal finalList.forEach((el, i) => { if (el.parentElement) return - if (horizontal) return container.insertBefore(el, _.get(finalList, `[${i - 1}]`)) + if (!canAnimate) return container.insertBefore(el, _.get(finalList, `[${i - 1}]`)) + canAnimate = false if (!_.get(finalList, `[${i - 1}]`)) return slideDownAppend($(container), el) slideDownBefore($(_.get(finalList, `[${i - 1}]`)), el) }) - - // update ages - updateAllAges() } export function atBottom(callback) { From d844185c18b938a7047d798aa72aba643621d380 Mon Sep 17 00:00:00 2001 From: jimmay5469 Date: Mon, 29 Oct 2018 14:59:56 -0400 Subject: [PATCH 19/42] Cleanup --- .../assets/js/pages/address.js | 29 +++++++++--- apps/block_scout_web/assets/js/utils.js | 14 +++--- .../address_transaction_controller.ex | 45 ++++++++++++------- .../address_transaction/index.html.eex | 3 ++ 4 files changed, 61 insertions(+), 30 deletions(-) diff --git a/apps/block_scout_web/assets/js/pages/address.js b/apps/block_scout_web/assets/js/pages/address.js index bed4421aa4..12bcd3a33d 100644 --- a/apps/block_scout_web/assets/js/pages/address.js +++ b/apps/block_scout_web/assets/js/pages/address.js @@ -27,6 +27,7 @@ export const initialState = { internalTransactionsBatch: [], validatedBlocks: [], + loadingNextPage: false, nextPage: null, beyondPageOne: null @@ -148,8 +149,14 @@ export function reducer (state = initialState, action) { balance: action.msg.balance }) } + case 'LOADING_NEXT_PAGE': { + return Object.assign({}, state, { + loadingNextPage: true + }) + } case 'NEXT_TRANSACTIONS_PAGE': { return Object.assign({}, state, { + loadingNextPage: false, nextPage: action.msg.nextPage, transactions: [ ...state.transactions, @@ -196,7 +203,7 @@ if ($addressDetailsPage.length) { blockHtml: el.outerHTML })).toArray(), - nextPage: $('[data-selector="next-page-button"]').length ? `${$('[data-selector="next-page-button"]').hide().attr("href")}&type=JSON` : null, + nextPage: $('[data-selector="next-page-button"]').length ? `${$('[data-selector="next-page-button"]').hide().attr('href')}&type=JSON` : null, beyondPageOne: !!blockNumber }) @@ -218,15 +225,23 @@ if ($addressDetailsPage.length) { blocksChannel.onError(() => store.dispatch({ type: 'CHANNEL_DISCONNECTED' })) blocksChannel.on('new_block', (msg) => store.dispatch({ type: 'RECEIVED_NEW_BLOCK', msg: humps.camelizeKeys(msg) })) - $('[data-selector="transactions-list"]').length && atBottom(function loadMoreTransactions() { - $.get(store.getState().nextPage).done(msg => { - store.dispatch({ type: 'NEXT_TRANSACTIONS_PAGE', msg: humps.camelizeKeys(msg) }) - setTimeout(() => atBottom(loadMoreTransactions), 1000) - }) + $('[data-selector="transactions-list"]').length && atBottom(function loadMoreTransactions () { + const nextPage = store.getState().nextPage + if (nextPage) { + store.dispatch({ type: 'LOADING_NEXT_PAGE' }) + $.get(nextPage).done(msg => { + store.dispatch({ type: 'NEXT_TRANSACTIONS_PAGE', msg: humps.camelizeKeys(msg) }) + }) + } }) }, render (state, oldState) { if (state.channelDisconnected) $('[data-selector="channel-disconnected-message"]').show() + if (state.loadingNextPage) { + $('[data-selector="loading-next-page"]').show() + } else { + $('[data-selector="loading-next-page"]').hide() + } if (oldState.balance !== state.balance) { $('[data-selector="balance-card"]').empty().append(state.balance) @@ -240,7 +255,7 @@ if ($addressDetailsPage.length) { const container = $('[data-selector="pending-transactions-list"]')[0] const newElements = _.map(state.pendingTransactions, ({ transactionHtml }) => $(transactionHtml)[0]) listMorph(container, newElements, { key: 'dataset.transactionHash' }) - if($('[data-selector="pending-transactions-count"]').length) $('[data-selector="pending-transactions-count"]')[0].innerHTML = numeral(state.pendingTransactions.filter(({ validated }) => !validated).length).format() + if ($('[data-selector="pending-transactions-count"]').length) $('[data-selector="pending-transactions-count"]')[0].innerHTML = numeral(state.pendingTransactions.filter(({ validated }) => !validated).length).format() } function updateTransactions () { const container = $('[data-selector="transactions-list"]')[0] diff --git a/apps/block_scout_web/assets/js/utils.js b/apps/block_scout_web/assets/js/utils.js index b7996827bc..53bd77351f 100644 --- a/apps/block_scout_web/assets/js/utils.js +++ b/apps/block_scout_web/assets/js/utils.js @@ -116,6 +116,7 @@ function smarterSlideUp ($el, { complete = _.noop } = {}) { } export function listMorph (container, newElements, { key, horizontal }) { + if (!container) return const oldElements = $(container).children().get() let currentList = _.map(oldElements, (el) => ({ id: _.get(el, key), el })) const newList = _.map(newElements, (el) => ({ id: _.get(el, key), el })) @@ -150,13 +151,14 @@ export function listMorph (container, newElements, { key, horizontal }) { }) } -export function atBottom(callback) { - $(window).on("scroll", function infiniteScrollChecker () { - var scrollHeight = $(document).height(); - var scrollPosition = $(window).height() + $(window).scrollTop(); +export function atBottom (callback) { + function infiniteScrollChecker () { + var scrollHeight = $(document).height() + var scrollPosition = $(window).height() + $(window).scrollTop() if ((scrollHeight - scrollPosition) / scrollHeight === 0) { - $(window).off("scroll", infiniteScrollChecker) callback() } - }); + } + infiniteScrollChecker() + $(window).on('scroll', infiniteScrollChecker) } diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex index 06eff87dd9..040d388985 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex @@ -33,26 +33,37 @@ defmodule BlockScoutWeb.AddressTransactionController do transactions_plus_one = Chain.address_to_transactions(address, full_options) {transactions, next_page} = split_list_by_page(transactions_plus_one) + next_page = + case next_page_params(next_page, transactions, params) do + nil -> + nil + + next_page_params -> + address_transaction_path( + conn, + :index, + address, + next_page_params + ) + end + json( conn, %{ - transactions: Enum.map(transactions, fn transaction -> - %{ - transaction_hash: Hash.to_string(transaction.hash), - transaction_html: View.render_to_string( - TransactionView, - "_tile.html", - current_address: address, - transaction: transaction - ) - } - end), - next_page: address_transaction_path( - conn, - :index, - address, - next_page_params(next_page, transactions, params) - ) + transactions: + Enum.map(transactions, fn transaction -> + %{ + transaction_hash: Hash.to_string(transaction.hash), + transaction_html: + View.render_to_string( + TransactionView, + "_tile.html", + current_address: address, + transaction: transaction + ) + } + end), + next_page: next_page } ) else diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_transaction/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_transaction/index.html.eex index 1708e41bb3..db56f3ad01 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address_transaction/index.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_transaction/index.html.eex @@ -74,6 +74,9 @@ <%= render(BlockScoutWeb.TransactionView, "_tile.html", current_address: @address, transaction: transaction) %> <% end %> +
+ <%= gettext("Loading") %>... +
<% else %>
<%= gettext "There are no transactions for this address." %> From 59da0d2216abdcfab439653f7fb3be6160a6197e Mon Sep 17 00:00:00 2001 From: jimmay5469 Date: Mon, 29 Oct 2018 17:35:19 -0400 Subject: [PATCH 20/42] Restructure --- .../assets/js/pages/address.js | 313 +++++++++++------- apps/block_scout_web/assets/js/utils.js | 35 +- 2 files changed, 224 insertions(+), 124 deletions(-) diff --git a/apps/block_scout_web/assets/js/pages/address.js b/apps/block_scout_web/assets/js/pages/address.js index 12bcd3a33d..fdbf5c2867 100644 --- a/apps/block_scout_web/assets/js/pages/address.js +++ b/apps/block_scout_web/assets/js/pages/address.js @@ -4,7 +4,7 @@ import URI from 'urijs' import humps from 'humps' import numeral from 'numeral' import socket from '../socket' -import { batchChannel, initRedux, listMorph, atBottom } from '../utils' +import { createStore, connectElements, batchChannel, listMorph, atBottom } from '../utils' import { updateAllCalculatedUsdValues } from '../lib/currency.js' import { loadTokenBalanceDropdown } from '../lib/token_balance_dropdown' @@ -35,24 +35,9 @@ export const initialState = { export function reducer (state = initialState, action) { switch (action.type) { - case 'PAGE_LOAD': { - return Object.assign({}, state, { - addressHash: action.addressHash, - filter: action.filter, - - balance: action.balance, - transactionCount: numeral(action.transactionCount).value(), - validationCount: action.validationCount ? numeral(action.validationCount).value() : null, - - pendingTransactions: action.pendingTransactions, - transactions: action.transactions, - internalTransactions: action.internalTransactions, - validatedBlocks: action.validatedBlocks, - - nextPage: action.nextPage, - - beyondPageOne: action.beyondPageOne - }) + case 'PAGE_LOAD': + case 'ELEMENTS_LOAD': { + return Object.assign({}, state, _.omit(action, 'type')) } case 'CHANNEL_DISCONNECTED': { if (state.beyondPageOne) return state @@ -154,7 +139,7 @@ export function reducer (state = initialState, action) { loadingNextPage: true }) } - case 'NEXT_TRANSACTIONS_PAGE': { + case 'RECEIVED_NEXT_TRANSACTIONS_PAGE': { return Object.assign({}, state, { loadingNextPage: false, nextPage: action.msg.nextPage, @@ -169,123 +154,207 @@ export function reducer (state = initialState, action) { } } -const $addressDetailsPage = $('[data-page="address-details"]') -if ($addressDetailsPage.length) { - initRedux(reducer, { - main (store) { - const addressHash = $addressDetailsPage[0].dataset.pageAddressHash - const addressChannel = socket.channel(`addresses:${addressHash}`, {}) - const { filter, blockNumber } = humps.camelizeKeys(URI(window.location).query(true)) - store.dispatch({ - type: 'PAGE_LOAD', - - addressHash, - filter, - - balance: $('[data-selector="balance-card"]').html(), - transactionCount: $('[data-selector="transaction-count"]').text(), - validationCount: $('[data-selector="validation-count"]') ? $('[data-selector="validation-count"]').text() : null, - - pendingTransactions: $('[data-selector="pending-transactions-list"]').children().map((index, el) => ({ - transactionHash: el.dataset.transactionHash, - transactionHtml: el.outerHTML - })).toArray(), - transactions: $('[data-selector="transactions-list"]').children().map((index, el) => ({ - transactionHash: el.dataset.transactionHash, - transactionHtml: el.outerHTML - })).toArray(), - internalTransactions: $('[data-selector="internal-transactions-list"]').children().map((index, el) => ({ - internalTransactionId: el.dataset.internalTransactionId, - internalTransactionHtml: el.outerHTML - })).toArray(), - validatedBlocks: $('[data-selector="validations-list"]').children().map((index, el) => ({ - blockNumber: parseInt(el.dataset.blockNumber), - blockHtml: el.outerHTML - })).toArray(), - - nextPage: $('[data-selector="next-page-button"]').length ? `${$('[data-selector="next-page-button"]').hide().attr('href')}&type=JSON` : null, - - beyondPageOne: !!blockNumber - }) - addressChannel.join() - addressChannel.onError(() => store.dispatch({ type: 'CHANNEL_DISCONNECTED' })) - addressChannel.on('balance', (msg) => { - store.dispatch({ type: 'RECEIVED_UPDATED_BALANCE', msg: humps.camelizeKeys(msg) }) - }) - addressChannel.on('internal_transaction', batchChannel((msgs) => - store.dispatch({ type: 'RECEIVED_NEW_INTERNAL_TRANSACTION_BATCH', msgs: humps.camelizeKeys(msgs) }) - )) - addressChannel.on('pending_transaction', (msg) => store.dispatch({ type: 'RECEIVED_NEW_PENDING_TRANSACTION', msg: humps.camelizeKeys(msg) })) - addressChannel.on('transaction', (msg) => { - store.dispatch({ type: 'RECEIVED_NEW_TRANSACTION', msg: humps.camelizeKeys(msg) }) - setTimeout(() => store.dispatch({ type: 'REMOVE_PENDING_TRANSACTION', msg: humps.camelizeKeys(msg) }), TRANSACTION_VALIDATED_MOVE_DELAY) - }) - const blocksChannel = socket.channel(`blocks:${addressHash}`, {}) - blocksChannel.join() - blocksChannel.onError(() => store.dispatch({ type: 'CHANNEL_DISCONNECTED' })) - blocksChannel.on('new_block', (msg) => store.dispatch({ type: 'RECEIVED_NEW_BLOCK', msg: humps.camelizeKeys(msg) })) - - $('[data-selector="transactions-list"]').length && atBottom(function loadMoreTransactions () { - const nextPage = store.getState().nextPage - if (nextPage) { - store.dispatch({ type: 'LOADING_NEXT_PAGE' }) - $.get(nextPage).done(msg => { - store.dispatch({ type: 'NEXT_TRANSACTIONS_PAGE', msg: humps.camelizeKeys(msg) }) - }) - } - }) +const elements = { + '[data-selector="channel-disconnected-message"]': { + render ($el, state) { + if (state.channelDisconnected) $el.show() + } + }, + '[data-selector="balance-card"]': { + load ($el) { + return { balance: $el.html() } + }, + render ($el, state, oldState) { + if (oldState.balance === state.balance) return + $el.empty().append(state.balance) + loadTokenBalanceDropdown() + updateAllCalculatedUsdValues() + } + }, + '[data-selector="transaction-count"]': { + load ($el) { + return { transactionCount: numeral($el.text()).value() } + }, + render ($el, state, oldState) { + if (oldState.transactionCount === state.transactionCount) return + $el.empty().append(numeral(state.transactionCount).format()) + } + }, + '[data-selector="validation-count"]': { + load ($el) { + return { validationCount: numeral($el.text()).value } }, - render (state, oldState) { - if (state.channelDisconnected) $('[data-selector="channel-disconnected-message"]').show() + render ($el, state, oldState) { + if (oldState.validationCount === state.validationCount) return + $el.empty().append(numeral(state.validationCount).format()) + } + }, + '[data-selector="loading-next-page"]': { + render ($el, state) { if (state.loadingNextPage) { - $('[data-selector="loading-next-page"]').show() + $el.show() } else { - $('[data-selector="loading-next-page"]').hide() + $el.hide() } - - if (oldState.balance !== state.balance) { - $('[data-selector="balance-card"]').empty().append(state.balance) - loadTokenBalanceDropdown() - updateAllCalculatedUsdValues() + } + }, + '[data-selector="pending-transactions-list"]': { + load ($el) { + return { + pendingTransactions: $el.children().map((index, el) => ({ + transactionHash: el.dataset.transactionHash, + transactionHtml: el.outerHTML + })).toArray() } - if (oldState.transactionCount !== state.transactionCount) $('[data-selector="transaction-count"]').empty().append(numeral(state.transactionCount).format()) - if (oldState.validationCount !== state.validationCount) $('[data-selector="validation-count"]').empty().append(numeral(state.validationCount).format()) - - if (oldState.pendingTransactions !== state.pendingTransactions) { - const container = $('[data-selector="pending-transactions-list"]')[0] - const newElements = _.map(state.pendingTransactions, ({ transactionHtml }) => $(transactionHtml)[0]) - listMorph(container, newElements, { key: 'dataset.transactionHash' }) - if ($('[data-selector="pending-transactions-count"]').length) $('[data-selector="pending-transactions-count"]')[0].innerHTML = numeral(state.pendingTransactions.filter(({ validated }) => !validated).length).format() + }, + render ($el, state, oldState) { + if (oldState.pendingTransactions === state.pendingTransactions) return + const container = $el[0] + const newElements = _.map(state.pendingTransactions, ({ transactionHtml }) => $(transactionHtml)[0]) + listMorph(container, newElements, { key: 'dataset.transactionHash' }) + } + }, + '[data-selector="pending-transactions-count"]': { + render ($el, state, oldState) { + if (oldState.pendingTransactions === state.pendingTransactions) return + $el[0].innerHTML = numeral(state.pendingTransactions.filter(({ validated }) => !validated).length).format() + } + }, + '[data-selector="transactions-list"]': { + load ($el, store) { + return { + transactions: $el.children().map((index, el) => ({ + transactionHash: el.dataset.transactionHash, + transactionHtml: el.outerHTML + })).toArray() } + }, + render ($el, state, oldState) { + if (oldState.transactions === state.transactions) return function updateTransactions () { - const container = $('[data-selector="transactions-list"]')[0] + const container = $el[0] const newElements = _.map(state.transactions, ({ transactionHtml }) => $(transactionHtml)[0]) listMorph(container, newElements, { key: 'dataset.transactionHash' }) } - if (oldState.transactions !== state.transactions) { - if ($('[data-selector="pending-transactions-list"]').is(':visible')) { - setTimeout(updateTransactions, TRANSACTION_VALIDATED_MOVE_DELAY + 400) - } else { - updateTransactions() - } + if ($('[data-selector="pending-transactions-list"]').is(':visible')) { + setTimeout(updateTransactions, TRANSACTION_VALIDATED_MOVE_DELAY + 400) + } else { + updateTransactions() } - if (oldState.internalTransactions !== state.internalTransactions) { - const container = $('[data-selector="internal-transactions-list"]')[0] - const newElements = _.map(state.internalTransactions, ({ internalTransactionHtml }) => $(internalTransactionHtml)[0]) - listMorph(container, newElements, { key: 'dataset.internalTransactionId' }) + } + }, + '[data-selector="internal-transactions-list"]': { + load ($el) { + return { + internalTransactions: $el.children().map((index, el) => ({ + internalTransactionId: el.dataset.internalTransactionId, + internalTransactionHtml: el.outerHTML + })).toArray() } + }, + render ($el, state, oldState) { + if (oldState.internalTransactions === state.internalTransactions) return + const container = $el[0] + const newElements = _.map(state.internalTransactions, ({ internalTransactionHtml }) => $(internalTransactionHtml)[0]) + listMorph(container, newElements, { key: 'dataset.internalTransactionId' }) + } + }, + '[data-selector="channel-batching-count"]': { + render ($el, state, oldState) { const $channelBatching = $('[data-selector="channel-batching-message"]') - if (state.internalTransactionsBatch.length) { - $channelBatching.show() - $('[data-selector="channel-batching-count"]')[0].innerHTML = numeral(state.internalTransactionsBatch.length).format() - } else { - $channelBatching.hide() + if (!state.internalTransactionsBatch.length) return $channelBatching.hide() + $channelBatching.show() + $el[0].innerHTML = numeral(state.internalTransactionsBatch.length).format() + } + }, + '[data-selector="validations-list"]': { + load ($el) { + return { + validatedBlocks: $el.children().map((index, el) => ({ + blockNumber: parseInt(el.dataset.blockNumber), + blockHtml: el.outerHTML + })).toArray() } - if (oldState.validatedBlocks !== state.validatedBlocks) { - const container = $('[data-selector="validations-list"]')[0] - const newElements = _.map(state.validatedBlocks, ({ blockHtml }) => $(blockHtml)[0]) - listMorph(container, newElements, { key: 'dataset.blockNumber' }) + }, + render ($el, state, oldState) { + if (oldState.validatedBlocks === state.validatedBlocks) return + const container = $el[0] + const newElements = _.map(state.validatedBlocks, ({ blockHtml }) => $(blockHtml)[0]) + listMorph(container, newElements, { key: 'dataset.blockNumber' }) + } + }, + '[data-selector="next-page-button"]': { + load ($el) { + return { + nextPage: `${$el.hide().attr('href')}&type=JSON` } } + } +} + +const $addressDetailsPage = $('[data-page="address-details"]') +if ($addressDetailsPage.length) { + const store = createStore(reducer) + const addressHash = $addressDetailsPage[0].dataset.pageAddressHash + const { filter, blockNumber } = humps.camelizeKeys(URI(window.location).query(true)) + store.dispatch({ + type: 'PAGE_LOAD', + addressHash, + filter, + beyondPageOne: !!blockNumber + }) + connectElements({ store, elements }) + + const addressChannel = socket.channel(`addresses:${addressHash}`, {}) + addressChannel.join() + addressChannel.onError(() => store.dispatch({ + type: 'CHANNEL_DISCONNECTED' + })) + addressChannel.on('balance', (msg) => store.dispatch({ + type: 'RECEIVED_UPDATED_BALANCE', + msg: humps.camelizeKeys(msg) + })) + addressChannel.on('internal_transaction', batchChannel((msgs) => store.dispatch({ + type: 'RECEIVED_NEW_INTERNAL_TRANSACTION_BATCH', + msgs: humps.camelizeKeys(msgs) + }))) + addressChannel.on('pending_transaction', (msg) => store.dispatch({ + type: 'RECEIVED_NEW_PENDING_TRANSACTION', + msg: humps.camelizeKeys(msg) + })) + addressChannel.on('transaction', (msg) => { + store.dispatch({ + type: 'RECEIVED_NEW_TRANSACTION', + msg: humps.camelizeKeys(msg) + }) + setTimeout(() => store.dispatch({ + type: 'REMOVE_PENDING_TRANSACTION', + msg: humps.camelizeKeys(msg) + }), TRANSACTION_VALIDATED_MOVE_DELAY) + }) + + const blocksChannel = socket.channel(`blocks:${addressHash}`, {}) + blocksChannel.join() + blocksChannel.onError(() => store.dispatch({ + type: 'CHANNEL_DISCONNECTED' + })) + blocksChannel.on('new_block', (msg) => store.dispatch({ + type: 'RECEIVED_NEW_BLOCK', + msg: humps.camelizeKeys(msg) + })) + + $('[data-selector="transactions-list"]').length && atBottom(function loadMoreTransactions () { + const nextPage = store.getState().nextPage + if (nextPage) { + store.dispatch({ + type: 'LOADING_NEXT_PAGE' + }) + $.get(nextPage).done(msg => { + store.dispatch({ + type: 'RECEIVED_NEXT_TRANSACTIONS_PAGE', + msg: humps.camelizeKeys(msg) + }) + }) + } }) } diff --git a/apps/block_scout_web/assets/js/utils.js b/apps/block_scout_web/assets/js/utils.js index 53bd77351f..b2fc62c2ff 100644 --- a/apps/block_scout_web/assets/js/utils.js +++ b/apps/block_scout_web/assets/js/utils.js @@ -1,6 +1,6 @@ import $ from 'jquery' import _ from 'lodash' -import { createStore } from 'redux' +import { createStore as reduxCreateStore } from 'redux' import morph from 'nanomorph' import { updateAllAges } from './lib/from_now' @@ -29,7 +29,7 @@ export function initRedux (reducer, { main, render, debug } = {}) { } if (!render) console.warn('initRedux: You have not passed a render function.') - const store = createStore(reducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()) + const store = createStore(reducer) if (debug) store.subscribe(() => { console.log(store.getState()) }) let oldState = store.getState() if (render) { @@ -42,6 +42,37 @@ export function initRedux (reducer, { main, render, debug } = {}) { if (main) main(store) } +export function createStore (reducer) { + return reduxCreateStore(reducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()) +} + +export function connectElements ({ elements, store }) { + function loadElements () { + return _.reduce(elements, (pageLoadParams, { load }, selector) => { + if (!load) return pageLoadParams + const $el = $(selector) + if (!$el.length) return pageLoadParams + const morePageLoadParams = load($el, store) + return _.isObject(morePageLoadParams) ? Object.assign(pageLoadParams, morePageLoadParams) : pageLoadParams + }, {}) + } + function renderElements (state, oldState) { + _.forIn(elements, ({ render }, selector) => { + if (!render) return + const $el = $(selector) + if (!$el.length) return + render($el, state, oldState) + }) + } + store.dispatch(Object.assign(loadElements(), { type: 'ELEMENTS_LOAD' })) + let oldState = store.getState() + store.subscribe(() => { + const state = store.getState() + renderElements(state, oldState) + oldState = state + }) +} + export function skippedBlockListBuilder (skippedBlockNumbers, newestBlock, oldestBlock) { for (let i = newestBlock - 1; i > oldestBlock; i--) skippedBlockNumbers.push(i) return skippedBlockNumbers From 0a80f1298521db0b44f4ae1f2c68e886b613cedc Mon Sep 17 00:00:00 2001 From: Ryan Arthur Date: Tue, 30 Oct 2018 15:04:48 -0400 Subject: [PATCH 21/42] Style infinite scroll loading indicator --- .../templates/address_transaction/index.html.eex | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_transaction/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_transaction/index.html.eex index db56f3ad01..2ba6e8f8ad 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address_transaction/index.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_transaction/index.html.eex @@ -74,9 +74,14 @@ <%= render(BlockScoutWeb.TransactionView, "_tile.html", current_address: @address, transaction: transaction) %> <% end %> -
+ + <% else %>
<%= gettext "There are no transactions for this address." %> From a55c602836a4c5d3e619d22fd80a55ed340352e7 Mon Sep 17 00:00:00 2001 From: jimmay5469 Date: Wed, 31 Oct 2018 09:46:07 -0400 Subject: [PATCH 22/42] Update tests --- .../assets/__tests__/pages/address.js | 379 ++++++++++-------- .../assets/js/pages/address.js | 5 +- .../channels/address_channel.ex | 1 - .../address_transaction_controller.ex | 5 +- .../internal_transaction/_tile.html.eex | 2 +- .../address_transaction_controller_test.exs | 38 +- 6 files changed, 214 insertions(+), 216 deletions(-) diff --git a/apps/block_scout_web/assets/__tests__/pages/address.js b/apps/block_scout_web/assets/__tests__/pages/address.js index 7691660a40..544ab59797 100644 --- a/apps/block_scout_web/assets/__tests__/pages/address.js +++ b/apps/block_scout_web/assets/__tests__/pages/address.js @@ -1,300 +1,327 @@ import { reducer, initialState } from '../../js/pages/address' -describe('PAGE_LOAD', () => { - test('page 1 without filter', () => { - const state = initialState +describe('RECEIVED_NEW_BLOCK', () => { + test('with new block', () => { + const state = Object.assign({}, initialState, { + validationCount: 30, + validatedBlocks: [{ blockNumber: 1, blockHtml: 'test 1' }] + }) const action = { - type: 'PAGE_LOAD', - addressHash: '1234', - beyondPageOne: false, - pendingTransactionHashes: ['1'] + type: 'RECEIVED_NEW_BLOCK', + msg: { blockNumber: 2, blockHtml: 'test 2' } } const output = reducer(state, action) - expect(output.addressHash).toBe('1234') - expect(output.beyondPageOne).toBe(false) - expect(output.filter).toBe(undefined) - expect(output.pendingTransactionHashes).toEqual(['1']) + expect(output.validationCount).toEqual(31) + expect(output.validatedBlocks).toEqual([ + { blockNumber: 2, blockHtml: 'test 2' }, + { blockNumber: 1, blockHtml: 'test 1' } + ]) }) - test('page 2 without filter', () => { - const state = initialState + test('when channel has been disconnected', () => { + const state = Object.assign({}, initialState, { + channelDisconnected: true, + validationCount: 30, + validatedBlocks: [{ blockNumber: 1, blockHtml: 'test 1' }] + }) const action = { - type: 'PAGE_LOAD', - addressHash: '1234', - beyondPageOne: true, - pendingTransactionHashes: ['1'] + type: 'RECEIVED_NEW_BLOCK', + msg: { blockNumber: 2, blockHtml: 'test 2' } } const output = reducer(state, action) - expect(output.addressHash).toBe('1234') - expect(output.beyondPageOne).toBe(true) - expect(output.filter).toBe(undefined) - expect(output.pendingTransactionHashes).toEqual(['1']) + expect(output.validationCount).toEqual(30) + expect(output.validatedBlocks).toEqual([ + { blockNumber: 1, blockHtml: 'test 1' } + ]) }) - test('page 1 with "to" filter', () => { - const state = initialState + test('beyond page one', () => { + const state = Object.assign({}, initialState, { + beyondPageOne: true, + validationCount: 30, + validatedBlocks: [{ blockNumber: 1, blockHtml: 'test 1' }] + }) const action = { - type: 'PAGE_LOAD', - addressHash: '1234', - beyondPageOne: false, - filter: 'to' + type: 'RECEIVED_NEW_BLOCK', + msg: { blockNumber: 2, blockHtml: 'test 2' } } const output = reducer(state, action) - expect(output.addressHash).toBe('1234') - expect(output.beyondPageOne).toBe(false) - expect(output.filter).toBe('to') + expect(output.validationCount).toEqual(31) + expect(output.validatedBlocks).toEqual([ + { blockNumber: 1, blockHtml: 'test 1' } + ]) }) - test('page 2 with "to" filter', () => { - const state = initialState +}) + +describe('RECEIVED_NEW_INTERNAL_TRANSACTION_BATCH', () => { + test('with new internal transaction', () => { + const state = Object.assign({}, initialState, { + internalTransactions: [{ internalTransactionHtml: 'test 1' }] + }) const action = { - type: 'PAGE_LOAD', - addressHash: '1234', - beyondPageOne: true, - filter: 'to' + type: 'RECEIVED_NEW_INTERNAL_TRANSACTION_BATCH', + msgs: [{ internalTransactionHtml: 'test 2' }] } const output = reducer(state, action) - expect(output.addressHash).toBe('1234') - expect(output.beyondPageOne).toBe(true) - expect(output.filter).toBe('to') + expect(output.internalTransactions).toEqual([ + { internalTransactionHtml: 'test 2' }, + { internalTransactionHtml: 'test 1' } + ]) }) -}) - -test('CHANNEL_DISCONNECTED', () => { - const state = initialState - const action = { - type: 'CHANNEL_DISCONNECTED' - } - const output = reducer(state, action) - - expect(output.channelDisconnected).toBe(true) -}) - -test('RECEIVED_UPDATED_BALANCE', () => { - const state = initialState - const action = { - type: 'RECEIVED_UPDATED_BALANCE', - msg: { - balance: 'hello world' + test('with batch of new internal transactions', () => { + const state = Object.assign({}, initialState, { + internalTransactions: [{ internalTransactionHtml: 'test 1' }] + }) + const action = { + type: 'RECEIVED_NEW_INTERNAL_TRANSACTION_BATCH', + msgs: [ + { internalTransactionHtml: 'test 2' }, + { internalTransactionHtml: 'test 3' }, + { internalTransactionHtml: 'test 4' }, + { internalTransactionHtml: 'test 5' }, + { internalTransactionHtml: 'test 6' }, + { internalTransactionHtml: 'test 7' }, + { internalTransactionHtml: 'test 8' }, + { internalTransactionHtml: 'test 9' }, + { internalTransactionHtml: 'test 10' }, + { internalTransactionHtml: 'test 11' }, + { internalTransactionHtml: 'test 12' }, + { internalTransactionHtml: 'test 13' } + ] } - } - const output = reducer(state, action) - - expect(output.balance).toBe('hello world') -}) + const output = reducer(state, action) -describe('RECEIVED_NEW_PENDING_TRANSACTION', () => { - test('single transaction', () => { - const state = initialState + expect(output.internalTransactions).toEqual([ + { internalTransactionHtml: 'test 1' } + ]) + expect(output.internalTransactionsBatch).toEqual([ + { internalTransactionHtml: 'test 13' }, + { internalTransactionHtml: 'test 12' }, + { internalTransactionHtml: 'test 11' }, + { internalTransactionHtml: 'test 10' }, + { internalTransactionHtml: 'test 9' }, + { internalTransactionHtml: 'test 8' }, + { internalTransactionHtml: 'test 7' }, + { internalTransactionHtml: 'test 6' }, + { internalTransactionHtml: 'test 5' }, + { internalTransactionHtml: 'test 4' }, + { internalTransactionHtml: 'test 3' }, + { internalTransactionHtml: 'test 2' }, + ]) + }) + test('after batch of new internal transactions', () => { + const state = Object.assign({}, initialState, { + internalTransactionsBatch: [{ internalTransactionHtml: 'test 1' }] + }) const action = { - type: 'RECEIVED_NEW_PENDING_TRANSACTION', - msg: { - transactionHash: '0x00', - transactionHtml: 'test' - } + type: 'RECEIVED_NEW_INTERNAL_TRANSACTION_BATCH', + msgs: [ + { internalTransactionHtml: 'test 2' } + ] } const output = reducer(state, action) - expect(output.newPendingTransactions).toEqual(['test']) - expect(output.transactionCount).toEqual(null) + expect(output.internalTransactionsBatch).toEqual([ + { internalTransactionHtml: 'test 2' }, + { internalTransactionHtml: 'test 1' } + ]) }) - test('single transaction after single transaction', () => { + test('when channel has been disconnected', () => { const state = Object.assign({}, initialState, { - newPendingTransactions: ['test 1'] + channelDisconnected: true, + internalTransactions: [{ internalTransactionHtml: 'test 1' }] }) const action = { - type: 'RECEIVED_NEW_PENDING_TRANSACTION', - msg: { - transactionHash: '0x02', - transactionHtml: 'test 2' - } + type: 'RECEIVED_NEW_INTERNAL_TRANSACTION_BATCH', + msgs: [{ internalTransactionHtml: 'test 2' }] } const output = reducer(state, action) - expect(output.newPendingTransactions).toEqual(['test 1', 'test 2']) - expect(output.pendingTransactionHashes.length).toEqual(1) + expect(output.internalTransactions).toEqual([ + { internalTransactionHtml: 'test 1' } + ]) }) - test('after disconnection', () => { + test('beyond page one', () => { const state = Object.assign({}, initialState, { - channelDisconnected: true + beyondPageOne: true, + internalTransactions: [{ internalTransactionHtml: 'test 1' }] }) const action = { - type: 'RECEIVED_NEW_PENDING_TRANSACTION', - msg: { - transactionHash: '0x00', - transactionHtml: 'test' - } + type: 'RECEIVED_NEW_INTERNAL_TRANSACTION_BATCH', + msgs: [{ internalTransactionHtml: 'test 2' }] } const output = reducer(state, action) - expect(output.newPendingTransactions).toEqual([]) - expect(output.pendingTransactionHashes).toEqual([]) + expect(output.internalTransactions).toEqual([ + { internalTransactionHtml: 'test 1' } + ]) }) - test('on page 2', () => { + test('with filtered out internal transaction', () => { const state = Object.assign({}, initialState, { - beyondPageOne: true + filter: 'to' }) const action = { - type: 'RECEIVED_NEW_PENDING_TRANSACTION', - msg: { - transactionHash: '0x00', - transactionHtml: 'test' - } + type: 'RECEIVED_NEW_INTERNAL_TRANSACTION_BATCH', + msgs: [{ internalTransactionHtml: 'test 2' }] } const output = reducer(state, action) - expect(output.newPendingTransactions).toEqual([]) - expect(output.pendingTransactionHashes).toEqual([]) + expect(output.internalTransactions).toEqual([]) }) }) -describe('RECEIVED_NEW_TRANSACTION', () => { - test('single transaction', () => { +describe('RECEIVED_NEW_PENDING_TRANSACTION', () => { + test('with new pending transaction', () => { const state = Object.assign({}, initialState, { - addressHash: '0x111' + pendingTransactions: [{ transactionHash: 1, transactionHtml: 'test 1' }] }) const action = { - type: 'RECEIVED_NEW_TRANSACTION', - msg: { - transactionHtml: 'test' - } + type: 'RECEIVED_NEW_PENDING_TRANSACTION', + msg: { transactionHash: 2, transactionHtml: 'test 2' } } const output = reducer(state, action) - expect(output.newTransactions).toEqual([{ transactionHtml: 'test' }]) - expect(output.transactionCount).toEqual(null) + expect(output.pendingTransactions).toEqual([ + { transactionHash: 2, transactionHtml: 'test 2' }, + { transactionHash: 1, transactionHtml: 'test 1' } + ]) }) - test('single transaction after single transaction', () => { + test('when channel has been disconnected', () => { const state = Object.assign({}, initialState, { - newTransactions: [{ transactionHtml: 'test 1' }] + channelDisconnected: true, + pendingTransactions: [{ transactionHash: 1, transactionHtml: 'test 1' }] }) const action = { - type: 'RECEIVED_NEW_TRANSACTION', - msg: { - transactionHtml: 'test 2' - } + type: 'RECEIVED_NEW_PENDING_TRANSACTION', + msg: { transactionHash: 2, transactionHtml: 'test 2' } } const output = reducer(state, action) - expect(output.newTransactions).toEqual([ - { transactionHtml: 'test 1' }, - { transactionHtml: 'test 2' } + expect(output.pendingTransactions).toEqual([ + { transactionHash: 1, transactionHtml: 'test 1' } ]) }) - test('after disconnection', () => { + test('beyond page one', () => { const state = Object.assign({}, initialState, { - channelDisconnected: true + beyondPageOne: true, + pendingTransactions: [{ transactionHash: 1, transactionHtml: 'test 1' }] }) const action = { - type: 'RECEIVED_NEW_TRANSACTION', - msg: { - transactionHtml: 'test' - } + type: 'RECEIVED_NEW_PENDING_TRANSACTION', + msg: { transactionHash: 2, transactionHtml: 'test 2' } } const output = reducer(state, action) - expect(output.newTransactions).toEqual([]) + expect(output.pendingTransactions).toEqual([ + { transactionHash: 1, transactionHtml: 'test 1' } + ]) }) - test('on page 2', () => { + test('with filtered out pending transaction', () => { const state = Object.assign({}, initialState, { - beyondPageOne: true, - transactionCount: 1, - addressHash: '0x111' + filter: 'to' }) const action = { - type: 'RECEIVED_NEW_TRANSACTION', - msg: { - transactionHtml: 'test' - } + type: 'RECEIVED_NEW_PENDING_TRANSACTION', + msg: { transactionHash: 2, transactionHtml: 'test 2' } } const output = reducer(state, action) - expect(output.newTransactions).toEqual([]) - expect(output.transactionCount).toEqual(1) + expect(output.pendingTransactions).toEqual([]) }) - test('transaction from current address with "from" filter', () => { +}) + +describe('RECEIVED_NEW_TRANSACTION', () => { + test('with new transaction', () => { const state = Object.assign({}, initialState, { - addressHash: '1234', - filter: 'from' + pendingTransactions: [{ transactionHash: 2, transactionHtml: 'test' }], + transactions: [{ transactionHash: 1, transactionHtml: 'test 1' }] }) const action = { type: 'RECEIVED_NEW_TRANSACTION', - msg: { - fromAddressHash: '1234', - transactionHtml: 'test' - } + msg: { transactionHash: 2, transactionHtml: 'test 2' } } const output = reducer(state, action) - expect(output.newTransactions).toEqual([ - { fromAddressHash: '1234', transactionHtml: 'test' } + expect(output.pendingTransactions).toEqual([ + { transactionHash: 2, transactionHtml: 'test 2', validated: true } + ]) + expect(output.transactions).toEqual([ + { transactionHash: 2, transactionHtml: 'test 2' }, + { transactionHash: 1, transactionHtml: 'test 1' } ]) }) - test('transaction from current address with "to" filter', () => { + test('when channel has been disconnected', () => { const state = Object.assign({}, initialState, { - addressHash: '1234', - filter: 'to' + channelDisconnected: true, + pendingTransactions: [{ transactionHash: 2, transactionHtml: 'test' }], + transactions: [{ transactionHash: 1, transactionHtml: 'test 1' }] }) const action = { type: 'RECEIVED_NEW_TRANSACTION', - msg: { - fromAddressHash: '1234', - transactionHtml: 'test' - } + msg: { transactionHash: 2, transactionHtml: 'test 2' } } const output = reducer(state, action) - expect(output.newTransactions).toEqual([]) + expect(output.pendingTransactions).toEqual([ + { transactionHash: 2, transactionHtml: 'test' } + ]) + expect(output.transactions).toEqual([ + { transactionHash: 1, transactionHtml: 'test 1' } + ]) }) - test('transaction to current address with "to" filter', () => { + test('beyond page one', () => { const state = Object.assign({}, initialState, { - addressHash: '1234', - filter: 'to' + beyondPageOne: true, + transactions: [{ transactionHash: 1, transactionHtml: 'test 1' }] }) const action = { type: 'RECEIVED_NEW_TRANSACTION', - msg: { - toAddressHash: '1234', - transactionHtml: 'test' - } + msg: { transactionHash: 2, transactionHtml: 'test 2' } } const output = reducer(state, action) - expect(output.newTransactions).toEqual([ - { toAddressHash: '1234', transactionHtml: 'test' } + expect(output.pendingTransactions).toEqual([]) + expect(output.transactions).toEqual([ + { transactionHash: 1, transactionHtml: 'test 1' } ]) }) - test('transaction to current address with "from" filter', () => { + test('with filtered out transaction', () => { const state = Object.assign({}, initialState, { - addressHash: '1234', - filter: 'from' + filter: 'to' }) const action = { type: 'RECEIVED_NEW_TRANSACTION', - msg: { - toAddressHash: '1234', - transactionHtml: 'test' - } + msg: { transactionHash: 2, transactionHtml: 'test 2' } } const output = reducer(state, action) - expect(output.newTransactions).toEqual([]) + expect(output.transactions).toEqual([]) }) - test('single transaction collated from pending', () => { - const state = initialState +}) + +describe('RECEIVED_NEXT_TRANSACTIONS_PAGE', () => { + test('with new transaction page', () => { + const state = Object.assign({}, initialState, { + loadingNextPage: true, + nextPage: '1', + transactions: [{ transactionHash: 1, transactionHtml: 'test 1' }] + }) const action = { - type: 'RECEIVED_NEW_TRANSACTION', + type: 'RECEIVED_NEXT_TRANSACTIONS_PAGE', msg: { - transactionHash: '0x00', - transactionHtml: 'test' + nextPage: '2', + transactions: [{ transactionHash: 2, transactionHtml: 'test 2' }] } } const output = reducer(state, action) - expect(output.newTransactions).toEqual([ - { transactionHash: '0x00', transactionHtml: 'test' } + expect(output.loadingNextPage).toEqual(false) + expect(output.nextPage).toEqual('2') + expect(output.transactions).toEqual([ + { transactionHash: 1, transactionHtml: 'test 1' }, + { transactionHash: 2, transactionHtml: 'test 2' } ]) - expect(output.transactionCount).toEqual(null) }) }) diff --git a/apps/block_scout_web/assets/js/pages/address.js b/apps/block_scout_web/assets/js/pages/address.js index fdbf5c2867..de44b6b407 100644 --- a/apps/block_scout_web/assets/js/pages/address.js +++ b/apps/block_scout_web/assets/js/pages/address.js @@ -103,8 +103,6 @@ export function reducer (state = initialState, action) { }) } case 'REMOVE_PENDING_TRANSACTION': { - if (state.channelDisconnected) return state - return Object.assign({}, state, { pendingTransactions: state.pendingTransactions.filter((transaction) => action.msg.transactionHash !== transaction.transactionHash) }) @@ -247,7 +245,6 @@ const elements = { load ($el) { return { internalTransactions: $el.children().map((index, el) => ({ - internalTransactionId: el.dataset.internalTransactionId, internalTransactionHtml: el.outerHTML })).toArray() } @@ -256,7 +253,7 @@ const elements = { if (oldState.internalTransactions === state.internalTransactions) return const container = $el[0] const newElements = _.map(state.internalTransactions, ({ internalTransactionHtml }) => $(internalTransactionHtml)[0]) - listMorph(container, newElements, { key: 'dataset.internalTransactionId' }) + listMorph(container, newElements, { key: 'dataset.key' }) } }, '[data-selector="channel-batching-count"]': { diff --git a/apps/block_scout_web/lib/block_scout_web/channels/address_channel.ex b/apps/block_scout_web/lib/block_scout_web/channels/address_channel.ex index 383dcf1897..6213b6aaf5 100644 --- a/apps/block_scout_web/lib/block_scout_web/channels/address_channel.ex +++ b/apps/block_scout_web/lib/block_scout_web/channels/address_channel.ex @@ -55,7 +55,6 @@ defmodule BlockScoutWeb.AddressChannel do push(socket, "internal_transaction", %{ to_address_hash: to_string(internal_transaction.to_address_hash), from_address_hash: to_string(internal_transaction.from_address_hash), - internal_transaction_id: to_string(internal_transaction.id), internal_transaction_html: rendered_internal_transaction }) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex index 040d388985..823963afaa 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex @@ -8,10 +8,10 @@ defmodule BlockScoutWeb.AddressTransactionController do import BlockScoutWeb.AddressController, only: [transaction_count: 1, validation_count: 1] import BlockScoutWeb.Chain, only: [current_filter: 1, paging_options: 1, next_page_params: 3, split_list_by_page: 1] + alias BlockScoutWeb.TransactionView alias Explorer.{Chain, Market} - alias Explorer.ExchangeRates.Token alias Explorer.Chain.Hash - alias BlockScoutWeb.{InternalTransactionView, AddressView, TransactionView} + alias Explorer.ExchangeRates.Token alias Phoenix.View def index(conn, %{"address_id" => address_hash_string, "type" => "JSON"} = params) do @@ -87,6 +87,7 @@ defmodule BlockScoutWeb.AddressTransactionController do :token_transfers => :optional } ] + |> Keyword.merge(paging_options(%{})) |> Keyword.merge(current_filter(params)) full_options = put_in(pending_options, [:necessity_by_association, :block], :required) diff --git a/apps/block_scout_web/lib/block_scout_web/templates/internal_transaction/_tile.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/internal_transaction/_tile.html.eex index 9125caf9bf..9a0630e97d 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/internal_transaction/_tile.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/internal_transaction/_tile.html.eex @@ -1,4 +1,4 @@ -
+
<%= gettext("Internal Transaction") %> diff --git a/apps/block_scout_web/test/block_scout_web/controllers/address_transaction_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/address_transaction_controller_test.exs index 0c71e32503..39b9d76fa6 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/address_transaction_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/address_transaction_controller_test.exs @@ -76,7 +76,7 @@ defmodule BlockScoutWeb.AddressTransactionControllerTest do 50 |> insert_list(:transaction, from_address: address) |> with_block() - |> Enum.map(& &1.hash) + |> Enum.map(&to_string(&1.hash)) %Transaction{block_number: block_number, index: index} = :transaction @@ -85,47 +85,21 @@ defmodule BlockScoutWeb.AddressTransactionControllerTest do conn = get(conn, address_transaction_path(BlockScoutWeb.Endpoint, :index, address.hash), %{ + "type" => "JSON", "block_number" => Integer.to_string(block_number), "index" => Integer.to_string(index) }) + {:ok, %{"transactions" => transactions}} = conn.resp_body |> Poison.decode() + actual_hashes = - conn.assigns.transactions - |> Enum.map(& &1.hash) + transactions + |> Enum.map(& &1["transaction_hash"]) |> Enum.reverse() assert second_page_hashes == actual_hashes end - test "does not return pending transactions if beyond page one", %{conn: conn} do - address = insert(:address) - - 50 - |> insert_list(:transaction, from_address: address) - |> with_block() - |> Enum.map(& &1.hash) - - %Transaction{block_number: block_number, index: index} = - :transaction - |> insert(from_address: address) - |> with_block() - - pending = insert(:transaction, from_address: address, to_address: address) - - conn = - get(conn, address_transaction_path(BlockScoutWeb.Endpoint, :index, address.hash), %{ - "block_number" => Integer.to_string(block_number), - "index" => Integer.to_string(index) - }) - - actual_pending_hashes = - conn.assigns.pending_transactions - |> Enum.map(& &1.hash) - |> Enum.reverse() - - refute Enum.member?(actual_pending_hashes, pending.hash) - end - test "next_page_params exist if not on last page", %{conn: conn} do address = insert(:address) block = %Block{number: number} = insert(:block) From 3a73db3edb84fe9331b2202f7715aeb9c656428d Mon Sep 17 00:00:00 2001 From: jimmay5469 Date: Wed, 31 Oct 2018 09:49:39 -0400 Subject: [PATCH 23/42] Fix channel disconnected and batching messages --- .../templates/address_internal_transaction/index.html.eex | 4 ++-- .../templates/address_validation/index.html.eex | 2 +- .../block_scout_web/templates/transaction/index.html.eex | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_internal_transaction/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_internal_transaction/index.html.eex index 17e929e972..e866c4ae19 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address_internal_transaction/index.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_internal_transaction/index.html.eex @@ -8,12 +8,12 @@
-
+ -
+
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_validation/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_validation/index.html.eex index 5049bf91ff..f9028ba3e6 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address_validation/index.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_validation/index.html.eex @@ -99,7 +99,7 @@
-
+
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/transaction/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/transaction/index.html.eex index 42aa38165f..79d67edb40 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/transaction/index.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/transaction/index.html.eex @@ -9,17 +9,17 @@ <%= gettext("Validated Transactions") %>

-
+ -
+ - + <%= for transaction <- @transactions do %> <%= render BlockScoutWeb.TransactionView, "_tile.html", transaction: transaction %> From 8b6903e13affd65ed5c9b10096dbfb57ad1721f2 Mon Sep 17 00:00:00 2001 From: jimmay5469 Date: Thu, 1 Nov 2018 09:50:29 -0400 Subject: [PATCH 24/42] Address feedback Co-authored-by: Ryan Arthur --- .../assets/__tests__/pages/address.js | 6 +-- .../assets/js/pages/address.js | 44 +++++++++++++----- apps/block_scout_web/assets/js/utils.js | 30 ++++++++++-- .../address_transaction_controller.ex | 46 +++++++++---------- .../address_transaction/index.html.eex | 3 ++ 5 files changed, 85 insertions(+), 44 deletions(-) diff --git a/apps/block_scout_web/assets/__tests__/pages/address.js b/apps/block_scout_web/assets/__tests__/pages/address.js index 544ab59797..55eedad035 100644 --- a/apps/block_scout_web/assets/__tests__/pages/address.js +++ b/apps/block_scout_web/assets/__tests__/pages/address.js @@ -305,20 +305,20 @@ describe('RECEIVED_NEXT_TRANSACTIONS_PAGE', () => { test('with new transaction page', () => { const state = Object.assign({}, initialState, { loadingNextPage: true, - nextPage: '1', + nextPageUrl: '1', transactions: [{ transactionHash: 1, transactionHtml: 'test 1' }] }) const action = { type: 'RECEIVED_NEXT_TRANSACTIONS_PAGE', msg: { - nextPage: '2', + nextPageUrl: '2', transactions: [{ transactionHash: 2, transactionHtml: 'test 2' }] } } const output = reducer(state, action) expect(output.loadingNextPage).toEqual(false) - expect(output.nextPage).toEqual('2') + expect(output.nextPageUrl).toEqual('2') expect(output.transactions).toEqual([ { transactionHash: 1, transactionHtml: 'test 1' }, { transactionHash: 2, transactionHtml: 'test 2' } diff --git a/apps/block_scout_web/assets/js/pages/address.js b/apps/block_scout_web/assets/js/pages/address.js index de44b6b407..fb969ac73f 100644 --- a/apps/block_scout_web/assets/js/pages/address.js +++ b/apps/block_scout_web/assets/js/pages/address.js @@ -4,7 +4,7 @@ import URI from 'urijs' import humps from 'humps' import numeral from 'numeral' import socket from '../socket' -import { createStore, connectElements, batchChannel, listMorph, atBottom } from '../utils' +import { createStore, connectElements, batchChannel, listMorph, onScrollBottom } from '../utils' import { updateAllCalculatedUsdValues } from '../lib/currency.js' import { loadTokenBalanceDropdown } from '../lib/token_balance_dropdown' @@ -28,7 +28,8 @@ export const initialState = { validatedBlocks: [], loadingNextPage: false, - nextPage: null, + pagingError: false, + nextPageUrl: null, beyondPageOne: null } @@ -137,10 +138,16 @@ export function reducer (state = initialState, action) { loadingNextPage: true }) } + case 'PAGING_ERROR': { + return Object.assign({}, state, { + loadingNextPage: false, + pagingError: true + }) + } case 'RECEIVED_NEXT_TRANSACTIONS_PAGE': { return Object.assign({}, state, { loadingNextPage: false, - nextPage: action.msg.nextPage, + nextPageUrl: action.msg.nextPageUrl, transactions: [ ...state.transactions, ...action.msg.transactions @@ -196,6 +203,13 @@ const elements = { } } }, + '[data-selector="paging-error-message"]': { + render ($el, state) { + if (state.pagingError) { + $el.show() + } + } + }, '[data-selector="pending-transactions-list"]': { load ($el) { return { @@ -283,7 +297,7 @@ const elements = { '[data-selector="next-page-button"]': { load ($el) { return { - nextPage: `${$el.hide().attr('href')}&type=JSON` + nextPageUrl: `${$el.hide().attr('href')}&type=JSON` } } } @@ -340,18 +354,24 @@ if ($addressDetailsPage.length) { msg: humps.camelizeKeys(msg) })) - $('[data-selector="transactions-list"]').length && atBottom(function loadMoreTransactions () { - const nextPage = store.getState().nextPage - if (nextPage) { + $('[data-selector="transactions-list"]').length && onScrollBottom(function loadMoreTransactions () { + const { loadingNextPage, nextPageUrl, pagingError } = store.getState() + if (!loadingNextPage && nextPageUrl && !pagingError) { store.dispatch({ type: 'LOADING_NEXT_PAGE' }) - $.get(nextPage).done(msg => { - store.dispatch({ - type: 'RECEIVED_NEXT_TRANSACTIONS_PAGE', - msg: humps.camelizeKeys(msg) + $.get(nextPageUrl) + .done(msg => { + store.dispatch({ + type: 'RECEIVED_NEXT_TRANSACTIONS_PAGE', + msg: humps.camelizeKeys(msg) + }) + }) + .fail(() => { + store.dispatch({ + type: 'PAGING_ERROR' + }) }) - }) } }) } diff --git a/apps/block_scout_web/assets/js/utils.js b/apps/block_scout_web/assets/js/utils.js index b2fc62c2ff..28f36b3cfd 100644 --- a/apps/block_scout_web/assets/js/utils.js +++ b/apps/block_scout_web/assets/js/utils.js @@ -146,7 +146,26 @@ function smarterSlideUp ($el, { complete = _.noop } = {}) { } } -export function listMorph (container, newElements, { key, horizontal }) { +// The goal of this function is to DOM diff lists, so upon completion `container.innerHTML` should be +// equivalent to `newElements.join('')`. +// +// We could simply do `container.innerHTML = newElements.join('')` but that would not be efficient and +// it not animate appropriately. We could also simply use `morph` (or a similar library) on the entire +// list, however that doesn't give us the proper amount of control for animations. +// +// This function will walk though, remove items currently in `container` which are not in the new list. +// Then it will swap the contents of the items that are in both lists in case the items were updated or +// the order changed. Finally, it will add elements to `container` which are in the new list and didn't +// already exist in the DOM. +// +// Params: +// container: the DOM element which contents need replaced +// newElements: a list of elements that need to be put into the container +// options: +// key: the path to the unique identifier of each element +// horizontal: our horizontal animations are handled in CSS, so passing in `true` will not play JS +// animations +export function listMorph (container, newElements, { key, horizontal } = {}) { if (!container) return const oldElements = $(container).children().get() let currentList = _.map(oldElements, (el) => ({ id: _.get(el, key), el })) @@ -182,14 +201,15 @@ export function listMorph (container, newElements, { key, horizontal }) { }) } -export function atBottom (callback) { +export function onScrollBottom (callback) { + const $window = $(window) function infiniteScrollChecker () { - var scrollHeight = $(document).height() - var scrollPosition = $(window).height() + $(window).scrollTop() + const scrollHeight = $(document).height() + const scrollPosition = $window.height() + $window.scrollTop() if ((scrollHeight - scrollPosition) / scrollHeight === 0) { callback() } } infiniteScrollChecker() - $(window).on('scroll', infiniteScrollChecker) + $window.on('scroll', infiniteScrollChecker) } diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex index 823963afaa..d33d54ccb8 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex @@ -14,26 +14,27 @@ defmodule BlockScoutWeb.AddressTransactionController do alias Explorer.ExchangeRates.Token alias Phoenix.View + @transaction_necessity_by_association [ + necessity_by_association: %{ + [created_contract_address: :names] => :optional, + [from_address: :names] => :optional, + [to_address: :names] => :optional, + :token_transfers => :optional + } + ] + def index(conn, %{"address_id" => address_hash_string, "type" => "JSON"} = params) do with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), {:ok, address} <- Chain.hash_to_address(address_hash) do - full_options = - [ - necessity_by_association: %{ - :block => :required, - [created_contract_address: :names] => :optional, - [from_address: :names] => :optional, - [to_address: :names] => :optional, - :token_transfers => :optional - } - ] + options = + @transaction_necessity_by_association + |> put_in([:necessity_by_association, :block], :required) |> Keyword.merge(paging_options(params)) |> Keyword.merge(current_filter(params)) - transactions_plus_one = Chain.address_to_transactions(address, full_options) - {transactions, next_page} = split_list_by_page(transactions_plus_one) + {transactions, next_page} = get_transactions_and_next_page(address, options) - next_page = + next_page_url = case next_page_params(next_page, transactions, params) do nil -> nil @@ -63,7 +64,7 @@ defmodule BlockScoutWeb.AddressTransactionController do ) } end), - next_page: next_page + next_page_url: next_page_url } ) else @@ -79,21 +80,13 @@ defmodule BlockScoutWeb.AddressTransactionController do with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), {:ok, address} <- Chain.hash_to_address(address_hash) do pending_options = - [ - necessity_by_association: %{ - [created_contract_address: :names] => :optional, - [from_address: :names] => :optional, - [to_address: :names] => :optional, - :token_transfers => :optional - } - ] + @transaction_necessity_by_association |> Keyword.merge(paging_options(%{})) |> Keyword.merge(current_filter(params)) full_options = put_in(pending_options, [:necessity_by_association, :block], :required) - transactions_plus_one = Chain.address_to_transactions(address, full_options) - {transactions, next_page} = split_list_by_page(transactions_plus_one) + {transactions, next_page} = get_transactions_and_next_page(address, full_options) pending_transactions = Chain.address_to_pending_transactions(address, pending_options) @@ -117,4 +110,9 @@ defmodule BlockScoutWeb.AddressTransactionController do not_found(conn) end end + + defp get_transactions_and_next_page(address, options) do + transactions_plus_one = Chain.address_to_transactions(address, options) + split_list_by_page(transactions_plus_one) + end end diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_transaction/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_transaction/index.html.eex index 2ba6e8f8ad..5842b7553b 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address_transaction/index.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_transaction/index.html.eex @@ -81,6 +81,9 @@ <%= gettext("Loading") %>...
+ <% else %>
From e6c6daef30ef630fc4245d2126b7cbdd4dbfb99c Mon Sep 17 00:00:00 2001 From: jimmay5469 Date: Thu, 1 Nov 2018 09:54:19 -0400 Subject: [PATCH 25/42] i18n --- apps/block_scout_web/priv/gettext/default.pot | 14 ++++++++++++-- .../priv/gettext/en/LC_MESSAGES/default.po | 14 ++++++++++++-- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/apps/block_scout_web/priv/gettext/default.pot b/apps/block_scout_web/priv/gettext/default.pot index baf9018b06..abc6ec1a5a 100644 --- a/apps/block_scout_web/priv/gettext/default.pot +++ b/apps/block_scout_web/priv/gettext/default.pot @@ -638,7 +638,7 @@ msgstr "" #, elixir-format #: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:72 -#: lib/block_scout_web/templates/address_transaction/index.html.eex:83 +#: lib/block_scout_web/templates/address_transaction/index.html.eex:96 #: lib/block_scout_web/templates/address_validation/index.html.eex:117 #: lib/block_scout_web/templates/block/index.html.eex:20 #: lib/block_scout_web/templates/block_transaction/index.html.eex:50 @@ -858,7 +858,7 @@ msgid "There are no tokens." msgstr "" #, elixir-format -#: lib/block_scout_web/templates/address_transaction/index.html.eex:77 +#: lib/block_scout_web/templates/address_transaction/index.html.eex:90 msgid "There are no transactions for this address." msgstr "" @@ -1211,3 +1211,13 @@ msgstr "" #: lib/block_scout_web/views/internal_transaction_view.ex:32 msgid "Suicide" msgstr "" + +#, elixir-format +#: lib/block_scout_web/templates/address_transaction/index.html.eex:85 +msgid "Error trying to fetch next page." +msgstr "" + +#, elixir-format +#: lib/block_scout_web/templates/address_transaction/index.html.eex:82 +msgid "Loading" +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 0bbaa29b2b..a804ee65a4 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 @@ -638,7 +638,7 @@ msgstr "" #, elixir-format #: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:72 -#: lib/block_scout_web/templates/address_transaction/index.html.eex:83 +#: lib/block_scout_web/templates/address_transaction/index.html.eex:96 #: lib/block_scout_web/templates/address_validation/index.html.eex:117 #: lib/block_scout_web/templates/block/index.html.eex:20 #: lib/block_scout_web/templates/block_transaction/index.html.eex:50 @@ -858,7 +858,7 @@ msgid "There are no tokens." msgstr "" #, elixir-format -#: lib/block_scout_web/templates/address_transaction/index.html.eex:77 +#: lib/block_scout_web/templates/address_transaction/index.html.eex:90 msgid "There are no transactions for this address." msgstr "" @@ -1211,3 +1211,13 @@ msgstr "" #: lib/block_scout_web/views/internal_transaction_view.ex:32 msgid "Suicide" msgstr "" + +#, elixir-format +#: lib/block_scout_web/templates/address_transaction/index.html.eex:85 +msgid "Error trying to fetch next page." +msgstr "" + +#, elixir-format +#: lib/block_scout_web/templates/address_transaction/index.html.eex:82 +msgid "Loading" +msgstr "" From cb6ff77852462d71115a4d725bcdf4c1727e4918 Mon Sep 17 00:00:00 2001 From: Alex Garibay Date: Thu, 1 Nov 2018 09:07:35 -0500 Subject: [PATCH 26/42] Try build again --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index cc733b35e0..92a2b1d9e5 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -14,7 +14,7 @@ jobs: working_directory: ~/app steps: - - run: apt-get update; apt-get -y install autoconf build-essential libgmp3-dev libtool + - run: sudo apt-get update; sudo apt-get -y install autoconf build-essential libgmp3-dev libtool - checkout From 764f7e9f3d91b76ca999699affcd7bc537395447 Mon Sep 17 00:00:00 2001 From: Alex Garibay Date: Thu, 1 Nov 2018 09:14:18 -0500 Subject: [PATCH 27/42] Try cleaning up build --- .circleci/config.yml | 6 +++--- apps/indexer/mix.exs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 92a2b1d9e5..fc294a3353 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -73,9 +73,9 @@ jobs: - run: mix compile # Ensure NIF is compiled for libsecp256k1 - - run: - command: make - working_directory: "deps/libsecp256k1" + # - run: + # command: make + # working_directory: "deps/libsecp256k1" # `deps` needs to be cached with `_build` because `_build` will symlink into `deps` diff --git a/apps/indexer/mix.exs b/apps/indexer/mix.exs index 64f2635196..44b5d3dea5 100644 --- a/apps/indexer/mix.exs +++ b/apps/indexer/mix.exs @@ -53,7 +53,7 @@ defmodule Indexer.MixProject do # Importing to database {:explorer, in_umbrella: true}, # libsecp2561k1 crypto functions - {:libsecp256k1, "~> 0.1.10", manager: :mix, override: true}, + {:libsecp256k1, "~> 0.1.10"}, # Log errors and application output to separate files {:logger_file_backend, "~> 0.0.10"}, # Mocking `EthereumJSONRPC.Transport`, so we avoid hitting real chains for local testing From 6b46ddf1b9e34bcdd33cc0c08f928eaba58138a8 Mon Sep 17 00:00:00 2001 From: Alex Garibay Date: Thu, 1 Nov 2018 09:19:21 -0500 Subject: [PATCH 28/42] Re-add libsecp256k1 make set --- .circleci/config.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index fc294a3353..92a2b1d9e5 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -73,9 +73,9 @@ jobs: - run: mix compile # Ensure NIF is compiled for libsecp256k1 - # - run: - # command: make - # working_directory: "deps/libsecp256k1" + - run: + command: make + working_directory: "deps/libsecp256k1" # `deps` needs to be cached with `_build` because `_build` will symlink into `deps` From c27292b7177dbad9ba6cead2956423b8105ce153 Mon Sep 17 00:00:00 2001 From: Sebastian Abondano Date: Wed, 31 Oct 2018 16:34:56 -0400 Subject: [PATCH 29/42] Add GraphiQL link to topnav Why: * For BlockScout users to learn about our GraphQL API. * Issue link: n/a This change addresses the need by: * Editing `_topnav.html.eex` template to include link to `/graphiql`. * Fixing `/api_docs/index.html.eex` template to use "BlockScout" instead of "Explorer". * Updates translation/gettext files. --- .../templates/api_docs/index.html.eex | 2 +- .../templates/layout/_topnav.html.eex | 20 ++++++++-- apps/block_scout_web/priv/gettext/default.pot | 39 ++++++++++++------- .../priv/gettext/en/LC_MESSAGES/default.po | 39 ++++++++++++------- 4 files changed, 65 insertions(+), 35 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/templates/api_docs/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/api_docs/index.html.eex index 6c9e707011..85cd28cd5d 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/api_docs/index.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/api_docs/index.html.eex @@ -4,7 +4,7 @@

API Documentation

[ <%= gettext "Base URL:" %> <%= @conn.host %>/api ] -

<%= gettext "This API is provided for developers transitioning their applications from Etherscan to Explorer. It supports GET and POST requests." %>

+

<%= gettext "This API is provided for developers transitioning their applications from Etherscan to BlockScout. It supports GET and POST requests." %>

diff --git a/apps/block_scout_web/lib/block_scout_web/templates/layout/_topnav.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/layout/_topnav.html.eex index 9dda45ecbe..dbe0afbb63 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/layout/_topnav.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/layout/_topnav.html.eex @@ -56,13 +56,25 @@ <%= gettext("Accounts") %> <% end %> -
diff --git a/apps/block_scout_web/priv/gettext/default.pot b/apps/block_scout_web/priv/gettext/default.pot index abc6ec1a5a..9ae0e22d88 100644 --- a/apps/block_scout_web/priv/gettext/default.pot +++ b/apps/block_scout_web/priv/gettext/default.pot @@ -83,11 +83,6 @@ msgstr "" msgid "A string with the name of the module to be invoked." msgstr "" -#, elixir-format -#: lib/block_scout_web/templates/layout/_topnav.html.eex:64 -msgid "API" -msgstr "" - #, elixir-format #: lib/block_scout_web/templates/api_docs/_metatags.html.eex:4 msgid "API endpoints for the %{subnetwork}" @@ -660,12 +655,12 @@ msgid "Owner Address" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/layout/_topnav.html.eex:95 +#: lib/block_scout_web/templates/layout/_topnav.html.eex:107 msgid "POA Core" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/layout/_topnav.html.eex:94 +#: lib/block_scout_web/templates/layout/_topnav.html.eex:106 msgid "POA Sokol" msgstr "" @@ -757,13 +752,13 @@ msgid "Responses" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/layout/_topnav.html.eex:71 -#: lib/block_scout_web/templates/layout/_topnav.html.eex:78 +#: lib/block_scout_web/templates/layout/_topnav.html.eex:83 +#: lib/block_scout_web/templates/layout/_topnav.html.eex:90 msgid "Search" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/layout/_topnav.html.eex:71 +#: lib/block_scout_web/templates/layout/_topnav.html.eex:83 msgid "Search by address, transaction hash, or block number" msgstr "" @@ -872,11 +867,6 @@ msgstr "" msgid "There are no transfers for this Token." msgstr "" -#, elixir-format -#: lib/block_scout_web/templates/api_docs/index.html.eex:7 -msgid "This API is provided for developers transitioning their applications from Etherscan to Explorer. It supports GET and POST requests." -msgstr "" - #, elixir-format #: lib/block_scout_web/templates/transaction/overview.html.eex:23 msgid "This transaction is pending confirmation." @@ -1221,3 +1211,22 @@ msgstr "" #: lib/block_scout_web/templates/address_transaction/index.html.eex:82 msgid "Loading" msgstr "" + +#: lib/block_scout_web/templates/layout/_topnav.html.eex:64 +msgid "APIs" +msgstr "" + +#, elixir-format +#: lib/block_scout_web/templates/layout/_topnav.html.eex:68 +msgid "GraphQL" +msgstr "" + +#, elixir-format +#: lib/block_scout_web/templates/api_docs/index.html.eex:7 +msgid "This API is provided for developers transitioning their applications from Etherscan to BlockScout. It supports GET and POST requests." +msgstr "" + +#, elixir-format +#: lib/block_scout_web/templates/layout/_topnav.html.eex:73 +msgid "RPC" +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 a804ee65a4..d55c8e73de 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 @@ -83,11 +83,6 @@ msgstr "" msgid "A string with the name of the module to be invoked." msgstr "" -#, elixir-format -#: lib/block_scout_web/templates/layout/_topnav.html.eex:64 -msgid "API" -msgstr "" - #, elixir-format #: lib/block_scout_web/templates/api_docs/_metatags.html.eex:4 msgid "API endpoints for the %{subnetwork}" @@ -660,12 +655,12 @@ msgid "Owner Address" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/layout/_topnav.html.eex:95 +#: lib/block_scout_web/templates/layout/_topnav.html.eex:107 msgid "POA Core" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/layout/_topnav.html.eex:94 +#: lib/block_scout_web/templates/layout/_topnav.html.eex:106 msgid "POA Sokol" msgstr "" @@ -757,13 +752,13 @@ msgid "Responses" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/layout/_topnav.html.eex:71 -#: lib/block_scout_web/templates/layout/_topnav.html.eex:78 +#: lib/block_scout_web/templates/layout/_topnav.html.eex:83 +#: lib/block_scout_web/templates/layout/_topnav.html.eex:90 msgid "Search" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/layout/_topnav.html.eex:71 +#: lib/block_scout_web/templates/layout/_topnav.html.eex:83 msgid "Search by address, transaction hash, or block number" msgstr "" @@ -872,11 +867,6 @@ msgstr "" msgid "There are no transfers for this Token." msgstr "" -#, elixir-format -#: lib/block_scout_web/templates/api_docs/index.html.eex:7 -msgid "This API is provided for developers transitioning their applications from Etherscan to Explorer. It supports GET and POST requests." -msgstr "" - #, elixir-format #: lib/block_scout_web/templates/transaction/overview.html.eex:23 msgid "This transaction is pending confirmation." @@ -1221,3 +1211,22 @@ msgstr "" #: lib/block_scout_web/templates/address_transaction/index.html.eex:82 msgid "Loading" msgstr "" + +#: lib/block_scout_web/templates/layout/_topnav.html.eex:64 +msgid "APIs" +msgstr "" + +#, elixir-format +#: lib/block_scout_web/templates/layout/_topnav.html.eex:68 +msgid "GraphQL" +msgstr "" + +#, elixir-format +#: lib/block_scout_web/templates/api_docs/index.html.eex:7 +msgid "This API is provided for developers transitioning their applications from Etherscan to BlockScout. It supports GET and POST requests." +msgstr "" + +#, elixir-format +#: lib/block_scout_web/templates/layout/_topnav.html.eex:73 +msgid "RPC" +msgstr "" From c12f7f4ec638892651702f76dbc18d3fc70621de Mon Sep 17 00:00:00 2001 From: Alex Garibay Date: Thu, 1 Nov 2018 12:40:13 -0500 Subject: [PATCH 30/42] Try reducing installed C dependencies --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 92a2b1d9e5..fda4307703 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -14,7 +14,7 @@ jobs: working_directory: ~/app steps: - - run: sudo apt-get update; sudo apt-get -y install autoconf build-essential libgmp3-dev libtool + - run: sudo apt-get update; sudo apt-get -y install autoconf build-essential libtool - checkout From 9730d08639a5ed361bc7dd3e2fed984265011843 Mon Sep 17 00:00:00 2001 From: Alex Garibay Date: Thu, 1 Nov 2018 13:04:20 -0500 Subject: [PATCH 31/42] Readd compiler dep --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index fda4307703..92a2b1d9e5 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -14,7 +14,7 @@ jobs: working_directory: ~/app steps: - - run: sudo apt-get update; sudo apt-get -y install autoconf build-essential libtool + - run: sudo apt-get update; sudo apt-get -y install autoconf build-essential libgmp3-dev libtool - checkout From af6a7358948e713dfebd4fa97972249d91948247 Mon Sep 17 00:00:00 2001 From: Felipe Renan Date: Fri, 26 Oct 2018 17:18:58 -0300 Subject: [PATCH 32/42] Ensure that token balances that gave errors will be scheduled --- apps/indexer/lib/indexer/token_balances.ex | 11 ++++---- .../test/indexer/token_balances_test.exs | 27 +++++++------------ 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/apps/indexer/lib/indexer/token_balances.ex b/apps/indexer/lib/indexer/token_balances.ex index 5e96615790..a92720eac1 100644 --- a/apps/indexer/lib/indexer/token_balances.ex +++ b/apps/indexer/lib/indexer/token_balances.ex @@ -29,7 +29,7 @@ defmodule Indexer.TokenBalances do token_balances |> Task.async_stream(&fetch_token_balance/1, on_timeout: :kill_task) |> Stream.map(&format_task_results/1) - |> Enum.filter(&ignore_request_with_timeouts/1) + |> Enum.filter(&ignore_request_with_errors/1) token_balances |> MapSet.new() @@ -70,11 +70,12 @@ defmodule Indexer.TokenBalances do |> TokenBalance.Fetcher.async_fetch() end - def format_task_results({:exit, :timeout}), do: {:error, :timeout} - def format_task_results({:ok, token_balance}), do: token_balance + defp format_task_results({:exit, :timeout}), do: {:error, :timeout} + defp format_task_results({:ok, token_balance}), do: token_balance - def ignore_request_with_timeouts({:error, :timeout}), do: false - def ignore_request_with_timeouts(_token_balance), do: true + defp ignore_request_with_errors({:error, :timeout}), do: false + defp ignore_request_with_errors(%{value: nil, value_fetched_at: nil, error: _error}), do: false + defp ignore_request_with_errors(_token_balance), do: true def log_fetching_errors(from, token_balances_params) do error_messages = diff --git a/apps/indexer/test/indexer/token_balances_test.exs b/apps/indexer/test/indexer/token_balances_test.exs index c59cd9e034..378559dc2e 100644 --- a/apps/indexer/test/indexer/token_balances_test.exs +++ b/apps/indexer/test/indexer/token_balances_test.exs @@ -45,28 +45,21 @@ defmodule Indexer.TokenBalancesTest do } = List.first(result) end - test "does not ignore calls that were returned with error" do - address = insert(:address) + test "ignores calls that gave errors to try fetch they again later" do + address = insert(:address, hash: "0x7113ffcb9c18a97da1b9cfc43e6cb44ed9165509") token = insert(:token, contract_address: build(:contract_address)) - address_hash_string = Hash.to_string(address.hash) - data = %{ - token_contract_address_hash: token.contract_address_hash, - address_hash: address_hash_string, - block_number: 1_000 - } + token_balances = [ + %{ + address_hash: to_string(address.hash), + block_number: 1_000, + token_contract_address_hash: to_string(token.contract_address_hash) + } + ] get_balance_from_blockchain_with_error() - {:ok, result} = TokenBalances.fetch_token_balances_from_blockchain([data]) - - assert %{ - value: nil, - token_contract_address_hash: token_contract_address_hash, - address_hash: address_hash, - block_number: 1_000, - value_fetched_at: nil - } = List.first(result) + assert TokenBalances.fetch_token_balances_from_blockchain(token_balances) == {:ok, []} end test "ignores results that raised :timeout" do From 233d539a006c7575db809d0fe3d12f565b3b1589 Mon Sep 17 00:00:00 2001 From: Felipe Renan Date: Fri, 26 Oct 2018 18:20:20 -0300 Subject: [PATCH 33/42] Add AddressCurrentTokenBalance schema --- .../chain/address/current_token_balance.ex | 60 +++++++++++++++++++ ..._create_address_current_token_balances.exs | 32 ++++++++++ apps/explorer/test/support/factory.ex | 11 ++++ 3 files changed, 103 insertions(+) create mode 100644 apps/explorer/lib/explorer/chain/address/current_token_balance.ex create mode 100644 apps/explorer/priv/repo/migrations/20181026180921_create_address_current_token_balances.exs diff --git a/apps/explorer/lib/explorer/chain/address/current_token_balance.ex b/apps/explorer/lib/explorer/chain/address/current_token_balance.ex new file mode 100644 index 0000000000..448ae7da34 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/address/current_token_balance.ex @@ -0,0 +1,60 @@ +defmodule Explorer.Chain.Address.CurrentTokenBalance do + @moduledoc """ + Represents the current token balance from addresses according to the last block. + """ + + use Ecto.Schema + import Ecto.Changeset + + alias Explorer.Chain.{Address, Block, Hash, Token} + + @typedoc """ + * `address` - The `t:Explorer.Chain.Address.t/0` that is the balance's owner. + * `address_hash` - The address hash foreign key. + * `token` - The `t:Explorer.Chain.Token/0` so that the address has the balance. + * `token_contract_address_hash` - The contract address hash foreign key. + * `block_number` - The block's number that the transfer took place. + * `value` - The value that's represents the balance. + """ + @type t :: %__MODULE__{ + address: %Ecto.Association.NotLoaded{} | Address.t(), + address_hash: Hash.Address.t(), + token: %Ecto.Association.NotLoaded{} | Token.t(), + token_contract_address_hash: Hash.Address, + block_number: Block.block_number(), + inserted_at: DateTime.t(), + updated_at: DateTime.t(), + value: Decimal.t() | nil + } + + schema "address_current_token_balances" do + field(:value, :decimal) + field(:block_number, :integer) + field(:value_fetched_at, :utc_datetime) + + belongs_to(:address, Address, foreign_key: :address_hash, references: :hash, type: Hash.Address) + + belongs_to( + :token, + Token, + foreign_key: :token_contract_address_hash, + references: :contract_address_hash, + type: Hash.Address + ) + + timestamps() + end + + @optional_fields ~w(value value_fetched_at)a + @required_fields ~w(address_hash block_number token_contract_address_hash)a + @allowed_fields @optional_fields ++ @required_fields + + @doc false + def changeset(%__MODULE__{} = token_balance, attrs) do + token_balance + |> cast(attrs, @allowed_fields) + |> validate_required(@required_fields) + |> foreign_key_constraint(:address_hash) + |> foreign_key_constraint(:token_contract_address_hash) + end +end diff --git a/apps/explorer/priv/repo/migrations/20181026180921_create_address_current_token_balances.exs b/apps/explorer/priv/repo/migrations/20181026180921_create_address_current_token_balances.exs new file mode 100644 index 0000000000..d497d1feda --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20181026180921_create_address_current_token_balances.exs @@ -0,0 +1,32 @@ +defmodule Explorer.Repo.Migrations.CreateAddressCurrentTokenBalances do + use Ecto.Migration + + def change do + create table(:address_current_token_balances) do + add(:address_hash, references(:addresses, column: :hash, type: :bytea), null: false) + add(:block_number, :bigint, null: false) + + add( + :token_contract_address_hash, + references(:tokens, column: :contract_address_hash, type: :bytea), + null: false + ) + + add(:value, :decimal, null: true) + add(:value_fetched_at, :utc_datetime, default: fragment("NULL"), null: true) + + timestamps(null: false, type: :utc_datetime) + end + + create(unique_index(:address_current_token_balances, ~w(address_hash token_contract_address_hash)a)) + + create( + index( + :address_current_token_balances, + [:value], + name: :address_current_token_balances_value, + where: "value IS NOT NULL" + ) + ) + end +end diff --git a/apps/explorer/test/support/factory.ex b/apps/explorer/test/support/factory.ex index f168091195..54f99731a8 100644 --- a/apps/explorer/test/support/factory.ex +++ b/apps/explorer/test/support/factory.ex @@ -13,6 +13,7 @@ defmodule Explorer.Factory do alias Explorer.Chain.{ Address, + Address.CurrentTokenBalance, Address.TokenBalance, Address.CoinBalance, Block, @@ -480,6 +481,16 @@ defmodule Explorer.Factory do } end + def address_current_token_balance_factory() do + %CurrentTokenBalance{ + address: build(:address), + token_contract_address_hash: insert(:token).contract_address_hash, + block_number: block_number(), + value: Enum.random(1..100_000), + value_fetched_at: DateTime.utc_now() + } + end + defmacrop left + right do quote do fragment("? + ?", unquote(left), unquote(right)) From ed183c988d454c7808423c74fa4a8a123c7b7d7d Mon Sep 17 00:00:00 2001 From: Felipe Renan Date: Fri, 26 Oct 2018 18:22:21 -0300 Subject: [PATCH 34/42] Import the current token balances --- apps/explorer/lib/explorer/chain/import.ex | 1 + .../import/address/current_token_balances.ex | 124 ++++++++++++++++++ .../address/current_token_balances_test.exs | 95 ++++++++++++++ .../test/explorer/chain/import_test.exs | 50 +++++++ .../lib/indexer/block/realtime/fetcher.ex | 1 + .../lib/indexer/token_balance/fetcher.ex | 8 +- 6 files changed, 278 insertions(+), 1 deletion(-) create mode 100644 apps/explorer/lib/explorer/chain/import/address/current_token_balances.ex create mode 100644 apps/explorer/test/explorer/chain/import/address/address/current_token_balances_test.exs diff --git a/apps/explorer/lib/explorer/chain/import.ex b/apps/explorer/lib/explorer/chain/import.ex index 3ba010e6be..28d62ee454 100644 --- a/apps/explorer/lib/explorer/chain/import.ex +++ b/apps/explorer/lib/explorer/chain/import.ex @@ -19,6 +19,7 @@ defmodule Explorer.Chain.Import do Import.Logs, Import.Tokens, Import.TokenTransfers, + Import.Address.CurrentTokenBalances, Import.Address.TokenBalances ] diff --git a/apps/explorer/lib/explorer/chain/import/address/current_token_balances.ex b/apps/explorer/lib/explorer/chain/import/address/current_token_balances.ex new file mode 100644 index 0000000000..11cf8cf460 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/import/address/current_token_balances.ex @@ -0,0 +1,124 @@ +defmodule Explorer.Chain.Import.Address.CurrentTokenBalances do + @moduledoc """ + Bulk imports `t:Explorer.Chain.Address.CurrentTokenBalance.t/0`. + """ + + require Ecto.Query + + import Ecto.Query, only: [from: 2] + + alias Ecto.{Changeset, Multi} + alias Explorer.Chain.Address.CurrentTokenBalance + alias Explorer.Chain.Import + + @behaviour Import.Runner + + # milliseconds + @timeout 60_000 + + @type imported :: [CurrentTokenBalance.t()] + + @impl Import.Runner + def ecto_schema_module, do: CurrentTokenBalance + + @impl Import.Runner + def option_key, do: :address_current_token_balances + + @impl Import.Runner + def imported_table_row do + %{ + value_type: "[#{ecto_schema_module()}.t()]", + value_description: "List of `t:#{ecto_schema_module()}.t/0`s" + } + end + + @impl Import.Runner + 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_current_token_balances, fn _ -> + insert(changes_list, insert_options) + end) + end + + @impl Import.Runner + def timeout, do: @timeout + + @spec insert([map()], %{ + optional(:on_conflict) => Import.Runner.on_conflict(), + required(:timeout) => timeout(), + required(:timestamps) => Import.timestamps() + }) :: + {:ok, [CurrentTokenBalance.t()]} + | {:error, [Changeset.t()]} + def 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) + + {:ok, _} = + Import.insert_changes_list( + unique_token_balances(changes_list), + conflict_target: ~w(address_hash token_contract_address_hash)a, + on_conflict: on_conflict, + for: CurrentTokenBalance, + returning: true, + timeout: timeout, + timestamps: timestamps + ) + end + + # Remove duplicated token balances based on `{address_hash, token_hash}` considering the last block + # to avoid `cardinality_violation` error in Postgres. This error happens when there are duplicated + # rows being inserted. + defp unique_token_balances(changes_list) do + changes_list + |> Enum.sort(&(&1.block_number > &2.block_number)) + |> Enum.uniq_by(fn %{address_hash: address_hash, token_contract_address_hash: token_hash} -> + {address_hash, token_hash} + end) + end + + defp default_on_conflict do + from( + current_token_balance in CurrentTokenBalance, + update: [ + set: [ + block_number: + fragment( + "CASE WHEN EXCLUDED.block_number > ? THEN EXCLUDED.block_number ELSE ? END", + current_token_balance.block_number, + current_token_balance.block_number + ), + inserted_at: + fragment( + "CASE WHEN EXCLUDED.block_number > ? THEN EXCLUDED.inserted_at ELSE ? END", + current_token_balance.block_number, + current_token_balance.inserted_at + ), + updated_at: + fragment( + "CASE WHEN EXCLUDED.block_number > ? THEN EXCLUDED.updated_at ELSE ? END", + current_token_balance.block_number, + current_token_balance.updated_at + ), + value: + fragment( + "CASE WHEN EXCLUDED.block_number > ? THEN EXCLUDED.value ELSE ? END", + current_token_balance.block_number, + current_token_balance.value + ), + value_fetched_at: + fragment( + "CASE WHEN EXCLUDED.block_number > ? THEN EXCLUDED.value_fetched_at ELSE ? END", + current_token_balance.block_number, + current_token_balance.value_fetched_at + ) + ] + ] + ) + end +end diff --git a/apps/explorer/test/explorer/chain/import/address/address/current_token_balances_test.exs b/apps/explorer/test/explorer/chain/import/address/address/current_token_balances_test.exs new file mode 100644 index 0000000000..a78b7046ef --- /dev/null +++ b/apps/explorer/test/explorer/chain/import/address/address/current_token_balances_test.exs @@ -0,0 +1,95 @@ +defmodule Explorer.Chain.Import.Address.CurrentTokenBalancesTest do + use Explorer.DataCase + + alias Explorer.Chain.Import.Address.CurrentTokenBalances + + alias Explorer.Chain.{Address.CurrentTokenBalance} + + describe "insert/2" do + setup do + address = insert(:address, hash: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca") + token = insert(:token) + + insert_options = %{ + timeout: :infinity, + timestamps: %{inserted_at: DateTime.utc_now(), updated_at: DateTime.utc_now()} + } + + %{address: address, token: token, insert_options: insert_options} + end + + test "inserts in the current token balances", %{address: address, token: token, insert_options: insert_options} do + changes = [ + %{ + address_hash: address.hash, + block_number: 1, + token_contract_address_hash: token.contract_address_hash, + value: Decimal.new(100) + } + ] + + CurrentTokenBalances.insert(changes, insert_options) + + current_token_balances = + CurrentTokenBalance + |> Explorer.Repo.all() + |> Enum.count() + + assert current_token_balances == 1 + end + + test "considers the last block upserting", %{address: address, token: token, insert_options: insert_options} do + insert( + :address_current_token_balance, + address: address, + block_number: 1, + token_contract_address_hash: token.contract_address_hash, + value: 100 + ) + + changes = [ + %{ + address_hash: address.hash, + block_number: 2, + token_contract_address_hash: token.contract_address_hash, + value: Decimal.new(200) + } + ] + + CurrentTokenBalances.insert(changes, insert_options) + + current_token_balance = Explorer.Repo.get_by(CurrentTokenBalance, address_hash: address.hash) + + assert current_token_balance.block_number == 2 + assert current_token_balance.value == Decimal.new(200) + end + + test "considers the last block when there are duplicated params", %{ + address: address, + token: token, + insert_options: insert_options + } do + changes = [ + %{ + address_hash: address.hash, + block_number: 4, + token_contract_address_hash: token.contract_address_hash, + value: Decimal.new(200) + }, + %{ + address_hash: address.hash, + block_number: 1, + token_contract_address_hash: token.contract_address_hash, + value: Decimal.new(100) + } + ] + + CurrentTokenBalances.insert(changes, insert_options) + + current_token_balance = Explorer.Repo.get_by(CurrentTokenBalance, address_hash: address.hash) + + assert current_token_balance.block_number == 4 + assert current_token_balance.value == Decimal.new(200) + end + end +end diff --git a/apps/explorer/test/explorer/chain/import_test.exs b/apps/explorer/test/explorer/chain/import_test.exs index ea4a485f6f..89c1c4f3eb 100644 --- a/apps/explorer/test/explorer/chain/import_test.exs +++ b/apps/explorer/test/explorer/chain/import_test.exs @@ -6,6 +6,7 @@ defmodule Explorer.Chain.ImportTest do alias Explorer.Chain.{ Address, Address.TokenBalance, + Address.CurrentTokenBalance, Block, Data, Log, @@ -395,6 +396,55 @@ defmodule Explorer.Chain.ImportTest do assert 3 == count end + test "inserts a current_token_balance" do + params = %{ + addresses: %{ + params: [ + %{hash: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca"}, + %{hash: "0x515c09c5bba1ed566b02a5b0599ec5d5d0aee73d"}, + %{hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b"} + ], + timeout: 5 + }, + tokens: %{ + on_conflict: :nothing, + params: [ + %{ + contract_address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b", + type: "ERC-20" + } + ], + timeout: 5 + }, + address_current_token_balances: %{ + params: [ + %{ + address_hash: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca", + token_contract_address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b", + block_number: "37", + value: 200 + }, + %{ + address_hash: "0x515c09c5bba1ed566b02a5b0599ec5d5d0aee73d", + token_contract_address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b", + block_number: "37", + value: 100 + } + ], + timeout: 5 + } + } + + Import.all(params) + + count = + CurrentTokenBalance + |> Explorer.Repo.all() + |> Enum.count() + + assert count == 2 + end + test "with empty map" do assert {:ok, %{}} == Import.all(%{}) end diff --git a/apps/indexer/lib/indexer/block/realtime/fetcher.ex b/apps/indexer/lib/indexer/block/realtime/fetcher.ex index f84e19ffc1..88682c149a 100644 --- a/apps/indexer/lib/indexer/block/realtime/fetcher.ex +++ b/apps/indexer/lib/indexer/block/realtime/fetcher.ex @@ -107,6 +107,7 @@ defmodule Indexer.Block.Realtime.Fetcher do |> put_in([:addresses, :params], balances_addresses_params) |> put_in([:blocks, :params, Access.all(), :consensus], true) |> put_in([Access.key(:address_coin_balances, %{}), :params], balances_params) + |> put_in([Access.key(:address_current_token_balances, %{}), :params], address_token_balances) |> put_in([Access.key(:address_token_balances), :params], address_token_balances) |> put_in([Access.key(:internal_transactions, %{}), :params], internal_transactions_params), {:ok, imported} = ok <- Chain.import(chain_import_options) do diff --git a/apps/indexer/lib/indexer/token_balance/fetcher.ex b/apps/indexer/lib/indexer/token_balance/fetcher.ex index 19df1853f9..f8cf8e65a6 100644 --- a/apps/indexer/lib/indexer/token_balance/fetcher.ex +++ b/apps/indexer/lib/indexer/token_balance/fetcher.ex @@ -80,7 +80,13 @@ defmodule Indexer.TokenBalance.Fetcher do end def import_token_balances(token_balances_params) do - case Chain.import(%{address_token_balances: %{params: token_balances_params}, timeout: :infinity}) do + import_params = %{ + address_token_balances: %{params: token_balances_params}, + address_current_token_balances: %{params: token_balances_params}, + timeout: :infinity + } + + case Chain.import(import_params) do {:ok, _} -> :ok From 3f7dd2bcdd40dcdaefadcabcc54d6e83ff3716e1 Mon Sep 17 00:00:00 2001 From: Felipe Renan Date: Tue, 30 Oct 2018 15:20:24 -0300 Subject: [PATCH 35/42] Move Token holder's query to Address.CurrentTokenBalance --- .../lib/block_scout_web/chain.ex | 4 +- .../tokens/holder_controller_test.exs | 8 +- .../features/viewing_tokens_test.exs | 2 +- apps/explorer/lib/explorer/chain.ex | 3 +- .../chain/address/current_token_balance.ex | 48 ++++++ .../explorer/chain/address/token_balance.ex | 53 +----- .../address/current_token_balance_test.exs | 149 ++++++++++++++++ apps/explorer/test/explorer/chain_test.exs | 161 ++---------------- 8 files changed, 218 insertions(+), 210 deletions(-) create mode 100644 apps/explorer/test/explorer/chain/address/current_token_balance_test.exs diff --git a/apps/block_scout_web/lib/block_scout_web/chain.ex b/apps/block_scout_web/lib/block_scout_web/chain.ex index bb3511a1b3..08e4e5c321 100644 --- a/apps/block_scout_web/lib/block_scout_web/chain.ex +++ b/apps/block_scout_web/lib/block_scout_web/chain.ex @@ -16,7 +16,7 @@ defmodule BlockScoutWeb.Chain do alias Explorer.Chain.{ Address, - Address.TokenBalance, + Address.CurrentTokenBalance, Block, InternalTransaction, Log, @@ -198,7 +198,7 @@ defmodule BlockScoutWeb.Chain do %{"token_name" => name, "token_type" => type, "token_inserted_at" => inserted_at_datetime} end - defp paging_params(%TokenBalance{address_hash: address_hash, value: value}) do + defp paging_params(%CurrentTokenBalance{address_hash: address_hash, value: value}) do %{"address_hash" => to_string(address_hash), "value" => Decimal.to_integer(value)} end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/tokens/holder_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/tokens/holder_controller_test.exs index 6ce8d7915b..66c8ce9de4 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/tokens/holder_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/tokens/holder_controller_test.exs @@ -22,7 +22,7 @@ defmodule BlockScoutWeb.Tokens.HolderControllerTest do insert_list( 2, - :token_balance, + :address_current_token_balance, token_contract_address_hash: token.contract_address_hash ) @@ -43,7 +43,7 @@ defmodule BlockScoutWeb.Tokens.HolderControllerTest do 1..50 |> Enum.map( &insert( - :token_balance, + :address_current_token_balance, token_contract_address_hash: token.contract_address_hash, value: &1 + 1000 ) @@ -52,7 +52,7 @@ defmodule BlockScoutWeb.Tokens.HolderControllerTest do token_balance = insert( - :token_balance, + :address_current_token_balance, token_contract_address_hash: token.contract_address_hash, value: 50000 ) @@ -78,7 +78,7 @@ defmodule BlockScoutWeb.Tokens.HolderControllerTest do Enum.each( 1..51, &insert( - :token_balance, + :address_current_token_balance, token_contract_address_hash: token.contract_address_hash, value: &1 + 1000 ) diff --git a/apps/block_scout_web/test/block_scout_web/features/viewing_tokens_test.exs b/apps/block_scout_web/test/block_scout_web/features/viewing_tokens_test.exs index 8a5d769e62..06f1255964 100644 --- a/apps/block_scout_web/test/block_scout_web/features/viewing_tokens_test.exs +++ b/apps/block_scout_web/test/block_scout_web/features/viewing_tokens_test.exs @@ -9,7 +9,7 @@ defmodule BlockScoutWeb.ViewingTokensTest do insert_list( 2, - :token_balance, + :address_current_token_balance, token_contract_address_hash: token.contract_address_hash ) diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index 2140e36b14..51eac0ed1a 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -23,6 +23,7 @@ defmodule Explorer.Chain do alias Explorer.Chain.{ Address, Address.CoinBalance, + Address.CurrentTokenBalance, Address.TokenBalance, Block, Data, @@ -2070,7 +2071,7 @@ defmodule Explorer.Chain do @spec fetch_token_holders_from_token_hash(Hash.Address.t(), [paging_options]) :: [TokenBalance.t()] def fetch_token_holders_from_token_hash(contract_address_hash, options) do contract_address_hash - |> TokenBalance.token_holders_ordered_by_value(options) + |> CurrentTokenBalance.token_holders_ordered_by_value(options) |> Repo.all() end diff --git a/apps/explorer/lib/explorer/chain/address/current_token_balance.ex b/apps/explorer/lib/explorer/chain/address/current_token_balance.ex index 448ae7da34..21f20e1f8c 100644 --- a/apps/explorer/lib/explorer/chain/address/current_token_balance.ex +++ b/apps/explorer/lib/explorer/chain/address/current_token_balance.ex @@ -5,9 +5,13 @@ defmodule Explorer.Chain.Address.CurrentTokenBalance do use Ecto.Schema import Ecto.Changeset + import Ecto.Query, only: [from: 2, limit: 2, order_by: 3, preload: 2, where: 3] + alias Explorer.{Chain, PagingOptions} alias Explorer.Chain.{Address, Block, Hash, Token} + @default_paging_options %PagingOptions{page_size: 50} + @typedoc """ * `address` - The `t:Explorer.Chain.Address.t/0` that is the balance's owner. * `address_hash` - The address hash foreign key. @@ -57,4 +61,48 @@ defmodule Explorer.Chain.Address.CurrentTokenBalance do |> foreign_key_constraint(:address_hash) |> foreign_key_constraint(:token_contract_address_hash) end + + {:ok, burn_address_hash} = Chain.string_to_address_hash("0x0000000000000000000000000000000000000000") + @burn_address_hash burn_address_hash + + @doc """ + Builds an `Ecto.Query` to fetch the token holders from the given token contract address hash. + + The Token Holders are the addresses that own a positive amount of the Token. So this query is + considering the following conditions: + + * The token balance from the last block. + * Balances greater than 0. + * Excluding the burn address (0x0000000000000000000000000000000000000000). + + """ + def token_holders_ordered_by_value(token_contract_address_hash, options \\ []) do + paging_options = Keyword.get(options, :paging_options, @default_paging_options) + + token_contract_address_hash + |> token_holders_query + |> preload(:address) + |> order_by([tb], desc: :value) + |> page_token_balances(paging_options) + |> limit(^paging_options.page_size) + end + + defp token_holders_query(token_contract_address_hash) do + from( + tb in __MODULE__, + where: tb.token_contract_address_hash == ^token_contract_address_hash, + where: tb.address_hash != ^@burn_address_hash, + where: tb.value > 0 + ) + end + + defp page_token_balances(query, %PagingOptions{key: nil}), do: query + + defp page_token_balances(query, %PagingOptions{key: {value, address_hash}}) do + where( + query, + [tb], + tb.value < ^value or (tb.value == ^value and tb.address_hash < ^address_hash) + ) + end end diff --git a/apps/explorer/lib/explorer/chain/address/token_balance.ex b/apps/explorer/lib/explorer/chain/address/token_balance.ex index 2b1c4f64b8..383eb24ec2 100644 --- a/apps/explorer/lib/explorer/chain/address/token_balance.ex +++ b/apps/explorer/lib/explorer/chain/address/token_balance.ex @@ -5,14 +5,12 @@ defmodule Explorer.Chain.Address.TokenBalance do use Ecto.Schema import Ecto.Changeset - import Ecto.Query, only: [from: 2, limit: 2, where: 3, subquery: 1, order_by: 3, preload: 2] + import Ecto.Query, only: [from: 2, subquery: 1] - alias Explorer.{Chain, PagingOptions} + alias Explorer.Chain alias Explorer.Chain.Address.TokenBalance alias Explorer.Chain.{Address, Block, Hash, Token} - @default_paging_options %PagingOptions{page_size: 50} - @typedoc """ * `address` - The `t:Explorer.Chain.Address.t/0` that is the balance's owner. * `address_hash` - The address hash foreign key. @@ -84,43 +82,6 @@ defmodule Explorer.Chain.Address.TokenBalance do from(tb in subquery(query), where: tb.value > 0, preload: :token) end - @doc """ - Builds an `Ecto.Query` to fetch the token holders from the given token contract address hash. - - The Token Holders are the addresses that own a positive amount of the Token. So this query is - considering the following conditions: - - * The token balance from the last block. - * Balances greater than 0. - * Excluding the burn address (0x0000000000000000000000000000000000000000). - - """ - def token_holders_from_token_hash(token_contract_address_hash) do - query = token_holders_query(token_contract_address_hash) - - from(tb in subquery(query), where: tb.value > 0) - end - - def token_holders_ordered_by_value(token_contract_address_hash, options) do - paging_options = Keyword.get(options, :paging_options, @default_paging_options) - - token_contract_address_hash - |> token_holders_from_token_hash() - |> order_by([tb], desc: tb.value, desc: tb.address_hash) - |> preload(:address) - |> page_token_balances(paging_options) - |> limit(^paging_options.page_size) - end - - defp token_holders_query(contract_address_hash) do - from( - tb in TokenBalance, - distinct: :address_hash, - where: tb.token_contract_address_hash == ^contract_address_hash and tb.address_hash != ^@burn_address_hash, - order_by: [desc: :block_number] - ) - end - @doc """ Builds an `Ecto.Query` to group all tokens with their number of holders. """ @@ -144,16 +105,6 @@ defmodule Explorer.Chain.Address.TokenBalance do ) end - defp page_token_balances(query, %PagingOptions{key: nil}), do: query - - defp page_token_balances(query, %PagingOptions{key: {value, address_hash}}) do - where( - query, - [tb], - tb.value < ^value or (tb.value == ^value and tb.address_hash < ^address_hash) - ) - end - @doc """ Builds an `Ecto.Query` to fetch the unfetched token balances. diff --git a/apps/explorer/test/explorer/chain/address/current_token_balance_test.exs b/apps/explorer/test/explorer/chain/address/current_token_balance_test.exs new file mode 100644 index 0000000000..f02538f96b --- /dev/null +++ b/apps/explorer/test/explorer/chain/address/current_token_balance_test.exs @@ -0,0 +1,149 @@ +defmodule Explorer.Chain.Address.CurrentTokenBalanceTest do + use Explorer.DataCase + + alias Explorer.{Chain, PagingOptions, Repo} + alias Explorer.Chain.Token + alias Explorer.Chain.Address.CurrentTokenBalance + + describe "token_holders_ordered_by_value/2" do + test "returns the last value for each address" do + %Token{contract_address_hash: contract_address_hash} = insert(:token) + address_a = insert(:address) + address_b = insert(:address) + + insert( + :address_current_token_balance, + address: address_a, + token_contract_address_hash: contract_address_hash, + value: 5000 + ) + + insert( + :address_current_token_balance, + address: address_b, + block_number: 1001, + token_contract_address_hash: contract_address_hash, + value: 4000 + ) + + token_holders_count = + contract_address_hash + |> CurrentTokenBalance.token_holders_ordered_by_value() + |> Repo.all() + |> Enum.count() + + assert token_holders_count == 2 + end + + test "sort by the highest value" do + %Token{contract_address_hash: contract_address_hash} = insert(:token) + address_a = insert(:address) + address_b = insert(:address) + address_c = insert(:address) + + insert( + :address_current_token_balance, + address: address_a, + token_contract_address_hash: contract_address_hash, + value: 5000 + ) + + insert( + :address_current_token_balance, + address: address_b, + token_contract_address_hash: contract_address_hash, + value: 4000 + ) + + insert( + :address_current_token_balance, + address: address_c, + token_contract_address_hash: contract_address_hash, + value: 15000 + ) + + token_holders_values = + contract_address_hash + |> CurrentTokenBalance.token_holders_ordered_by_value() + |> Repo.all() + |> Enum.map(&Decimal.to_integer(&1.value)) + + assert token_holders_values == [15_000, 5_000, 4_000] + end + + test "returns only token balances that have value greater than 0" do + %Token{contract_address_hash: contract_address_hash} = insert(:token) + + insert( + :address_current_token_balance, + token_contract_address_hash: contract_address_hash, + value: 0 + ) + + result = + contract_address_hash + |> CurrentTokenBalance.token_holders_ordered_by_value() + |> Repo.all() + + assert result == [] + end + + test "ignores the burn address" do + {:ok, burn_address_hash} = Chain.string_to_address_hash("0x0000000000000000000000000000000000000000") + + burn_address = insert(:address, hash: burn_address_hash) + + %Token{contract_address_hash: contract_address_hash} = insert(:token) + + insert( + :address_current_token_balance, + address: burn_address, + token_contract_address_hash: contract_address_hash, + value: 1000 + ) + + result = + contract_address_hash + |> CurrentTokenBalance.token_holders_ordered_by_value() + |> Repo.all() + + assert result == [] + end + + test "paginates the result by value and different address" do + address_a = build(:address, hash: "0xcb2cf1fd3199584ac5faa16c6aca49472dc6495a") + address_b = build(:address, hash: "0x5f26097334b6a32b7951df61fd0c5803ec5d8354") + + %Token{contract_address_hash: contract_address_hash} = insert(:token) + + first_page = + insert( + :address_current_token_balance, + address: address_a, + token_contract_address_hash: contract_address_hash, + value: 4000 + ) + + second_page = + insert( + :address_current_token_balance, + address: address_b, + token_contract_address_hash: contract_address_hash, + value: 4000 + ) + + paging_options = %PagingOptions{ + key: {first_page.value, first_page.address_hash}, + page_size: 2 + } + + result_paginated = + contract_address_hash + |> CurrentTokenBalance.token_holders_ordered_by_value(paging_options: paging_options) + |> Repo.all() + |> Enum.map(& &1.address_hash) + + assert result_paginated == [second_page.address_hash] + end + end +end diff --git a/apps/explorer/test/explorer/chain_test.exs b/apps/explorer/test/explorer/chain_test.exs index 414a54e217..0634f02bf0 100644 --- a/apps/explorer/test/explorer/chain_test.exs +++ b/apps/explorer/test/explorer/chain_test.exs @@ -2956,173 +2956,32 @@ defmodule Explorer.ChainTest do end describe "fetch_token_holders_from_token_hash/2" do - test "returns the last value for each address" do + test "returns the token holders" do %Token{contract_address_hash: contract_address_hash} = insert(:token) - address = insert(:address) + address_a = insert(:address) + address_b = insert(:address) insert( - :token_balance, - address: address, - block_number: 1000, + :address_current_token_balance, + address: address_a, token_contract_address_hash: contract_address_hash, value: 5000 ) insert( - :token_balance, - block_number: 1001, - token_contract_address_hash: contract_address_hash, - value: 4000 - ) - - insert( - :token_balance, - address: address, - block_number: 1002, - token_contract_address_hash: contract_address_hash, - value: 2000 - ) - - values = - contract_address_hash - |> Chain.fetch_token_holders_from_token_hash([]) - |> Enum.map(&Decimal.to_integer(&1.value)) - - assert values == [4000, 2000] - end - - test "sort by the highest value" do - %Token{contract_address_hash: contract_address_hash} = insert(:token) - - insert( - :token_balance, - block_number: 1000, - token_contract_address_hash: contract_address_hash, - value: 2000 - ) - - insert( - :token_balance, + :address_current_token_balance, + address: address_b, block_number: 1001, token_contract_address_hash: contract_address_hash, - value: 1000 - ) - - insert( - :token_balance, - block_number: 1002, - token_contract_address_hash: contract_address_hash, value: 4000 ) - insert( - :token_balance, - block_number: 1002, - token_contract_address_hash: contract_address_hash, - value: 3000 - ) - - values = + token_holders_count = contract_address_hash |> Chain.fetch_token_holders_from_token_hash([]) - |> Enum.map(&Decimal.to_integer(&1.value)) - - assert values == [4000, 3000, 2000, 1000] - end - - test "returns only token balances that have value" do - %Token{contract_address_hash: contract_address_hash} = insert(:token) - - insert( - :token_balance, - token_contract_address_hash: contract_address_hash, - value: 0 - ) - - assert Chain.fetch_token_holders_from_token_hash(contract_address_hash, []) == [] - end - - test "returns an empty list when there are no address with value greater than 0" do - %Token{contract_address_hash: contract_address_hash} = insert(:token) - - insert(:token_balance, value: 1000) - - assert Chain.fetch_token_holders_from_token_hash(contract_address_hash, []) == [] - end - - test "ignores the burn address" do - {:ok, burn_address_hash} = Chain.string_to_address_hash("0x0000000000000000000000000000000000000000") - - burn_address = insert(:address, hash: burn_address_hash) - - %Token{contract_address_hash: contract_address_hash} = insert(:token) - - insert( - :token_balance, - address: burn_address, - token_contract_address_hash: contract_address_hash, - value: 1000 - ) - - assert Chain.fetch_token_holders_from_token_hash(contract_address_hash, []) == [] - end - - test "paginates the result by value and different address" do - address_a = build(:address, hash: "0xcb2cf1fd3199584ac5faa16c6aca49472dc6495a") - address_b = build(:address, hash: "0x5f26097334b6a32b7951df61fd0c5803ec5d8354") - - %Token{contract_address_hash: contract_address_hash} = insert(:token) - - first_page = - insert( - :token_balance, - address: address_a, - token_contract_address_hash: contract_address_hash, - value: 4000 - ) - - second_page = - insert( - :token_balance, - address: address_b, - token_contract_address_hash: contract_address_hash, - value: 4000 - ) - - paging_options = %PagingOptions{ - key: {first_page.value, first_page.address_hash}, - page_size: 2 - } - - holders_paginated = - contract_address_hash - |> Chain.fetch_token_holders_from_token_hash(paging_options: paging_options) - |> Enum.map(& &1.address_hash) - - assert holders_paginated == [second_page.address_hash] - end - - test "considers the last block only if it has value" do - address = insert(:address, hash: "0x5f26097334b6a32b7951df61fd0c5803ec5d8354") - %Token{contract_address_hash: contract_address_hash} = insert(:token) - - insert( - :token_balance, - address: address, - block_number: 1000, - token_contract_address_hash: contract_address_hash, - value: 5000 - ) - - insert( - :token_balance, - address: address, - block_number: 1002, - token_contract_address_hash: contract_address_hash, - value: 0 - ) + |> Enum.count() - assert Chain.fetch_token_holders_from_token_hash(contract_address_hash, []) == [] + assert token_holders_count == 2 end end From a660b662fd4056d1f1396dae0cb1416ba2819259 Mon Sep 17 00:00:00 2001 From: Sebastian Abondano Date: Wed, 31 Oct 2018 16:12:36 -0400 Subject: [PATCH 36/42] Adds hash to GraphQL block object Why: * For GraphQL API consumers to be able to fetch the hash for a given block. Sample usage: ``` query ($number: Int!) { block(number: $number) { hash } } ``` * Issue link: n/a This change addresses the need by: * Editing the block object in `BlockScoutWeb.Schema.Types` to include a hash field of type `:full_hash`. --- apps/block_scout_web/lib/block_scout_web/schema/types.ex | 1 + .../test/block_scout_web/schema/query/block_test.exs | 2 ++ 2 files changed, 3 insertions(+) diff --git a/apps/block_scout_web/lib/block_scout_web/schema/types.ex b/apps/block_scout_web/lib/block_scout_web/schema/types.ex index 66da7a55d4..14d2cb3c43 100644 --- a/apps/block_scout_web/lib/block_scout_web/schema/types.ex +++ b/apps/block_scout_web/lib/block_scout_web/schema/types.ex @@ -12,6 +12,7 @@ defmodule BlockScoutWeb.Schema.Types do structure that they form is called a "blockchain". """ object :block do + field(:hash, :full_hash) field(:consensus, :boolean) field(:difficulty, :decimal) field(:gas_limit, :decimal) diff --git a/apps/block_scout_web/test/block_scout_web/schema/query/block_test.exs b/apps/block_scout_web/test/block_scout_web/schema/query/block_test.exs index a635614f27..432c5ebf2a 100644 --- a/apps/block_scout_web/test/block_scout_web/schema/query/block_test.exs +++ b/apps/block_scout_web/test/block_scout_web/schema/query/block_test.exs @@ -8,6 +8,7 @@ defmodule BlockScoutWeb.Schema.Query.BlockTest do query = """ query ($number: Int!) { block(number: $number) { + hash consensus difficulty gas_limit @@ -31,6 +32,7 @@ defmodule BlockScoutWeb.Schema.Query.BlockTest do assert json_response(conn, 200) == %{ "data" => %{ "block" => %{ + "hash" => to_string(block.hash), "consensus" => block.consensus, "difficulty" => to_string(block.difficulty), "gas_limit" => to_string(block.gas_limit), From 67c68b6f416453eea691be66bbf9ef348f8449c2 Mon Sep 17 00:00:00 2001 From: Sebastian Abondano Date: Wed, 31 Oct 2018 14:54:46 -0400 Subject: [PATCH 37/42] GraphQL API query to get addresses by hash Why: * We'd like to support GraphQL API queries for getting single or multiple addresses by hashes. API users could use this instead of the `balance` and `balancemulti` actions on the RPC API. Sample document: ``` query ($hashes: [AddressHash!]!) { addresses(hashes: $hashes) { hash fetched_coin_balance fetched_coin_balance_block_number contract_code } } ``` * Issue link: n/a This change addresses the need by: * Creating `BlockScoutWeb.Resolvers.Address` with a single resolver function that gets addresses by a list of hashes. * Adding `:data` scalar to `BlockScoutWeb.Schema.Scalars`. * Adding `address` object type to `BlockScoutWeb.Schema.Types`. Uses new `:data` scalar mentioned above. * Adding `addresses` field to query in `BlockScoutWeb.Schema`. Uses the new `address` object type and the resolver function mentioned above. * Editing `Abinthe.Plug` and `GraphiQL` in router to analyze complexity and set `max_complexity` to 50. --- .../lib/block_scout_web/resolvers/address.ex | 12 ++ .../lib/block_scout_web/router.ex | 10 +- .../lib/block_scout_web/schema.ex | 9 +- .../lib/block_scout_web/schema/scalars.ex | 19 ++- .../lib/block_scout_web/schema/types.ex | 10 ++ .../schema/query/address_test.exs | 142 ++++++++++++++++++ 6 files changed, 198 insertions(+), 4 deletions(-) create mode 100644 apps/block_scout_web/lib/block_scout_web/resolvers/address.ex create mode 100644 apps/block_scout_web/test/block_scout_web/schema/query/address_test.exs diff --git a/apps/block_scout_web/lib/block_scout_web/resolvers/address.ex b/apps/block_scout_web/lib/block_scout_web/resolvers/address.ex new file mode 100644 index 0000000000..f731f1c7cd --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/resolvers/address.ex @@ -0,0 +1,12 @@ +defmodule BlockScoutWeb.Resolvers.Address do + @moduledoc false + + alias Explorer.Chain + + def get_by(_, %{hashes: hashes}, _) do + case Chain.hashes_to_addresses(hashes) do + [] -> {:error, "Addresses not found."} + result -> {:ok, result} + end + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/router.ex b/apps/block_scout_web/lib/block_scout_web/router.ex index b249f03174..dcf9717329 100644 --- a/apps/block_scout_web/lib/block_scout_web/router.ex +++ b/apps/block_scout_web/lib/block_scout_web/router.ex @@ -38,12 +38,18 @@ defmodule BlockScoutWeb.Router do }) end - forward("/graphql", Absinthe.Plug, schema: BlockScoutWeb.Schema) + forward("/graphql", Absinthe.Plug, + schema: BlockScoutWeb.Schema, + analyze_complexity: true, + max_complexity: 50 + ) forward("/graphiql", Absinthe.Plug.GraphiQL, schema: BlockScoutWeb.Schema, interface: :playground, - socket: BlockScoutWeb.UserSocket + socket: BlockScoutWeb.UserSocket, + analyze_complexity: true, + max_complexity: 50 ) scope "/", BlockScoutWeb do diff --git a/apps/block_scout_web/lib/block_scout_web/schema.ex b/apps/block_scout_web/lib/block_scout_web/schema.ex index a50d29f3b9..03b2ee7c8e 100644 --- a/apps/block_scout_web/lib/block_scout_web/schema.ex +++ b/apps/block_scout_web/lib/block_scout_web/schema.ex @@ -3,11 +3,18 @@ defmodule BlockScoutWeb.Schema do use Absinthe.Schema - alias BlockScoutWeb.Resolvers.{Block, Transaction} + alias BlockScoutWeb.Resolvers.{Address, Block, Transaction} import_types(BlockScoutWeb.Schema.Types) query do + @desc "Gets addresses by address hash." + field :addresses, list_of(:address) do + arg(:hashes, non_null(list_of(non_null(:address_hash)))) + resolve(&Address.get_by/3) + complexity(fn %{hashes: hashes}, child_complexity -> length(hashes) * child_complexity end) + end + @desc "Gets a block by number." field :block, :block do arg(:number, non_null(:integer)) diff --git a/apps/block_scout_web/lib/block_scout_web/schema/scalars.ex b/apps/block_scout_web/lib/block_scout_web/schema/scalars.ex index 5f71182cab..de42a24512 100644 --- a/apps/block_scout_web/lib/block_scout_web/schema/scalars.ex +++ b/apps/block_scout_web/lib/block_scout_web/schema/scalars.ex @@ -3,7 +3,7 @@ defmodule BlockScoutWeb.Schema.Scalars do use Absinthe.Schema.Notation - alias Explorer.Chain.{Hash, Wei} + alias Explorer.Chain.{Data, Hash, Wei} alias Explorer.Chain.Hash.{Address, Full, Nonce} @desc """ @@ -24,6 +24,23 @@ defmodule BlockScoutWeb.Schema.Scalars do serialize(&to_string/1) end + @desc """ + An unpadded hexadecimal number with 0 or more digits. Each pair of digits + maps directly to a byte in the underlying binary representation. When + interpreted as a number, it should be treated as big-endian. + """ + scalar :data do + parse(fn + %Absinthe.Blueprint.Input.String{value: value} -> + Data.cast(value) + + _ -> + :error + end) + + serialize(&to_string/1) + end + @desc """ A 32-byte [KECCAK-256](https://en.wikipedia.org/wiki/SHA-3) hash. """ diff --git a/apps/block_scout_web/lib/block_scout_web/schema/types.ex b/apps/block_scout_web/lib/block_scout_web/schema/types.ex index 14d2cb3c43..b88306585b 100644 --- a/apps/block_scout_web/lib/block_scout_web/schema/types.ex +++ b/apps/block_scout_web/lib/block_scout_web/schema/types.ex @@ -6,6 +6,16 @@ defmodule BlockScoutWeb.Schema.Types do import_types(Absinthe.Type.Custom) import_types(BlockScoutWeb.Schema.Scalars) + @desc """ + A stored representation of a Web3 address. + """ + object :address do + field(:hash, :address_hash) + field(:fetched_coin_balance, :wei) + field(:fetched_coin_balance_block_number, :integer) + field(:contract_code, :data) + end + @desc """ A package of data that contains zero or more transactions, the hash of the previous block ("parent"), and optionally other data. Because each block (except for the initial "genesis block") points to the previous block, the data diff --git a/apps/block_scout_web/test/block_scout_web/schema/query/address_test.exs b/apps/block_scout_web/test/block_scout_web/schema/query/address_test.exs new file mode 100644 index 0000000000..4854cf4738 --- /dev/null +++ b/apps/block_scout_web/test/block_scout_web/schema/query/address_test.exs @@ -0,0 +1,142 @@ +defmodule BlockScoutWeb.Schema.Query.AddressTest do + use BlockScoutWeb.ConnCase + + describe "address field" do + test "with valid argument 'hashes', returns all expected fields", %{conn: conn} do + address = insert(:address, fetched_coin_balance: 100) + + query = """ + query ($hashes: [AddressHash!]!) { + addresses(hashes: $hashes) { + hash + fetched_coin_balance + fetched_coin_balance_block_number + contract_code + } + } + """ + + variables = %{"hashes" => to_string(address.hash)} + + conn = get(conn, "/graphql", query: query, variables: variables) + + assert json_response(conn, 200) == %{ + "data" => %{ + "addresses" => [ + %{ + "hash" => to_string(address.hash), + "fetched_coin_balance" => to_string(address.fetched_coin_balance.value), + "fetched_coin_balance_block_number" => address.fetched_coin_balance_block_number, + "contract_code" => nil + } + ] + } + } + end + + test "with contract address, `contract_code` is serialized as expected", %{conn: conn} do + address = insert(:contract_address, fetched_coin_balance: 100) + + query = """ + query ($hashes: [AddressHash!]!) { + addresses(hashes: $hashes) { + contract_code + } + } + """ + + variables = %{"hashes" => to_string(address.hash)} + + conn = get(conn, "/graphql", query: query, variables: variables) + + assert json_response(conn, 200) == %{ + "data" => %{ + "addresses" => [ + %{ + "contract_code" => to_string(address.contract_code) + } + ] + } + } + end + + test "errors for non-existent address hashes", %{conn: conn} do + address = build(:address) + + query = """ + query ($hashes: [AddressHash!]!) { + addresses(hashes: $hashes) { + fetched_coin_balance + } + } + """ + + variables = %{"hashes" => [to_string(address.hash)]} + + conn = get(conn, "/graphql", query: query, variables: variables) + + assert %{"errors" => [error]} = json_response(conn, 200) + assert error["message"] =~ ~s(Addresses not found.) + end + + test "errors if argument 'hashes' is missing", %{conn: conn} do + query = """ + query { + addresses { + fetched_coin_balance + } + } + """ + + variables = %{} + + conn = get(conn, "/graphql", query: query, variables: variables) + + assert %{"errors" => [error]} = json_response(conn, 200) + assert error["message"] == ~s(In argument "hashes": Expected type "[AddressHash!]!", found null.) + end + + test "errors if argument 'hashes' is not a list of address hashes", %{conn: conn} do + query = """ + query ($hashes: [AddressHash!]!) { + addresses(hashes: $hashes) { + fetched_coin_balance + } + } + """ + + variables = %{"hashes" => ["someInvalidHash"]} + + conn = get(conn, "/graphql", query: query, variables: variables) + + assert %{"errors" => [error]} = json_response(conn, 200) + assert error["message"] =~ ~s(Argument "hashes" has invalid value) + end + + test "correlates complexity to size of 'hashes' argument", %{conn: conn} do + # max of 12 addresses with four fields of complexity 1 can be fetched + # per query: + # 12 * 4 = 48, which is less than a max complexity of 50 + hashes = 13 |> build_list(:address) |> Enum.map(&to_string(&1.hash)) + + query = """ + query ($hashes: [AddressHash!]!) { + addresses(hashes: $hashes) { + hash + fetched_coin_balance + fetched_coin_balance_block_number + contract_code + } + } + """ + + variables = %{"hashes" => hashes} + + conn = get(conn, "/graphql", query: query, variables: variables) + + assert %{"errors" => [error1, error2]} = json_response(conn, 200) + assert error1["message"] =~ ~s(Field addresses is too complex) + assert error2["message"] =~ ~s(Operation is too complex) + end + end +end From ef001687640a246e7171c773a1bd094906e83971 Mon Sep 17 00:00:00 2001 From: svenski123 Date: Sat, 3 Nov 2018 02:04:48 +0000 Subject: [PATCH 38/42] Performance improvements to generation of top accounts page The list_top_addresses query is now a left join of addresses and transactions tables, returning the top 250 address and their respective transaction counts at the same time. The call to transaction_count() which hits the database has been removed from the per account tile template, and the value is now passed in from the above query. balance_percentage/2() has been added, taking total_supply as an argument rather than querying the database. The per account tile template now uses this version, with total_supply queried once and passed in. It may be worth adding an index to the addresses table i.e. create index addresses_fetched_coin_balance_hash_index on addresses (fetched_coin_balance desc nulls last, hash asc); Note the list_top_address query makes use of a SQL fragment as the version of Ecto used by blockscout does not support coalesce. --- .../controllers/address_controller.ex | 5 +++-- .../templates/address/_tile.html.eex | 4 ++-- .../templates/address/index.html.eex | 3 ++- .../lib/block_scout_web/views/address_view.ex | 10 +++++++--- apps/explorer/lib/explorer/chain.ex | 18 ++++++++++++------ 5 files changed, 26 insertions(+), 14 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_controller.ex index 3c29ffa4f3..22825ae63a 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_controller.ex @@ -7,9 +7,10 @@ defmodule BlockScoutWeb.AddressController do def index(conn, _params) do render(conn, "index.html", - addresses: Chain.list_top_addresses(), + address_tx_count_pairs: Chain.list_top_addresses(), address_estimated_count: Chain.address_estimated_count(), - exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null() + exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(), + total_supply: Chain.total_supply() ) end diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address/_tile.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address/_tile.html.eex index 8963b44a54..eddf794799 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address/_tile.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address/_tile.html.eex @@ -15,7 +15,7 @@ - <%= transaction_count(@address) %> + <%= @tx_count %> <%= gettext "Transactions sent" %> <% if validator?(@address) do %> @@ -36,7 +36,7 @@ data-usd-exchange-rate="<%= @exchange_rate.usd_value %>"> - (<%= balance_percentage(@address) %>) + (<%= balance_percentage(@address, @total_supply) %>)
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address/index.html.eex index 13db6de433..851aa9a0d7 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address/index.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address/index.html.eex @@ -9,9 +9,10 @@

- <%= for {address, index} <- Enum.with_index(@addresses, 1) do %> + <%= for {{address, tx_count}, index} <- Enum.with_index(@address_tx_count_pairs, 1) do %> <%= render "_tile.html", address: address, index: index, exchange_rate: @exchange_rate, + total_supply: @total_supply, tx_count: tx_count, validation_count: validation_count(address) %> <% end %> diff --git a/apps/block_scout_web/lib/block_scout_web/views/address_view.ex b/apps/block_scout_web/lib/block_scout_web/views/address_view.ex index 041d1e9aa7..a9fdfbc746 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/address_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/address_view.ex @@ -1,7 +1,7 @@ defmodule BlockScoutWeb.AddressView do use BlockScoutWeb, :view - import BlockScoutWeb.AddressController, only: [transaction_count: 1, validation_count: 1] + import BlockScoutWeb.AddressController, only: [validation_count: 1] alias BlockScoutWeb.LayoutView alias Explorer.Chain @@ -94,16 +94,20 @@ defmodule BlockScoutWeb.AddressView do format_wei_value(balance, :ether) end - def balance_percentage(%Address{fetched_coin_balance: balance}) do + def balance_percentage(%Address{fetched_coin_balance: balance}, total_supply) do balance |> Wei.to(:ether) - |> Decimal.div(Decimal.new(Chain.total_supply())) + |> Decimal.div(Decimal.new(total_supply)) |> Decimal.mult(100) |> Decimal.round(4) |> Decimal.to_string(:normal) |> Kernel.<>("% #{gettext("Market Cap")}") end + def balance_percentage(%Address{fetched_coin_balance: _} = address) do + balance_percentage(address, Chain.total_supply()) + end + def balance_block_number(%Address{fetched_coin_balance_block_number: nil}), do: "" def balance_block_number(%Address{fetched_coin_balance_block_number: fetched_coin_balance_block_number}) do diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index 51eac0ed1a..e48eeb957a 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -920,13 +920,19 @@ defmodule Explorer.Chain do Lists the top 250 `t:Explorer.Chain.Address.t/0`'s' in descending order based on coin balance. """ - @spec list_top_addresses :: [Address.t()] + @spec list_top_addresses :: [{Address.t(), non_neg_integer()}] def list_top_addresses do - Address - |> limit(250) - |> order_by(desc: :fetched_coin_balance, asc: :hash) - |> where([address], address.fetched_coin_balance > ^0) - |> Repo.all() + query = + from a in Address, + left_join: t in Transaction, + on: a.hash == t.from_address_hash, + where: a.fetched_coin_balance > ^0, + group_by: [a.hash, a.fetched_coin_balance], + order_by: [desc: a.fetched_coin_balance, asc: a.hash], + select: {a, fragment("coalesce(1 + max(?), 0)", t.nonce)}, + limit: 250 + + Repo.all(query) end @doc """ From 20543a2ca0ebc02835a1ac606b59afdf6843ba58 Mon Sep 17 00:00:00 2001 From: svenski123 Date: Sat, 3 Nov 2018 03:17:28 +0000 Subject: [PATCH 39/42] Fixup test cases --- apps/explorer/test/explorer/chain_test.exs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/explorer/test/explorer/chain_test.exs b/apps/explorer/test/explorer/chain_test.exs index 0634f02bf0..a27a28df6f 100644 --- a/apps/explorer/test/explorer/chain_test.exs +++ b/apps/explorer/test/explorer/chain_test.exs @@ -1182,7 +1182,10 @@ defmodule Explorer.ChainTest do |> Enum.map(&insert(:address, fetched_coin_balance: &1)) |> Enum.map(& &1.hash) - assert address_hashes == Enum.map(Chain.list_top_addresses(), & &1.hash) + assert address_hashes == + Chain.list_top_addresses() + |> Enum.map(fn {address, _transaction_count} -> address end) + |> Enum.map(& &1.hash) end test "with top addresses in order with matching value" do @@ -1201,7 +1204,10 @@ defmodule Explorer.ChainTest do |> insert(fetched_coin_balance: 4, hash: Enum.fetch!(test_hashes, 4)) |> Map.fetch!(:hash) - assert [first_result_hash | tail] == Enum.map(Chain.list_top_addresses(), & &1.hash) + assert [first_result_hash | tail] == + Chain.list_top_addresses() + |> Enum.map(fn {address, _transaction_count} -> address end) + |> Enum.map(& &1.hash) end end From ae9f4140ea002fa56a3b934f2102f00595630467 Mon Sep 17 00:00:00 2001 From: svenski123 Date: Sat, 3 Nov 2018 03:41:24 +0000 Subject: [PATCH 40/42] Fixup test cases --- .../controllers/address_controller_test.exs | 4 +++- apps/explorer/lib/explorer/chain.ex | 17 +++++++++-------- apps/explorer/test/explorer/chain_test.exs | 12 ++++++------ 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/address_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/address_controller_test.exs index 1ad14d2952..071667cf77 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/address_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/address_controller_test.exs @@ -10,7 +10,9 @@ defmodule BlockScoutWeb.AddressControllerTest do conn = get(conn, address_path(conn, :index)) - assert conn.assigns.addresses |> Enum.map(& &1.hash) == address_hashes + assert conn.assigns.address_tx_count_pairs + |> Enum.map(fn {address, _transaction_count} -> address end) + |> Enum.map(& &1.hash) == address_hashes end end diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index e48eeb957a..8cfb97bfcf 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -923,14 +923,15 @@ defmodule Explorer.Chain do @spec list_top_addresses :: [{Address.t(), non_neg_integer()}] def list_top_addresses do query = - from a in Address, - left_join: t in Transaction, - on: a.hash == t.from_address_hash, - where: a.fetched_coin_balance > ^0, - group_by: [a.hash, a.fetched_coin_balance], - order_by: [desc: a.fetched_coin_balance, asc: a.hash], - select: {a, fragment("coalesce(1 + max(?), 0)", t.nonce)}, - limit: 250 + from(a in Address, + left_join: t in Transaction, + on: a.hash == t.from_address_hash, + where: a.fetched_coin_balance > ^0, + group_by: [a.hash, a.fetched_coin_balance], + order_by: [desc: a.fetched_coin_balance, asc: a.hash], + select: {a, fragment("coalesce(1 + max(?), 0)", t.nonce)}, + limit: 250 + ) Repo.all(query) end diff --git a/apps/explorer/test/explorer/chain_test.exs b/apps/explorer/test/explorer/chain_test.exs index a27a28df6f..0ed17acb4e 100644 --- a/apps/explorer/test/explorer/chain_test.exs +++ b/apps/explorer/test/explorer/chain_test.exs @@ -1183,9 +1183,9 @@ defmodule Explorer.ChainTest do |> Enum.map(& &1.hash) assert address_hashes == - Chain.list_top_addresses() - |> Enum.map(fn {address, _transaction_count} -> address end) - |> Enum.map(& &1.hash) + Chain.list_top_addresses() + |> Enum.map(fn {address, _transaction_count} -> address end) + |> Enum.map(& &1.hash) end test "with top addresses in order with matching value" do @@ -1205,9 +1205,9 @@ defmodule Explorer.ChainTest do |> Map.fetch!(:hash) assert [first_result_hash | tail] == - Chain.list_top_addresses() - |> Enum.map(fn {address, _transaction_count} -> address end) - |> Enum.map(& &1.hash) + Chain.list_top_addresses() + |> Enum.map(fn {address, _transaction_count} -> address end) + |> Enum.map(& &1.hash) end end From b2e0f71071aa37115a9bae66ed3a44b06d0e1967 Mon Sep 17 00:00:00 2001 From: svenski123 Date: Sat, 3 Nov 2018 04:03:45 +0000 Subject: [PATCH 41/42] Commit results of mix gettext.extract --merge --- apps/block_scout_web/priv/gettext/default.pot | 10 +++++----- .../priv/gettext/en/LC_MESSAGES/default.po | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/block_scout_web/priv/gettext/default.pot b/apps/block_scout_web/priv/gettext/default.pot index 9ae0e22d88..761c33ba42 100644 --- a/apps/block_scout_web/priv/gettext/default.pot +++ b/apps/block_scout_web/priv/gettext/default.pot @@ -227,7 +227,7 @@ msgstr "" #: lib/block_scout_web/templates/address_validation/index.html.eex:90 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:122 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:141 -#: lib/block_scout_web/views/address_view.ex:210 +#: lib/block_scout_web/views/address_view.ex:214 msgid "Code" msgstr "" @@ -508,7 +508,7 @@ msgstr "" #: lib/block_scout_web/templates/transaction/_tabs.html.eex:14 #: lib/block_scout_web/templates/transaction/_tabs.html.eex:43 #: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:10 -#: lib/block_scout_web/views/address_view.ex:209 +#: lib/block_scout_web/views/address_view.ex:213 #: lib/block_scout_web/views/transaction_view.ex:176 msgid "Internal Transactions" msgstr "" @@ -725,7 +725,7 @@ msgstr "" #: lib/block_scout_web/templates/address_validation/index.html.eex:53 #: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:33 #: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:75 -#: lib/block_scout_web/views/address_view.ex:211 +#: lib/block_scout_web/views/address_view.ex:215 #: lib/block_scout_web/views/tokens/overview_view.ex:37 msgid "Read Contract" msgstr "" @@ -930,7 +930,7 @@ msgstr "" #: lib/block_scout_web/templates/address_validation/index.html.eex:18 #: lib/block_scout_web/templates/address_validation/index.html.eex:62 #: lib/block_scout_web/templates/address_validation/index.html.eex:70 -#: lib/block_scout_web/views/address_view.ex:207 +#: lib/block_scout_web/views/address_view.ex:211 msgid "Tokens" msgstr "" @@ -991,7 +991,7 @@ msgstr "" #: lib/block_scout_web/templates/block_transaction/index.html.eex:35 #: lib/block_scout_web/templates/chain/show.html.eex:71 #: lib/block_scout_web/templates/layout/_topnav.html.eex:35 -#: lib/block_scout_web/views/address_view.ex:208 +#: lib/block_scout_web/views/address_view.ex:212 msgid "Transactions" 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 d55c8e73de..14a11d48f5 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 @@ -227,7 +227,7 @@ msgstr "" #: lib/block_scout_web/templates/address_validation/index.html.eex:90 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:122 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:141 -#: lib/block_scout_web/views/address_view.ex:210 +#: lib/block_scout_web/views/address_view.ex:214 msgid "Code" msgstr "" @@ -508,7 +508,7 @@ msgstr "" #: lib/block_scout_web/templates/transaction/_tabs.html.eex:14 #: lib/block_scout_web/templates/transaction/_tabs.html.eex:43 #: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:10 -#: lib/block_scout_web/views/address_view.ex:209 +#: lib/block_scout_web/views/address_view.ex:213 #: lib/block_scout_web/views/transaction_view.ex:176 msgid "Internal Transactions" msgstr "" @@ -725,7 +725,7 @@ msgstr "" #: lib/block_scout_web/templates/address_validation/index.html.eex:53 #: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:33 #: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:75 -#: lib/block_scout_web/views/address_view.ex:211 +#: lib/block_scout_web/views/address_view.ex:215 #: lib/block_scout_web/views/tokens/overview_view.ex:37 msgid "Read Contract" msgstr "" @@ -930,7 +930,7 @@ msgstr "" #: lib/block_scout_web/templates/address_validation/index.html.eex:18 #: lib/block_scout_web/templates/address_validation/index.html.eex:62 #: lib/block_scout_web/templates/address_validation/index.html.eex:70 -#: lib/block_scout_web/views/address_view.ex:207 +#: lib/block_scout_web/views/address_view.ex:211 msgid "Tokens" msgstr "" @@ -991,7 +991,7 @@ msgstr "" #: lib/block_scout_web/templates/block_transaction/index.html.eex:35 #: lib/block_scout_web/templates/chain/show.html.eex:71 #: lib/block_scout_web/templates/layout/_topnav.html.eex:35 -#: lib/block_scout_web/views/address_view.ex:208 +#: lib/block_scout_web/views/address_view.ex:212 msgid "Transactions" msgstr "" From 920925ccdc4e8858c1a79bbf419365e75e0eb837 Mon Sep 17 00:00:00 2001 From: Lucas Narciso Date: Fri, 2 Nov 2018 01:41:52 -0300 Subject: [PATCH 42/42] Fix address[] display when reading Smart Contracts --- .../templates/smart_contract/_functions.html.eex | 4 ++-- .../lib/block_scout_web/views/smart_contract_view.ex | 6 ++++++ .../views/tokens/smart_contract_view_test.exs | 10 ++++++++++ 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/templates/smart_contract/_functions.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/smart_contract/_functions.html.eex index e5f193652a..3008b37cf8 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/smart_contract/_functions.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/smart_contract/_functions.html.eex @@ -45,13 +45,13 @@
<%= output["value"] %> - + <%= gettext("WEI")%> <%= gettext("ETH")%>
<% else %> - <%= output["value"] %> + <%= values(output["value"], output["type"]) %> <% end %> <% end %> <% end %> diff --git a/apps/block_scout_web/lib/block_scout_web/views/smart_contract_view.ex b/apps/block_scout_web/lib/block_scout_web/views/smart_contract_view.ex index 0d499104be..c96c5efe91 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/smart_contract_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/smart_contract_view.ex @@ -10,6 +10,12 @@ defmodule BlockScoutWeb.SmartContractView do def named_argument?(%{"name" => _}), do: true def named_argument?(_), do: false + def values(addresses, type) when type == "address[]" do + addresses + |> Enum.map(&values(&1, "address")) + |> Enum.join(", ") + end + def values(value, type) when type in ["address", "address payable"] do {:ok, address} = Explorer.Chain.Hash.Address.cast(value) to_string(address) diff --git a/apps/block_scout_web/test/block_scout_web/views/tokens/smart_contract_view_test.exs b/apps/block_scout_web/test/block_scout_web/views/tokens/smart_contract_view_test.exs index 503a206c25..5ab8133bf5 100644 --- a/apps/block_scout_web/test/block_scout_web/views/tokens/smart_contract_view_test.exs +++ b/apps/block_scout_web/test/block_scout_web/views/tokens/smart_contract_view_test.exs @@ -80,6 +80,16 @@ defmodule BlockScoutWeb.SmartContractViewTest do assert SmartContractView.values(value, "address payable") == "0x5f26097334b6a32b7951df61fd0c5803ec5d8354" end + test "convert each value to string and join them when receiving 'address[]' as the type" do + value = [ + <<95, 38, 9, 115, 52, 182, 163, 43, 121, 81, 223, 97, 253, 12, 88, 3, 236, 93, 131, 84>>, + <<207, 38, 14, 163, 23, 85, 86, 55, 197, 95, 112, 229, 93, 186, 141, 90, 216, 65, 76, 176>> + ] + + assert SmartContractView.values(value, "address[]") == + "0x5f26097334b6a32b7951df61fd0c5803ec5d8354, 0xcf260ea317555637c55f70e55dba8d5ad8414cb0" + end + test "returns the value when the type is neither 'address' nor 'address payable'" do value = "POA"