From 03fd6f954080be1fa4abaaeb0a3073d29936fb7f Mon Sep 17 00:00:00 2001 From: saneery Date: Tue, 21 May 2019 15:21:00 +0300 Subject: [PATCH 01/57] create staking_pool schema --- .../lib/explorer/chain/staking_pool.ex | 73 +++++++++++++++++++ .../20190521104412_create_staking_pools.exs | 28 +++++++ .../test/explorer/chain/staking_pool_test.exs | 18 +++++ apps/explorer/test/support/factory.ex | 37 +++++----- 4 files changed, 139 insertions(+), 17 deletions(-) create mode 100644 apps/explorer/lib/explorer/chain/staking_pool.ex create mode 100644 apps/explorer/priv/repo/migrations/20190521104412_create_staking_pools.exs create mode 100644 apps/explorer/test/explorer/chain/staking_pool_test.exs diff --git a/apps/explorer/lib/explorer/chain/staking_pool.ex b/apps/explorer/lib/explorer/chain/staking_pool.ex new file mode 100644 index 0000000000..59bffdd43e --- /dev/null +++ b/apps/explorer/lib/explorer/chain/staking_pool.ex @@ -0,0 +1,73 @@ +defmodule Explorer.Chain.StakingPool do + @moduledoc """ + The representation of staking pool from POSDAO network. + Staking pools might be candidate or validator. + """ + use Ecto.Schema + import Ecto.Changeset + + alias Explorer.Chain.{ + Wei, + Address, + Hash + } + + @attrs ~w( + is_active delegators_count staked_amount self_staked_amount is_validator + was_validator_count is_banned was_banned_count banned_until likelihood + staked_ratio min_delegators_stake min_candidate_stake + staking_address_hash mining_address_hash + )a + + schema "staking_pools" do + field(:banned_until, :integer) + field(:delegators_count, :integer) + field(:is_active, :boolean, default: false) + field(:is_banned, :boolean, default: false) + field(:is_validator, :boolean, default: false) + field(:likelihood, :decimal) + field(:staked_ratio, :decimal) + field(:min_candidate_stake, Wei) + field(:min_delegators_stake, Wei) + field(:self_staked_amount, Wei) + field(:staked_amount, Wei) + field(:was_banned_count, :integer) + field(:was_validator_count, :integer) + + belongs_to( + :staking_address, + Address, + foreign_key: :staking_address_hash, + references: :hash, + type: Hash.Address + ) + + belongs_to( + :mining_address, + Address, + foreign_key: :mining_address_hash, + references: :hash, + type: Hash.Address + ) + + timestamps() + end + + @doc false + def changeset(staking_pool, attrs) do + staking_pool + |> cast(attrs, @attrs) + |> validate_required(@attrs) + |> validate_staked_amount() + end + + defp validate_staked_amount(%{valid?: false} = c), do: c + + defp validate_staked_amount(changeset) do + if get_field(changeset, :staked_amount) < get_field(changeset, :self_staked_amount) do + add_error(changeset, :staked_amount, "must be greater than self_staked_amount") + else + changeset + end + end +end diff --git a/apps/explorer/priv/repo/migrations/20190521104412_create_staking_pools.exs b/apps/explorer/priv/repo/migrations/20190521104412_create_staking_pools.exs new file mode 100644 index 0000000000..2499641eda --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20190521104412_create_staking_pools.exs @@ -0,0 +1,28 @@ +defmodule Explorer.Repo.Migrations.CreateStakingPools do + use Ecto.Migration + + def change do + create table(:staking_pools) do + add(:is_active, :boolean, default: false, null: false) + add(:delegators_count, :integer) + add(:staked_amount, :numeric, precision: 100) + add(:self_staked_amount, :numeric, precision: 100) + add(:is_validator, :boolean, default: false, null: false) + add(:was_validator_count, :integer) + add(:is_banned, :boolean, default: false, null: false) + add(:was_banned_count, :integer) + add(:banned_until, :bigint) + add(:likelihood, :decimal) + add(:staked_ratio, :decimal) + add(:min_delegators_stake, :numeric, precision: 100) + add(:min_candidate_stake, :numeric, precision: 100) + add(:staking_address_hash, references(:addresses, on_delete: :nothing, column: :hash, type: :bytea)) + add(:mining_address_hash, references(:addresses, on_delete: :nothing, column: :hash, type: :bytea)) + + timestamps() + end + + create(index(:staking_pools, [:staking_address_hash])) + create(index(:staking_pools, [:mining_address_hash])) + end +end diff --git a/apps/explorer/test/explorer/chain/staking_pool_test.exs b/apps/explorer/test/explorer/chain/staking_pool_test.exs new file mode 100644 index 0000000000..78d01a0f19 --- /dev/null +++ b/apps/explorer/test/explorer/chain/staking_pool_test.exs @@ -0,0 +1,18 @@ +defmodule Explorer.Chain.StakingPoolTest do + use Explorer.DataCase + + alias Explorer.Chain.StakingPool + + describe "changeset/2" do + test "with valid attributes" do + params = params_for(:staking_pool) + changeset = StakingPool.changeset(%StakingPool{}, params) |> IO.inspect() + assert changeset.valid? + end + + test "with invalid attributes" do + changeset = StakingPool.changeset(%StakingPool{}, %{staking_address_hash: 0}) + refute changeset.valid? + end + end +end diff --git a/apps/explorer/test/support/factory.ex b/apps/explorer/test/support/factory.ex index 115b07e066..6a08c2df8b 100644 --- a/apps/explorer/test/support/factory.ex +++ b/apps/explorer/test/support/factory.ex @@ -26,7 +26,8 @@ defmodule Explorer.Factory do SmartContract, Token, TokenTransfer, - Transaction + Transaction, + StakingPool } alias Explorer.Market.MarketHistory @@ -611,22 +612,24 @@ defmodule Explorer.Factory do end def staking_pool_factory do - %{ - address_hash: address_hash(), - metadata: %{ - banned_unitil: 0, - delegators_count: 0, - is_active: true, - is_banned: false, - is_validator: true, - mining_address: address_hash(), - retries_count: 1, - staked_amount: 25, - was_banned_count: 0, - was_validator_count: 1 - }, - name: "anonymous", - primary: true + wei_per_ether = 1_000_000_000_000_000_000 + + %StakingPool{ + staking_address_hash: address_hash(), + mining_address_hash: address_hash(), + banned_until: 0, + delegators_count: 0, + is_active: true, + is_banned: false, + is_validator: true, + staked_amount: wei_per_ether * 500, + self_staked_amount: wei_per_ether * 300, + was_banned_count: 0, + was_validator_count: 1, + min_delegators_stake: wei_per_ether * 100, + min_candidate_stake: wei_per_ether * 200, + staked_ratio: 0, + likelihood: 0, } end end From 85401da1d0ff3ab2196a0f12ab482f6e61538c9c Mon Sep 17 00:00:00 2001 From: saneery Date: Thu, 23 May 2019 11:33:47 +0300 Subject: [PATCH 02/57] add cast! and dump! to Wei module --- apps/explorer/lib/explorer/chain/wei.ex | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/apps/explorer/lib/explorer/chain/wei.ex b/apps/explorer/lib/explorer/chain/wei.ex index 4b12d3deb0..0739885cf8 100644 --- a/apps/explorer/lib/explorer/chain/wei.ex +++ b/apps/explorer/lib/explorer/chain/wei.ex @@ -68,6 +68,11 @@ defmodule Explorer.Chain.Wei do @impl Ecto.Type def cast(_), do: :error + def cast!(arg) do + {:ok, wei} = cast(arg) + wei + end + @impl Ecto.Type def dump(%__MODULE__{value: %Decimal{} = decimal}) do {:ok, decimal} @@ -76,6 +81,11 @@ defmodule Explorer.Chain.Wei do @impl Ecto.Type def dump(_), do: :error + def dump!(arg) do + {:ok, decimal} = dump(arg) + decimal + end + @impl Ecto.Type def load(%Decimal{} = decimal) do {:ok, %__MODULE__{value: decimal}} From d2740b0056750bb619d7916c73dc0bc0de66dda0 Mon Sep 17 00:00:00 2001 From: saneery Date: Thu, 23 May 2019 11:43:33 +0300 Subject: [PATCH 03/57] add convert wei to integer --- apps/explorer/lib/explorer/chain/staking_pool.ex | 8 +++++--- apps/explorer/lib/explorer/chain/wei.ex | 5 +++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/apps/explorer/lib/explorer/chain/staking_pool.ex b/apps/explorer/lib/explorer/chain/staking_pool.ex index 59bffdd43e..7b69acefbf 100644 --- a/apps/explorer/lib/explorer/chain/staking_pool.ex +++ b/apps/explorer/lib/explorer/chain/staking_pool.ex @@ -15,7 +15,7 @@ defmodule Explorer.Chain.StakingPool do @attrs ~w( is_active delegators_count staked_amount self_staked_amount is_validator was_validator_count is_banned was_banned_count banned_until likelihood - staked_ratio min_delegators_stake min_candidate_stake + staked_ratio min_delegator_stake min_candidate_stake staking_address_hash mining_address_hash )a @@ -28,11 +28,12 @@ defmodule Explorer.Chain.StakingPool do field(:likelihood, :decimal) field(:staked_ratio, :decimal) field(:min_candidate_stake, Wei) - field(:min_delegators_stake, Wei) + field(:min_delegator_stake, Wei) field(:self_staked_amount, Wei) field(:staked_amount, Wei) field(:was_banned_count, :integer) field(:was_validator_count, :integer) + field(:is_deleted, :boolean, default: false) belongs_to( :staking_address, @@ -50,7 +51,7 @@ defmodule Explorer.Chain.StakingPool do type: Hash.Address ) - timestamps() + timestamps(null: false, type: :utc_datetime_usec) end @doc false @@ -59,6 +60,7 @@ defmodule Explorer.Chain.StakingPool do |> cast(attrs, @attrs) |> validate_required(@attrs) |> validate_staked_amount() + |> unique_constraint(:staking_address_hash) end defp validate_staked_amount(%{valid?: false} = c), do: c diff --git a/apps/explorer/lib/explorer/chain/wei.ex b/apps/explorer/lib/explorer/chain/wei.ex index 0739885cf8..8b557b4ed5 100644 --- a/apps/explorer/lib/explorer/chain/wei.ex +++ b/apps/explorer/lib/explorer/chain/wei.ex @@ -248,6 +248,11 @@ defmodule Explorer.Chain.Wei do @spec to(t(), :wei) :: wei() def to(%__MODULE__{value: wei}, :wei), do: wei + + @spec to(t(), :integer) :: integer() + def to(%__MODULE__{value: wei}, :integer) do + Decimal.to_integer(wei) + end end defimpl Inspect, for: Explorer.Chain.Wei do From 16393b289073e22267a8a349673dd6d0846b883d Mon Sep 17 00:00:00 2001 From: saneery Date: Thu, 23 May 2019 11:56:58 +0300 Subject: [PATCH 04/57] add typespec to staking_pool schema --- .../lib/explorer/chain/staking_pool.ex | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/apps/explorer/lib/explorer/chain/staking_pool.ex b/apps/explorer/lib/explorer/chain/staking_pool.ex index 7b69acefbf..10e678df25 100644 --- a/apps/explorer/lib/explorer/chain/staking_pool.ex +++ b/apps/explorer/lib/explorer/chain/staking_pool.ex @@ -7,9 +7,28 @@ defmodule Explorer.Chain.StakingPool do import Ecto.Changeset alias Explorer.Chain.{ - Wei, Address, - Hash + Hash, + Wei + } + + @type t :: %__MODULE__{ + staking_address_hash: Hash.Address.t(), + mining_address_hash: Hash.Address.t(), + banned_until: boolean, + delegators_count: integer, + is_active: boolean, + is_banned: boolean, + is_validator: boolean, + likelihood: integer, + staked_ratio: Decimal.t(), + min_candidate_stake: Wei.t(), + min_delegator_stake: Wei.t(), + self_staked_amount: Wei.t(), + staked_amount: Wei.t(), + was_banned_count: integer, + was_validator_count: integer, + is_deleted: boolean } @attrs ~w( From f4c25a38de58ae7942e843af88ae3449f64ea2a4 Mon Sep 17 00:00:00 2001 From: saneery Date: Thu, 23 May 2019 11:57:19 +0300 Subject: [PATCH 05/57] update migration --- .../20190521104412_create_staking_pools.exs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/apps/explorer/priv/repo/migrations/20190521104412_create_staking_pools.exs b/apps/explorer/priv/repo/migrations/20190521104412_create_staking_pools.exs index 2499641eda..aabef14d1a 100644 --- a/apps/explorer/priv/repo/migrations/20190521104412_create_staking_pools.exs +++ b/apps/explorer/priv/repo/migrations/20190521104412_create_staking_pools.exs @@ -4,6 +4,7 @@ defmodule Explorer.Repo.Migrations.CreateStakingPools do def change do create table(:staking_pools) do add(:is_active, :boolean, default: false, null: false) + add(:is_deleted, :boolean, default: false, null: false) add(:delegators_count, :integer) add(:staked_amount, :numeric, precision: 100) add(:self_staked_amount, :numeric, precision: 100) @@ -14,15 +15,15 @@ defmodule Explorer.Repo.Migrations.CreateStakingPools do add(:banned_until, :bigint) add(:likelihood, :decimal) add(:staked_ratio, :decimal) - add(:min_delegators_stake, :numeric, precision: 100) + add(:min_delegator_stake, :numeric, precision: 100) add(:min_candidate_stake, :numeric, precision: 100) - add(:staking_address_hash, references(:addresses, on_delete: :nothing, column: :hash, type: :bytea)) - add(:mining_address_hash, references(:addresses, on_delete: :nothing, column: :hash, type: :bytea)) + add(:staking_address_hash, :bytea) + add(:mining_address_hash, :bytea) - timestamps() + timestamps(null: false, type: :utc_datetime_usec) end - create(index(:staking_pools, [:staking_address_hash])) + create(index(:staking_pools, [:staking_address_hash], unique: true)) create(index(:staking_pools, [:mining_address_hash])) end end From b13019f4e865a21f1e11466de9ca473907483ec3 Mon Sep 17 00:00:00 2001 From: saneery Date: Thu, 23 May 2019 11:57:53 +0300 Subject: [PATCH 06/57] update staking pools import runner --- .../chain/import/runner/staking_pools.ex | 55 +++++++++++-------- .../import/runner/staking_pools_test.exs | 19 ++++--- .../test/explorer/chain/staking_pool_test.exs | 2 +- apps/explorer/test/support/factory.ex | 4 +- 4 files changed, 48 insertions(+), 32 deletions(-) diff --git a/apps/explorer/lib/explorer/chain/import/runner/staking_pools.ex b/apps/explorer/lib/explorer/chain/import/runner/staking_pools.ex index b21c0c4441..b5024f30cd 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/staking_pools.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/staking_pools.ex @@ -1,12 +1,12 @@ defmodule Explorer.Chain.Import.Runner.StakingPools do @moduledoc """ - Bulk imports staking pools to Address.Name tabe. + Bulk imports staking pools to StakingPool tabe. """ require Ecto.Query alias Ecto.{Changeset, Multi, Repo} - alias Explorer.Chain.{Address, Import} + alias Explorer.Chain.{Import, StakingPool, Wei} import Ecto.Query, only: [from: 2] @@ -15,10 +15,10 @@ defmodule Explorer.Chain.Import.Runner.StakingPools do # milliseconds @timeout 60_000 - @type imported :: [Address.Name.t()] + @type imported :: [StakingPool.t()] @impl Import.Runner - def ecto_schema_module, do: Address.Name + def ecto_schema_module, do: StakingPool @impl Import.Runner def option_key, do: :staking_pools @@ -53,17 +53,15 @@ defmodule Explorer.Chain.Import.Runner.StakingPools do def timeout, do: @timeout defp mark_as_deleted(repo, changes_list, %{timeout: timeout}) when is_list(changes_list) do - addresses = Enum.map(changes_list, & &1.address_hash) + addresses = Enum.map(changes_list, & &1.staking_address_hash) query = from( - address_name in Address.Name, - where: - address_name.address_hash not in ^addresses and - fragment("(?->>'is_pool')::boolean = true", address_name.metadata), + pool in StakingPool, + where: pool.staking_address_hash not in ^addresses, update: [ set: [ - metadata: fragment("? || '{\"deleted\": true}'::jsonb", address_name.metadata) + is_deleted: true ] ] ) @@ -83,7 +81,7 @@ defmodule Explorer.Chain.Import.Runner.StakingPools do required(:timeout) => timeout, required(:timestamps) => Import.timestamps() }) :: - {:ok, [Address.Name.t()]} + {:ok, [StakingPool.t()]} | {:error, [Changeset.t()]} defp insert(repo, 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) @@ -92,10 +90,10 @@ defmodule Explorer.Chain.Import.Runner.StakingPools do Import.insert_changes_list( repo, stakes_ratio(changes_list), - conflict_target: {:unsafe_fragment, "(address_hash) where \"primary\" = true"}, + conflict_target: :staking_address_hash, on_conflict: on_conflict, - for: Address.Name, - returning: [:address_hash], + for: StakingPool, + returning: [:staking_address_hash], timeout: timeout, timestamps: timestamps ) @@ -103,11 +101,23 @@ defmodule Explorer.Chain.Import.Runner.StakingPools do defp default_on_conflict do from( - name in Address.Name, + name in StakingPool, update: [ set: [ - name: fragment("EXCLUDED.name"), - metadata: fragment("EXCLUDED.metadata"), + mining_address_hash: fragment("EXCLUDED.mining_address_hash"), + delegators_count: fragment("EXCLUDED.delegators_count"), + is_active: fragment("EXCLUDED.is_active"), + is_banned: fragment("EXCLUDED.is_banned"), + is_validator: fragment("EXCLUDED.is_validator"), + likelihood: fragment("EXCLUDED.likelihood"), + staked_ratio: fragment("EXCLUDED.staked_ratio"), + min_candidate_stake: fragment("EXCLUDED.min_candidate_stake"), + min_delegator_stake: fragment("EXCLUDED.min_delegator_stake"), + self_staked_amount: fragment("EXCLUDED.self_staked_amount"), + staked_amount: fragment("EXCLUDED.staked_amount"), + was_banned_count: fragment("EXCLUDED.was_banned_count"), + was_validator_count: fragment("EXCLUDED.was_validator_count"), + is_deleted: fragment("EXCLUDED.is_deleted"), inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", name.inserted_at), updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", name.updated_at) ] @@ -117,17 +127,18 @@ defmodule Explorer.Chain.Import.Runner.StakingPools do # Calculates staked ratio for each pool defp stakes_ratio(pools) do - active_pools = Enum.filter(pools, & &1.metadata[:is_active]) + active_pools = Enum.filter(pools, & &1.is_active) stakes_total = - Enum.reduce(pools, 0, fn pool, acc -> - acc + pool.metadata[:staked_amount] + pools + |> Enum.reduce(0, fn pool, acc -> + acc + Wei.to(pool.staked_amount, :integer) end) Enum.map(active_pools, fn pool -> - staked_ratio = if stakes_total > 0, do: pool.metadata[:staked_amount] / stakes_total, else: 0 + staked_ratio = if stakes_total > 0, do: Wei.to(pool.staked_amount, :integer) / stakes_total, else: 0 - put_in(pool, [:metadata, :staked_ratio], staked_ratio) + Map.put(pool, :staked_ratio, staked_ratio) end) end end diff --git a/apps/explorer/test/explorer/chain/import/runner/staking_pools_test.exs b/apps/explorer/test/explorer/chain/import/runner/staking_pools_test.exs index af25368679..138d7f3854 100644 --- a/apps/explorer/test/explorer/chain/import/runner/staking_pools_test.exs +++ b/apps/explorer/test/explorer/chain/import/runner/staking_pools_test.exs @@ -5,25 +5,30 @@ defmodule Explorer.Chain.Import.Runner.StakingPoolsTest do alias Ecto.Multi alias Explorer.Chain.Import.Runner.StakingPools + alias Explorer.Chain.StakingPool describe "run/1" do test "insert new pools list" do - pools = [pool1, pool2, pool3, pool4] = build_list(4, :staking_pool) + pools = + [pool1, pool2] = + [params_for(:staking_pool), params_for(:staking_pool)] + |> Enum.map(fn param -> + changeset = StakingPool.changeset(%StakingPool{}, param) + changeset.changes + end) assert {:ok, %{insert_staking_pools: list}} = run_changes(pools) assert Enum.count(list) == Enum.count(pools) saved_list = - Explorer.Chain.Address.Name + Explorer.Chain.StakingPool |> Repo.all() |> Enum.reduce(%{}, fn pool, acc -> - Map.put(acc, pool.address_hash, pool) + Map.put(acc, pool.staking_address_hash, pool) end) - assert saved_list[pool1.address_hash].metadata["staked_ratio"] == 0.25 - assert saved_list[pool2.address_hash].metadata["staked_ratio"] == 0.25 - assert saved_list[pool3.address_hash].metadata["staked_ratio"] == 0.25 - assert saved_list[pool4.address_hash].metadata["staked_ratio"] == 0.25 + assert saved_list[pool1.staking_address_hash].staked_ratio == Decimal.from_float(0.5) + assert saved_list[pool2.staking_address_hash].staked_ratio == Decimal.from_float(0.5) end end diff --git a/apps/explorer/test/explorer/chain/staking_pool_test.exs b/apps/explorer/test/explorer/chain/staking_pool_test.exs index 78d01a0f19..22450071a5 100644 --- a/apps/explorer/test/explorer/chain/staking_pool_test.exs +++ b/apps/explorer/test/explorer/chain/staking_pool_test.exs @@ -6,7 +6,7 @@ defmodule Explorer.Chain.StakingPoolTest do describe "changeset/2" do test "with valid attributes" do params = params_for(:staking_pool) - changeset = StakingPool.changeset(%StakingPool{}, params) |> IO.inspect() + changeset = StakingPool.changeset(%StakingPool{}, params) assert changeset.valid? end diff --git a/apps/explorer/test/support/factory.ex b/apps/explorer/test/support/factory.ex index 6a08c2df8b..45bf3f0b54 100644 --- a/apps/explorer/test/support/factory.ex +++ b/apps/explorer/test/support/factory.ex @@ -626,10 +626,10 @@ defmodule Explorer.Factory do self_staked_amount: wei_per_ether * 300, was_banned_count: 0, was_validator_count: 1, - min_delegators_stake: wei_per_ether * 100, + min_delegator_stake: wei_per_ether * 100, min_candidate_stake: wei_per_ether * 200, staked_ratio: 0, - likelihood: 0, + likelihood: 0 } end end From e3c3d4be03e15dc7ce51aa0a192339b6fbcd55c3 Mon Sep 17 00:00:00 2001 From: saneery Date: Thu, 23 May 2019 13:48:41 +0300 Subject: [PATCH 07/57] calculating stakes ratio and likelihood after insert --- .../chain/import/runner/staking_pools.ex | 58 +++++++++++++------ .../lib/explorer/chain/staking_pool.ex | 41 +++++++------ .../20190521104412_create_staking_pools.exs | 4 +- .../import/runner/staking_pools_test.exs | 4 +- apps/explorer/test/support/factory.ex | 4 +- 5 files changed, 68 insertions(+), 43 deletions(-) diff --git a/apps/explorer/lib/explorer/chain/import/runner/staking_pools.ex b/apps/explorer/lib/explorer/chain/import/runner/staking_pools.ex index b5024f30cd..3a698ad9f5 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/staking_pools.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/staking_pools.ex @@ -6,7 +6,7 @@ defmodule Explorer.Chain.Import.Runner.StakingPools do require Ecto.Query alias Ecto.{Changeset, Multi, Repo} - alias Explorer.Chain.{Import, StakingPool, Wei} + alias Explorer.Chain.{Import, StakingPool} import Ecto.Query, only: [from: 2] @@ -47,6 +47,9 @@ defmodule Explorer.Chain.Import.Runner.StakingPools do |> Multi.run(:insert_staking_pools, fn repo, _ -> insert(repo, changes_list, insert_options) end) + |> Multi.run(:calculate_stakes_ratio, fn repo, _ -> + calculate_stakes_ratio(repo, insert_options) + end) end @impl Import.Runner @@ -61,7 +64,8 @@ defmodule Explorer.Chain.Import.Runner.StakingPools do where: pool.staking_address_hash not in ^addresses, update: [ set: [ - is_deleted: true + is_deleted: true, + is_active: false ] ] ) @@ -89,7 +93,7 @@ defmodule Explorer.Chain.Import.Runner.StakingPools do {:ok, _} = Import.insert_changes_list( repo, - stakes_ratio(changes_list), + changes_list, conflict_target: :staking_address_hash, on_conflict: on_conflict, for: StakingPool, @@ -125,20 +129,38 @@ defmodule Explorer.Chain.Import.Runner.StakingPools do ) end - # Calculates staked ratio for each pool - defp stakes_ratio(pools) do - active_pools = Enum.filter(pools, & &1.is_active) - - stakes_total = - pools - |> Enum.reduce(0, fn pool, acc -> - acc + Wei.to(pool.staked_amount, :integer) - end) - - Enum.map(active_pools, fn pool -> - staked_ratio = if stakes_total > 0, do: Wei.to(pool.staked_amount, :integer) / stakes_total, else: 0 - - Map.put(pool, :staked_ratio, staked_ratio) - end) + defp calculate_stakes_ratio(repo, %{timeout: timeout}) do + try do + total_query = + from( + pool in StakingPool, + where: pool.is_active == true, + select: sum(pool.staked_amount) + ) + + total = repo.one!(total_query) + + if total > 0 do + query = + from( + p in StakingPool, + where: p.is_active == true, + update: [ + set: [ + staked_ratio: p.staked_amount / ^total * 100, + likelihood: p.staked_amount / ^total * 100 + ] + ] + ) + + {count, _} = repo.update_all(query, [], timeout: timeout) + {:ok, count} + else + {:ok, 0} + end + rescue + postgrex_error in Postgrex.Error -> + {:error, %{exception: postgrex_error}} + end end end diff --git a/apps/explorer/lib/explorer/chain/staking_pool.ex b/apps/explorer/lib/explorer/chain/staking_pool.ex index 10e678df25..e04e5bd19f 100644 --- a/apps/explorer/lib/explorer/chain/staking_pool.ex +++ b/apps/explorer/lib/explorer/chain/staking_pool.ex @@ -13,23 +13,23 @@ defmodule Explorer.Chain.StakingPool do } @type t :: %__MODULE__{ - staking_address_hash: Hash.Address.t(), - mining_address_hash: Hash.Address.t(), - banned_until: boolean, - delegators_count: integer, - is_active: boolean, - is_banned: boolean, - is_validator: boolean, - likelihood: integer, - staked_ratio: Decimal.t(), - min_candidate_stake: Wei.t(), - min_delegator_stake: Wei.t(), - self_staked_amount: Wei.t(), - staked_amount: Wei.t(), - was_banned_count: integer, - was_validator_count: integer, - is_deleted: boolean - } + staking_address_hash: Hash.Address.t(), + mining_address_hash: Hash.Address.t(), + banned_until: boolean, + delegators_count: integer, + is_active: boolean, + is_banned: boolean, + is_validator: boolean, + likelihood: integer, + staked_ratio: Decimal.t(), + min_candidate_stake: Wei.t(), + min_delegator_stake: Wei.t(), + self_staked_amount: Wei.t(), + staked_amount: Wei.t(), + was_banned_count: integer, + was_validator_count: integer, + is_deleted: boolean + } @attrs ~w( is_active delegators_count staked_amount self_staked_amount is_validator @@ -37,6 +37,11 @@ defmodule Explorer.Chain.StakingPool do staked_ratio min_delegator_stake min_candidate_stake staking_address_hash mining_address_hash )a + @req_attrs ~w( + is_active delegators_count staked_amount self_staked_amount is_validator + was_validator_count is_banned was_banned_count banned_until min_delegator_stake + min_candidate_stake staking_address_hash mining_address_hash + )a schema "staking_pools" do field(:banned_until, :integer) @@ -77,7 +82,7 @@ defmodule Explorer.Chain.StakingPool do def changeset(staking_pool, attrs) do staking_pool |> cast(attrs, @attrs) - |> validate_required(@attrs) + |> validate_required(@req_attrs) |> validate_staked_amount() |> unique_constraint(:staking_address_hash) end diff --git a/apps/explorer/priv/repo/migrations/20190521104412_create_staking_pools.exs b/apps/explorer/priv/repo/migrations/20190521104412_create_staking_pools.exs index aabef14d1a..739815e79b 100644 --- a/apps/explorer/priv/repo/migrations/20190521104412_create_staking_pools.exs +++ b/apps/explorer/priv/repo/migrations/20190521104412_create_staking_pools.exs @@ -13,8 +13,8 @@ defmodule Explorer.Repo.Migrations.CreateStakingPools do add(:is_banned, :boolean, default: false, null: false) add(:was_banned_count, :integer) add(:banned_until, :bigint) - add(:likelihood, :decimal) - add(:staked_ratio, :decimal) + add(:likelihood, :decimal, precision: 5, scale: 2) + add(:staked_ratio, :decimal, precision: 5, scale: 2) add(:min_delegator_stake, :numeric, precision: 100) add(:min_candidate_stake, :numeric, precision: 100) add(:staking_address_hash, :bytea) diff --git a/apps/explorer/test/explorer/chain/import/runner/staking_pools_test.exs b/apps/explorer/test/explorer/chain/import/runner/staking_pools_test.exs index 138d7f3854..1c56fe294c 100644 --- a/apps/explorer/test/explorer/chain/import/runner/staking_pools_test.exs +++ b/apps/explorer/test/explorer/chain/import/runner/staking_pools_test.exs @@ -27,8 +27,8 @@ defmodule Explorer.Chain.Import.Runner.StakingPoolsTest do Map.put(acc, pool.staking_address_hash, pool) end) - assert saved_list[pool1.staking_address_hash].staked_ratio == Decimal.from_float(0.5) - assert saved_list[pool2.staking_address_hash].staked_ratio == Decimal.from_float(0.5) + assert saved_list[pool1.staking_address_hash].staked_ratio == Decimal.new("50.00") + assert saved_list[pool2.staking_address_hash].staked_ratio == Decimal.new("50.00") end end diff --git a/apps/explorer/test/support/factory.ex b/apps/explorer/test/support/factory.ex index 45bf3f0b54..627f8b1ee7 100644 --- a/apps/explorer/test/support/factory.ex +++ b/apps/explorer/test/support/factory.ex @@ -627,9 +627,7 @@ defmodule Explorer.Factory do was_banned_count: 0, was_validator_count: 1, min_delegator_stake: wei_per_ether * 100, - min_candidate_stake: wei_per_ether * 200, - staked_ratio: 0, - likelihood: 0 + min_candidate_stake: wei_per_ether * 200 } end end From 679e45cfa1f1bd1f180809c0af0ccf208435a444 Mon Sep 17 00:00:00 2001 From: saneery Date: Thu, 23 May 2019 13:57:06 +0300 Subject: [PATCH 08/57] min candidate and delegator stake are constants --- .../lib/explorer/chain/import/runner/staking_pools.ex | 6 ++---- apps/explorer/lib/explorer/chain/staking_pool.ex | 11 +++-------- .../20190521104412_create_staking_pools.exs | 2 -- apps/explorer/test/support/factory.ex | 4 +--- 4 files changed, 6 insertions(+), 17 deletions(-) diff --git a/apps/explorer/lib/explorer/chain/import/runner/staking_pools.ex b/apps/explorer/lib/explorer/chain/import/runner/staking_pools.ex index 3a698ad9f5..d30ca934fa 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/staking_pools.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/staking_pools.ex @@ -115,8 +115,6 @@ defmodule Explorer.Chain.Import.Runner.StakingPools do is_validator: fragment("EXCLUDED.is_validator"), likelihood: fragment("EXCLUDED.likelihood"), staked_ratio: fragment("EXCLUDED.staked_ratio"), - min_candidate_stake: fragment("EXCLUDED.min_candidate_stake"), - min_delegator_stake: fragment("EXCLUDED.min_delegator_stake"), self_staked_amount: fragment("EXCLUDED.self_staked_amount"), staked_amount: fragment("EXCLUDED.staked_amount"), was_banned_count: fragment("EXCLUDED.was_banned_count"), @@ -140,7 +138,7 @@ defmodule Explorer.Chain.Import.Runner.StakingPools do total = repo.one!(total_query) - if total > 0 do + if total > Decimal.new(0) do query = from( p in StakingPool, @@ -156,7 +154,7 @@ defmodule Explorer.Chain.Import.Runner.StakingPools do {count, _} = repo.update_all(query, [], timeout: timeout) {:ok, count} else - {:ok, 0} + {:ok, 1} end rescue postgrex_error in Postgrex.Error -> diff --git a/apps/explorer/lib/explorer/chain/staking_pool.ex b/apps/explorer/lib/explorer/chain/staking_pool.ex index e04e5bd19f..297ee8d06d 100644 --- a/apps/explorer/lib/explorer/chain/staking_pool.ex +++ b/apps/explorer/lib/explorer/chain/staking_pool.ex @@ -22,8 +22,6 @@ defmodule Explorer.Chain.StakingPool do is_validator: boolean, likelihood: integer, staked_ratio: Decimal.t(), - min_candidate_stake: Wei.t(), - min_delegator_stake: Wei.t(), self_staked_amount: Wei.t(), staked_amount: Wei.t(), was_banned_count: integer, @@ -34,13 +32,12 @@ defmodule Explorer.Chain.StakingPool do @attrs ~w( is_active delegators_count staked_amount self_staked_amount is_validator was_validator_count is_banned was_banned_count banned_until likelihood - staked_ratio min_delegator_stake min_candidate_stake - staking_address_hash mining_address_hash + staked_ratio staking_address_hash mining_address_hash )a @req_attrs ~w( is_active delegators_count staked_amount self_staked_amount is_validator - was_validator_count is_banned was_banned_count banned_until min_delegator_stake - min_candidate_stake staking_address_hash mining_address_hash + was_validator_count is_banned was_banned_count banned_until + staking_address_hash mining_address_hash )a schema "staking_pools" do @@ -51,8 +48,6 @@ defmodule Explorer.Chain.StakingPool do field(:is_validator, :boolean, default: false) field(:likelihood, :decimal) field(:staked_ratio, :decimal) - field(:min_candidate_stake, Wei) - field(:min_delegator_stake, Wei) field(:self_staked_amount, Wei) field(:staked_amount, Wei) field(:was_banned_count, :integer) diff --git a/apps/explorer/priv/repo/migrations/20190521104412_create_staking_pools.exs b/apps/explorer/priv/repo/migrations/20190521104412_create_staking_pools.exs index 739815e79b..94483112f2 100644 --- a/apps/explorer/priv/repo/migrations/20190521104412_create_staking_pools.exs +++ b/apps/explorer/priv/repo/migrations/20190521104412_create_staking_pools.exs @@ -15,8 +15,6 @@ defmodule Explorer.Repo.Migrations.CreateStakingPools do add(:banned_until, :bigint) add(:likelihood, :decimal, precision: 5, scale: 2) add(:staked_ratio, :decimal, precision: 5, scale: 2) - add(:min_delegator_stake, :numeric, precision: 100) - add(:min_candidate_stake, :numeric, precision: 100) add(:staking_address_hash, :bytea) add(:mining_address_hash, :bytea) diff --git a/apps/explorer/test/support/factory.ex b/apps/explorer/test/support/factory.ex index 627f8b1ee7..97a449f33e 100644 --- a/apps/explorer/test/support/factory.ex +++ b/apps/explorer/test/support/factory.ex @@ -625,9 +625,7 @@ defmodule Explorer.Factory do staked_amount: wei_per_ether * 500, self_staked_amount: wei_per_ether * 300, was_banned_count: 0, - was_validator_count: 1, - min_delegator_stake: wei_per_ether * 100, - min_candidate_stake: wei_per_ether * 200 + was_validator_count: 1 } end end From 109bba9dc1074ca73b0c7d05d00c58b984e6fce0 Mon Sep 17 00:00:00 2001 From: saneery Date: Thu, 23 May 2019 14:18:14 +0300 Subject: [PATCH 09/57] update pools reader test --- apps/explorer/lib/explorer/staking/pools_reader.ex | 4 ++-- apps/explorer/test/explorer/staking/pools_reader_test.exs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/explorer/lib/explorer/staking/pools_reader.ex b/apps/explorer/lib/explorer/staking/pools_reader.ex index 608fea3863..1c88e09493 100644 --- a/apps/explorer/lib/explorer/staking/pools_reader.ex +++ b/apps/explorer/lib/explorer/staking/pools_reader.ex @@ -38,8 +38,8 @@ defmodule Explorer.Staking.PoolsReader do { :ok, %{ - staking_address: staking_address, - mining_address: mining_address, + staking_address_hash: staking_address, + mining_address_hash: mining_address, is_active: is_active, delegators_count: delegators_count, staked_amount: staked_amount, diff --git a/apps/explorer/test/explorer/staking/pools_reader_test.exs b/apps/explorer/test/explorer/staking/pools_reader_test.exs index bb3af9fbcc..9608a2b3f5 100644 --- a/apps/explorer/test/explorer/staking/pools_reader_test.exs +++ b/apps/explorer/test/explorer/staking/pools_reader_test.exs @@ -40,11 +40,11 @@ defmodule Explorer.Token.PoolsReaderTest do is_active: true, is_banned: false, is_validator: true, - mining_address: + mining_address_hash: <<187, 202, 168, 212, 130, 137, 187, 31, 252, 249, 128, 141, 154, 164, 177, 210, 21, 5, 76, 120>>, staked_amount: 0, self_staked_amount: 0, - staking_address: <<11, 47, 94, 47, 60, 189, 134, 78, 170, 44, 100, 46, 55, 105, 193, 88, 35, 97, 202, 246>>, + staking_address_hash: <<11, 47, 94, 47, 60, 189, 134, 78, 170, 44, 100, 46, 55, 105, 193, 88, 35, 97, 202, 246>>, was_banned_count: 0, was_validator_count: 2 } From 8d2ecb9f6124cf7eed95ed7136ac56a2f4d2d226 Mon Sep 17 00:00:00 2001 From: saneery Date: Thu, 23 May 2019 14:18:32 +0300 Subject: [PATCH 10/57] update pools fetcher --- .../lib/indexer/fetcher/staking_pools.ex | 34 +++++++------------ .../indexer/fetcher/staking_pools_test.exs | 8 ++--- 2 files changed, 16 insertions(+), 26 deletions(-) diff --git a/apps/indexer/lib/indexer/fetcher/staking_pools.ex b/apps/indexer/lib/indexer/fetcher/staking_pools.ex index fe4ab84c28..1632229e08 100644 --- a/apps/indexer/lib/indexer/fetcher/staking_pools.ex +++ b/apps/indexer/lib/indexer/fetcher/staking_pools.ex @@ -9,6 +9,7 @@ defmodule Indexer.Fetcher.StakingPools do require Logger alias Explorer.Chain + alias Explorer.Chain.StakingPool alias Explorer.Staking.PoolsReader alias Indexer.BufferedTask alias Indexer.Fetcher.StakingPools.Supervisor, as: StakingPoolsSupervisor @@ -71,7 +72,7 @@ defmodule Indexer.Fetcher.StakingPools do def entry(pool_address) do %{ - staking_address: pool_address, + staking_address_hash: pool_address, retries_count: 0 } end @@ -79,7 +80,7 @@ defmodule Indexer.Fetcher.StakingPools do defp fetch_from_blockchain(addresses) do addresses |> Enum.filter(&(&1.retries_count <= @max_retries)) - |> Enum.map(fn %{staking_address: staking_address} = pool -> + |> Enum.map(fn %{staking_address_hash: staking_address} = pool -> case PoolsReader.pool_data(staking_address) do {:ok, data} -> Map.merge(pool, data) @@ -93,11 +94,17 @@ defmodule Indexer.Fetcher.StakingPools do defp import_pools(pools) do {failed, success} = Enum.reduce(pools, {[], []}, fn - %{error: _error, staking_address: address}, {failed, success} -> + %{error: _error, staking_address_hash: address}, {failed, success} -> {[address | failed], success} - pool, {failed, success} -> - {failed, [changeset(pool) | success]} + %{staking_address_hash: address} = pool, {failed, success} -> + changeset = StakingPool.changeset(%StakingPool{}, pool) + + if changeset.valid? do + {failed, [changeset.changes | success]} + else + {[address | failed], success} + end end) import_params = %{ @@ -117,21 +124,4 @@ defmodule Indexer.Fetcher.StakingPools do failed end - - defp changeset(%{staking_address: staking_address} = pool) do - {:ok, mining_address} = Chain.Hash.Address.cast(pool[:mining_address]) - - data = - pool - |> Map.delete(:staking_address) - |> Map.put(:mining_address, mining_address) - |> Map.put(:is_pool, true) - - %{ - name: "anonymous", - primary: true, - address_hash: staking_address, - metadata: data - } - end end diff --git a/apps/indexer/test/indexer/fetcher/staking_pools_test.exs b/apps/indexer/test/indexer/fetcher/staking_pools_test.exs index 13e2c0d7ee..257c9e558c 100644 --- a/apps/indexer/test/indexer/fetcher/staking_pools_test.exs +++ b/apps/indexer/test/indexer/fetcher/staking_pools_test.exs @@ -6,7 +6,7 @@ defmodule Indexer.Fetcher.StakingPoolsTest do alias Indexer.Fetcher.StakingPools alias Explorer.Staking.PoolsReader - alias Explorer.Chain.Address + alias Explorer.Chain.StakingPool @moduletag :capture_log @@ -33,15 +33,15 @@ defmodule Indexer.Fetcher.StakingPoolsTest do success_address = list |> List.first() - |> Map.get(:staking_address) + |> Map.get(:staking_address_hash) get_pool_data_from_blockchain() assert {:retry, retry_list} = StakingPools.run(list, nil) assert Enum.count(retry_list) == 2 - pool = Explorer.Repo.get_by(Address.Name, address_hash: success_address) - assert pool.name == "anonymous" + pool = Explorer.Repo.get_by(StakingPool, staking_address_hash: success_address) + assert pool.is_active == true end end From 4728d4696a302c6d3663c10759b288f926a7fb3e Mon Sep 17 00:00:00 2001 From: saneery Date: Thu, 23 May 2019 15:02:15 +0300 Subject: [PATCH 11/57] add delegators table and schema --- .../chain/staking_pools_delegators.ex | 64 +++++++++++++++++++ ...112839_create_staking_pools_delegators.exs | 26 ++++++++ .../chain/staking_pools_delegators_test.exs | 18 ++++++ apps/explorer/test/support/factory.ex | 17 ++++- 4 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 apps/explorer/lib/explorer/chain/staking_pools_delegators.ex create mode 100644 apps/explorer/priv/repo/migrations/20190523112839_create_staking_pools_delegators.exs create mode 100644 apps/explorer/test/explorer/chain/staking_pools_delegators_test.exs diff --git a/apps/explorer/lib/explorer/chain/staking_pools_delegators.ex b/apps/explorer/lib/explorer/chain/staking_pools_delegators.ex new file mode 100644 index 0000000000..d40edfaad8 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/staking_pools_delegators.ex @@ -0,0 +1,64 @@ +defmodule Explorer.Chain.StakingPoolsDelegators do + @moduledoc """ + The representation of delegators from POSDAO network. + Delegators make stakes on staking pools and withdraw from them. + """ + use Ecto.Schema + import Ecto.Changeset + + alias Explorer.Chain.{ + Address, + Hash, + StakingPool, + Wei + } + + @type t :: %__MODULE__{ + pool_address_hash: Hash.Address.t(), + delegator_address_hash: Hash.Address.t(), + max_ordered_withdraw_allowed: Wei.t(), + max_withdraw_allowed: Wei.t(), + ordered_withdraw: Wei.t(), + stake_amount: Wei.t(), + ordered_withdraw_epoch: integer() + } + + @attrs ~w( + pool_address_hash delegator_address_hash max_ordered_withdraw_allowed + max_withdraw_allowed ordered_withdraw stake_amount ordered_withdraw_epoch + )a + + schema "staking_pools_delegators" do + field(:max_ordered_withdraw_allowed, Wei) + field(:max_withdraw_allowed, Wei) + field(:ordered_withdraw, Wei) + field(:ordered_withdraw_epoch, :integer) + field(:stake_amount, Wei) + + belongs_to( + :staking_pool, + StakingPool, + foreign_key: :pool_address_hash, + references: :staking_address_hash, + type: Hash.Address + ) + + belongs_to( + :delegator_address, + Address, + foreign_key: :delegator_address_hash, + references: :hash, + type: Hash.Address + ) + + timestamps(null: false, type: :utc_datetime_usec) + end + + @doc false + def changeset(staking_pools_delegators, attrs) do + staking_pools_delegators + |> cast(attrs, @attrs) + |> validate_required(@attrs) + |> unique_constraint(:pool_address_hash, name: :pools_delegator_index) + end +end diff --git a/apps/explorer/priv/repo/migrations/20190523112839_create_staking_pools_delegators.exs b/apps/explorer/priv/repo/migrations/20190523112839_create_staking_pools_delegators.exs new file mode 100644 index 0000000000..1f243771fb --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20190523112839_create_staking_pools_delegators.exs @@ -0,0 +1,26 @@ +defmodule Explorer.Repo.Migrations.CreateStakingPoolsDelegators do + use Ecto.Migration + + def change do + create table(:staking_pools_delegators) do + add(:delegator_address_hash, :bytea) + add(:pool_address_hash, :bytea) + add(:stake_amount, :numeric, precision: 100) + add(:ordered_withdraw, :numeric, precision: 100) + add(:max_withdraw_allowed, :numeric, precision: 100) + add(:max_ordered_withdraw_allowed, :numeric, precision: 100) + add(:ordered_withdraw_epoch, :integer) + + timestamps(null: false, type: :utc_datetime_usec) + end + + create(index(:staking_pools_delegators, [:delegator_address_hash])) + + create( + index(:staking_pools_delegators, [:delegator_address_hash, :pool_address_hash], + unique: true, + name: :pools_delegator_index + ) + ) + end +end diff --git a/apps/explorer/test/explorer/chain/staking_pools_delegators_test.exs b/apps/explorer/test/explorer/chain/staking_pools_delegators_test.exs new file mode 100644 index 0000000000..a27dada2eb --- /dev/null +++ b/apps/explorer/test/explorer/chain/staking_pools_delegators_test.exs @@ -0,0 +1,18 @@ +defmodule Explorer.Chain.StakingPoolsDelegatorsTest do + use Explorer.DataCase + + alias Explorer.Chain.StakingPoolsDelegators + + describe "changeset/2" do + test "with valid attributes" do + params = params_for(:staking_pools_delegators) + changeset = StakingPoolsDelegators.changeset(%StakingPoolsDelegators{}, params) + assert changeset.valid? + end + + test "with invalid attributes" do + changeset = StakingPoolsDelegators.changeset(%StakingPoolsDelegators{}, %{pool_address_hash: 0}) + refute changeset.valid? + end + end +end diff --git a/apps/explorer/test/support/factory.ex b/apps/explorer/test/support/factory.ex index 97a449f33e..58a16c97b9 100644 --- a/apps/explorer/test/support/factory.ex +++ b/apps/explorer/test/support/factory.ex @@ -27,7 +27,8 @@ defmodule Explorer.Factory do Token, TokenTransfer, Transaction, - StakingPool + StakingPool, + StakingPoolsDelegators } alias Explorer.Market.MarketHistory @@ -628,4 +629,18 @@ defmodule Explorer.Factory do was_validator_count: 1 } end + + def staking_pools_delegators_factory do + wei_per_ether = 1_000_000_000_000_000_000 + + %StakingPoolsDelegators{ + pool_address_hash: address_hash(), + delegator_address_hash: address_hash(), + max_ordered_withdraw_allowed: wei_per_ether * 100, + max_withdraw_allowed: wei_per_ether * 50, + ordered_withdraw: wei_per_ether * 600, + stake_amount: wei_per_ether * 200, + ordered_withdraw_epoch: 2 + } + end end From b6d30d4229961b1afa0c6571a46dce93e5434c91 Mon Sep 17 00:00:00 2001 From: saneery Date: Thu, 23 May 2019 15:02:44 +0300 Subject: [PATCH 12/57] mix format --- apps/explorer/test/explorer/staking/pools_reader_test.exs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/explorer/test/explorer/staking/pools_reader_test.exs b/apps/explorer/test/explorer/staking/pools_reader_test.exs index 9608a2b3f5..ec17f7b5f7 100644 --- a/apps/explorer/test/explorer/staking/pools_reader_test.exs +++ b/apps/explorer/test/explorer/staking/pools_reader_test.exs @@ -44,7 +44,8 @@ defmodule Explorer.Token.PoolsReaderTest do <<187, 202, 168, 212, 130, 137, 187, 31, 252, 249, 128, 141, 154, 164, 177, 210, 21, 5, 76, 120>>, staked_amount: 0, self_staked_amount: 0, - staking_address_hash: <<11, 47, 94, 47, 60, 189, 134, 78, 170, 44, 100, 46, 55, 105, 193, 88, 35, 97, 202, 246>>, + staking_address_hash: + <<11, 47, 94, 47, 60, 189, 134, 78, 170, 44, 100, 46, 55, 105, 193, 88, 35, 97, 202, 246>>, was_banned_count: 0, was_validator_count: 2 } From 2a25481663fcff78e881c17399f99df84c73f04d Mon Sep 17 00:00:00 2001 From: saneery Date: Mon, 27 May 2019 10:56:59 +0300 Subject: [PATCH 13/57] import staking pools delegators --- .../chain/import/runner/staking_pools.ex | 54 ++++++----- .../import/runner/staking_pools_delegators.ex | 91 +++++++++++++++++++ .../chain/import/stage/address_referencing.ex | 3 +- .../lib/explorer/chain/staking_pool.ex | 3 + .../lib/explorer/staking/pools_reader.ex | 47 ++++++++-- .../lib/indexer/fetcher/staking_pools.ex | 23 ++++- 6 files changed, 180 insertions(+), 41 deletions(-) create mode 100644 apps/explorer/lib/explorer/chain/import/runner/staking_pools_delegators.ex diff --git a/apps/explorer/lib/explorer/chain/import/runner/staking_pools.ex b/apps/explorer/lib/explorer/chain/import/runner/staking_pools.ex index d30ca934fa..d7655674d8 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/staking_pools.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/staking_pools.ex @@ -128,37 +128,35 @@ defmodule Explorer.Chain.Import.Runner.StakingPools do end defp calculate_stakes_ratio(repo, %{timeout: timeout}) do - try do - total_query = - from( - pool in StakingPool, - where: pool.is_active == true, - select: sum(pool.staked_amount) - ) + total_query = + from( + pool in StakingPool, + where: pool.is_active == true, + select: sum(pool.staked_amount) + ) + + total = repo.one!(total_query) - total = repo.one!(total_query) - - if total > Decimal.new(0) do - query = - from( - p in StakingPool, - where: p.is_active == true, - update: [ - set: [ - staked_ratio: p.staked_amount / ^total * 100, - likelihood: p.staked_amount / ^total * 100 - ] + if total > Decimal.new(0) do + query = + from( + p in StakingPool, + where: p.is_active == true, + update: [ + set: [ + staked_ratio: p.staked_amount / ^total * 100, + likelihood: p.staked_amount / ^total * 100 ] - ) + ] + ) - {count, _} = repo.update_all(query, [], timeout: timeout) - {:ok, count} - else - {:ok, 1} - end - rescue - postgrex_error in Postgrex.Error -> - {:error, %{exception: postgrex_error}} + {count, _} = repo.update_all(query, [], timeout: timeout) + {:ok, count} + else + {:ok, 1} end + rescue + postgrex_error in Postgrex.Error -> + {:error, %{exception: postgrex_error}} end end diff --git a/apps/explorer/lib/explorer/chain/import/runner/staking_pools_delegators.ex b/apps/explorer/lib/explorer/chain/import/runner/staking_pools_delegators.ex new file mode 100644 index 0000000000..5d44d68a92 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/import/runner/staking_pools_delegators.ex @@ -0,0 +1,91 @@ +defmodule Explorer.Chain.Import.Runner.StakingPoolsDelegators do + @moduledoc """ + Bulk imports staking pools to StakingPoolsDelegators tabe. + """ + + require Ecto.Query + + alias Ecto.{Changeset, Multi, Repo} + alias Explorer.Chain.{Import, StakingPoolsDelegators} + + import Ecto.Query, only: [from: 2] + + @behaviour Import.Runner + + # milliseconds + @timeout 60_000 + + @type imported :: [StakingPoolsDelegators.t()] + + @impl Import.Runner + def ecto_schema_module, do: StakingPoolsDelegators + + @impl Import.Runner + def option_key, do: :staking_pools_delegators + + @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 + |> Multi.run(:insert_staking_pools_delegators, fn repo, _ -> + insert(repo, changes_list, insert_options) + end) + end + + @impl Import.Runner + def timeout, do: @timeout + + @spec insert(Repo.t(), [map()], %{ + optional(:on_conflict) => Import.Runner.on_conflict(), + required(:timeout) => timeout, + required(:timestamps) => Import.timestamps() + }) :: + {:ok, [StakingPoolsDelegators.t()]} + | {:error, [Changeset.t()]} + defp insert(repo, 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( + repo, + changes_list, + conflict_target: [:pool_address_hash, :delegator_address_hash], + on_conflict: on_conflict, + for: StakingPoolsDelegators, + returning: [:pool_address_hash, :delegator_address_hash], + timeout: timeout, + timestamps: timestamps + ) + end + + defp default_on_conflict do + from( + name in StakingPoolsDelegators, + update: [ + set: [ + stake_amount: fragment("EXCLUDED.stake_amount"), + ordered_withdraw: fragment("EXCLUDED.ordered_withdraw"), + max_withdraw_allowed: fragment("EXCLUDED.max_withdraw_allowed"), + max_ordered_withdraw_allowed: fragment("EXCLUDED.max_ordered_withdraw_allowed"), + ordered_withdraw_epoch: fragment("EXCLUDED.ordered_withdraw_epoch"), + inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", name.inserted_at), + updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", name.updated_at) + ] + ] + ) + end +end diff --git a/apps/explorer/lib/explorer/chain/import/stage/address_referencing.ex b/apps/explorer/lib/explorer/chain/import/stage/address_referencing.ex index 0e42bdc1f9..b5037f3b04 100644 --- a/apps/explorer/lib/explorer/chain/import/stage/address_referencing.ex +++ b/apps/explorer/lib/explorer/chain/import/stage/address_referencing.ex @@ -25,7 +25,8 @@ defmodule Explorer.Chain.Import.Stage.AddressReferencing do Runner.TokenTransfers, Runner.Address.CurrentTokenBalances, Runner.Address.TokenBalances, - Runner.StakingPools + Runner.StakingPools, + Runner.StakingPoolsDelegators ] @impl Stage diff --git a/apps/explorer/lib/explorer/chain/staking_pool.ex b/apps/explorer/lib/explorer/chain/staking_pool.ex index 297ee8d06d..31c5b1cc26 100644 --- a/apps/explorer/lib/explorer/chain/staking_pool.ex +++ b/apps/explorer/lib/explorer/chain/staking_pool.ex @@ -9,6 +9,7 @@ defmodule Explorer.Chain.StakingPool do alias Explorer.Chain.{ Address, Hash, + StakingPoolsDelegators, Wei } @@ -53,6 +54,7 @@ defmodule Explorer.Chain.StakingPool do field(:was_banned_count, :integer) field(:was_validator_count, :integer) field(:is_deleted, :boolean, default: false) + has_many(:delegators, StakingPoolsDelegators) belongs_to( :staking_address, @@ -77,6 +79,7 @@ defmodule Explorer.Chain.StakingPool do def changeset(staking_pool, attrs) do staking_pool |> cast(attrs, @attrs) + |> cast_assoc(:delegators) |> validate_required(@req_attrs) |> validate_staked_amount() |> unique_constraint(:staking_address_hash) diff --git a/apps/explorer/lib/explorer/staking/pools_reader.ex b/apps/explorer/lib/explorer/staking/pools_reader.ex index 1c88e09493..0c04c8e570 100644 --- a/apps/explorer/lib/explorer/staking/pools_reader.ex +++ b/apps/explorer/lib/explorer/staking/pools_reader.ex @@ -24,10 +24,11 @@ defmodule Explorer.Staking.PoolsReader do @spec pool_data(String.t()) :: {:ok, map()} | :error def pool_data(staking_address) do with {:ok, [mining_address]} <- call_validators_method("miningByStakingAddress", [staking_address]), - data = fetch_data(staking_address, mining_address), + data = fetch_pool_data(staking_address, mining_address), {:ok, [is_active]} <- data["isPoolActive"], {:ok, [delegator_addresses]} <- data["poolDelegators"], delegators_count = Enum.count(delegator_addresses), + delegators = delegators_data(delegator_addresses, staking_address), {:ok, [staked_amount]} <- data["stakeAmountTotalMinusOrderedWithdraw"], {:ok, [self_staked_amount]} <- data["stakeAmountMinusOrderedWithdraw"], {:ok, [is_validator]} <- data["isValidator"], @@ -48,7 +49,8 @@ defmodule Explorer.Staking.PoolsReader do was_validator_count: was_validator_count, is_banned: is_banned, banned_until: banned_until, - was_banned_count: was_banned_count + was_banned_count: was_banned_count, + delegators: delegators } } else @@ -57,6 +59,35 @@ defmodule Explorer.Staking.PoolsReader do end end + defp delegators_data(delegators, pool_address) do + Enum.map(delegators, fn address -> + data = + call_methods([ + {:staking, "stakeAmount", [pool_address, address]}, + {:staking, "orderedWithdrawAmount", [pool_address, address]}, + {:staking, "maxWithdrawAllowed", [pool_address, address]}, + {:staking, "maxWithdrawOrderAllowed", [pool_address, address]}, + {:staking, "orderWithdrawEpoch", [pool_address, address]} + ]) + + {:ok, [stake_amount]} = data["stakeAmount"] + {:ok, [ordered_withdraw]} = data["orderedWithdrawAmount"] + {:ok, [max_withdraw_allowed]} = data["maxWithdrawAllowed"] + {:ok, [max_ordered_withdraw_allowed]} = data["maxWithdrawOrderAllowed"] + {:ok, [ordered_withdraw_epoch]} = data["orderWithdrawEpoch"] + + %{ + delegator_address_hash: address, + pool_address_hash: pool_address, + stake_amount: stake_amount, + ordered_withdraw: ordered_withdraw, + max_withdraw_allowed: max_withdraw_allowed, + max_ordered_withdraw_allowed: max_ordered_withdraw_allowed, + ordered_withdraw_epoch: ordered_withdraw_epoch + } + end) + end + defp call_staking_method(method, params) do %{^method => resp} = Reader.query_contract(config(:staking_contract_address), abi("staking.json"), %{ @@ -75,10 +106,8 @@ defmodule Explorer.Staking.PoolsReader do resp end - defp fetch_data(staking_address, mining_address) do - contract_abi = abi("staking.json") ++ abi("validators.json") - - methods = [ + defp fetch_pool_data(staking_address, mining_address) do + call_methods([ {:staking, "isPoolActive", [staking_address]}, {:staking, "poolDelegators", [staking_address]}, {:staking, "stakeAmountTotalMinusOrderedWithdraw", [staking_address]}, @@ -88,7 +117,11 @@ defmodule Explorer.Staking.PoolsReader do {:validators, "isValidatorBanned", [mining_address]}, {:validators, "bannedUntil", [mining_address]}, {:validators, "banCounter", [mining_address]} - ] + ]) + end + + defp call_methods(methods) do + contract_abi = abi("staking.json") ++ abi("validators.json") methods |> Enum.map(&format_request/1) diff --git a/apps/indexer/lib/indexer/fetcher/staking_pools.ex b/apps/indexer/lib/indexer/fetcher/staking_pools.ex index 1632229e08..e5fb5485a6 100644 --- a/apps/indexer/lib/indexer/fetcher/staking_pools.ex +++ b/apps/indexer/lib/indexer/fetcher/staking_pools.ex @@ -94,21 +94,22 @@ defmodule Indexer.Fetcher.StakingPools do defp import_pools(pools) do {failed, success} = Enum.reduce(pools, {[], []}, fn - %{error: _error, staking_address_hash: address}, {failed, success} -> - {[address | failed], success} + %{error: _error} = pool, {failed, success} -> + {[pool | failed], success} - %{staking_address_hash: address} = pool, {failed, success} -> + pool, {failed, success} -> changeset = StakingPool.changeset(%StakingPool{}, pool) if changeset.valid? do {failed, [changeset.changes | success]} else - {[address | failed], success} + {[pool | failed], success} end end) import_params = %{ - staking_pools: %{params: success}, + staking_pools: %{params: remove_assoc(success)}, + staking_pools_delegators: %{params: delegators_list(success)}, timeout: :infinity } @@ -124,4 +125,16 @@ defmodule Indexer.Fetcher.StakingPools do failed end + + defp delegators_list(pools) do + Enum.reduce(pools, [], fn pool, acc -> + pool.delegators + |> Enum.map(pool.delegators, &Map.get(&1, :changes)) + |> Enum.concat(acc) + end) + end + + defp remove_assoc(pools) do + Enum.map(pools, &Map.delete(&1, :delegators)) + end end From 93bd9f0a00280408fd6f939ea9cc7ee285f1680c Mon Sep 17 00:00:00 2001 From: saneery Date: Mon, 27 May 2019 11:04:43 +0300 Subject: [PATCH 14/57] rename delegators module --- .../import/runner/staking_pools_delegators.ex | 14 +++++++------- .../lib/explorer/chain/staking_pool.ex | 4 ++-- ...elegators.ex => staking_pools_delegator.ex} | 6 +++--- ...3112839_create_staking_pools_delegators.exs | 2 +- .../chain/staking_pools_delegator_test.exs | 18 ++++++++++++++++++ .../chain/staking_pools_delegators_test.exs | 18 ------------------ apps/explorer/test/support/factory.ex | 6 +++--- 7 files changed, 34 insertions(+), 34 deletions(-) rename apps/explorer/lib/explorer/chain/{staking_pools_delegators.ex => staking_pools_delegator.ex} (92%) create mode 100644 apps/explorer/test/explorer/chain/staking_pools_delegator_test.exs delete mode 100644 apps/explorer/test/explorer/chain/staking_pools_delegators_test.exs diff --git a/apps/explorer/lib/explorer/chain/import/runner/staking_pools_delegators.ex b/apps/explorer/lib/explorer/chain/import/runner/staking_pools_delegators.ex index 5d44d68a92..22d62592c3 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/staking_pools_delegators.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/staking_pools_delegators.ex @@ -1,12 +1,12 @@ defmodule Explorer.Chain.Import.Runner.StakingPoolsDelegators do @moduledoc """ - Bulk imports staking pools to StakingPoolsDelegators tabe. + Bulk imports staking pools to StakingPoolsDelegator tabe. """ require Ecto.Query alias Ecto.{Changeset, Multi, Repo} - alias Explorer.Chain.{Import, StakingPoolsDelegators} + alias Explorer.Chain.{Import, StakingPoolsDelegator} import Ecto.Query, only: [from: 2] @@ -15,10 +15,10 @@ defmodule Explorer.Chain.Import.Runner.StakingPoolsDelegators do # milliseconds @timeout 60_000 - @type imported :: [StakingPoolsDelegators.t()] + @type imported :: [StakingPoolsDelegator.t()] @impl Import.Runner - def ecto_schema_module, do: StakingPoolsDelegators + def ecto_schema_module, do: StakingPoolsDelegator @impl Import.Runner def option_key, do: :staking_pools_delegators @@ -54,7 +54,7 @@ defmodule Explorer.Chain.Import.Runner.StakingPoolsDelegators do required(:timeout) => timeout, required(:timestamps) => Import.timestamps() }) :: - {:ok, [StakingPoolsDelegators.t()]} + {:ok, [StakingPoolsDelegator.t()]} | {:error, [Changeset.t()]} defp insert(repo, 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) @@ -65,7 +65,7 @@ defmodule Explorer.Chain.Import.Runner.StakingPoolsDelegators do changes_list, conflict_target: [:pool_address_hash, :delegator_address_hash], on_conflict: on_conflict, - for: StakingPoolsDelegators, + for: StakingPoolsDelegator, returning: [:pool_address_hash, :delegator_address_hash], timeout: timeout, timestamps: timestamps @@ -74,7 +74,7 @@ defmodule Explorer.Chain.Import.Runner.StakingPoolsDelegators do defp default_on_conflict do from( - name in StakingPoolsDelegators, + name in StakingPoolsDelegator, update: [ set: [ stake_amount: fragment("EXCLUDED.stake_amount"), diff --git a/apps/explorer/lib/explorer/chain/staking_pool.ex b/apps/explorer/lib/explorer/chain/staking_pool.ex index 31c5b1cc26..bb5635702d 100644 --- a/apps/explorer/lib/explorer/chain/staking_pool.ex +++ b/apps/explorer/lib/explorer/chain/staking_pool.ex @@ -9,7 +9,7 @@ defmodule Explorer.Chain.StakingPool do alias Explorer.Chain.{ Address, Hash, - StakingPoolsDelegators, + StakingPoolsDelegator, Wei } @@ -54,7 +54,7 @@ defmodule Explorer.Chain.StakingPool do field(:was_banned_count, :integer) field(:was_validator_count, :integer) field(:is_deleted, :boolean, default: false) - has_many(:delegators, StakingPoolsDelegators) + has_many(:delegators, StakingPoolsDelegator) belongs_to( :staking_address, diff --git a/apps/explorer/lib/explorer/chain/staking_pools_delegators.ex b/apps/explorer/lib/explorer/chain/staking_pools_delegator.ex similarity index 92% rename from apps/explorer/lib/explorer/chain/staking_pools_delegators.ex rename to apps/explorer/lib/explorer/chain/staking_pools_delegator.ex index d40edfaad8..8fabc6c447 100644 --- a/apps/explorer/lib/explorer/chain/staking_pools_delegators.ex +++ b/apps/explorer/lib/explorer/chain/staking_pools_delegator.ex @@ -1,4 +1,4 @@ -defmodule Explorer.Chain.StakingPoolsDelegators do +defmodule Explorer.Chain.StakingPoolsDelegator do @moduledoc """ The representation of delegators from POSDAO network. Delegators make stakes on staking pools and withdraw from them. @@ -55,8 +55,8 @@ defmodule Explorer.Chain.StakingPoolsDelegators do end @doc false - def changeset(staking_pools_delegators, attrs) do - staking_pools_delegators + def changeset(staking_pools_delegator, attrs) do + staking_pools_delegator |> cast(attrs, @attrs) |> validate_required(@attrs) |> unique_constraint(:pool_address_hash, name: :pools_delegator_index) diff --git a/apps/explorer/priv/repo/migrations/20190523112839_create_staking_pools_delegators.exs b/apps/explorer/priv/repo/migrations/20190523112839_create_staking_pools_delegators.exs index 1f243771fb..ccf9c8c372 100644 --- a/apps/explorer/priv/repo/migrations/20190523112839_create_staking_pools_delegators.exs +++ b/apps/explorer/priv/repo/migrations/20190523112839_create_staking_pools_delegators.exs @@ -1,4 +1,4 @@ -defmodule Explorer.Repo.Migrations.CreateStakingPoolsDelegators do +defmodule Explorer.Repo.Migrations.CreateStakingPoolsDelegator do use Ecto.Migration def change do diff --git a/apps/explorer/test/explorer/chain/staking_pools_delegator_test.exs b/apps/explorer/test/explorer/chain/staking_pools_delegator_test.exs new file mode 100644 index 0000000000..222658e92e --- /dev/null +++ b/apps/explorer/test/explorer/chain/staking_pools_delegator_test.exs @@ -0,0 +1,18 @@ +defmodule Explorer.Chain.StakingPoolsDelegatorTest do + use Explorer.DataCase + + alias Explorer.Chain.StakingPoolsDelegator + + describe "changeset/2" do + test "with valid attributes" do + params = params_for(:staking_pools_delegator) + changeset = StakingPoolsDelegator.changeset(%StakingPoolsDelegator{}, params) + assert changeset.valid? + end + + test "with invalid attributes" do + changeset = StakingPoolsDelegator.changeset(%StakingPoolsDelegator{}, %{pool_address_hash: 0}) + refute changeset.valid? + end + end +end diff --git a/apps/explorer/test/explorer/chain/staking_pools_delegators_test.exs b/apps/explorer/test/explorer/chain/staking_pools_delegators_test.exs deleted file mode 100644 index a27dada2eb..0000000000 --- a/apps/explorer/test/explorer/chain/staking_pools_delegators_test.exs +++ /dev/null @@ -1,18 +0,0 @@ -defmodule Explorer.Chain.StakingPoolsDelegatorsTest do - use Explorer.DataCase - - alias Explorer.Chain.StakingPoolsDelegators - - describe "changeset/2" do - test "with valid attributes" do - params = params_for(:staking_pools_delegators) - changeset = StakingPoolsDelegators.changeset(%StakingPoolsDelegators{}, params) - assert changeset.valid? - end - - test "with invalid attributes" do - changeset = StakingPoolsDelegators.changeset(%StakingPoolsDelegators{}, %{pool_address_hash: 0}) - refute changeset.valid? - end - end -end diff --git a/apps/explorer/test/support/factory.ex b/apps/explorer/test/support/factory.ex index 58a16c97b9..9d0a2e9997 100644 --- a/apps/explorer/test/support/factory.ex +++ b/apps/explorer/test/support/factory.ex @@ -28,7 +28,7 @@ defmodule Explorer.Factory do TokenTransfer, Transaction, StakingPool, - StakingPoolsDelegators + StakingPoolsDelegator } alias Explorer.Market.MarketHistory @@ -630,10 +630,10 @@ defmodule Explorer.Factory do } end - def staking_pools_delegators_factory do + def staking_pools_delegator_factory do wei_per_ether = 1_000_000_000_000_000_000 - %StakingPoolsDelegators{ + %StakingPoolsDelegator{ pool_address_hash: address_hash(), delegator_address_hash: address_hash(), max_ordered_withdraw_allowed: wei_per_ether * 100, From 083f5e6f5d1a6b836b9f22c2f7a4dfcbfc8f928e Mon Sep 17 00:00:00 2001 From: saneery Date: Mon, 27 May 2019 11:20:00 +0300 Subject: [PATCH 15/57] staking pools delegators importer test --- .../lib/explorer/chain/staking_pool.ex | 2 +- .../runner/staking_pools_delegators_test.exs | 33 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 apps/explorer/test/explorer/chain/import/runner/staking_pools_delegators_test.exs diff --git a/apps/explorer/lib/explorer/chain/staking_pool.ex b/apps/explorer/lib/explorer/chain/staking_pool.ex index bb5635702d..7949661c23 100644 --- a/apps/explorer/lib/explorer/chain/staking_pool.ex +++ b/apps/explorer/lib/explorer/chain/staking_pool.ex @@ -54,7 +54,7 @@ defmodule Explorer.Chain.StakingPool do field(:was_banned_count, :integer) field(:was_validator_count, :integer) field(:is_deleted, :boolean, default: false) - has_many(:delegators, StakingPoolsDelegator) + has_many(:delegators, StakingPoolsDelegator, foreign_key: :pool_address_hash) belongs_to( :staking_address, diff --git a/apps/explorer/test/explorer/chain/import/runner/staking_pools_delegators_test.exs b/apps/explorer/test/explorer/chain/import/runner/staking_pools_delegators_test.exs new file mode 100644 index 0000000000..4916f2ef84 --- /dev/null +++ b/apps/explorer/test/explorer/chain/import/runner/staking_pools_delegators_test.exs @@ -0,0 +1,33 @@ +defmodule Explorer.Chain.Import.Runner.StakingPoolsDelegatorsTest do + use Explorer.DataCase + + import Explorer.Factory + + alias Ecto.Multi + alias Explorer.Chain.Import.Runner.StakingPoolsDelegators + alias Explorer.Chain.StakingPoolsDelegator + + describe "run/1" do + test "insert new pools list" do + delegators = + [delegator1, delegator2] = + [params_for(:staking_pools_delegator), params_for(:staking_pools_delegator)] + |> Enum.map(fn param -> + changeset = StakingPoolsDelegator.changeset(%StakingPoolsDelegator{}, param) + changeset.changes + end) + + assert {:ok, %{insert_staking_pools_delegators: list}} = run_changes(delegators) + assert Enum.count(list) == Enum.count(delegators) + end + end + + defp run_changes(changes) do + Multi.new() + |> StakingPoolsDelegators.run(changes, %{ + timeout: :infinity, + timestamps: %{inserted_at: DateTime.utc_now(), updated_at: DateTime.utc_now()} + }) + |> Repo.transaction() + end +end From 7abfd0844bd2a36e50e38963b03f4f8d5de0e89f Mon Sep 17 00:00:00 2001 From: saneery Date: Mon, 27 May 2019 11:23:27 +0300 Subject: [PATCH 16/57] update pools reader test --- apps/explorer/test/explorer/staking/pools_reader_test.exs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/explorer/test/explorer/staking/pools_reader_test.exs b/apps/explorer/test/explorer/staking/pools_reader_test.exs index ec17f7b5f7..aeea00c1e5 100644 --- a/apps/explorer/test/explorer/staking/pools_reader_test.exs +++ b/apps/explorer/test/explorer/staking/pools_reader_test.exs @@ -47,7 +47,8 @@ defmodule Explorer.Token.PoolsReaderTest do staking_address_hash: <<11, 47, 94, 47, 60, 189, 134, 78, 170, 44, 100, 46, 55, 105, 193, 88, 35, 97, 202, 246>>, was_banned_count: 0, - was_validator_count: 2 + was_validator_count: 2, + delegators: [] } } From 4808a8f06382166c42763bc057c232cc762c5e1f Mon Sep 17 00:00:00 2001 From: saneery Date: Mon, 27 May 2019 12:44:51 +0300 Subject: [PATCH 17/57] update pools reader test --- .../runner/staking_pools_delegators_test.exs | 11 +- .../explorer/staking/pools_reader_test.exs | 172 ++++++++++++++---- .../lib/indexer/fetcher/staking_pools.ex | 2 +- 3 files changed, 143 insertions(+), 42 deletions(-) diff --git a/apps/explorer/test/explorer/chain/import/runner/staking_pools_delegators_test.exs b/apps/explorer/test/explorer/chain/import/runner/staking_pools_delegators_test.exs index 4916f2ef84..9a11e3edc8 100644 --- a/apps/explorer/test/explorer/chain/import/runner/staking_pools_delegators_test.exs +++ b/apps/explorer/test/explorer/chain/import/runner/staking_pools_delegators_test.exs @@ -10,12 +10,11 @@ defmodule Explorer.Chain.Import.Runner.StakingPoolsDelegatorsTest do describe "run/1" do test "insert new pools list" do delegators = - [delegator1, delegator2] = - [params_for(:staking_pools_delegator), params_for(:staking_pools_delegator)] - |> Enum.map(fn param -> - changeset = StakingPoolsDelegator.changeset(%StakingPoolsDelegator{}, param) - changeset.changes - end) + [params_for(:staking_pools_delegator), params_for(:staking_pools_delegator)] + |> Enum.map(fn param -> + changeset = StakingPoolsDelegator.changeset(%StakingPoolsDelegator{}, param) + changeset.changes + end) assert {:ok, %{insert_staking_pools_delegators: list}} = run_changes(delegators) assert Enum.count(list) == Enum.count(delegators) diff --git a/apps/explorer/test/explorer/staking/pools_reader_test.exs b/apps/explorer/test/explorer/staking/pools_reader_test.exs index aeea00c1e5..6fb5940010 100644 --- a/apps/explorer/test/explorer/staking/pools_reader_test.exs +++ b/apps/explorer/test/explorer/staking/pools_reader_test.exs @@ -30,27 +30,38 @@ defmodule Explorer.Token.PoolsReaderTest do test "get_pool_data success" do get_pool_data_from_blockchain() - address = <<11, 47, 94, 47, 60, 189, 134, 78, 170, 44, 100, 46, 55, 105, 193, 88, 35, 97, 202, 246>> + address = <<219, 156, 178, 71, 141, 145, 119, 25, 197, 56, 98, 0, 134, 114, 22, 104, 8, 37, 133, 119>> - response = { - :ok, - %{ - banned_until: 0, - delegators_count: 0, - is_active: true, - is_banned: false, - is_validator: true, - mining_address_hash: - <<187, 202, 168, 212, 130, 137, 187, 31, 252, 249, 128, 141, 154, 164, 177, 210, 21, 5, 76, 120>>, - staked_amount: 0, - self_staked_amount: 0, - staking_address_hash: - <<11, 47, 94, 47, 60, 189, 134, 78, 170, 44, 100, 46, 55, 105, 193, 88, 35, 97, 202, 246>>, - was_banned_count: 0, - was_validator_count: 2, - delegators: [] - } - } + response = + {:ok, + %{ + banned_until: 0, + is_active: true, + is_banned: false, + is_validator: true, + was_banned_count: 0, + was_validator_count: 2, + delegators: [ + %{ + delegator_address_hash: + <<243, 231, 124, 74, 245, 235, 47, 51, 175, 255, 118, 25, 216, 209, 231, 81, 215, 24, 164, 145>>, + max_ordered_withdraw_allowed: 1_000_000_000_000_000_000, + max_withdraw_allowed: 1_000_000_000_000_000_000, + ordered_withdraw: 0, + ordered_withdraw_epoch: 0, + pool_address_hash: + <<219, 156, 178, 71, 141, 145, 119, 25, 197, 56, 98, 0, 134, 114, 22, 104, 8, 37, 133, 119>>, + stake_amount: 1_000_000_000_000_000_000 + } + ], + delegators_count: 1, + mining_address_hash: + <<190, 105, 235, 9, 104, 34, 106, 24, 8, 151, 94, 26, 31, 33, 39, 102, 127, 43, 255, 179>>, + self_staked_amount: 2_000_000_000_000_000_000, + staked_amount: 3_000_000_000_000_000_000, + staking_address_hash: + <<219, 156, 178, 71, 141, 145, 119, 25, 197, 56, 98, 0, 134, 114, 22, 104, 8, 37, 133, 119>> + }} assert PoolsReader.pool_data(address) == response end @@ -103,7 +114,7 @@ defmodule Explorer.Token.PoolsReaderTest do expect( EthereumJSONRPC.Mox, :json_rpc, - 2, + 3, fn requests, _opts -> {:ok, Enum.map(requests, fn @@ -112,13 +123,13 @@ defmodule Explorer.Token.PoolsReaderTest do id: id, method: "eth_call", params: [ - %{data: "0x005351750000000000000000000000000b2f5e2f3cbd864eaa2c642e3769c1582361caf6", to: _}, + %{data: "0x00535175000000000000000000000000db9cb2478d917719c53862008672166808258577", to: _}, "latest" ] } -> %{ id: id, - result: "0x000000000000000000000000bbcaa8d48289bb1ffcf9808d9aa4b1d215054c78" + result: "0x000000000000000000000000be69eb0968226a1808975e1a1f2127667f2bffb3" } # isPoolActive @@ -126,7 +137,7 @@ defmodule Explorer.Token.PoolsReaderTest do id: id, method: "eth_call", params: [ - %{data: "0xa711e6a10000000000000000000000000b2f5e2f3cbd864eaa2c642e3769c1582361caf6", to: _}, + %{data: "0xa711e6a1000000000000000000000000db9cb2478d917719c53862008672166808258577", to: _}, "latest" ] } -> @@ -140,14 +151,14 @@ defmodule Explorer.Token.PoolsReaderTest do id: id, method: "eth_call", params: [ - %{data: "0x9ea8082b0000000000000000000000000b2f5e2f3cbd864eaa2c642e3769c1582361caf6", to: _}, + %{data: "0x9ea8082b000000000000000000000000db9cb2478d917719c53862008672166808258577", to: _}, "latest" ] } -> %{ id: id, result: - "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000" + "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000001000000000000000000000000f3e77c4af5eb2f33afff7619d8d1e751d718a491" } # stakeAmountTotalMinusOrderedWithdraw @@ -155,13 +166,13 @@ defmodule Explorer.Token.PoolsReaderTest do id: id, method: "eth_call", params: [ - %{data: "0x234fbf2b0000000000000000000000000b2f5e2f3cbd864eaa2c642e3769c1582361caf6", to: _}, + %{data: "0x234fbf2b000000000000000000000000db9cb2478d917719c53862008672166808258577", to: _}, "latest" ] } -> %{ id: id, - result: "0x0000000000000000000000000000000000000000000000000000000000000000" + result: "0x00000000000000000000000000000000000000000000000029a2241af62c0000" } # stakeAmountMinusOrderedWithdraw @@ -172,7 +183,7 @@ defmodule Explorer.Token.PoolsReaderTest do params: [ %{ data: - "0x58daab6a0000000000000000000000000b2f5e2f3cbd864eaa2c642e3769c1582361caf60000000000000000000000000b2f5e2f3cbd864eaa2c642e3769c1582361caf6", + "0x58daab6a000000000000000000000000db9cb2478d917719c53862008672166808258577000000000000000000000000db9cb2478d917719c53862008672166808258577", to: _ }, "latest" @@ -180,7 +191,7 @@ defmodule Explorer.Token.PoolsReaderTest do } -> %{ id: id, - result: "0x0000000000000000000000000000000000000000000000000000000000000000" + result: "0x0000000000000000000000000000000000000000000000001bc16d674ec80000" } # isValidator @@ -188,7 +199,7 @@ defmodule Explorer.Token.PoolsReaderTest do id: id, method: "eth_call", params: [ - %{data: "0xfacd743b000000000000000000000000bbcaa8d48289bb1ffcf9808d9aa4b1d215054c78", to: _}, + %{data: "0xfacd743b000000000000000000000000be69eb0968226a1808975e1a1f2127667f2bffb3", to: _}, "latest" ] } -> @@ -202,7 +213,7 @@ defmodule Explorer.Token.PoolsReaderTest do id: id, method: "eth_call", params: [ - %{data: "0xb41832e4000000000000000000000000bbcaa8d48289bb1ffcf9808d9aa4b1d215054c78", to: _}, + %{data: "0xb41832e4000000000000000000000000be69eb0968226a1808975e1a1f2127667f2bffb3", to: _}, "latest" ] } -> @@ -216,7 +227,7 @@ defmodule Explorer.Token.PoolsReaderTest do id: id, method: "eth_call", params: [ - %{data: "0xa92252ae000000000000000000000000bbcaa8d48289bb1ffcf9808d9aa4b1d215054c78", to: _}, + %{data: "0xa92252ae000000000000000000000000be69eb0968226a1808975e1a1f2127667f2bffb3", to: _}, "latest" ] } -> @@ -230,7 +241,7 @@ defmodule Explorer.Token.PoolsReaderTest do id: id, method: "eth_call", params: [ - %{data: "0x5836d08a000000000000000000000000bbcaa8d48289bb1ffcf9808d9aa4b1d215054c78", to: _}, + %{data: "0x5836d08a000000000000000000000000be69eb0968226a1808975e1a1f2127667f2bffb3", to: _}, "latest" ] } -> @@ -244,7 +255,98 @@ defmodule Explorer.Token.PoolsReaderTest do id: id, method: "eth_call", params: [ - %{data: "0x1d0cd4c6000000000000000000000000bbcaa8d48289bb1ffcf9808d9aa4b1d215054c78", to: _}, + %{data: "0x1d0cd4c6000000000000000000000000be69eb0968226a1808975e1a1f2127667f2bffb3", to: _}, + "latest" + ] + } -> + %{ + id: id, + result: "0x0000000000000000000000000000000000000000000000000000000000000000" + } + + # DELEGATOR + # stakeAmount + %{ + id: id, + method: "eth_call", + params: [ + %{ + data: + "0xa697ecff000000000000000000000000db9cb2478d917719c53862008672166808258577000000000000000000000000f3e77c4af5eb2f33afff7619d8d1e751d718a491", + to: _ + }, + "latest" + ] + } -> + %{ + id: id, + result: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000" + } + + # orderedWithdrawAmount + %{ + id: id, + method: "eth_call", + params: [ + %{ + data: + "0xe9ab0300000000000000000000000000db9cb2478d917719c53862008672166808258577000000000000000000000000f3e77c4af5eb2f33afff7619d8d1e751d718a491", + to: _ + }, + "latest" + ] + } -> + %{ + id: id, + result: "0x0000000000000000000000000000000000000000000000000000000000000000" + } + + # maxWithdrawAllowed + %{ + id: id, + method: "eth_call", + params: [ + %{ + data: + "0x6bda1577000000000000000000000000db9cb2478d917719c53862008672166808258577000000000000000000000000f3e77c4af5eb2f33afff7619d8d1e751d718a491", + to: _ + }, + "latest" + ] + } -> + %{ + id: id, + result: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000" + } + + # maxWithdrawOrderAllowed + %{ + id: id, + method: "eth_call", + params: [ + %{ + data: + "0x950a6513000000000000000000000000db9cb2478d917719c53862008672166808258577000000000000000000000000f3e77c4af5eb2f33afff7619d8d1e751d718a491", + to: _ + }, + "latest" + ] + } -> + %{ + id: id, + result: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000" + } + + # orderWithdrawEpoch + %{ + id: id, + method: "eth_call", + params: [ + %{ + data: + "0xa4205967000000000000000000000000db9cb2478d917719c53862008672166808258577000000000000000000000000f3e77c4af5eb2f33afff7619d8d1e751d718a491", + to: _ + }, "latest" ] } -> diff --git a/apps/indexer/lib/indexer/fetcher/staking_pools.ex b/apps/indexer/lib/indexer/fetcher/staking_pools.ex index e5fb5485a6..1576eb56e2 100644 --- a/apps/indexer/lib/indexer/fetcher/staking_pools.ex +++ b/apps/indexer/lib/indexer/fetcher/staking_pools.ex @@ -129,7 +129,7 @@ defmodule Indexer.Fetcher.StakingPools do defp delegators_list(pools) do Enum.reduce(pools, [], fn pool, acc -> pool.delegators - |> Enum.map(pool.delegators, &Map.get(&1, :changes)) + |> Enum.map(&Map.get(&1, :changes)) |> Enum.concat(acc) end) end From 0c0662d86e6340e51cfce84fc300ba037165072f Mon Sep 17 00:00:00 2001 From: saneery Date: Mon, 27 May 2019 13:58:03 +0300 Subject: [PATCH 18/57] update method that return pools list --- apps/explorer/lib/explorer/chain.ex | 46 +++++++--------------- apps/explorer/test/explorer/chain_test.exs | 34 ++++++++-------- 2 files changed, 31 insertions(+), 49 deletions(-) diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index 0977df1dd2..ed76150bc3 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -34,6 +34,7 @@ defmodule Explorer.Chain do BlockNumberCache, Data, DecompiledSmartContract, + StakingPool, Hash, Import, InternalTransaction, @@ -2909,7 +2910,7 @@ defmodule Explorer.Chain do def staking_pools(filter, %PagingOptions{page_size: page_size, page_number: page_number} \\ @default_paging_options) do off = page_size * (page_number - 1) - Address.Name + StakingPool |> staking_pool_filter(filter) |> limit(^page_size) |> offset(^off) @@ -2919,55 +2920,36 @@ defmodule Explorer.Chain do @doc "Get count of staking pools from the DB" @spec staking_pools_count(filter :: :validator | :active | :inactive) :: integer def staking_pools_count(filter) do - Address.Name + StakingPool |> staking_pool_filter(filter) - |> Repo.aggregate(:count, :address_hash) + |> Repo.aggregate(:count, :staking_address_hash) end defp staking_pool_filter(query, :validator) do where( query, - [address], - fragment( - """ - (?->>'is_active')::boolean = true and - (?->>'deleted')::boolean is not true and - (?->>'is_validator')::boolean = true - """, - address.metadata, - address.metadata, - address.metadata - ) + [pool], + pool.is_active == true and + pool.is_deleted == false and + pool.is_validator == true ) end defp staking_pool_filter(query, :active) do where( query, - [address], - fragment( - """ - (?->>'is_active')::boolean = true and - (?->>'deleted')::boolean is not true - """, - address.metadata, - address.metadata - ) + [pool], + pool.is_active == true and + pool.is_deleted == false ) end defp staking_pool_filter(query, :inactive) do where( query, - [address], - fragment( - """ - (?->>'is_active')::boolean = false and - (?->>'deleted')::boolean is not true - """, - address.metadata, - address.metadata - ) + [pool], + pool.is_active == false and + pool.is_deleted == false ) end diff --git a/apps/explorer/test/explorer/chain_test.exs b/apps/explorer/test/explorer/chain_test.exs index cafd742e2a..78b938d6f3 100644 --- a/apps/explorer/test/explorer/chain_test.exs +++ b/apps/explorer/test/explorer/chain_test.exs @@ -3951,54 +3951,54 @@ defmodule Explorer.ChainTest do describe "staking_pools/3" do test "validators staking pools" do - inserted_validator = insert(:address_name, primary: true, metadata: %{is_active: true, is_validator: true}) - insert(:address_name, primary: true, metadata: %{is_active: true, is_validator: false}) + inserted_validator = insert(:staking_pool, is_active: true, is_validator: true) + insert(:staking_pool, is_active: true, is_validator: false) options = %PagingOptions{page_size: 20, page_number: 1} assert [gotten_validator] = Chain.staking_pools(:validator, options) - assert inserted_validator.address_hash == gotten_validator.address_hash + assert inserted_validator.staking_address_hash == gotten_validator.staking_address_hash end test "active staking pools" do - inserted_validator = insert(:address_name, primary: true, metadata: %{is_active: true}) - insert(:address_name, primary: true, metadata: %{is_active: false}) + inserted_pool = insert(:staking_pool, is_active: true) + insert(:staking_pool, is_active: false) options = %PagingOptions{page_size: 20, page_number: 1} - assert [gotten_validator] = Chain.staking_pools(:active, options) - assert inserted_validator.address_hash == gotten_validator.address_hash + assert [gotten_pool] = Chain.staking_pools(:active, options) + assert inserted_pool.staking_address_hash == gotten_pool.staking_address_hash end test "inactive staking pools" do - insert(:address_name, primary: true, metadata: %{is_active: true}) - inserted_validator = insert(:address_name, primary: true, metadata: %{is_active: false}) + insert(:staking_pool, is_active: true) + inserted_pool = insert(:staking_pool, is_active: false) options = %PagingOptions{page_size: 20, page_number: 1} - assert [gotten_validator] = Chain.staking_pools(:inactive, options) - assert inserted_validator.address_hash == gotten_validator.address_hash + assert [gotten_pool] = Chain.staking_pools(:inactive, options) + assert inserted_pool.staking_address_hash == gotten_pool.staking_address_hash end end describe "staking_pools_count/1" do test "validators staking pools" do - insert(:address_name, primary: true, metadata: %{is_active: true, is_validator: true}) - insert(:address_name, primary: true, metadata: %{is_active: true, is_validator: false}) + insert(:staking_pool, is_active: true, is_validator: true) + insert(:staking_pool, is_active: true, is_validator: false) assert Chain.staking_pools_count(:validator) == 1 end test "active staking pools" do - insert(:address_name, primary: true, metadata: %{is_active: true}) - insert(:address_name, primary: true, metadata: %{is_active: false}) + insert(:staking_pool, is_active: true) + insert(:staking_pool, is_active: false) assert Chain.staking_pools_count(:active) == 1 end test "inactive staking pools" do - insert(:address_name, primary: true, metadata: %{is_active: true}) - insert(:address_name, primary: true, metadata: %{is_active: false}) + insert(:staking_pool, is_active: true) + insert(:staking_pool, is_active: false) assert Chain.staking_pools_count(:inactive) == 1 end From f0b228e69a938ed8d1dd89624aaeaddfd0cd84b1 Mon Sep 17 00:00:00 2001 From: saneery Date: Mon, 27 May 2019 14:16:46 +0300 Subject: [PATCH 19/57] add changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d3d9ef4a3..8c00acabfe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ - [#1933](https://github.com/poanetwork/blockscout/pull/1933) - add eth_BlockNumber json rpc method - [#1952](https://github.com/poanetwork/blockscout/pull/1952) - feat: exclude empty contracts by default - [#1954](https://github.com/poanetwork/blockscout/pull/1954) - feat: use creation init on self destruct +- [#2036](https://github.com/poanetwork/blockscout/pull/2036) - New tables for staking pools and delegators ### Fixes From e0bed0ea1c7a6843bc225ef9ff412a5275062ba1 Mon Sep 17 00:00:00 2001 From: saneery Date: Mon, 27 May 2019 14:17:51 +0300 Subject: [PATCH 20/57] update doc --- .../explorer/chain/import/runner/staking_pools_delegators.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/explorer/lib/explorer/chain/import/runner/staking_pools_delegators.ex b/apps/explorer/lib/explorer/chain/import/runner/staking_pools_delegators.ex index 22d62592c3..cf7391dc89 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/staking_pools_delegators.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/staking_pools_delegators.ex @@ -1,6 +1,6 @@ defmodule Explorer.Chain.Import.Runner.StakingPoolsDelegators do @moduledoc """ - Bulk imports staking pools to StakingPoolsDelegator tabe. + Bulk imports delegators to StakingPoolsDelegator tabe. """ require Ecto.Query From 83cca0aa283dfaa44feca3bebb9fafc87173d24a Mon Sep 17 00:00:00 2001 From: saneery Date: Mon, 27 May 2019 14:19:20 +0300 Subject: [PATCH 21/57] remove cast! and dump! functions from Wei module --- apps/explorer/lib/explorer/chain/wei.ex | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/apps/explorer/lib/explorer/chain/wei.ex b/apps/explorer/lib/explorer/chain/wei.ex index 8b557b4ed5..4b12d3deb0 100644 --- a/apps/explorer/lib/explorer/chain/wei.ex +++ b/apps/explorer/lib/explorer/chain/wei.ex @@ -68,11 +68,6 @@ defmodule Explorer.Chain.Wei do @impl Ecto.Type def cast(_), do: :error - def cast!(arg) do - {:ok, wei} = cast(arg) - wei - end - @impl Ecto.Type def dump(%__MODULE__{value: %Decimal{} = decimal}) do {:ok, decimal} @@ -81,11 +76,6 @@ defmodule Explorer.Chain.Wei do @impl Ecto.Type def dump(_), do: :error - def dump!(arg) do - {:ok, decimal} = dump(arg) - decimal - end - @impl Ecto.Type def load(%Decimal{} = decimal) do {:ok, %__MODULE__{value: decimal}} @@ -248,11 +238,6 @@ defmodule Explorer.Chain.Wei do @spec to(t(), :wei) :: wei() def to(%__MODULE__{value: wei}, :wei), do: wei - - @spec to(t(), :integer) :: integer() - def to(%__MODULE__{value: wei}, :integer) do - Decimal.to_integer(wei) - end end defimpl Inspect, for: Explorer.Chain.Wei do From 1ea565d1a9bff872b4ae1b764360c8144ae42c1e Mon Sep 17 00:00:00 2001 From: saneery Date: Mon, 27 May 2019 14:23:48 +0300 Subject: [PATCH 22/57] mix credo --- apps/explorer/lib/explorer/chain.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index ed76150bc3..c6d88bc524 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -34,12 +34,12 @@ defmodule Explorer.Chain do BlockNumberCache, Data, DecompiledSmartContract, - StakingPool, Hash, Import, InternalTransaction, Log, SmartContract, + StakingPool, Token, TokenTransfer, Transaction, From e0468fffdd1cdbe986529abb0e740ff15d133e74 Mon Sep 17 00:00:00 2001 From: Ayrat Badykov Date: Mon, 27 May 2019 15:50:57 +0300 Subject: [PATCH 23/57] add endpoint to search address logs by topic --- .../controllers/address_logs_controller.ex | 43 ++++++++++++++++++ .../lib/block_scout_web/router.ex | 2 + apps/explorer/lib/explorer/chain.ex | 14 +++++- apps/explorer/test/explorer/chain_test.exs | 44 +++++++++++++++++++ 4 files changed, 102 insertions(+), 1 deletion(-) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_logs_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_logs_controller.ex index f79d9aa08d..8731efbb4f 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_logs_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_logs_controller.ex @@ -6,9 +6,11 @@ defmodule BlockScoutWeb.AddressLogsController do import BlockScoutWeb.AddressController, only: [transaction_count: 1, validation_count: 1] import BlockScoutWeb.Chain, only: [paging_options: 1, next_page_params: 3, split_list_by_page: 1] + alias BlockScoutWeb.AddressLogsView alias Explorer.{Chain, Market} alias Explorer.ExchangeRates.Token alias Indexer.Fetcher.CoinBalanceOnDemand + alias Phoenix.View use BlockScoutWeb, :controller @@ -43,4 +45,45 @@ defmodule BlockScoutWeb.AddressLogsController do not_found(conn) end end + + def search_logs(conn, %{"topic" => topic, "address_id" => address_hash_string} = params) do + with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), + {:ok, address} <- Chain.hash_to_address(address_hash) do + topic = String.trim(topic) + logs_plus_one = Chain.address_to_logs(address, topic: topic) + + {results, next_page} = split_list_by_page(logs_plus_one) + + next_page_url = + case next_page_params(next_page, results, params) do + nil -> + nil + + next_page_params -> + address_logs_path(conn, :index, address, Map.delete(next_page_params, "type")) + end + + items = + results + |> Enum.map(fn log -> + View.render_to_string( + AddressLogsView, + "_logs.html", + log: log, + conn: conn + ) + end) + + json( + conn, + %{ + items: items, + next_page_path: next_page_url + } + ) + else + _ -> + not_found(conn) + 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 b2f639d763..aa19725811 100644 --- a/apps/block_scout_web/lib/block_scout_web/router.ex +++ b/apps/block_scout_web/lib/block_scout_web/router.ex @@ -238,6 +238,8 @@ defmodule BlockScoutWeb.Router do get("/search", ChainController, :search) + get("/search_logs", AddressLogsController, :search_logs) + get("/token_autocomplete", ChainController, :token_autocomplete) get("/chain_blocks", ChainController, :chain_blocks, as: :chain_blocks) diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index 9446388531..d5117453bf 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -291,8 +291,9 @@ defmodule Explorer.Chain do paging_options = Keyword.get(options, :paging_options) || %PagingOptions{page_size: 50} {block_number, transaction_index, log_index} = paging_options.key || {BlockNumberCache.max_number(), 0, 0} + topic = Keyword.get(options, :topic) - query = + base_query = from(log in Log, inner_join: transaction in assoc(log, :transaction), order_by: [desc: transaction.block_number, desc: transaction.index], @@ -307,6 +308,17 @@ defmodule Explorer.Chain do select: log ) + query = + if topic do + from(log in base_query, + where: + log.first_topic == ^topic or log.second_topic == ^topic or log.third_topic == ^topic or + log.fourth_topic == ^topic + ) + else + base_query + end + query |> Repo.all() |> Enum.take(paging_options.page_size) diff --git a/apps/explorer/test/explorer/chain_test.exs b/apps/explorer/test/explorer/chain_test.exs index 0234672183..6f3de49226 100644 --- a/apps/explorer/test/explorer/chain_test.exs +++ b/apps/explorer/test/explorer/chain_test.exs @@ -93,6 +93,50 @@ defmodule Explorer.ChainTest do assert Enum.count(Chain.address_to_logs(address, paging_options: paging_options2)) == 50 end + + test "searches logs by topic when the first topic matches" do + address = insert(:address) + + transaction1 = + :transaction + |> insert(to_address: address) + |> with_block() + + insert(:log, transaction: transaction1, index: 1, address: address) + + transaction2 = + :transaction + |> insert(from_address: address) + |> with_block() + + insert(:log, transaction: transaction2, index: 2, address: address, first_topic: "test") + + [found_log] = Chain.address_to_logs(address, topic: "test") + + assert found_log.transaction.hash == transaction2.hash + end + + test "searches logs by topic when the fourth topic matches" do + address = insert(:address) + + transaction1 = + :transaction + |> insert(to_address: address) + |> with_block() + + insert(:log, transaction: transaction1, index: 1, address: address, fourth_topic: "test") + + transaction2 = + :transaction + |> insert(from_address: address) + |> with_block() + + insert(:log, transaction: transaction2, index: 2, address: address) + + [found_log] = Chain.address_to_logs(address, topic: "test") + + assert found_log.transaction.hash == transaction1.hash + end end describe "address_to_transactions_with_rewards/2" do From c71d372831cdc8d2ce16394fcf231bd136666d6d Mon Sep 17 00:00:00 2001 From: Ayrat Badykov Date: Tue, 28 May 2019 14:59:36 +0300 Subject: [PATCH 24/57] add js logic for logs search --- apps/block_scout_web/assets/js/app.js | 1 + .../assets/js/pages/address/logs.js | 64 +++++++++++++ .../templates/address_logs/_logs.html.eex | 54 +++++++++++ .../templates/address_logs/index.html.eex | 89 +++++-------------- 4 files changed, 139 insertions(+), 69 deletions(-) create mode 100644 apps/block_scout_web/assets/js/pages/address/logs.js create mode 100644 apps/block_scout_web/lib/block_scout_web/templates/address_logs/_logs.html.eex diff --git a/apps/block_scout_web/assets/js/app.js b/apps/block_scout_web/assets/js/app.js index f10e3d696d..0893b8b4ae 100644 --- a/apps/block_scout_web/assets/js/app.js +++ b/apps/block_scout_web/assets/js/app.js @@ -23,6 +23,7 @@ import './locale' import './pages/address' import './pages/address/coin_balances' import './pages/address/transactions' +import './pages/address/logs' import './pages/address/validations' import './pages/address/internal_transactions' import './pages/blocks' diff --git a/apps/block_scout_web/assets/js/pages/address/logs.js b/apps/block_scout_web/assets/js/pages/address/logs.js new file mode 100644 index 0000000000..ddfe3e9bfc --- /dev/null +++ b/apps/block_scout_web/assets/js/pages/address/logs.js @@ -0,0 +1,64 @@ +import $ from 'jquery' +import _ from 'lodash' +import URI from 'urijs' +import humps from 'humps' +import { subscribeChannel } from '../../socket' +import { connectElements } from '../../lib/redux_helpers.js' +import { createAsyncLoadStore } from '../../lib/async_listing_load' + +export const initialState = { + addressHash: null +} + +export function reducer (state, action) { + switch (action.type) { + case 'PAGE_LOAD': + case 'ELEMENTS_LOAD': { + return Object.assign({}, state, _.omit(action, 'type')) + } + default: + return state + } +} + +const elements = { + '[data-search-field]' : { + render ($el, state) { + $el + } + }, + '[data-search-button]' : { + render ($el, state) { + $el + } + } +} + +if ($('[data-page="address-logs"]').length) { + console.log('iffff') + const store = createAsyncLoadStore(reducer, initialState, 'dataset.identifierHash') + const addressHash = $('[data-page="address-details"]')[0].dataset.pageAddressHash + const $element = $('[data-async-listing]') + + + connectElements({ store, elements }) + + store.dispatch({ + type: 'PAGE_LOAD', + addressHash: addressHash}) + + function loadSearchItems () { + var topic = $('[data-search-field]').val(); + var path = "/search_logs?topic=" + topic + "&address_id=" + store.getState().addressHash + store.dispatch({type: 'START_REQUEST'}) + $.getJSON(path, {type: 'JSON'}) + .done(response => store.dispatch(Object.assign({type: 'ITEMS_FETCHED'}, humps.camelizeKeys(response)))) + .fail(() => store.dispatch({type: 'REQUEST_ERROR'})) + .always(() => store.dispatch({type: 'FINISH_REQUEST'})) + } + + + $element.on('click', '[data-search-button]', (event) => { + loadSearchItems() + }) +} diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_logs/_logs.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_logs/_logs.html.eex new file mode 100644 index 0000000000..66f75362c3 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_logs/_logs.html.eex @@ -0,0 +1,54 @@ +
+
+
<%= gettext "Transaction" %>
+
+

+ <%= link( + @log.transaction, + to: transaction_path(@conn, :show, @log.transaction), + "data-test": "log_address_link", + "data-address-hash": @log.transaction + ) %> +

+
+
<%= gettext "Topics" %>
+
+
+ <%= unless is_nil(@log.first_topic) do %> +
+ [0] + <%= @log.first_topic %> +
+ <% end %> + <%= unless is_nil(@log.second_topic) do %> +
+ [1] + <%= @log.second_topic %> +
+ <% end %> + <%= unless is_nil(@log.third_topic) do %> +
+ [2] + <%= @log.third_topic %> +
+ <% end %> + <%= unless is_nil(@log.fourth_topic) do %> +
+ [3] + <%= @log.fourth_topic %> +
+ <% end %> +
+
+
+ <%= gettext "Data" %> +
+
+ <%= unless is_nil(@log.data) do %> +
+ <%= @log.data %> +
+ <% end %> +
+
+
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_logs/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_logs/index.html.eex index 22f446924f..f873465372 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address_logs/index.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_logs/index.html.eex @@ -1,82 +1,33 @@
<%= render BlockScoutWeb.AddressView, "overview.html", assigns %> +
<%= render BlockScoutWeb.AddressView, "_tabs.html", assigns %> -
- +

<%= gettext "Logs" %>

- <%= if @next_page_url do %> - <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "top", cur_page_number: "1", show_pagination_limit: true, next_page_path: @next_page_url %> - <% end %> + id='search-text-input' /> + + + <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "top", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> - <%= if !@next_page_url do %> - <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "top", cur_page_number: "1", show_pagination_limit: true %> - <% end %> + - <%= if Enum.count(@logs) > 0 do %> - <%= for log <- @logs do %> -
-
-
<%= gettext "Transaction" %>
-
-

- <%= link( - log.transaction, - to: transaction_path(@conn, :show, log.transaction), - "data-test": "log_address_link", - "data-address-hash": log.transaction - ) %> -

-
-
<%= gettext "Topics" %>
-
-
- <%= unless is_nil(log.first_topic) do %> -
- [0] - <%= log.first_topic %> -
- <% end %> - <%= unless is_nil(log.second_topic) do %> -
- [1] - <%= log.second_topic %> -
- <% end %> - <%= unless is_nil(log.third_topic) do %> -
- [2] - <%= log.third_topic %> -
- <% end %> - <%= unless is_nil(log.fourth_topic) do %> -
- [3] - <%= log.fourth_topic %> -
- <% end %> -
-
-
- <%= gettext "Data" %> -
-
- <%= unless is_nil(log.data) do %> -
- <%= log.data %> -
- <% end %> -
-
+
+
+ <%= gettext "There are no logs for this address." %>
- <% end %> - <% else %> -
- <%= gettext "There are no logs for this address." %> -
- <% end %> +
+ +
+ + <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "bottom", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> +
+
+
From 8a49b546c7b8576adf9e859ff8c611267ac91fee Mon Sep 17 00:00:00 2001 From: Ayrat Badykov Date: Tue, 28 May 2019 15:25:00 +0300 Subject: [PATCH 25/57] free pages stack on search start --- apps/block_scout_web/assets/js/pages/address/logs.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/block_scout_web/assets/js/pages/address/logs.js b/apps/block_scout_web/assets/js/pages/address/logs.js index ddfe3e9bfc..444e3a9372 100644 --- a/apps/block_scout_web/assets/js/pages/address/logs.js +++ b/apps/block_scout_web/assets/js/pages/address/logs.js @@ -16,6 +16,9 @@ export function reducer (state, action) { case 'ELEMENTS_LOAD': { return Object.assign({}, state, _.omit(action, 'type')) } + case 'START_SEARCH': { + return Object.assign({}, state, {pagesStack: []}) + } default: return state } @@ -59,6 +62,9 @@ if ($('[data-page="address-logs"]').length) { $element.on('click', '[data-search-button]', (event) => { + store.dispatch({ + type: 'START_SEARCH', + addressHash: addressHash}) loadSearchItems() }) } From 1d3954fc625505f08ac7fdf6c1ce0ab7a75b955d Mon Sep 17 00:00:00 2001 From: saneery Date: Tue, 28 May 2019 15:26:36 +0300 Subject: [PATCH 26/57] change variable name in queries --- .../lib/explorer/chain/import/runner/staking_pools.ex | 6 +++--- .../chain/import/runner/staking_pools_delegators.ex | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/explorer/lib/explorer/chain/import/runner/staking_pools.ex b/apps/explorer/lib/explorer/chain/import/runner/staking_pools.ex index d7655674d8..ca83fa62ee 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/staking_pools.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/staking_pools.ex @@ -105,7 +105,7 @@ defmodule Explorer.Chain.Import.Runner.StakingPools do defp default_on_conflict do from( - name in StakingPool, + pool in StakingPool, update: [ set: [ mining_address_hash: fragment("EXCLUDED.mining_address_hash"), @@ -120,8 +120,8 @@ defmodule Explorer.Chain.Import.Runner.StakingPools do was_banned_count: fragment("EXCLUDED.was_banned_count"), was_validator_count: fragment("EXCLUDED.was_validator_count"), is_deleted: fragment("EXCLUDED.is_deleted"), - inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", name.inserted_at), - updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", name.updated_at) + inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", pool.inserted_at), + updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", pool.updated_at) ] ] ) diff --git a/apps/explorer/lib/explorer/chain/import/runner/staking_pools_delegators.ex b/apps/explorer/lib/explorer/chain/import/runner/staking_pools_delegators.ex index cf7391dc89..420882a833 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/staking_pools_delegators.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/staking_pools_delegators.ex @@ -74,7 +74,7 @@ defmodule Explorer.Chain.Import.Runner.StakingPoolsDelegators do defp default_on_conflict do from( - name in StakingPoolsDelegator, + delegator in StakingPoolsDelegator, update: [ set: [ stake_amount: fragment("EXCLUDED.stake_amount"), @@ -82,8 +82,8 @@ defmodule Explorer.Chain.Import.Runner.StakingPoolsDelegators do max_withdraw_allowed: fragment("EXCLUDED.max_withdraw_allowed"), max_ordered_withdraw_allowed: fragment("EXCLUDED.max_ordered_withdraw_allowed"), ordered_withdraw_epoch: fragment("EXCLUDED.ordered_withdraw_epoch"), - inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", name.inserted_at), - updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", name.updated_at) + inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", delegator.inserted_at), + updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", delegator.updated_at) ] ] ) From 35ef44a1abfe6f5b7e197acf087fb153366f5176 Mon Sep 17 00:00:00 2001 From: Ayrat Badykov Date: Tue, 28 May 2019 15:54:44 +0300 Subject: [PATCH 27/57] add cancel search button --- .../assets/js/pages/address/logs.js | 20 ++++++++++++++++--- .../templates/address_logs/index.html.eex | 1 + 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/apps/block_scout_web/assets/js/pages/address/logs.js b/apps/block_scout_web/assets/js/pages/address/logs.js index 444e3a9372..1479caeb58 100644 --- a/apps/block_scout_web/assets/js/pages/address/logs.js +++ b/apps/block_scout_web/assets/js/pages/address/logs.js @@ -7,7 +7,8 @@ import { connectElements } from '../../lib/redux_helpers.js' import { createAsyncLoadStore } from '../../lib/async_listing_load' export const initialState = { - addressHash: null + addressHash: null, + isSearch: false } export function reducer (state, action) { @@ -17,7 +18,7 @@ export function reducer (state, action) { return Object.assign({}, state, _.omit(action, 'type')) } case 'START_SEARCH': { - return Object.assign({}, state, {pagesStack: []}) + return Object.assign({}, state, {pagesStack: [], isSearch: true}) } default: return state @@ -34,11 +35,19 @@ const elements = { render ($el, state) { $el } + }, + '[data-cancel-search-button]' : { + render ($el, state) { + if (!state.isSearch) { + return $el.hide() + } + + return $el.show() + } } } if ($('[data-page="address-logs"]').length) { - console.log('iffff') const store = createAsyncLoadStore(reducer, initialState, 'dataset.identifierHash') const addressHash = $('[data-page="address-details"]')[0].dataset.pageAddressHash const $element = $('[data-async-listing]') @@ -67,4 +76,9 @@ if ($('[data-page="address-logs"]').length) { addressHash: addressHash}) loadSearchItems() }) + + $element.on('click', '[data-cancel-search-button]', (event) => { + console.log('click') + window.location.replace(window.location.href.split('?')[0]) + }) } diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_logs/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_logs/index.html.eex index f873465372..68897c51da 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address_logs/index.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_logs/index.html.eex @@ -9,6 +9,7 @@ id='search-text-input' /> + <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "top", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> From 0ca5f88b28caa33be58046f7b12cb2469b0a359e Mon Sep 17 00:00:00 2001 From: Ayrat Badykov Date: Wed, 29 May 2019 09:56:42 +0300 Subject: [PATCH 28/57] fix gettext --- apps/block_scout_web/priv/gettext/default.pot | 17 +++- .../priv/gettext/en/LC_MESSAGES/default.po | 85 +++++++++++-------- 2 files changed, 62 insertions(+), 40 deletions(-) diff --git a/apps/block_scout_web/priv/gettext/default.pot b/apps/block_scout_web/priv/gettext/default.pot index c62c47d890..d14665131d 100644 --- a/apps/block_scout_web/priv/gettext/default.pot +++ b/apps/block_scout_web/priv/gettext/default.pot @@ -500,7 +500,7 @@ msgstr "" #, elixir-format #: lib/block_scout_web/templates/address/_tabs.html.eex:26 -#: lib/block_scout_web/templates/address_logs/index.html.eex:7 +#: lib/block_scout_web/templates/address_logs/index.html.eex:8 #: lib/block_scout_web/templates/transaction/_tabs.html.eex:17 #: lib/block_scout_web/templates/transaction_log/index.html.eex:8 #: lib/block_scout_web/views/address_view.ex:314 @@ -674,6 +674,7 @@ msgid "Responses" msgstr "" #, elixir-format +#: lib/block_scout_web/templates/address_logs/index.html.eex:11 #: lib/block_scout_web/templates/layout/_topnav.html.eex:111 #: lib/block_scout_web/templates/layout/_topnav.html.eex:128 msgid "Search" @@ -1217,7 +1218,7 @@ msgstr "" #, elixir-format #: lib/block_scout_web/templates/address_coin_balance/index.html.eex:34 #: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:61 -#: lib/block_scout_web/templates/address_logs/index.html.eex:12 +#: lib/block_scout_web/templates/address_logs/index.html.eex:17 #: lib/block_scout_web/templates/address_token/index.html.eex:13 #: lib/block_scout_web/templates/address_token_transfer/index.html.eex:20 #: lib/block_scout_web/templates/address_transaction/index.html.eex:57 @@ -1682,7 +1683,7 @@ msgid "Displaying the init data provided of the creating transaction." msgstr "" #, elixir-format -#: lib/block_scout_web/templates/address_logs/index.html.eex:17 +#: lib/block_scout_web/templates/address_logs/index.html.eex:22 msgid "There are no logs for this address." msgstr "" @@ -1720,3 +1721,13 @@ msgstr "" #: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:17 msgid "There are no token transfers for this transaction" msgstr "" + +#, elixir-format +#: lib/block_scout_web/templates/address_logs/index.html.eex:12 +msgid "Cancel Search" +msgstr "" + +#, elixir-format +#: lib/block_scout_web/templates/address_logs/index.html.eex:10 +msgid "Topic" +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 23a85c844b..3fd1cd6e9e 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 @@ -145,7 +145,7 @@ msgid "Block %{block_number} - %{subnetwork} Explorer" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/transaction/overview.html.eex:73 +#: lib/block_scout_web/templates/transaction/overview.html.eex:72 msgid "Block Confirmations" msgstr "" @@ -160,7 +160,7 @@ msgid "Block Mined, awaiting import..." msgstr "" #, elixir-format -#: lib/block_scout_web/templates/transaction/overview.html.eex:59 +#: lib/block_scout_web/templates/transaction/overview.html.eex:58 msgid "Block Number" msgstr "" @@ -190,7 +190,7 @@ msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:32 #: lib/block_scout_web/templates/address/overview.html.eex:95 #: lib/block_scout_web/templates/address_validation/index.html.eex:13 -#: lib/block_scout_web/views/address_view.ex:308 +#: lib/block_scout_web/views/address_view.ex:313 msgid "Blocks Validated" msgstr "" @@ -207,8 +207,8 @@ msgstr "" #, elixir-format #: lib/block_scout_web/templates/address/_validator_metadata_modal.html.eex:37 -#: lib/block_scout_web/templates/address/overview.html.eex:141 -#: lib/block_scout_web/templates/address/overview.html.eex:149 +#: lib/block_scout_web/templates/address/overview.html.eex:142 +#: lib/block_scout_web/templates/address/overview.html.eex:150 #: lib/block_scout_web/templates/tokens/overview/_details.html.eex:106 #: lib/block_scout_web/templates/tokens/overview/_details.html.eex:114 msgid "Close" @@ -218,7 +218,7 @@ msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:42 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:165 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:187 -#: lib/block_scout_web/views/address_view.ex:304 +#: lib/block_scout_web/views/address_view.ex:309 msgid "Code" msgstr "" @@ -382,7 +382,7 @@ msgstr "" #: lib/block_scout_web/templates/layout/app.html.eex:55 #: lib/block_scout_web/templates/transaction/_pending_tile.html.eex:20 #: lib/block_scout_web/templates/transaction/_tile.html.eex:30 -#: lib/block_scout_web/templates/transaction/overview.html.eex:210 +#: lib/block_scout_web/templates/transaction/overview.html.eex:209 #: lib/block_scout_web/views/wei_helpers.ex:72 msgid "Ether" msgstr "POA" @@ -452,7 +452,7 @@ msgstr "" #, elixir-format #: lib/block_scout_web/templates/internal_transaction/_tile.html.eex:39 -#: lib/block_scout_web/templates/transaction/_tile.html.eex:76 +#: lib/block_scout_web/templates/transaction/_tile.html.eex:74 msgid "IN" msgstr "" @@ -476,7 +476,7 @@ msgstr "" #: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:19 #: lib/block_scout_web/templates/transaction/_tabs.html.eex:11 #: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:6 -#: lib/block_scout_web/views/address_view.ex:303 +#: lib/block_scout_web/views/address_view.ex:308 #: lib/block_scout_web/views/transaction_view.ex:339 msgid "Internal Transactions" msgstr "" @@ -494,16 +494,16 @@ msgid "Less than" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/transaction/overview.html.eex:238 +#: lib/block_scout_web/templates/transaction/overview.html.eex:237 msgid "Limit" msgstr "" #, elixir-format #: lib/block_scout_web/templates/address/_tabs.html.eex:26 -#: lib/block_scout_web/templates/address_logs/index.html.eex:7 +#: lib/block_scout_web/templates/address_logs/index.html.eex:8 #: lib/block_scout_web/templates/transaction/_tabs.html.eex:17 #: lib/block_scout_web/templates/transaction_log/index.html.eex:8 -#: lib/block_scout_web/views/address_view.ex:309 +#: lib/block_scout_web/views/address_view.ex:314 #: lib/block_scout_web/views/transaction_view.ex:340 msgid "Logs" msgstr "" @@ -574,13 +574,13 @@ msgstr "" #, elixir-format #: lib/block_scout_web/templates/block/overview.html.eex:73 -#: lib/block_scout_web/templates/transaction/overview.html.eex:80 +#: lib/block_scout_web/templates/transaction/overview.html.eex:79 msgid "Nonce" msgstr "" #, elixir-format #: lib/block_scout_web/templates/internal_transaction/_tile.html.eex:37 -#: lib/block_scout_web/templates/transaction/_tile.html.eex:72 +#: lib/block_scout_web/templates/transaction/_tile.html.eex:70 msgid "OUT" msgstr "" @@ -634,7 +634,7 @@ msgstr "" #, elixir-format #: lib/block_scout_web/templates/address/overview.html.eex:33 -#: lib/block_scout_web/templates/address/overview.html.eex:140 +#: lib/block_scout_web/templates/address/overview.html.eex:141 #: lib/block_scout_web/templates/tokens/overview/_details.html.eex:35 #: lib/block_scout_web/templates/tokens/overview/_details.html.eex:105 msgid "QR Code" @@ -648,7 +648,7 @@ msgstr "" #, elixir-format #: lib/block_scout_web/templates/address/_tabs.html.eex:58 #: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:25 -#: lib/block_scout_web/views/address_view.ex:306 +#: lib/block_scout_web/views/address_view.ex:311 #: lib/block_scout_web/views/tokens/overview_view.ex:37 msgid "Read Contract" msgstr "" @@ -674,6 +674,7 @@ msgid "Responses" msgstr "" #, elixir-format +#: lib/block_scout_web/templates/address_logs/index.html.eex:11 #: lib/block_scout_web/templates/layout/_topnav.html.eex:111 #: lib/block_scout_web/templates/layout/_topnav.html.eex:128 msgid "Search" @@ -705,7 +706,7 @@ msgstr "" #, elixir-format #: lib/block_scout_web/templates/transaction/_pending_tile.html.eex:21 #: lib/block_scout_web/templates/transaction/_tile.html.eex:34 -#: lib/block_scout_web/templates/transaction/overview.html.eex:85 +#: lib/block_scout_web/templates/transaction/overview.html.eex:84 msgid "TX Fee" msgstr "" @@ -802,8 +803,8 @@ msgstr "" #, elixir-format #: lib/block_scout_web/templates/tokens/transfer/_token_transfer.html.eex:5 -#: lib/block_scout_web/templates/transaction/overview.html.eex:180 -#: lib/block_scout_web/templates/transaction/overview.html.eex:194 +#: lib/block_scout_web/templates/transaction/overview.html.eex:179 +#: lib/block_scout_web/templates/transaction/overview.html.eex:193 #: lib/block_scout_web/templates/transaction_token_transfer/_token_transfer.html.eex:4 #: lib/block_scout_web/views/transaction_view.ex:284 msgid "Token Transfer" @@ -823,7 +824,7 @@ msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:8 #: lib/block_scout_web/templates/address_token/index.html.eex:8 #: lib/block_scout_web/templates/address_token_transfer/index.html.eex:9 -#: lib/block_scout_web/views/address_view.ex:301 +#: lib/block_scout_web/views/address_view.ex:306 msgid "Tokens" msgstr "" @@ -881,7 +882,7 @@ msgstr "" #: lib/block_scout_web/templates/block_transaction/index.html.eex:18 #: lib/block_scout_web/templates/chain/show.html.eex:108 #: lib/block_scout_web/templates/layout/_topnav.html.eex:35 -#: lib/block_scout_web/views/address_view.ex:302 +#: lib/block_scout_web/views/address_view.ex:307 msgid "Transactions" msgstr "" @@ -917,7 +918,7 @@ msgid "Unique Token" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/transaction/overview.html.eex:232 +#: lib/block_scout_web/templates/transaction/overview.html.eex:231 msgid "Used" msgstr "" @@ -937,7 +938,7 @@ msgid "Validations" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/transaction/overview.html.eex:210 +#: lib/block_scout_web/templates/transaction/overview.html.eex:209 msgid "Value" msgstr "" @@ -958,12 +959,12 @@ msgid "View Contract" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/transaction/_tile.html.eex:56 +#: lib/block_scout_web/templates/transaction/_tile.html.eex:55 msgid "View Less Transfers" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/transaction/_tile.html.eex:55 +#: lib/block_scout_web/templates/transaction/_tile.html.eex:54 msgid "View More Transfers" msgstr "" @@ -1101,7 +1102,7 @@ msgid "This API is provided for developers transitioning their applications from msgstr "" #, elixir-format -#: lib/block_scout_web/templates/transaction/overview.html.eex:110 +#: lib/block_scout_web/templates/transaction/overview.html.eex:109 msgid "Raw Input" msgstr "" @@ -1217,7 +1218,7 @@ msgstr "" #, elixir-format #: lib/block_scout_web/templates/address_coin_balance/index.html.eex:34 #: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:61 -#: lib/block_scout_web/templates/address_logs/index.html.eex:12 +#: lib/block_scout_web/templates/address_logs/index.html.eex:17 #: lib/block_scout_web/templates/address_token/index.html.eex:13 #: lib/block_scout_web/templates/address_token_transfer/index.html.eex:20 #: lib/block_scout_web/templates/address_transaction/index.html.eex:57 @@ -1257,7 +1258,7 @@ msgstr "" #, elixir-format #: lib/block_scout_web/templates/address/_tabs.html.eex:20 -#: lib/block_scout_web/views/address_view.ex:307 +#: lib/block_scout_web/views/address_view.ex:312 msgid "Coin Balance History" msgstr "" @@ -1499,18 +1500,18 @@ msgid "Transactions Sent" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/transaction/overview.html.eex:102 +#: lib/block_scout_web/templates/transaction/overview.html.eex:101 msgid "Transaction Speed" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/transaction/overview.html.eex:116 -#: lib/block_scout_web/templates/transaction/overview.html.eex:120 +#: lib/block_scout_web/templates/transaction/overview.html.eex:115 +#: lib/block_scout_web/templates/transaction/overview.html.eex:119 msgid "Hex (Default)" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/transaction/overview.html.eex:123 +#: lib/block_scout_web/templates/transaction/overview.html.eex:122 msgid "UTF-8" msgstr "" @@ -1561,7 +1562,7 @@ msgid "Copy Decompiled Contract Code" msgstr "" #, elixir-format -#: lib/block_scout_web/views/address_view.ex:305 +#: lib/block_scout_web/views/address_view.ex:310 msgid "Decompiled Code" msgstr "" @@ -1586,12 +1587,12 @@ msgid "Optimization runs" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/transaction/overview.html.eex:180 +#: lib/block_scout_web/templates/transaction/overview.html.eex:179 msgid "ERC-20" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/transaction/overview.html.eex:194 +#: lib/block_scout_web/templates/transaction/overview.html.eex:193 msgid "ERC-721" msgstr "" @@ -1611,7 +1612,7 @@ msgid "View All Transactions" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/transaction/overview.html.eex:228 +#: lib/block_scout_web/templates/transaction/overview.html.eex:227 msgid "Gas" msgstr "" @@ -1682,7 +1683,7 @@ msgid "Displaying the init data provided of the creating transaction." msgstr "" #, elixir-format -#: lib/block_scout_web/templates/address_logs/index.html.eex:17 +#: lib/block_scout_web/templates/address_logs/index.html.eex:22 msgid "There are no logs for this address." msgstr "" @@ -1720,3 +1721,13 @@ msgstr "" #: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:17 msgid "There are no token transfers for this transaction" msgstr "" + +#, elixir-format, fuzzy +#: lib/block_scout_web/templates/address_logs/index.html.eex:12 +msgid "Cancel Search" +msgstr "" + +#, elixir-format, fuzzy +#: lib/block_scout_web/templates/address_logs/index.html.eex:10 +msgid "Topic" +msgstr "" From fb158da8fad284d346881e4b075c1e398e501427 Mon Sep 17 00:00:00 2001 From: Ayrat Badykov Date: Wed, 29 May 2019 10:01:54 +0300 Subject: [PATCH 29/57] fix js indent --- .../assets/js/pages/address/logs.js | 112 +++++++++--------- apps/explorer/lib/explorer/chain.ex | 2 +- 2 files changed, 57 insertions(+), 57 deletions(-) diff --git a/apps/block_scout_web/assets/js/pages/address/logs.js b/apps/block_scout_web/assets/js/pages/address/logs.js index 1479caeb58..6cbced717e 100644 --- a/apps/block_scout_web/assets/js/pages/address/logs.js +++ b/apps/block_scout_web/assets/js/pages/address/logs.js @@ -7,78 +7,78 @@ import { connectElements } from '../../lib/redux_helpers.js' import { createAsyncLoadStore } from '../../lib/async_listing_load' export const initialState = { - addressHash: null, - isSearch: false + addressHash: null, + isSearch: false } export function reducer (state, action) { - switch (action.type) { - case 'PAGE_LOAD': - case 'ELEMENTS_LOAD': { - return Object.assign({}, state, _.omit(action, 'type')) - } - case 'START_SEARCH': { - return Object.assign({}, state, {pagesStack: [], isSearch: true}) - } - default: - return state - } + switch (action.type) { + case 'PAGE_LOAD': + case 'ELEMENTS_LOAD': { + return Object.assign({}, state, _.omit(action, 'type')) + } + case 'START_SEARCH': { + return Object.assign({}, state, {pagesStack: [], isSearch: true}) + } + default: + return state + } } const elements = { - '[data-search-field]' : { - render ($el, state) { - $el - } - }, - '[data-search-button]' : { - render ($el, state) { - $el - } - }, - '[data-cancel-search-button]' : { - render ($el, state) { - if (!state.isSearch) { - return $el.hide() - } + '[data-search-field]' : { + render ($el, state) { + $el + } + }, + '[data-search-button]' : { + render ($el, state) { + $el + } + }, + '[data-cancel-search-button]' : { + render ($el, state) { + if (!state.isSearch) { + return $el.hide() + } - return $el.show() - } + return $el.show() } + } } if ($('[data-page="address-logs"]').length) { - const store = createAsyncLoadStore(reducer, initialState, 'dataset.identifierHash') - const addressHash = $('[data-page="address-details"]')[0].dataset.pageAddressHash - const $element = $('[data-async-listing]') + const store = createAsyncLoadStore(reducer, initialState, 'dataset.identifierHash') + const addressHash = $('[data-page="address-details"]')[0].dataset.pageAddressHash + const $element = $('[data-async-listing]') - connectElements({ store, elements }) + connectElements({ store, elements }) - store.dispatch({ - type: 'PAGE_LOAD', - addressHash: addressHash}) + store.dispatch({ + type: 'PAGE_LOAD', + addressHash: addressHash}) - function loadSearchItems () { - var topic = $('[data-search-field]').val(); - var path = "/search_logs?topic=" + topic + "&address_id=" + store.getState().addressHash - store.dispatch({type: 'START_REQUEST'}) - $.getJSON(path, {type: 'JSON'}) - .done(response => store.dispatch(Object.assign({type: 'ITEMS_FETCHED'}, humps.camelizeKeys(response)))) - .fail(() => store.dispatch({type: 'REQUEST_ERROR'})) - .always(() => store.dispatch({type: 'FINISH_REQUEST'})) - } + function loadSearchItems () { + var topic = $('[data-search-field]').val(); + var path = "/search_logs?topic=" + topic + "&address_id=" + store.getState().addressHash + store.dispatch({type: 'START_REQUEST'}) + $.getJSON(path, {type: 'JSON'}) + .done(response => store.dispatch(Object.assign({type: 'ITEMS_FETCHED'}, humps.camelizeKeys(response)))) + .fail(() => store.dispatch({type: 'REQUEST_ERROR'})) + .always(() => store.dispatch({type: 'FINISH_REQUEST'})) + } - $element.on('click', '[data-search-button]', (event) => { - store.dispatch({ - type: 'START_SEARCH', - addressHash: addressHash}) - loadSearchItems() - }) + $element.on('click', '[data-search-button]', (event) => { + store.dispatch({ + type: 'START_SEARCH', + addressHash: addressHash}) + loadSearchItems() + }) - $element.on('click', '[data-cancel-search-button]', (event) => { - console.log('click') - window.location.replace(window.location.href.split('?')[0]) - }) + $element.on('click', '[data-cancel-search-button]', (event) => { + console.log('click') + window.location.replace(window.location.href.split('?')[0]) + }) } diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index d5117453bf..a52965a92b 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -280,7 +280,7 @@ defmodule Explorer.Chain do |> Enum.take(paging_options.page_size) end - @spec address_to_logs(Address.t(), [paging_options]) :: [ + @spec address_to_logs(Address.t(), [Keyword.t()]) :: [ Log.t() ] def address_to_logs( From e7ace9cae65ecfee65527de599ed1b1890be2578 Mon Sep 17 00:00:00 2001 From: Ayrat Badykov Date: Wed, 29 May 2019 10:20:11 +0300 Subject: [PATCH 30/57] fix credo --- .../assets/js/pages/address/logs.js | 18 ++++++------- apps/explorer/lib/explorer/chain.ex | 27 +++++++++---------- 2 files changed, 22 insertions(+), 23 deletions(-) diff --git a/apps/block_scout_web/assets/js/pages/address/logs.js b/apps/block_scout_web/assets/js/pages/address/logs.js index 6cbced717e..a9ad5104c3 100644 --- a/apps/block_scout_web/assets/js/pages/address/logs.js +++ b/apps/block_scout_web/assets/js/pages/address/logs.js @@ -13,15 +13,15 @@ export const initialState = { export function reducer (state, action) { switch (action.type) { - case 'PAGE_LOAD': - case 'ELEMENTS_LOAD': { - return Object.assign({}, state, _.omit(action, 'type')) - } - case 'START_SEARCH': { - return Object.assign({}, state, {pagesStack: [], isSearch: true}) - } - default: - return state + case 'PAGE_LOAD': + case 'ELEMENTS_LOAD': { + return Object.assign({}, state, _.omit(action, 'type')) + } + case 'START_SEARCH': { + return Object.assign({}, state, {pagesStack: [], isSearch: true}) + } + default: + return state } } diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index a52965a92b..d2b88ba722 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -280,7 +280,7 @@ defmodule Explorer.Chain do |> Enum.take(paging_options.page_size) end - @spec address_to_logs(Address.t(), [Keyword.t()]) :: [ + @spec address_to_logs(Address.t(), Keyword.t()) :: [ Log.t() ] def address_to_logs( @@ -291,7 +291,6 @@ defmodule Explorer.Chain do paging_options = Keyword.get(options, :paging_options) || %PagingOptions{page_size: 50} {block_number, transaction_index, log_index} = paging_options.key || {BlockNumberCache.max_number(), 0, 0} - topic = Keyword.get(options, :topic) base_query = from(log in Log, @@ -308,22 +307,22 @@ defmodule Explorer.Chain do select: log ) - query = - if topic do - from(log in base_query, - where: - log.first_topic == ^topic or log.second_topic == ^topic or log.third_topic == ^topic or - log.fourth_topic == ^topic - ) - else - base_query - end - - query + base_query + |> filter_topic(options) |> Repo.all() |> Enum.take(paging_options.page_size) end + defp filter_topic(base_query, topic: topic) do + from(log in base_query, + where: + log.first_topic == ^topic or log.second_topic == ^topic or log.third_topic == ^topic or + log.fourth_topic == ^topic + ) + end + + defp filter_topic(base_query, _), do: base_query + @doc """ Finds all `t:Explorer.Chain.Transaction.t/0`s given the address_hash and the token contract address hash. From 0036876b24c5abec5751ea528acda3465dda7c98 Mon Sep 17 00:00:00 2001 From: Ayrat Badykov Date: Wed, 29 May 2019 10:25:48 +0300 Subject: [PATCH 31/57] remove extra spaces --- apps/block_scout_web/assets/js/pages/address/logs.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/block_scout_web/assets/js/pages/address/logs.js b/apps/block_scout_web/assets/js/pages/address/logs.js index a9ad5104c3..063480619e 100644 --- a/apps/block_scout_web/assets/js/pages/address/logs.js +++ b/apps/block_scout_web/assets/js/pages/address/logs.js @@ -26,17 +26,17 @@ export function reducer (state, action) { } const elements = { - '[data-search-field]' : { + '[data-search-field]': { render ($el, state) { $el } }, - '[data-search-button]' : { + '[data-search-button]': { render ($el, state) { $el } }, - '[data-cancel-search-button]' : { + '[data-cancel-search-button]': { render ($el, state) { if (!state.isSearch) { return $el.hide() From 3a3e8cf260d0b86a2b69d1993991aeeb32e0a636 Mon Sep 17 00:00:00 2001 From: Ayrat Badykov Date: Wed, 29 May 2019 10:32:29 +0300 Subject: [PATCH 32/57] fix js style --- .../assets/js/pages/address/logs.js | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/apps/block_scout_web/assets/js/pages/address/logs.js b/apps/block_scout_web/assets/js/pages/address/logs.js index 063480619e..99ac742679 100644 --- a/apps/block_scout_web/assets/js/pages/address/logs.js +++ b/apps/block_scout_web/assets/js/pages/address/logs.js @@ -47,29 +47,27 @@ const elements = { } } +function loadSearchItems () { + var topic = $('[data-search-field]').val(); + var path = "/search_logs?topic=" + topic + "&address_id=" + store.getState().addressHash + store.dispatch({type: 'START_REQUEST'}) + $.getJSON(path, {type: 'JSON'}) + .done(response => store.dispatch(Object.assign({type: 'ITEMS_FETCHED'}, humps.camelizeKeys(response)))) + .fail(() => store.dispatch({type: 'REQUEST_ERROR'})) + .always(() => store.dispatch({type: 'FINISH_REQUEST'})) +} + if ($('[data-page="address-logs"]').length) { const store = createAsyncLoadStore(reducer, initialState, 'dataset.identifierHash') const addressHash = $('[data-page="address-details"]')[0].dataset.pageAddressHash const $element = $('[data-async-listing]') - connectElements({ store, elements }) store.dispatch({ type: 'PAGE_LOAD', addressHash: addressHash}) - function loadSearchItems () { - var topic = $('[data-search-field]').val(); - var path = "/search_logs?topic=" + topic + "&address_id=" + store.getState().addressHash - store.dispatch({type: 'START_REQUEST'}) - $.getJSON(path, {type: 'JSON'}) - .done(response => store.dispatch(Object.assign({type: 'ITEMS_FETCHED'}, humps.camelizeKeys(response)))) - .fail(() => store.dispatch({type: 'REQUEST_ERROR'})) - .always(() => store.dispatch({type: 'FINISH_REQUEST'})) - } - - $element.on('click', '[data-search-button]', (event) => { store.dispatch({ type: 'START_SEARCH', From 40d50cef4c8d10defca74dc3cf52bcb4470e2af8 Mon Sep 17 00:00:00 2001 From: Ayrat Badykov Date: Wed, 29 May 2019 10:36:25 +0300 Subject: [PATCH 33/57] remove function --- .../assets/js/pages/address/logs.js | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/apps/block_scout_web/assets/js/pages/address/logs.js b/apps/block_scout_web/assets/js/pages/address/logs.js index 99ac742679..c82bc7856e 100644 --- a/apps/block_scout_web/assets/js/pages/address/logs.js +++ b/apps/block_scout_web/assets/js/pages/address/logs.js @@ -47,16 +47,6 @@ const elements = { } } -function loadSearchItems () { - var topic = $('[data-search-field]').val(); - var path = "/search_logs?topic=" + topic + "&address_id=" + store.getState().addressHash - store.dispatch({type: 'START_REQUEST'}) - $.getJSON(path, {type: 'JSON'}) - .done(response => store.dispatch(Object.assign({type: 'ITEMS_FETCHED'}, humps.camelizeKeys(response)))) - .fail(() => store.dispatch({type: 'REQUEST_ERROR'})) - .always(() => store.dispatch({type: 'FINISH_REQUEST'})) -} - if ($('[data-page="address-logs"]').length) { const store = createAsyncLoadStore(reducer, initialState, 'dataset.identifierHash') const addressHash = $('[data-page="address-details"]')[0].dataset.pageAddressHash @@ -72,7 +62,13 @@ if ($('[data-page="address-logs"]').length) { store.dispatch({ type: 'START_SEARCH', addressHash: addressHash}) - loadSearchItems() + var topic = $('[data-search-field]').val(); + var path = "/search_logs?topic=" + topic + "&address_id=" + store.getState().addressHash + store.dispatch({type: 'START_REQUEST'}) + $.getJSON(path, {type: 'JSON'}) + .done(response => store.dispatch(Object.assign({type: 'ITEMS_FETCHED'}, humps.camelizeKeys(response)))) + .fail(() => store.dispatch({type: 'REQUEST_ERROR'})) + .always(() => store.dispatch({type: 'FINISH_REQUEST'})) }) $element.on('click', '[data-cancel-search-button]', (event) => { From cb0508e99165563e59589cbdfead95920736470d Mon Sep 17 00:00:00 2001 From: Ayrat Badykov Date: Wed, 29 May 2019 10:43:29 +0300 Subject: [PATCH 34/57] fix remaining js issues --- apps/block_scout_web/assets/js/pages/address/logs.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/apps/block_scout_web/assets/js/pages/address/logs.js b/apps/block_scout_web/assets/js/pages/address/logs.js index c82bc7856e..463ccced1b 100644 --- a/apps/block_scout_web/assets/js/pages/address/logs.js +++ b/apps/block_scout_web/assets/js/pages/address/logs.js @@ -1,6 +1,5 @@ import $ from 'jquery' import _ from 'lodash' -import URI from 'urijs' import humps from 'humps' import { subscribeChannel } from '../../socket' import { connectElements } from '../../lib/redux_helpers.js' @@ -28,12 +27,12 @@ export function reducer (state, action) { const elements = { '[data-search-field]': { render ($el, state) { - $el + return $el } }, '[data-search-button]': { render ($el, state) { - $el + return $el } }, '[data-cancel-search-button]': { @@ -62,8 +61,8 @@ if ($('[data-page="address-logs"]').length) { store.dispatch({ type: 'START_SEARCH', addressHash: addressHash}) - var topic = $('[data-search-field]').val(); - var path = "/search_logs?topic=" + topic + "&address_id=" + store.getState().addressHash + var topic = $('[data-search-field]').val() + var path = '/search_logs?topic=' + topic + '&address_id=' + store.getState().addressHash store.dispatch({type: 'START_REQUEST'}) $.getJSON(path, {type: 'JSON'}) .done(response => store.dispatch(Object.assign({type: 'ITEMS_FETCHED'}, humps.camelizeKeys(response)))) From 548db9a2a1965abeeb9adec0d3c05722ff48f30e Mon Sep 17 00:00:00 2001 From: Ayrat Badykov Date: Wed, 29 May 2019 10:46:07 +0300 Subject: [PATCH 35/57] remove unused import --- apps/block_scout_web/assets/js/pages/address/logs.js | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/block_scout_web/assets/js/pages/address/logs.js b/apps/block_scout_web/assets/js/pages/address/logs.js index 463ccced1b..bdcdf57087 100644 --- a/apps/block_scout_web/assets/js/pages/address/logs.js +++ b/apps/block_scout_web/assets/js/pages/address/logs.js @@ -1,7 +1,6 @@ import $ from 'jquery' import _ from 'lodash' import humps from 'humps' -import { subscribeChannel } from '../../socket' import { connectElements } from '../../lib/redux_helpers.js' import { createAsyncLoadStore } from '../../lib/async_listing_load' From fb42dedfdcc8a00a8a634046ed9c91474d4de544 Mon Sep 17 00:00:00 2001 From: Ayrat Badykov Date: Wed, 29 May 2019 11:23:55 +0300 Subject: [PATCH 36/57] add CHANGELOG entry --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 043d57d31f..8dca2fdef7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,8 @@ - [#1999](https://github.com/poanetwork/blockscout/pull/1999) - load data async on addresses page - [#2002](https://github.com/poanetwork/blockscout/pull/2002) - Get estimated count of blocks when cache is empty - [#1807](https://github.com/poanetwork/blockscout/pull/1807) - New theming capabilites. -- [#2040](https://github.com/poanetwork/blockscout/pull/2040) - Verification links to other explorers for ETH +- [#2040](https://github.com/poanetwork/blockscout/pull/2040) - Verification links to other explorers for ETH +- [#2037](https://github.com/poanetwork/blockscout/pull/2037) - add address logs search functionality ### Fixes - [#2043](https://github.com/poanetwork/blockscout/pull/2043) - Fixed modal dialog width for 'verify other explorers' From 7341e08c8ec65213ed572b457cf617b5ee507fa2 Mon Sep 17 00:00:00 2001 From: Ayrat Badykov Date: Wed, 29 May 2019 13:10:52 +0300 Subject: [PATCH 37/57] remove debug log --- apps/block_scout_web/assets/js/pages/address/logs.js | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/block_scout_web/assets/js/pages/address/logs.js b/apps/block_scout_web/assets/js/pages/address/logs.js index bdcdf57087..7c9f806c01 100644 --- a/apps/block_scout_web/assets/js/pages/address/logs.js +++ b/apps/block_scout_web/assets/js/pages/address/logs.js @@ -70,7 +70,6 @@ if ($('[data-page="address-logs"]').length) { }) $element.on('click', '[data-cancel-search-button]', (event) => { - console.log('click') window.location.replace(window.location.href.split('?')[0]) }) } From 43357ff7941fc1525a4090e0d083a5fad5d341c8 Mon Sep 17 00:00:00 2001 From: maxgrapps Date: Wed, 29 May 2019 15:00:13 +0300 Subject: [PATCH 38/57] log search form styles added --- apps/block_scout_web/assets/css/app.scss | 2 +- .../assets/css/components/_log-search.scss | 60 +++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 apps/block_scout_web/assets/css/components/_log-search.scss diff --git a/apps/block_scout_web/assets/css/app.scss b/apps/block_scout_web/assets/css/app.scss index f658c087ef..9a71ee173f 100644 --- a/apps/block_scout_web/assets/css/app.scss +++ b/apps/block_scout_web/assets/css/app.scss @@ -122,7 +122,7 @@ $fa-font-path: "~@fortawesome/fontawesome-free/webfonts"; @import "components/alerts"; @import "components/verify_other_explorers"; @import "components/errors"; - +@import "components/log-search"; :export { dashboardBannerChartAxisFontColor: $dashboard-banner-chart-axis-font-color; dashboardLineColorMarket: $dashboard-line-color-market; diff --git a/apps/block_scout_web/assets/css/components/_log-search.scss b/apps/block_scout_web/assets/css/components/_log-search.scss new file mode 100644 index 0000000000..51ca8713d0 --- /dev/null +++ b/apps/block_scout_web/assets/css/components/_log-search.scss @@ -0,0 +1,60 @@ +.logs-topbar { + padding-bottom: 30px; + @media (min-width: 600px) { + display: flex; + justify-content: space-between; + } + .pagination-container.position-top { + padding-top: 0 !important; + } +} + +.logs-search { + display: flex; + @media (max-width: 599px) { + margin-bottom: 30px; + } +} + +.logs-search-input, .logs-search-btn, .logs-search-btn-cancel { + height: 24px; + background-color: #f5f6fa; + border: 1px solid #f5f6fa; + color: #333; + border-radius: 2px; + outline: none; + font-family: Nunito, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + font-size: 12px; + font-weight: 600; +} + +.logs-search-input { + padding-left: 6px; + display: inline-flex; + flex-grow: 2; + min-width: 160px; + &::placeholder { + color: #a3a9b5; + } +} + +.logs-search-btn { + margin-left: 6px; + color: #a3a9b5; + transition: .1s ease-in; + cursor: pointer; + &:hover { + background-color: $primary; + color: #fff; + border-color: $primary; + } +} + +.logs-search-btn-cancel { + color: #a3a9b5; + cursor: pointer; + transition: .1s ease-in; + &:hover { + color: #333; + } +} \ No newline at end of file From ab74b9884515190f2dffcce8dec93c721233d6a5 Mon Sep 17 00:00:00 2001 From: maxgrapps <50101080+maxgrapps@users.noreply.github.com> Date: Wed, 29 May 2019 15:07:33 +0300 Subject: [PATCH 39/57] Update index.html.eex --- .../templates/address_logs/index.html.eex | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_logs/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_logs/index.html.eex index 68897c51da..bb5b2ce8d8 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address_logs/index.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_logs/index.html.eex @@ -6,12 +6,15 @@

