From 3bd600d5b5307b0492191cfb6ef29082f1f4b8fa Mon Sep 17 00:00:00 2001 From: Nitesh Yadav <45528152+imnik11@users.noreply.github.com> Date: Tue, 21 Sep 2021 19:11:46 +0530 Subject: [PATCH] Added Beacon Explorer (#76) * Create interface to fetch the transactions from the BeaconChain (chain where all the transaction are listed) and to split by days using pages * Added UTC to `format_date` helper --- lib/archethic/beacon_chain/summary_timer.ex | 23 ++- lib/archethic/pub_sub.ex | 15 ++ .../sync/beacon_summary_handler.ex | 6 +- lib/archethic_web/live/chains/beacon_live.ex | 138 ++++++++++++++++++ lib/archethic_web/router.ex | 1 + .../explorer/beacon_chain_index.html.leex | 57 ++++++++ .../templates/layout/root.html.eex | 7 +- lib/archethic_web/views/layout_helpers.ex | 2 +- mix.exs | 2 +- 9 files changed, 242 insertions(+), 9 deletions(-) create mode 100644 lib/archethic_web/live/chains/beacon_live.ex create mode 100644 lib/archethic_web/templates/explorer/beacon_chain_index.html.leex diff --git a/lib/archethic/beacon_chain/summary_timer.ex b/lib/archethic/beacon_chain/summary_timer.ex index c68e4703b..85c6801b5 100644 --- a/lib/archethic/beacon_chain/summary_timer.ex +++ b/lib/archethic/beacon_chain/summary_timer.ex @@ -4,10 +4,11 @@ defmodule ArchEthic.BeaconChain.SummaryTimer do """ use GenServer - alias Crontab.CronExpression.Parser, as: CronParser alias Crontab.DateChecker alias Crontab.Scheduler, as: CronScheduler + alias ArchEthic.PubSub + alias ArchEthic.Utils @doc """ Create a new summary timer @@ -78,7 +79,7 @@ defmodule ArchEthic.BeaconChain.SummaryTimer do interval = Keyword.get(opts, :interval) :ets.new(:archethic_summary_timer, [:named_table, :public, read_concurrency: true]) :ets.insert(:archethic_summary_timer, {:interval, interval}) - + schedule_next_summary_time(interval) {:ok, %{interval: interval}, :hibernate} end @@ -98,6 +99,24 @@ defmodule ArchEthic.BeaconChain.SummaryTimer do end end + def handle_info( + :next_summary_time, + state = %{ + interval: interval + } + ) do + timer = schedule_next_summary_time(interval) + + slot_time = DateTime.utc_now() |> Utils.truncate_datetime() + + PubSub.notify_next_summary_time(next_summary(slot_time)) + {:noreply, Map.put(state, :timer, timer), :hibernate} + end + + defp schedule_next_summary_time(interval) do + Process.send_after(self(), :next_summary_time, Utils.time_offset(interval) * 1000) + end + def config_change(nil), do: :ok def config_change(conf) do diff --git a/lib/archethic/pub_sub.ex b/lib/archethic/pub_sub.ex index bde199ccb..73e0f9b26 100644 --- a/lib/archethic/pub_sub.ex +++ b/lib/archethic/pub_sub.ex @@ -67,6 +67,13 @@ defmodule ArchEthic.PubSub do dispatch(:new_oracle_data, {:new_oracle_data, data}) end + @doc """ + Notify next summary time beacon chain to the subscribers + """ + def notify_next_summary_time(date = %DateTime{}) do + dispatch(:next_summary_time, {:next_summary_time, date}) + end + @doc """ Register a process to a new transaction publication by type """ @@ -131,6 +138,14 @@ defmodule ArchEthic.PubSub do Registry.register(PubSubRegistry, :new_transaction_number, []) end + @doc """ + Register a process to sent next summary time of beacon summary + """ + @spec register_to_next_summary_time :: {:ok, pid()} + def register_to_next_summary_time do + Registry.register(PubSubRegistry, :next_summary_time, []) + end + @doc """ Register to a new oracle data """ diff --git a/lib/archethic/self_repair/sync/beacon_summary_handler.ex b/lib/archethic/self_repair/sync/beacon_summary_handler.ex index 9d166a9ff..72ada759e 100644 --- a/lib/archethic/self_repair/sync/beacon_summary_handler.ex +++ b/lib/archethic/self_repair/sync/beacon_summary_handler.ex @@ -57,11 +57,11 @@ defmodule ArchEthic.SelfRepair.Sync.BeaconSummaryHandler do |> Stream.map(fn {:ok, {:ok, summary}} -> summary end) end - defp download_summary(beacon_address, nodes, patch, prev_result \\ nil) + def download_summary(beacon_address, nodes, patch, prev_result \\ nil) - defp download_summary(_beacon_address, [], _, %NotFound{}), do: {:ok, %NotFound{}} + def download_summary(_beacon_address, [], _, %NotFound{}), do: {:ok, %NotFound{}} - defp download_summary(beacon_address, nodes, patch, prev_result) do + def download_summary(beacon_address, nodes, patch, prev_result) do case P2P.reply_first(nodes, %GetBeaconSummary{address: beacon_address}, patch: patch, node_ack?: true diff --git a/lib/archethic_web/live/chains/beacon_live.ex b/lib/archethic_web/live/chains/beacon_live.ex new file mode 100644 index 000000000..552c2e80d --- /dev/null +++ b/lib/archethic_web/live/chains/beacon_live.ex @@ -0,0 +1,138 @@ +defmodule ArchEthicWeb.BeaconChainLive do + @moduledoc false + use ArchEthicWeb, :live_view + + alias ArchEthic.BeaconChain + alias ArchEthic.BeaconChain.Summary, as: BeaconSummary + alias ArchEthic.BeaconChain.SummaryTimer + alias ArchEthic.Crypto + alias ArchEthic.Election + alias ArchEthic.P2P + alias ArchEthic.P2P.Node + alias ArchEthic.PubSub + alias ArchEthic.SelfRepair.Sync.BeaconSummaryHandler + alias ArchEthicWeb.ExplorerView + alias Phoenix.View + + defp list_transaction_by_date(date = %DateTime{}) do + Enum.map(BeaconChain.list_subsets(), fn subset -> + b_address = Crypto.derive_beacon_chain_address(subset, date, true) + node_list = P2P.authorized_nodes() + nodes = Election.beacon_storage_nodes(subset, date, node_list) + %Node{network_patch: patch} = P2P.get_node_info() + + {b_address, nodes, patch} + end) + |> Task.async_stream( + fn {address, nodes, patch} -> + BeaconSummaryHandler.download_summary(address, nodes, patch) + end, + on_timeout: :kill_task, + max_concurrency: 256 + ) + |> Stream.filter(&match?({:ok, {:ok, %BeaconSummary{}}}, &1)) + |> Stream.flat_map(fn {:ok, + {:ok, %BeaconSummary{transaction_summaries: transaction_summaries}}} -> + transaction_summaries + end) + end + + defp list_transaction_by_date(nil), do: [] + + def mount(_params, _session, socket) do + next_summary_time = BeaconChain.next_summary_date(DateTime.utc_now()) + + if connected?(socket) do + PubSub.register_to_next_summary_time() + end + + beacon_dates = get_beacon_dates() + + new_assign = + socket + |> assign(:next_summary_time, next_summary_time) + |> assign(:dates, beacon_dates) + |> assign(:current_date_page, 1) + |> assign( + :transactions, + list_transaction_by_date(Enum.at(beacon_dates, 0)) + ) + + {:ok, new_assign} + end + + def render(assigns) do + View.render(ExplorerView, "beacon_chain_index.html", assigns) + end + + def handle_params(%{"page" => page}, _uri, socket = %{assigns: %{dates: dates}}) do + case Integer.parse(page) do + {number, ""} when number > 0 and is_list(dates) -> + if number > length(dates) do + {:noreply, + push_redirect(socket, to: Routes.live_path(socket, __MODULE__, %{"page" => 1}))} + else + transactions = + dates + |> Enum.at(number - 1) + |> list_transaction_by_date() + + new_assign = + socket + |> assign(:current_date_page, number) + |> assign(:transactions, transactions) + + {:noreply, new_assign} + end + + _ -> + {:noreply, socket} + end + end + + def handle_params(%{}, _, socket) do + {:noreply, socket} + end + + @spec handle_event(<<_::32>>, map, Phoenix.LiveView.Socket.t()) :: + {:noreply, Phoenix.LiveView.Socket.t()} + def handle_event("goto", %{"page" => page}, socket) do + {:noreply, push_redirect(socket, to: Routes.live_path(socket, __MODULE__, %{"page" => page}))} + end + + def handle_info( + {:next_summary_time, next_summary_date}, + socket = %{assigns: %{current_date_page: page, dates: dates}} + ) do + new_dates = [next_summary_date | dates] + + transactions = + new_dates + |> Enum.at(page - 1) + |> list_transaction_by_date() + + new_next_summary = + if :gt == DateTime.compare(next_summary_date, DateTime.utc_now()) do + next_summary_date + else + BeaconChain.next_summary_date(DateTime.utc_now()) + end + + new_assign = + socket + |> assign(:transactions, transactions) + |> assign(:dates, new_dates) + |> assign(:next_summary_time, new_next_summary) + + {:noreply, new_assign} + end + + defp get_beacon_dates do + %Node{enrollment_date: enrollment_date} = + P2P.list_nodes() |> Enum.sort_by(& &1.enrollment_date, {:asc, DateTime}) |> Enum.at(0) + + enrollment_date + |> SummaryTimer.previous_summaries() + |> Enum.sort({:desc, DateTime}) + end +end diff --git a/lib/archethic_web/router.ex b/lib/archethic_web/router.ex index f899d603c..ffc3f7b4d 100644 --- a/lib/archethic_web/router.ex +++ b/lib/archethic_web/router.ex @@ -38,6 +38,7 @@ defmodule ArchEthicWeb.Router do get("/chain", ExplorerController, :chain) live("/chain/oracle", OracleChainLive) + live("/chain/beacon", BeaconChainLive) live("/nodes", NodeListLive) live("/node/:public_key", NodeDetailsLive) diff --git a/lib/archethic_web/templates/explorer/beacon_chain_index.html.leex b/lib/archethic_web/templates/explorer/beacon_chain_index.html.leex new file mode 100644 index 000000000..0cb1c5b83 --- /dev/null +++ b/lib/archethic_web/templates/explorer/beacon_chain_index.html.leex @@ -0,0 +1,57 @@ +

Beacon chain

+ +

Next Beacon Summary Time <%= format_date(@next_summary_time) %>

+ +
+
+ +
+
+ + +
+
+
+ <%= if Enum.count(@dates)!= 0 do %> +

Transaction chain for <%= format_date(Enum.at(@dates, @current_date_page - 1)) %>

+ <%else %> +

Transaction chain for <%= format_date(@next_summary_time) %>

+ +

There is no transaction yet

+ <% end %> +
+
+ <%= for tx <- @transactions do %> +
+
+ <%= link to: Routes.live_path(@socket, ArchEthicWeb.TransactionDetailsLive, Base.encode16(tx.address)) do%> + <%= Base.encode16(tx.address) %> + <% end %> +
+
+ <%= format_date(tx.timestamp) %> +
+
+ <%= tx.type %> +
+
+ <% end %> +
+
+
+
+
+ diff --git a/lib/archethic_web/templates/layout/root.html.eex b/lib/archethic_web/templates/layout/root.html.eex index 299098d33..67d84818b 100644 --- a/lib/archethic_web/templates/layout/root.html.eex +++ b/lib/archethic_web/templates/layout/root.html.eex @@ -25,7 +25,7 @@