<%= gettext "Logs" %>

+
+ - id='search-text-input' /> - - - - <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "top", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> + <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "top", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> +
+ +
- id='search-text-input' /> - - - - <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "top", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> + <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "top", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> + diff --git a/apps/block_scout_web/priv/gettext/default.pot b/apps/block_scout_web/priv/gettext/default.pot index 94187803dd..ff1da15027 100644 --- a/apps/block_scout_web/priv/gettext/default.pot +++ b/apps/block_scout_web/priv/gettext/default.pot @@ -674,7 +674,7 @@ msgid "Responses" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/address_logs/index.html.eex:13 +#: lib/block_scout_web/templates/address_logs/index.html.eex:14 #: lib/block_scout_web/templates/layout/_topnav.html.eex:111 #: lib/block_scout_web/templates/layout/_topnav.html.eex:128 msgid "Search" @@ -1218,7 +1218,7 @@ msgstr "" #, elixir-format #: lib/block_scout_web/templates/address_coin_balance/index.html.eex:34 #: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:61 -#: lib/block_scout_web/templates/address_logs/index.html.eex:20 +#: lib/block_scout_web/templates/address_logs/index.html.eex:21 #: lib/block_scout_web/templates/address_token/index.html.eex:13 #: lib/block_scout_web/templates/address_token_transfer/index.html.eex:20 #: lib/block_scout_web/templates/address_transaction/index.html.eex:57 @@ -1683,7 +1683,7 @@ msgid "Displaying the init data provided of the creating transaction." msgstr "" #, elixir-format -#: lib/block_scout_web/templates/address_logs/index.html.eex:25 +#: lib/block_scout_web/templates/address_logs/index.html.eex:26 msgid "There are no logs for this address." msgstr "" @@ -1723,6 +1723,6 @@ msgid "There are no token transfers for this transaction" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/address_logs/index.html.eex:11 +#: lib/block_scout_web/templates/address_logs/index.html.eex:12 msgid "Topic" 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 059c7d4e4a..00e030a367 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 @@ -674,7 +674,7 @@ msgid "Responses" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/address_logs/index.html.eex:13 +#: lib/block_scout_web/templates/address_logs/index.html.eex:14 #: lib/block_scout_web/templates/layout/_topnav.html.eex:111 #: lib/block_scout_web/templates/layout/_topnav.html.eex:128 msgid "Search" @@ -1218,7 +1218,7 @@ msgstr "" #, elixir-format #: lib/block_scout_web/templates/address_coin_balance/index.html.eex:34 #: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:61 -#: lib/block_scout_web/templates/address_logs/index.html.eex:20 +#: lib/block_scout_web/templates/address_logs/index.html.eex:21 #: lib/block_scout_web/templates/address_token/index.html.eex:13 #: lib/block_scout_web/templates/address_token_transfer/index.html.eex:20 #: lib/block_scout_web/templates/address_transaction/index.html.eex:57 @@ -1683,7 +1683,7 @@ msgid "Displaying the init data provided of the creating transaction." msgstr "" #, elixir-format -#: lib/block_scout_web/templates/address_logs/index.html.eex:25 +#: lib/block_scout_web/templates/address_logs/index.html.eex:26 msgid "There are no logs for this address." msgstr "" @@ -1723,6 +1723,6 @@ msgid "There are no token transfers for this transaction" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/address_logs/index.html.eex:11 +#: lib/block_scout_web/templates/address_logs/index.html.eex:12 msgid "Topic" msgstr "" From 77e8f457e006a0f151a67050b61ba844764e1b05 Mon Sep 17 00:00:00 2001 From: zachdaniel Date: Wed, 29 May 2019 14:06:23 -0400 Subject: [PATCH 43/57] fix: uniq by hash, instead of transaction --- CHANGELOG.md | 1 + apps/explorer/lib/explorer/chain.ex | 2 +- .../lib/explorer/chain/token_transfer.ex | 60 +++---------------- 3 files changed, 11 insertions(+), 52 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 775c006a5c..84592d868b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,7 @@ - [#2008](https://github.com/poanetwork/blockscout/pull/2008) - add new function clause for xDai network beneficiaries - [#2009](https://github.com/poanetwork/blockscout/pull/2009) - addresses page improvements - [#2027](https://github.com/poanetwork/blockscout/pull/2027) - fix: `BlocksTransactionsMismatch` ignoring blocks without transactions +- [#2062](https://github.com/poanetwork/blockscout/pull/2062) - fix: uniq by hash, instead of transaction ### Chore diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index 9446388531..cc2375c2ef 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -266,7 +266,7 @@ defmodule Explorer.Chain do queries |> Stream.flat_map(&Repo.all/1) - |> Stream.uniq() + |> Stream.uniq_by(& &1.hash) |> Stream.concat(rewards_list) |> Enum.sort_by(fn item -> case item do diff --git a/apps/explorer/lib/explorer/chain/token_transfer.ex b/apps/explorer/lib/explorer/chain/token_transfer.ex index fa7ba99347..905d576aa1 100644 --- a/apps/explorer/lib/explorer/chain/token_transfer.ex +++ b/apps/explorer/lib/explorer/chain/token_transfer.ex @@ -204,59 +204,17 @@ defmodule Explorer.Chain.TokenTransfer do transaction_hashes_from_token_transfers_sql(address_bytes, paging_options) end - defp transaction_hashes_from_token_transfers_sql(address_bytes, %PagingOptions{key: nil, page_size: page_size}) do - {:ok, %Postgrex.Result{rows: transaction_hashes_from_token_transfers}} = - Repo.query( - """ - SELECT transaction_hash - FROM - ( - SELECT transaction_hash - FROM token_transfers - WHERE from_address_hash = $1 - - UNION - - SELECT transaction_hash - FROM token_transfers - WHERE to_address_hash = $1 - ) as token_transfers_transaction_hashes - LIMIT $2 - """, - [address_bytes, page_size] - ) - - List.flatten(transaction_hashes_from_token_transfers) - end - - defp transaction_hashes_from_token_transfers_sql(address_bytes, %PagingOptions{ - key: {block_number, _index}, - page_size: page_size - }) do - {:ok, %Postgrex.Result{rows: transaction_hashes_from_token_transfers}} = - Repo.query( - """ - SELECT transaction_hash - FROM - ( - SELECT transaction_hash - FROM token_transfers - WHERE from_address_hash = $1 - AND block_number < $2 - - UNION - - SELECT transaction_hash - FROM token_transfers - WHERE to_address_hash = $1 - AND block_number < $2 - ) as token_transfers_transaction_hashes - LIMIT $3 - """, - [address_bytes, block_number, page_size] + defp transaction_hashes_from_token_transfers_sql(address_bytes, %PagingOptions{page_size: page_size} = paging_options) do + query = + from(token_transfer in TokenTransfer, + where: token_transfer.to_address_hash == ^address_bytes or token_transfer.from_address_hash == ^address_bytes, + select: type(token_transfer.transaction_hash, :binary), + limit: ^page_size ) - List.flatten(transaction_hashes_from_token_transfers) + query + |> page_transaction_hashes_from_token_transfers(paging_options) + |> Repo.all() end defp page_transaction_hashes_from_token_transfers(query, %PagingOptions{key: nil}), do: query From 030c17c58bdd330fe54060585e9a8ccfb42fda9e Mon Sep 17 00:00:00 2001 From: Ayrat Badykov Date: Thu, 30 May 2019 09:35:53 +0300 Subject: [PATCH 44/57] do not load associations for internal trxs for transaction page --- .../transaction_internal_transaction_controller.ex | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/transaction_internal_transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/transaction_internal_transaction_controller.ex index e318a74e78..7c24b8d3b9 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/transaction_internal_transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/transaction_internal_transaction_controller.ex @@ -10,18 +10,7 @@ defmodule BlockScoutWeb.TransactionInternalTransactionController do def index(conn, %{"transaction_id" => hash_string, "type" => "JSON"} = params) do with {:ok, hash} <- Chain.string_to_transaction_hash(hash_string), - {:ok, transaction} <- - Chain.hash_to_transaction( - hash, - necessity_by_association: %{ - :block => :optional, - [created_contract_address: :names] => :optional, - [from_address: :names] => :optional, - [to_address: :names] => :optional, - [to_address: :smart_contract] => :optional, - :token_transfers => :optional - } - ) do + {:ok, transaction} <- Chain.hash_to_transaction(hash) do full_options = Keyword.merge( [ From e02b27975725d5df0e06e90b536b4e8d888db174 Mon Sep 17 00:00:00 2001 From: maxgrapps Date: Thu, 30 May 2019 12:56:03 +0300 Subject: [PATCH 45/57] fixed length of logs search input --- apps/block_scout_web/assets/css/components/_log-search.scss | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/block_scout_web/assets/css/components/_log-search.scss b/apps/block_scout_web/assets/css/components/_log-search.scss index 51ca8713d0..a31de73262 100644 --- a/apps/block_scout_web/assets/css/components/_log-search.scss +++ b/apps/block_scout_web/assets/css/components/_log-search.scss @@ -11,6 +11,7 @@ .logs-search { display: flex; + position: relative; @media (max-width: 599px) { margin-bottom: 30px; } @@ -34,7 +35,7 @@ flex-grow: 2; min-width: 160px; &::placeholder { - color: #a3a9b5; + color: #a3a9b5; } } @@ -54,6 +55,9 @@ color: #a3a9b5; cursor: pointer; transition: .1s ease-in; + position: absolute; + top: 0; + left: 136px; &:hover { color: #333; } From 0195642d71928a971cbd329f72d06df0cc44a46b Mon Sep 17 00:00:00 2001 From: maxgrapps <50101080+maxgrapps@users.noreply.github.com> Date: Thu, 30 May 2019 12:58:08 +0300 Subject: [PATCH 46/57] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f131c6a582..5e0a0e99b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ - [#2012](https://github.com/poanetwork/blockscout/pull/2012) - make all pages pagination async ### Fixes +- [#2066](https://github.com/poanetwork/blockscout/pull/2066) - fixed length of logs search input - [#2056](https://github.com/poanetwork/blockscout/pull/2056) - log search form styles added - [#2043](https://github.com/poanetwork/blockscout/pull/2043) - Fixed modal dialog width for 'verify other explorers' - [#2025](https://github.com/poanetwork/blockscout/pull/2025) - Added a new color to display transactions' errors. From 580598e7c09d7920b725a5cfa40b272f83a365c4 Mon Sep 17 00:00:00 2001 From: Ayrat Badykov Date: Thu, 30 May 2019 13:35:59 +0300 Subject: [PATCH 47/57] fix log page number --- apps/block_scout_web/assets/js/lib/async_listing_load.js | 4 +++- apps/block_scout_web/assets/js/pages/address/logs.js | 2 +- .../lib/block_scout_web/templates/address_logs/_logs.html.eex | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/block_scout_web/assets/js/lib/async_listing_load.js b/apps/block_scout_web/assets/js/lib/async_listing_load.js index e22f6e399d..3fe14be121 100644 --- a/apps/block_scout_web/assets/js/lib/async_listing_load.js +++ b/apps/block_scout_web/assets/js/lib/async_listing_load.js @@ -105,7 +105,9 @@ export function asyncReducer (state = asyncInitialState, action) { state.pagesStack.push(window.location.href.split('?')[0]) } - state.pagesStack.push(state.nextPagePath) + if (state.pagesStack[state.pagesStack.length - 1] !== state.nextPagePath) { + state.pagesStack.push(state.nextPagePath) + } return Object.assign({}, state, { beyondPageOne: true }) } diff --git a/apps/block_scout_web/assets/js/pages/address/logs.js b/apps/block_scout_web/assets/js/pages/address/logs.js index 7c9f806c01..ffe8ad5676 100644 --- a/apps/block_scout_web/assets/js/pages/address/logs.js +++ b/apps/block_scout_web/assets/js/pages/address/logs.js @@ -46,7 +46,7 @@ const elements = { } if ($('[data-page="address-logs"]').length) { - const store = createAsyncLoadStore(reducer, initialState, 'dataset.identifierHash') + const store = createAsyncLoadStore(reducer, initialState, 'dataset.identifierLog') const addressHash = $('[data-page="address-details"]')[0].dataset.pageAddressHash const $element = $('[data-async-listing]') diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_logs/_logs.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_logs/_logs.html.eex index 66f75362c3..1b5cb5c4fd 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address_logs/_logs.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_logs/_logs.html.eex @@ -1,4 +1,4 @@ -
+
">
<%= gettext "Transaction" %>
From 4e7d74f05178fefbc473468ca35936d0095a88d2 Mon Sep 17 00:00:00 2001 From: Ayrat Badykov Date: Thu, 30 May 2019 13:43:19 +0300 Subject: [PATCH 48/57] fix CR issues --- .../block_scout_web/controllers/address_logs_controller.ex | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_logs_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_logs_controller.ex index c702856a06..db76b9d447 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_logs_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_logs_controller.ex @@ -76,7 +76,10 @@ defmodule BlockScoutWeb.AddressLogsController do with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), {:ok, address} <- Chain.hash_to_address(address_hash) do topic = String.trim(topic) - logs_plus_one = Chain.address_to_logs(address, topic: topic) + + formatted_topic = if String.starts_with?(topic, "0x"), do: topic, else: "0x" <> topic + + logs_plus_one = Chain.address_to_logs(address, topic: formatted_topic) {results, next_page} = split_list_by_page(logs_plus_one) From 6702a8cdde86a3cfc575905726f93dd3a7f7139e Mon Sep 17 00:00:00 2001 From: Ayrat Badykov Date: Thu, 30 May 2019 15:41:36 +0300 Subject: [PATCH 49/57] stop click twice --- apps/block_scout_web/assets/js/lib/async_listing_load.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/block_scout_web/assets/js/lib/async_listing_load.js b/apps/block_scout_web/assets/js/lib/async_listing_load.js index 3fe14be121..3a3eb2fd6c 100644 --- a/apps/block_scout_web/assets/js/lib/async_listing_load.js +++ b/apps/block_scout_web/assets/js/lib/async_listing_load.js @@ -281,12 +281,14 @@ function firstPageLoad (store) { event.preventDefault() loadItemsNext() store.dispatch({type: 'NAVIGATE_TO_OLDER'}) + event.stopImmediatePropagation() }) $element.on('click', '[data-prev-page-button]', (event) => { event.preventDefault() loadItemsPrev() store.dispatch({type: 'NAVIGATE_TO_NEWER'}) + event.stopImmediatePropagation() }) } From 3ae753f3370121159efb988dfcea4ab5c7217ab6 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Thu, 30 May 2019 15:50:52 +0300 Subject: [PATCH 50/57] Add docs folder for docsify integration --- docs/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/.gitkeep diff --git a/docs/.gitkeep b/docs/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 From f7bbfe4f20be7842328a562d3f8c203be18b8164 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Thu, 30 May 2019 15:56:54 +0300 Subject: [PATCH 51/57] Docsify setup --- docs/.nojekyll | 0 docs/README.md | 323 ++++++++++++++++++++++++++++++++++++++++++++++++ docs/index.html | 21 ++++ 3 files changed, 344 insertions(+) create mode 100644 docs/.nojekyll create mode 100644 docs/README.md create mode 100644 docs/index.html diff --git a/docs/.nojekyll b/docs/.nojekyll new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000000..f53a43e897 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,323 @@ +

+ + + +

+ +

BlockScout

+

Blockchain Explorer for inspecting and analyzing EVM Chains.

+
+ +[![CircleCI](https://circleci.com/gh/poanetwork/blockscout.svg?style=svg&circle-token=f8823a3d0090407c11f87028c73015a331dbf604)](https://circleci.com/gh/poanetwork/blockscout) [![Coverage Status](https://coveralls.io/repos/github/poanetwork/blockscout/badge.svg?branch=master)](https://coveralls.io/github/poanetwork/blockscout?branch=master) [![Join the chat at https://gitter.im/poanetwork/blockscout](https://badges.gitter.im/poanetwork/blockscout.svg)](https://gitter.im/poanetwork/blockscout?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) + +
+ +BlockScout provides a comprehensive, easy-to-use interface for users to view, confirm, and inspect transactions on **all EVM** (Ethereum Virtual Machine) blockchains. This includes the Ethereum main and test networks as well as **Ethereum forks and sidechains**. + +Following is an overview of the project and instructions for [getting started](#getting-started). + +Visit the [POA BlockScout forum](https://forum.poa.network/c/blockscout) for additional deployment instructions, FAQs, troubleshooting, and other BlockScout related items. You can also post and answer questions here. + +You can also access the dev chatroom on our [Gitter Channel](https://gitter.im/poanetwork/blockscout). + +## About BlockScout + +BlockScout is an Elixir application that allows users to search transactions, view accounts and balances, and verify smart contracts on the entire Ethereum network including all forks and sidechains. + +Currently available block explorers (i.e. Etherscan and Etherchain) are closed systems which are not independently verifiable. As Ethereum sidechains continue to proliferate in both private and public settings, transparent tools are needed to analyze and validate transactions. + + +### Features + +- [x] **Open source development**: The code is community driven and available for anyone to use, explore and improve. + +- [x] **Real time transaction tracking**: Transactions are updated in real time - no page refresh required. Infinite scrolling is also enabled. + +- [x] **Smart contract interaction**: Users can read and verify Solidity smart contracts and access pre-existing contracts to fast-track development. Support for Vyper, LLL, and Web Assembly contracts is in progress. + +- [x] **Token support**: ERC20 and ERC721 tokens are supported. Future releases will support additional token types including ERC223 and ERC1155. + +- [x] **User customization**: Users can easily deploy on a network and customize the Bootstrap interface. + +- [x] **Ethereum sidechain networks**: BlockScout supports the Ethereum mainnet, Ethereum testnets, POA network, and forks like Ethereum Classic, xDAI, additional sidechains, and private EVM networks. + +### Supported Projects + +| **Hosted Mainnets** | **Hosted Testnets** | **Additional Chains using BlockScout** | +|--------------------------------------------------------|-------------------------------------------------------|----------------------------------------------------| +| [Aerum](https://blockscout.com/aerum/mainnet) | [Goerli Testnet](https://blockscout.com/eth/goerli) | [ARTIS](https://explorer.sigma1.artis.network) | +| [Callisto](https://blockscout.com/callisto/mainnet) | [Kovan Testnet](https://blockscout.com/eth/kovan) | [Ether-1](https://blocks.ether1.wattpool.net/) | +| [Ethereum Classic](https://blockscout.com/etc/mainnet) | [POA Sokol Testnet](https://blockscout.com/poa/sokol) | [Fuse Network](https://explorer.fuse.io/) | +| [Ethereum Mainnet](https://blockscout.com/eth/mainnet) | [Rinkeby Testnet](https://blockscout.com/eth/rinkeby) | [Oasis Labs](https://blockexplorer.oasiscloud.io/) | +| [POA Core Network](https://blockscout.com/poa/core) | [Ropsten Testnet](https://blockscout.com/eth/ropsten) | [Petrichor](https://explorer.petrachor.com/) | +| [RSK](https://blockscout.com/rsk/mainnet) | | [PIRL](http://pirl.es/) | +| [xDai Chain](https://blockscout.com/poa/dai) | | [SafeChain](https://explorer.safechain.io) | +| | | [SpringChain](https://explorer.springrole.com/) | +| | | [Kotti Testnet](https://kottiexplorer.ethernode.io/) | + + +### Visual Interface + +Interface for the POA network _updated 02/2019_ + +![BlockScout Example](explorer_example_2_2019.gif) + + +### Umbrella Project Organization + +This repository is an [umbrella project](https://elixir-lang.org/getting-started/mix-otp/dependencies-and-umbrella-projects.html). Each directory under `apps/` is a separate [Mix](https://hexdocs.pm/mix/Mix.html) project and [OTP application](https://hexdocs.pm/elixir/Application.html), but the projects can use each other as a dependency in their `mix.exs`. + +Each OTP application has a restricted domain. + +| Directory | OTP Application | Namespace | Purpose | +|:------------------------|:--------------------|:------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `apps/ethereum_jsonrpc` | `:ethereum_jsonrpc` | `EthereumJSONRPC` | Ethereum JSONRPC client. It is allowed to know `Explorer`'s param format, but it cannot directly depend on `:explorer` | +| `apps/explorer` | `:explorer` | `Explorer` | Storage for the indexed chain. Can read and write to the backing storage. MUST be able to boot in a read-only mode when run independently from `:indexer`, so cannot depend on `:indexer` as that would start `:indexer` indexing. | +| `apps/block_scout_web` | `:block_scout_web` | `BlockScoutWeb` | Phoenix interface to `:explorer`. The minimum interface to allow web access should go in `:block_scout_web`. Any business rules or interface not tied directly to `Phoenix` or `Plug` should go in `:explorer`. MUST be able to boot in a read-only mode when run independently from `:indexer`, so cannot depend on `:indexer` as that would start `:indexer` indexing. | +| `apps/indexer` | `:indexer` | `Indexer` | Uses `:ethereum_jsonrpc` to index chain and batch import data into `:explorer`. Any process, `Task`, or `GenServer` that automatically reads from the chain and writes to `:explorer` should be in `:indexer`. This restricts automatic writes to `:indexer` and read-only mode can be achieved by not running `:indexer`. | + + +## Getting Started + +### Requirements + +| Dependency | Mac | Linux | +|-------------|-----|-------| +| [Erlang/OTP 21.0.4](https://github.com/erlang/otp) | `brew install erlang` | [Erlang Install Example](https://github.com/poanetwork/blockscout-terraform/blob/33f68e816e36dc2fb055911fa0372531f0e956e7/modules/stack/libexec/init.sh#L134) | +| [Elixir 1.8.1](https://elixir-lang.org/) | :point_up: | [Elixir Install Example](https://github.com/poanetwork/blockscout-terraform/blob/33f68e816e36dc2fb055911fa0372531f0e956e7/modules/stack/libexec/init.sh#L138) | +| [Postgres 10.3](https://www.postgresql.org/) | `brew install postgresql` | [Postgres Install Example](https://github.com/poanetwork/blockscout-terraform/blob/33f68e816e36dc2fb055911fa0372531f0e956e7/modules/stack/libexec/init.sh#L187) | +| [Node.js 10.x.x](https://nodejs.org/en/) | `brew install node` | [Node.js Install Example](https://github.com/poanetwork/blockscout-terraform/blob/33f68e816e36dc2fb055911fa0372531f0e956e7/modules/stack/libexec/init.sh#L66) | +| [Automake](https://www.gnu.org/software/automake/) | `brew install automake` | [Automake Install Example](https://github.com/poanetwork/blockscout-terraform/blob/33f68e816e36dc2fb055911fa0372531f0e956e7/modules/stack/libexec/init.sh#L72) | +| [Libtool](https://www.gnu.org/software/libtool/) | `brew install libtool` | [Libtool Install Example](https://github.com/poanetwork/blockscout-terraform/blob/33f68e816e36dc2fb055911fa0372531f0e956e7/modules/stack/libexec/init.sh#L62) | +| [Inotify-tools](https://github.com/rvoicilas/inotify-tools/wiki) | Not Required | Ubuntu - `apt-get install inotify-tools` | +| [GCC Compiler](https://gcc.gnu.org/) | `brew install gcc` | [GCC Compiler Example](https://github.com/poanetwork/blockscout-terraform/blob/33f68e816e36dc2fb055911fa0372531f0e956e7/modules/stack/libexec/init.sh#L70) | +| [GMP](https://gmplib.org/) | `brew install gmp` | [Install GMP Devel](https://github.com/poanetwork/blockscout-terraform/blob/33f68e816e36dc2fb055911fa0372531f0e956e7/modules/stack/libexec/init.sh#L74) | + +### Build and Run + +#### Playbook Deployment + +We use [Ansible](https://docs.ansible.com/ansible/latest/index.html) & [Terraform](https://www.terraform.io/intro/getting-started/install.html) to build the correct infrastructure to run BlockScout. See [https://github.com/poanetwork/blockscout-terraform](https://github.com/poanetwork/blockscout-terraform) for details and instructions. + +#### Manual Deployment + +See [Manual BlockScout Deployment](https://forum.poa.network/t/manual-blockscout-deployment/2458) for instructions. + +#### Environment Variables + +Our forum contains a [full list of BlockScout environment variables](https://forum.poa.network/t/faq-blockscout-environment-variables/1814). + +#### Configuring EVM Chains + +* **CSS:** Update the import instruction in `apps/block_scout_web/assets/css/theme/_variables.scss` to select a preset css file. This is reflected in the `production-${chain}` branch for each instance. For example, in the `production-xdai` branch, it is set to `@import "dai-variables"`. + +* **ENV:** Update the [environment variables](https://forum.poa.network/t/faq-blockscout-environment-variables/1814) to match the chain specs. + +#### Automating Restarts + +By default `blockscout` does not restart if it crashes. To enable automated +restarts, set the environment variable `HEART_COMMAND` to whatever command you run to start `blockscout`. Configure the heart beat timeout to change how long it waits before considering the application unresponsive. At that point, it will kill the current blockscout instance and execute the `HEART_COMMAND`. By default a crash dump is not written unless you set `ERL_CRASH_DUMP_SECONDS` to a positive or negative integer. See the [heart](http://erlang.org/doc/man/heart.html) documentation for more information. + + +#### CircleCI Updates + +To monitor build status, configure your local [CCMenu](http://ccmenu.org/) with the following url: [`https://circleci.com/gh/poanetwork/blockscout.cc.xml?circle-token=f8823a3d0090407c11f87028c73015a331dbf604`](https://circleci.com/gh/poanetwork/blockscout.cc.xml?circle-token=f8823a3d0090407c11f87028c73015a331dbf604) + + +## Testing + +### Requirements + + * PhantomJS (for wallaby) + +### Running the tests + + 1. Build the assets. + `cd apps/block_scout_web/assets && npm run build; cd -` + + 2. Format the Elixir code. + `mix format` + + 3. Run the test suite with coverage for whole umbrella project. This step can be run with different configuration outlined below. + `mix coveralls.html --umbrella` + + 4. Lint the Elixir code. + `mix credo --strict` + + 5. Run the dialyzer. + `mix dialyzer --halt-exit-status` + + 6. Check the Elixir code for vulnerabilities. + `cd apps/explorer && mix sobelow --config; cd -` + `cd apps/block_scout_web && mix sobelow --config; cd -` + + 7. Lint the JavaScript code. + `cd apps/block_scout_web/assets && npm run eslint; cd -` + + 8. Test the JavaScript code. + `cd apps/block_scout_web/assets && npm run test; cd -` + +#### Parity + +##### Mox + +**This is the default setup. `mix coveralls.html --umbrella` will work on its own, but to be explicit, use the following setup**: + +```shell +export ETHEREUM_JSONRPC_CASE=EthereumJSONRPC.Case.Parity.Mox +export ETHEREUM_JSONRPC_WEB_SOCKET_CASE=EthereumJSONRPC.WebSocket.Case.Mox +mix coveralls.html --umbrella --exclude no_parity +``` + +##### HTTP / WebSocket + +```shell +export ETHEREUM_JSONRPC_CASE=EthereumJSONRPC.Case.Parity.HTTPWebSocket +export ETHEREUM_JSONRPC_WEB_SOCKET_CASE=EthereumJSONRPC.WebSocket.Case.Parity +mix coveralls.html --umbrella --exclude no_parity +``` + +| Protocol | URL | +|:----------|:-----------------------------------| +| HTTP | `http://localhost:8545` | +| WebSocket | `ws://localhost:8546` | + +#### Geth + +##### Mox + +```shell +export ETHEREUM_JSONRPC_CASE=EthereumJSONRPC.Case.Geth.Mox +export ETHEREUM_JSONRPC_WEB_SOCKET_CASE=EthereumJSONRPC.WebSocket.Case.Mox +mix coveralls.html --umbrella --exclude no_geth +``` + +##### HTTP / WebSocket + +```shell +export ETHEREUM_JSONRPC_CASE=EthereumJSONRPC.Case.Geth.HTTPWebSocket +export ETHEREUM_JSONRPC_WEB_SOCKET_CASE=EthereumJSONRPC.WebSocket.Case.Geth +mix coveralls.html --umbrella --exclude no_geth +``` + +| Protocol | URL | +|:----------|:--------------------------------------------------| +| HTTP | `https://mainnet.infura.io/8lTvJTKmHPCHazkneJsY` | +| WebSocket | `wss://mainnet.infura.io/ws/8lTvJTKmHPCHazkneJsY` | + +### API Documentation + +To view Modules and API Reference documentation: + +1. Generate documentation. +`mix docs` +2. View the generated docs. +`open doc/index.html` + +## Front-end + +### Javascript + +All Javascript files are under [apps/block_scout_web/assets/js](https://github.com/poanetwork/blockscout/tree/master/apps/block_scout_web/assets/js) and the main file is [app.js](https://github.com/poanetwork/blockscout/blob/master/apps/block_scout_web/assets/js/app.js). This file imports all javascript used in the application. If you want to create a new JS file consider creating into [/js/pages](https://github.com/poanetwork/blockscout/tree/master/apps/block_scout_web/assets/js/pages) or [/js/lib](https://github.com/poanetwork/blockscout/tree/master/apps/block_scout_web/assets/js/lib), as follows: + +#### js/lib +This folder contains all scripts that can be reused in any page or can be used as a helper to some component. + +#### js/pages +This folder contains the scripts that are specific for some page. + +#### Redux +This project uses Redux to control the state in some pages. There are pages that have things happening in real-time thanks to the Phoenix channels, e.g. Address page, so the page state changes a lot depending on which events it is listening. The redux is also used to load some contents asynchronous, see [async_listing_load.js](https://github.com/poanetwork/blockscout/blob/master/apps/block_scout_web/assets/js/lib/async_listing_load.js). + +To understand how to build new pages that need redux in this project, see the [redux_helpers.js](https://github.com/poanetwork/blockscout/blob/master/apps/block_scout_web/assets/js/lib/redux_helpers.js) + +## Internationalization + +The app is currently internationalized. It is only localized to U.S. English. To translate new strings. + +1. To setup translation file. +`cd apps/block_scout_web; mix gettext.extract --merge; cd -` +2. To edit the new strings, go to `apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po`. + +## Metrics + +BlockScout is setup to export [Prometheus](https://prometheus.io/) metrics at `/metrics`. + +### Prometheus + +1. Install prometheus: `brew install prometheus` +2. Start the web server `iex -S mix phx.server` +3. Start prometheus: `prometheus --config.file=prometheus.yml` + +### Grafana + +1. Install grafana: `brew install grafana` +2. Install Pie Chart panel plugin: `grafana-cli plugins install grafana-piechart-panel` +3. Start grafana: `brew services start grafana` +4. Add Prometheus as a Data Source + 1. `open http://localhost:3000/datasources` + 2. Click "+ Add data source" + 3. Put "Prometheus" for "Name" + 4. Change "Type" to "Prometheus" + 5. Set "URL" to "http://localhost:9090" + 6. Set "Scrape Interval" to "10s" +5. Add the dashboards from https://github.com/deadtrickster/beam-dashboards: + For each `*.json` file in the repo. + 1. `open http://localhost:3000/dashboard/import` + 2. Copy the contents of the JSON file in the "Or paste JSON" entry + 3. Click "Load" +6. View the dashboards. (You will need to click-around and use BlockScout for the web-related metrics to show up.) + +## Tracing + +Blockscout supports tracing via +[Spandex](http://git@github.com:spandex-project/spandex.git). Each application +has its own tracer, that is configured internally to that application. In order +to enable it, visit each application's `config/.ex` and update its tracer +configuration to change `disabled?: true` to `disabled?: false`. Do this for +each application you'd like included in your trace data. + +Currently, only [Datadog](https://www.datadoghq.com/) is supported as a +tracing backend, but more will be added soon. + +### DataDog + +If you would like to use DataDog, after enabling `Spandex`, set +`"DATADOG_HOST"` and `"DATADOG_PORT"` environment variables to the +host/port that your Datadog agent is running on. For more information on +Datadog and the Datadog agent, see their +[documentation](https://docs.datadoghq.com/). + +### Other + +If you want to use a different backend, remove the +`SpandexDatadog.ApiServer` `Supervisor.child_spec` from +`Explorer.Application` and follow any instructions provided in `Spandex` +for setting up that backend. + +## Memory Usage + +The work queues for building the index of all blocks, balances (coin and token), and internal transactions can grow quite large. By default, the soft-limit is 1 GiB, which can be changed in `apps/indexer/config/config.exs`: + +``` +config :indexer, memory_limit: 1 <<< 30 +``` + +Memory usage is checked once per minute. If the soft-limit is reached, the shrinkable work queues will shed half their load. The shed load will be restored from the database, the same as when a restart of the server occurs, so rebuilding the work queue will be slower, but use less memory. + +If all queues are at their minimum size, then no more memory can be reclaimed and an error will be logged. + +## Acknowledgements + +We would like to thank the [EthPrize foundation](http://ethprize.io/) for their funding support. + +## Contributing + +See [CONTRIBUTING.md](CONTRIBUTING.md) for contribution and pull request protocol. We expect contributors to follow our [code of conduct](CODE_OF_CONDUCT.md) when submitting code or comments. + + +## License + +[![License: GPL v3.0](https://img.shields.io/badge/License-GPL%20v3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) + +This project is licensed under the GNU General Public License v3.0. See the [LICENSE](LICENSE) file for details. diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000000..f48a914259 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,21 @@ + + + + + Document + + + + + + +
+ + + + From 146280d857837c2397ab5bb54d9a2430537f91c5 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Thu, 30 May 2019 16:05:36 +0300 Subject: [PATCH 52/57] Add CHANGELOG entry and add 1.3.15 changelog --- CHANGELOG.md | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e3fe6e3a9..697a40007e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,17 +6,14 @@ - [#1928](https://github.com/poanetwork/blockscout/pull/1928) - pagination styles were updated - [#1940](https://github.com/poanetwork/blockscout/pull/1940) - qr modal button and background issue - [#1907](https://github.com/poanetwork/blockscout/pull/1907) - dropdown color bug fix (lukso theme) and tooltip color bug fix -- [#1857](https://github.com/poanetwork/blockscout/pull/1857) - Re-implement Geth JS internal transaction tracer in Elixir - [#1859](https://github.com/poanetwork/blockscout/pull/1859) - feat: show raw transaction traces - [#1941](https://github.com/poanetwork/blockscout/pull/1941) - feat: add on demand fetching and stale attr to rpc - [#1957](https://github.com/poanetwork/blockscout/pull/1957) - Calculate stakes ratio before insert pools - [#1956](https://github.com/poanetwork/blockscout/pull/1956) - add logs tab to address - [#1952](https://github.com/poanetwork/blockscout/pull/1952) - feat: exclude empty contracts by default -- [#1989](https://github.com/poanetwork/blockscout/pull/1989) - fix: consolidate address w/ balance one at a time - [#1954](https://github.com/poanetwork/blockscout/pull/1954) - feat: use creation init on self destruct - [#1974](https://github.com/poanetwork/blockscout/pull/1974) - feat: previous page button logic - [#1999](https://github.com/poanetwork/blockscout/pull/1999) - load data async on addresses page -- [#2002](https://github.com/poanetwork/blockscout/pull/2002) - Get estimated count of blocks when cache is empty - [#1807](https://github.com/poanetwork/blockscout/pull/1807) - New theming capabilites. - [#2040](https://github.com/poanetwork/blockscout/pull/2040) - Verification links to other explorers for ETH - [#2012](https://github.com/poanetwork/blockscout/pull/2012) - make all pages pagination async @@ -30,7 +27,6 @@ - [#1944](https://github.com/poanetwork/blockscout/pull/1944) - fixed styles for token's dropdown. - [#1926](https://github.com/poanetwork/blockscout/pull/1926) - status label alignment - [#1849](https://github.com/poanetwork/blockscout/pull/1849) - Improve chains menu -- [#1869](https://github.com/poanetwork/blockscout/pull/1869) - Fix output and gas extraction in JS tracer for Geth - [#1868](https://github.com/poanetwork/blockscout/pull/1868) - fix: logs list endpoint performance - [#1822](https://github.com/poanetwork/blockscout/pull/1822) - Fix style breaks in decompiled contract code view - [#1885](https://github.com/poanetwork/blockscout/pull/1885) - highlight reserved words in decompiled code @@ -40,7 +36,6 @@ - [#1915](https://github.com/poanetwork/blockscout/pull/1915) - fallback to 2 latest evm versions - [#1937](https://github.com/poanetwork/blockscout/pull/1937) - Check the presence of overlap[i] object before retrieving properties from it - [#1960](https://github.com/poanetwork/blockscout/pull/1960) - do not remove bold text in decompiled contacts -- [#1992](https://github.com/poanetwork/blockscout/pull/1992) - fix: support https for wobserver polling - [#1966](https://github.com/poanetwork/blockscout/pull/1966) - fix: add fields for contract filter performance - [#2017](https://github.com/poanetwork/blockscout/pull/2017) - fix: fix to/from filters on tx list pages - [#2008](https://github.com/poanetwork/blockscout/pull/2008) - add new function clause for xDai network beneficiaries @@ -48,7 +43,6 @@ - [#2052](https://github.com/poanetwork/blockscout/pull/2052) - allow bytes32 for name and symbol - [#2047](https://github.com/poanetwork/blockscout/pull/2047) - fix: show creating internal transactions - [#2014](https://github.com/poanetwork/blockscout/pull/2014) - fix: use better queries for listLogs endpoint -- [#2027](https://github.com/poanetwork/blockscout/pull/2027) - fix: `BlocksTransactionsMismatch` ignoring blocks without transactions ### Chore @@ -60,11 +54,25 @@ - [#2000](https://github.com/poanetwork/blockscout/pull/2000) - docker/Makefile: always set a container name - [#2018](https://github.com/poanetwork/blockscout/pull/2018) - Use PORT env variable in dev config - [#2055](https://github.com/poanetwork/blockscout/pull/2055) - Increase timeout for geth indexers +- [#2069](https://github.com/poanetwork/blockscout/pull/2069) - Docsify integration: static docs page generation -## 1.3.14-beta +## 1.3.15-beta ### Features +- [#1857](https://github.com/poanetwork/blockscout/pull/1857) - Re-implement Geth JS internal transaction tracer in Elixir +- [#1989](https://github.com/poanetwork/blockscout/pull/1989) - fix: consolidate address w/ balance one at a time +- [#2002](https://github.com/poanetwork/blockscout/pull/2002) - Get estimated count of blocks when cache is empty + +### Fixes + +- [#1869](https://github.com/poanetwork/blockscout/pull/1869) - Fix output and gas extraction in JS tracer for Geth +- [#1992](https://github.com/poanetwork/blockscout/pull/1992) - fix: support https for wobserver polling +- [#2027](https://github.com/poanetwork/blockscout/pull/2027) - fix: `BlocksTransactionsMismatch` ignoring blocks without transactions + + +## 1.3.14-beta + - [#1812](https://github.com/poanetwork/blockscout/pull/1812) - add pagination to addresses page - [#1920](https://github.com/poanetwork/blockscout/pull/1920) - fix: remove source code fields from list endpoint - [#1876](https://github.com/poanetwork/blockscout/pull/1876) - async calculate a count of blocks From 86938b6b0e872d3ac33d2c6763b28a73c88e6d70 Mon Sep 17 00:00:00 2001 From: pasqu4le Date: Thu, 30 May 2019 16:11:52 +0200 Subject: [PATCH 53/57] Reduce max_concurrency of BlocksTransactionsMismatch --- .../lib/indexer/temporary/blocks_transactions_mismatch.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/indexer/lib/indexer/temporary/blocks_transactions_mismatch.ex b/apps/indexer/lib/indexer/temporary/blocks_transactions_mismatch.ex index 82dd0a2e55..49b33b7c44 100644 --- a/apps/indexer/lib/indexer/temporary/blocks_transactions_mismatch.ex +++ b/apps/indexer/lib/indexer/temporary/blocks_transactions_mismatch.ex @@ -24,7 +24,7 @@ defmodule Indexer.Temporary.BlocksTransactionsMismatch do @defaults [ flush_interval: :timer.seconds(3), max_batch_size: 10, - max_concurrency: 4, + max_concurrency: 1, task_supervisor: Indexer.Temporary.BlocksTransactionsMismatch.TaskSupervisor, metadata: [fetcher: :blocks_transactions_mismatch] ] From af88504c520004402db67d5c7838a8e46450a295 Mon Sep 17 00:00:00 2001 From: pasqu4le Date: Thu, 30 May 2019 16:20:14 +0200 Subject: [PATCH 54/57] update CHANGELOG with PR 2070 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e3fe6e3a9..53ea42cdbb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,7 @@ - [#2047](https://github.com/poanetwork/blockscout/pull/2047) - fix: show creating internal transactions - [#2014](https://github.com/poanetwork/blockscout/pull/2014) - fix: use better queries for listLogs endpoint - [#2027](https://github.com/poanetwork/blockscout/pull/2027) - fix: `BlocksTransactionsMismatch` ignoring blocks without transactions +- [#2070](https://github.com/poanetwork/blockscout/pull/2070) - reduce `max_concurrency` of `BlocksTransactionsMismatch` fetcher ### Chore From 638b5f26462eb45eababccee0dc9ed19d92bcd86 Mon Sep 17 00:00:00 2001 From: maxgrapps Date: Thu, 30 May 2019 22:45:16 +0300 Subject: [PATCH 55/57] blockscout theme --- .../assets/css/theme/_neutral_variables.scss | 59 ++++++++++++++++-- .../assets/static/images/blockscout_logo.png | Bin 0 -> 4688 bytes .../static/images/blockscout_logo@2x.png | Bin 0 -> 10766 bytes .../static/images/blockscout_logo@3x.png | Bin 0 -> 18370 bytes 4 files changed, 55 insertions(+), 4 deletions(-) create mode 100644 apps/block_scout_web/assets/static/images/blockscout_logo.png create mode 100644 apps/block_scout_web/assets/static/images/blockscout_logo@2x.png create mode 100644 apps/block_scout_web/assets/static/images/blockscout_logo@3x.png diff --git a/apps/block_scout_web/assets/css/theme/_neutral_variables.scss b/apps/block_scout_web/assets/css/theme/_neutral_variables.scss index 515f5eafaf..ac5145fc64 100644 --- a/apps/block_scout_web/assets/css/theme/_neutral_variables.scss +++ b/apps/block_scout_web/assets/css/theme/_neutral_variables.scss @@ -1,8 +1,59 @@ -$primary: #262d62; -$secondary: #687bf6; -$tertiary: #687bf6; +// $primary: #262d62; +// $secondary: #687bf6; +// $tertiary: #687bf6; $dashboard-line-color-price: #8286a9 !default; $base-border-color: #e2e5ec !default; -$common-container-margin: 50px !default; \ No newline at end of file +$common-container-margin: 50px !default; + +// general +$primary: #5c34a2; +$secondary: #87e1a9; +$tertiary: #bf9cff; +$additional-font: #fff; + +// footer +$footer-background-color: #3c226a; +$footer-title-color: #fff; +$footer-text-color: #bda6e7; +$footer-item-disc-color: $secondary; +.footer-logo { filter: brightness(0) invert(1); } + +// dashboard +$dashboard-line-color-price: $tertiary; // price left border + +$dashboard-banner-chart-legend-value-color: $additional-font; // chart labels + +$dashboard-stats-item-value-color: $additional-font; // stat values + +$dashboard-stats-item-border-color: $secondary; // stat border + +$dashboard-banner-gradient-start: $primary; // gradient begin + +$dashboard-banner-gradient-end: lighten($primary, 5); // gradient end + +$dashboard-banner-network-plain-container-background-color: #865bd4; // stats bg + + +// navigation +.navbar { box-shadow: 0px 0px 30px 0px rgba(21, 53, 80, 0.12); } // header shadow +$header-icon-border-color-hover: $primary; // top border on hover +$header-icon-color-hover: $primary; // nav icon on hover +.dropdown-item:hover, .dropdown-item:focus { background-color: $primary !important; } // dropdown item on hover + +// buttons +$btn-line-bg: #fff; // button bg +$btn-line-color: $primary; // button border and font color && hover bg color +$btn-copy-color: $primary; // btn copy +$btn-qr-color: $primary; // btn qr-code + +//links & tile +.tile a { color: $primary !important; } // links color for badges +.tile-type-block { + border-left: 4px solid $primary; +} // tab active bg + +// card +$card-background-1: $primary; +$card-tab-active: $primary; diff --git a/apps/block_scout_web/assets/static/images/blockscout_logo.png b/apps/block_scout_web/assets/static/images/blockscout_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..3037a3f8186b9146786ff2417fb4f78631821a4f GIT binary patch literal 4688 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 literal 0 HcmV?d00001 diff --git a/apps/block_scout_web/assets/static/images/blockscout_logo@2x.png b/apps/block_scout_web/assets/static/images/blockscout_logo@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..b561d20287731ad8d376bfad726f4cf0db98c48f GIT binary patch literal 10766 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%0A0x7)d^KQBtlCndxA;^)8$Q6H76s%?n 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 literal 0 HcmV?d00001 From ad5ed095128b99e7cf5f4b9a3f6f224e0b0ab899 Mon Sep 17 00:00:00 2001 From: maxgrapps <50101080+maxgrapps@users.noreply.github.com> Date: Thu, 30 May 2019 22:48:12 +0300 Subject: [PATCH 56/57] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e3fe6e3a9..b7bb111e6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## Current ### Features +- [#2073](https://github.com/poanetwork/blockscout/pull/2073) - blockscout default theme - [#1963](https://github.com/poanetwork/blockscout/pull/1963), [#1959](https://github.com/poanetwork/blockscout/pull/1959), [#1948](https://github.com/poanetwork/blockscout/pull/1948), [#1936](https://github.com/poanetwork/blockscout/pull/1936), [#1925](https://github.com/poanetwork/blockscout/pull/1925), [#1922](https://github.com/poanetwork/blockscout/pull/1922), [#1903](https://github.com/poanetwork/blockscout/pull/1903), [#1874](https://github.com/poanetwork/blockscout/pull/1874), [#1895](https://github.com/poanetwork/blockscout/pull/1895), [#2031](https://github.com/poanetwork/blockscout/pull/2031) - added new themes and logos for poa, eth, rinkeby, goerli, ropsten, kovan, sokol, xdai, etc, rsk - [#2010](https://github.com/poanetwork/blockscout/pull/2010) - added "block not found" and "tx not found pages" - [#1928](https://github.com/poanetwork/blockscout/pull/1928) - pagination styles were updated From 7c5c7ed3191407da6151fc61762e590f5a27e173 Mon Sep 17 00:00:00 2001 From: maxgrapps <50101080+maxgrapps@users.noreply.github.com> Date: Fri, 31 May 2019 10:11:43 +0300 Subject: [PATCH 57/57] Add files via upload --- .../assets/static/images/blockscout_logo.png | Bin 4688 -> 3699 bytes .../static/images/blockscout_logo@2x.png | Bin 10766 -> 8819 bytes .../static/images/blockscout_logo@3x.png | Bin 18370 -> 13359 bytes 3 files changed, 0 insertions(+), 0 deletions(-) diff --git a/apps/block_scout_web/assets/static/images/blockscout_logo.png b/apps/block_scout_web/assets/static/images/blockscout_logo.png index 3037a3f8186b9146786ff2417fb4f78631821a4f..3587f655565fe10e6a303784a57b6a943ad7da67 100644 GIT binary patch literal 3699 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 diff --git a/apps/block_scout_web/assets/static/images/blockscout_logo@2x.png b/apps/block_scout_web/assets/static/images/blockscout_logo@2x.png index b561d20287731ad8d376bfad726f4cf0db98c48f..42238823f2af22ebb1176d57c9c768c6dcd0a3f1 100644 GIT binary patch literal 8819 zcmV-(B8=UMP)KoTH{WRuQEIvaB~4m`l;fQ=8^=Wy6Q95DE>pLzZ{+&lX?Y%o5spWhi{nI!YT z0XUK|2mwM8ifFY-+Psrf_pAE4y62nOnc3OZtPn?+D7v?&L-p5P|F5dP`l=PfFyKh& zo9})KRF?`aO}WUWsek8ktEb{}f#z1zxV{^g4-%sCxCF7(%ym2tTzI^SOI1a7 zDWKp|QHc2>C?d(J>joG)fS%JKm&3oaxXc>JI%G4rj-1W%GOaJfZP(!X`M5j|mp=Th zAD7SK{y)d#ev9WZzVe&X!6Annstsv1ILC72+mYW^wB3)qhWQ_mzl;1eyYo~^|>M!^8ZEt z=MrF2OzV-K#(Oy1c?}$Ls7Af{^%p*Fu8@m?@Ir0}xg28Y%DW-NF0oX_`bb}_8Gql3 z+ZM9RBe^l`*06h1;Oe0JQ{U{n#=ee=ud7(Dju0mA?pR)rVgAox_eZ!s!UZy2AstA=+<1vE>GSKDsqQ%>?*PAG-l+Cwap8I2lcFZ9^0kLv72Qr^}O3HiaI@IFz?8^Lpuwe&?B zDe>Aql!w5|l)(_dl1b(umCQminuSzc$kmAaKasC+UI&L9dK~~|=wP%7Et8K0>O2r? z^nk~wnbr$gC}OEtfm(Bk+bk=Bd-iLQpQ!P@d=AJ+^0>h3*M#Lm7Do=jjIK)-iLkOz zMvvbGBzq>6h3I%1V&mz{hI*pcH_s35b6yFD96HK?5)6C36sY%j++>YtN_7`PcSrS^ z0E3|NPL6Z@8TlpTKA2swb{_Ka$R~RP+GXUnnZRX&$g}|$1Q)cF(2{YhvB~t;ans*9 zuY^Mm9X&uH^tcJZ0}ND_D;@)RZ>h1$p6T)98GFR{t%e?F%?=2#*urG zsf~72dG3wKpF(~S`FnhxHv*jm=Jbu-$lr$A#kabV-+_E0@&eX}3FJe_Tah0^-tWLf z?EuBYN;T(1!@Rw_#-Vj#5W;PK%G}jh0SYXcp@T6w<}%mx_xu8nAH`>K8^7c;p_h?q z*n877`~3%)0qrZn98G)T4f3Ae#!3$ipuejDkbIxpxsv5gFWLiOrZ_z3z{QaR3T2Nj zI$FYz)p58d3L^t?2%-Dq_GuyoKa9{C#NZym*JcFy9M6Oey{eDgFG z)jVlE{04eYo2B)}BRtoc?8?>`Z6_Z(#g?fBG8ALH82LivFEI$cS&7=>U0}|UyBo|@ zkEwvN;*_S6M;~};QsqWG>O}KxCWe#8N<)5@UZk#d;l%53?dy?$QO0s?Lp}xh6JQRx z13#}PSz#QSO60oVCxJ;nn4_1ptxwvItT-Bo_zWN8W*2b>yFqV8`Z$=eR3b_y{c<_f zDM#-?UQyMhls91WR4;QYm~o`ngD5DyuLSQzrdgfeVOQWK>6mLB+^7i9mSwh|V>7oD zfeCQ?5`Zjc>(okg^uLMQ?)Yk~=#qf4f9nLP+mvBqnsDb0s1Rt-AlT#*><_Ep4Wg;z zH$Zjgz)*9b8@M#|W2Pl^T{mS?azaMUX9$@|cI!R@VNKkdQW|3a5!=Lbj6Uc|IgNn{^E30>}r!#BAcaW;ggk+LW>az@-GJ z)B$f0IVPf1KrRcKTLabSo?;Sc8UjxJzuR9nrB@QF| z_b<)h*d0*)NZOm4$^nY7PH2uxW4jlEjc!{B6Flvo0;QR>Q80WK!#{XmKNe#!MqP9xwLp1~wa(OAi?_4nFP!V#EAU_XgU~bb4 zuXP5PL*l0)&j&NGcL4b@jumY^LV3>yb5!nWpxNg|hH8a2tED4-A$L9-8N?{iy9fD? z$af;|t-=zZZ0+|WpUfbbWmor6*`R~_N5|s`Vp(y%C;~!kJW~K57I_dnGD%$=lTH%r5St(@OM$;$gSsY<08($r z1;Ym@BR6{-o6PqdgMjiuY5l$$DaMWbq7}a*G|IA!Uw9gN{s8i8)0~fjpfvRR-V(o2 zaCrlm=?PSA?=6hIQrqeiu+v%T~%jFT#Xw3P%{?%y!iL1K)?!47SYZ8OvjF(y~x^nvM5-&m+!@3 zOQVy)nS$oevV|wp5?&$tE5WWQ46Eo%QtRB7K$Z0&gXH?I_1 zl7V=e)!qewvgf(6b)iPjg>48Xl#x2xpF|77RLaKrKG^vFcIaGR4~9a@h!%-qDkZJ! zxXstB!u;j{w6FKU>dPBo_wOcQ?}L+&O`5<&v5CfcKD1oOB5_UMO{yZYGHqD`QL2%_ zWNaXb*D+%sR7mNhceTOs=QV@Nr`WQ}a(n7kXkG1t=G9&}?mc1X+Y*EAzZ!=~pK0BY ztJH+|PHlqMIT_pNyk+YfA3jQeK`+d^OKcBu&)y|Poc6H=aPSDPd2tb#e2)W@$s*U{ zSKMnZWb5OCGUvNl0uy>LpGW>-kqhz;-ftDmJPVeFcE2yR!(Z6#C^ci0D)apnE2q`d zk|lEsTPyz$`48;U&7_3IPb4sjv&DE%ksEUbTNR7TocneOOb+pLe@gnDMwn>C>9hRz zr&-dm$}yBcz5C|&u7E;uk}(7m%e`59Q7fGKfex{EDW#-k z&RQDg76mbN+24j>{y9F_cH1}%ZA}S9A42z`Wf6i3g=O^8ph6YbP!>^#R=kB-Lrelp zMl)qhlpF$I4u;Xvpco0!!c`%7`zJfW-{@jFtB_mKq3vWJG_7rgUB8XMzI!7AP=|XG z0tm5fC`Mg0}aRta2A1v6*G zqK}raVs(z6+|3r*vn66n=jMkS#H{wq!2hZC{yVK$*F>V6F7-jI2dB=e$(R zv`h$0CZgscH`8OtW&U~~lbOb)_OG6Lybra*aV=?*Kf=JR1#c)mG= zPYO?9l`Y|K~5fauH<_BiLYro%6?c^8;Q~B;H&Wb&|WwY$xp^7`ULksS82Cd=zXzeTui=kYaZERiDK!BigS@D>aXEee&A6q1>4O#9TW4-YBzz*0s-Uk^yXWPeg ziG}seaD4M3IB{N=7)G|_ZrSL871p&N^X4esNqm$!z2 zPyCxU1d|5a(jeL~)(5}b^CXO=BH*IoR+lOOfrfa+2rd5GH!%Ra$NFJmT{B#?{6q+P z{kB`td4(UMJz0qC&Y=aO!^GhwLl$_^s705h@7E51b1fCh#4d-{SgT>)yl-YEL zT2A-C{2nj#KbR72GuodbAY7V^W|#Xy+V0o+`5of;!E>zK3cD!(3RAN`s|_y73O8NU zTz-+~mH1tr2Ue?DyWcL7liZ$f)W8zl1o_F8)LBrf`PImE+^%F@Jp$(NYOz~q(QM;a z3ukZ+ouBPWX$0xma(NV0`FwOVty(TaLx&eOythNZfD3mCD*$`)jIGfE?Y*oD=)7pd;VUcnK=Av$CL2l;RE>? z3(CFVJzns2sJ6>SZG~WR%`x;G{|oZ7VYLbY(&@-8tyklEWXl{|Y6Kx&qkAo$)Ykw< z_`$joL>Ykbgf-yUL|4W}7T0pQE{o{oBg4@odOnYe0EBDoBf^ zsG(*zbe&qyE`!1`4#~a?wiYO0!4g?xkfFl%#K~+59^1dYAW++Uem-tUF_AR8Fyy-U z>pa5snF1zgO@v$hXd&eIxU8HS4<}sEVxQkt)M3@Reb5mI7sy%a9!OprRQ9|GNju1_ zLwExMP*Xl3kJ=1Z&pCzwrM?E%!1f|Dd^!*~sYv-sDI`>gvCM`jen5#^w-p67AFdJv zXJy4p0o`Jfzm2_Nme<%;rcFyx2NdS2lxp`(QOv0vHO!#RQq`PO#cudqO5(Dp`?N6U znHZ2YvXIgXEwG;WkPQ~4*i=kKViop{4x;NbVGq-~+YRt_<$=m{Tg)1>q^(_RnnYT@ zZPRΝ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 literal 10766 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%0A0x7)d^KQBtlCndxA;^)8$Q6H76s%?n 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 FV1g}^!BPMK literal 18370 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