diff --git a/explorer/lib/explorer/models/aggregated_proofs.ex b/explorer/lib/explorer/models/aggregated_proofs.ex index ae60e3bc54..639603843c 100644 --- a/explorer/lib/explorer/models/aggregated_proofs.ex +++ b/explorer/lib/explorer/models/aggregated_proofs.ex @@ -1,6 +1,7 @@ defmodule AggregatedProofs do require Logger use Ecto.Schema + import Ecto.Query import Ecto.Changeset @primary_key {:id, :binary_id, autogenerate: true} @@ -59,4 +60,30 @@ defmodule AggregatedProofs do |> Explorer.Repo.update() end end + + def get_aggregated_proof_by_id(id) do + Explorer.Repo.get_by(AggregatedProofs, id: id) + end + + def get_paginated_proofs(%{page: page, page_size: size}) do + query = + from(proof in AggregatedProofs, + order_by: [desc: proof.block_number], + limit: ^size, + offset: ^((page - 1) * size), + select: proof + ) + + Explorer.Repo.all(query) + end + + def get_last_page(page_size) do + total_proofs = Explorer.Repo.aggregate(AggregatedProofs, :count, :id) + last_page = div(total_proofs, page_size) + if rem(total_proofs, page_size) > 0, do: last_page + 1, else: last_page + end + + def get_number_of_agg_proofs() do + Explorer.Repo.aggregate(AggregatedProofs, :count, :id) + end end diff --git a/explorer/lib/explorer/models/aggregation_mode_proof.ex b/explorer/lib/explorer/models/aggregation_mode_proof.ex index d23aa62283..947982cf4d 100644 --- a/explorer/lib/explorer/models/aggregation_mode_proof.ex +++ b/explorer/lib/explorer/models/aggregation_mode_proof.ex @@ -2,6 +2,7 @@ defmodule AggregationModeProof do require Logger use Ecto.Schema import Ecto.Changeset + import Ecto.Query # Different from proofs.ex (we could use the same but the hashes are constructed different) @primary_key {:id, :id, autogenerate: true} @@ -47,4 +48,14 @@ defmodule AggregationModeProof do |> Explorer.Repo.update() end end + + def get_all_proof_hashes(id) do + query = + from(proof in AggregationModeProof, + where: proof.agg_proof_id == ^id, + select: proof.proof_hash + ) + + Explorer.Repo.all(query) + end end diff --git a/explorer/lib/explorer_web/components/agg_proofs_table.ex b/explorer/lib/explorer_web/components/agg_proofs_table.ex new file mode 100644 index 0000000000..727f51cb4f --- /dev/null +++ b/explorer/lib/explorer_web/components/agg_proofs_table.ex @@ -0,0 +1,50 @@ +defmodule ExplorerWeb.AggProofsTable do + use Phoenix.Component + use ExplorerWeb, :live_component + + attr(:agg_proofs, :list, required: true) + + def agg_proofs_table(assigns) do + ~H""" + <.table id="agg_proofs" rows={@proofs}> + <:col :let={proof} label="Merkle root" class="text-left"> + <.link navigate={~p"/aggregated_proofs/#{proof.id}"}> + + <%= Helpers.shorten_hash(proof.merkle_root, 6) %> + <.right_arrow /> + <.tooltip> + <%= proof.merkle_root %> + + + + + <:col :let={proof} label="Age"> + + <%= proof.age %> + + + <:col :let={proof} label="Block Number"> + <%= proof.block_number |> Helpers.format_number() %> + + + <:col :let={proof} label="Blob versioned hash" class="text-left"> + <.a href={ + "#{Helpers.get_blobscan_url()}/blob/#{proof.blob_versioned_hash}"} + class="inline-flex gap-x-3 items-center group-hover:text-foreground/80 no-underline font-normal" + > + + <%= Helpers.shorten_hash(proof.blob_versioned_hash, 6) %> + <.tooltip> + <%= proof.blob_versioned_hash %> + + + + + + <:col :let={proof} label="Number of proofs"> + <%= proof.number_of_proofs |> Helpers.format_number() %> + + + """ + end +end diff --git a/explorer/lib/explorer_web/components/contracts.ex b/explorer/lib/explorer_web/components/contracts.ex index 48652bf215..d82f8910ec 100644 --- a/explorer/lib/explorer_web/components/contracts.ex +++ b/explorer/lib/explorer_web/components/contracts.ex @@ -11,6 +11,10 @@ defmodule ContractsComponent do {:ok, assign(socket, contracts: [ + %{ + contract_name: "AlignedProofAggregationService", + address: addresses["alignedProofAggregationService"] + }, %{ contract_name: "AlignedServiceManager", address: addresses["alignedLayerServiceManager"] diff --git a/explorer/lib/explorer_web/components/core_components.ex b/explorer/lib/explorer_web/components/core_components.ex index e9b93e9c83..d7b6ca29a7 100644 --- a/explorer/lib/explorer_web/components/core_components.ex +++ b/explorer/lib/explorer_web/components/core_components.ex @@ -497,6 +497,39 @@ defmodule ExplorerWeb.CoreComponents do """ end + @doc """ + Renders a dynamic badge component for the batcher. + """ + attr(:class, :string, default: nil) + attr(:status, :integer) + slot(:inner_block, default: nil) + + def dynamic_badge_for_agg_proof(assigns) do + ~H""" + <.badge + variant={ + case @status do + 0 -> "accent" + 1 -> "destructive" + 2 -> "foreground" + end + } + class={ + classes([ + @class + ]) + } + > + <%= case @status do + 0 -> "Verified" + 1 -> "Failed" + 2 -> "Missed" + end %> + <%= render_slot(@inner_block) %> + + """ + end + @doc """ Renders a selector dropdown on hover component with buttons that trigger actions on click. diff --git a/explorer/lib/explorer_web/components/footer.ex b/explorer/lib/explorer_web/components/footer.ex index 44cfac3d9d..57d1eafa12 100644 --- a/explorer/lib/explorer_web/components/footer.ex +++ b/explorer/lib/explorer_web/components/footer.ex @@ -9,6 +9,7 @@ defmodule FooterComponent do {"General", [ {"Batches", "/batches"}, + {"Aggregation", "/aggregated_proofs"}, {"Operators", "/operators"}, {"Restake", "/restaked"} ]}, diff --git a/explorer/lib/explorer_web/components/nav.ex b/explorer/lib/explorer_web/components/nav.ex index ff29adb71a..2cd6805372 100644 --- a/explorer/lib/explorer_web/components/nav.ex +++ b/explorer/lib/explorer_web/components/nav.ex @@ -27,112 +27,32 @@ defmodule NavComponent do @impl true def render(assigns) do ~H""" - + """ + end + + @doc """ + Renders a dropdown on hover component with links. + """ + attr(:title, :list, doc: "the selector title") + attr(:class, :list, doc: "class for selector") + attr(:links, :string, doc: "the links to render: (name, link, class)") + + def nav_links_dropdown(assigns) do + ~H""" +
+
+

<%= @title %>

+
+ +
+
+ <%= for {name, route, class} <- @links do %> <.link - class="hover:text-foreground" - target="_blank" - href="https://github.com/yetanotherco/aligned_layer" + class={ + classes([ + "group/link text-card-foreground w-full flex items-center justify-between", + class + ]) + } + navigate={route} > - GitHub + <%= name %> -
+ <% end %>
- - + """ end diff --git a/explorer/lib/explorer_web/live/pages/agg_proof/index.ex b/explorer/lib/explorer_web/live/pages/agg_proof/index.ex new file mode 100644 index 0000000000..73f525036d --- /dev/null +++ b/explorer/lib/explorer_web/live/pages/agg_proof/index.ex @@ -0,0 +1,30 @@ +defmodule ExplorerWeb.AggProof.Index do + require Logger + use ExplorerWeb, :live_view + + @impl true + def mount(%{"id" => id}, _, socket) do + agg_proof = + AggregatedProofs.get_aggregated_proof_by_id(id) + + { + :ok, + assign( + socket, + agg_proof: agg_proof, + proof_hashes: :empty + ) + } + end + + @impl true + def handle_event("show_proofs", _value, socket) do + proofs = AggregationModeProof.get_all_proof_hashes(socket.assigns.agg_proof.id) + {:noreply, assign(socket, proof_hashes: proofs)} + end + + @impl true + def handle_event("hide_proofs", _value, socket) do + {:noreply, assign(socket, proof_hashes: :empty)} + end +end diff --git a/explorer/lib/explorer_web/live/pages/agg_proof/index.html.heex b/explorer/lib/explorer_web/live/pages/agg_proof/index.html.heex new file mode 100644 index 0000000000..0a7012b995 --- /dev/null +++ b/explorer/lib/explorer_web/live/pages/agg_proof/index.html.heex @@ -0,0 +1,127 @@ +
+ <%= if @agg_proof != :empty do %> + <.card_preheding class="text-4xl sm:text-5xl font-bold font-foreground"> + Aggregated proof details + + <.card + class="relative px-4 py-5 min-h-fit flex flex-col" + inner_class="font-semibold inline-flex flex-col text-base gap-y-4 text-muted-foreground [&>div>p]:text-foreground [&>div>a]:text-foreground [&>div>*]:break-all [&>div>*]:font-normal [&>div]:flex [&>div]:flex-col [&>div]:lg:flex-row [&>div>h3]:basis-1/4" + > +
+

+ Merkle root: +

+

+ <%= @agg_proof.merkle_root %> + <.live_component + module={CopyToClipboardButtonComponent} + text_to_copy={@agg_proof.merkle_root} + id={"copy_batch_hash_#{@agg_proof.merkle_root}"} + class="inline-flex" + /> +

+
+ +
+

+ Number of Proofs included: +

+

<%= @agg_proof.number_of_proofs %>

+
+ +
+

+ Proofs included: +

+ <%= if @proof_hashes != :empty do %> + <%= if @proof_hashes == :nil do %> +

+ Proofs couldn't be shown for this aggregated proof +

+ <% else %> +
+
+

+ <%= proof %> + <.live_component + module={CopyToClipboardButtonComponent} + text_to_copy={proof} + id={"copy_proof_batch_hash_#{proof}_#{Utils.random_id("cp_#{index}")}"} + class="inline-flex" + /> +

+
+ <.button class="w-fit text-foreground" phx-click="hide_proofs"> + <.icon name="hero-eye-slash" class="size-4" /> Hide Proofs + +
+ <% end %> + <% else %> + <.button class="w-fit text-foreground font-semibold" phx-click="show_proofs"> + <.icon name="hero-eye" class="size-4" /> Show Proofs + + <% end %> +
+ <.divider /> +
+

+ Block Number: +

+ <.a + target="_blank" + rel="noopener" + href={ + "#{Helpers.get_etherescan_url()}/block/#{@agg_proof.block_number}" + } + class="hover:text-foreground/80" + > + <%= @agg_proof.block_number |> Helpers.format_number() %> + +
+
+

+ Transaction Hash: +

+ <.a + target="_blank" + rel="noopener" + href={"#{Helpers.get_etherescan_url()}/tx/#{@agg_proof.tx_hash}"} + class="hover:text-foreground/80" + > + <%= @agg_proof.tx_hash %> + +
+ +
+

+ Blob versioned hash: +

+ <.a + target="_blank" + rel="noopener" + href={"#{Helpers.get_blobscan_url()}/blob/#{@agg_proof.blob_versioned_hash}"} + class="hover:text-foreground/80" + > + <%= @agg_proof.blob_versioned_hash %> + +
+ + <% else %> +
+

Oops!

+

+ The batch you are looking for
doesn't exist. +

+ block not found + <.link navigate={~p"/"}> + <.button> + Go Home + + +
+ <% end %> +
diff --git a/explorer/lib/explorer_web/live/pages/agg_proofs/index.ex b/explorer/lib/explorer_web/live/pages/agg_proofs/index.ex new file mode 100644 index 0000000000..c4623ff2e9 --- /dev/null +++ b/explorer/lib/explorer_web/live/pages/agg_proofs/index.ex @@ -0,0 +1,52 @@ +defmodule ExplorerWeb.AggProofs.Index do + require Logger + import ExplorerWeb.AggProofsTable + use ExplorerWeb, :live_view + + @page_size 15 + + @impl true + def mount(_, params, socket) do + current_page = get_current_page(params) + + proofs = + AggregatedProofs.get_paginated_proofs(%{ + page: current_page, + page_size: @page_size + }) + |> Enum.map(fn proof -> + proof |> Map.merge(%{age: proof.block_timestamp |> Helpers.parse_timeago()}) + end) + + { + :ok, + assign( + socket, + proofs: proofs, + current_page: current_page, + last_page: AggregatedProofs.get_last_page(@page_size) + ) + } + end + + @impl true + def handle_event("change_page", %{"page" => page}, socket) do + {:noreply, push_navigate(socket, to: ~p"/batches?page=#{page}")} + end + + defp get_current_page(params) do + case params |> Map.get("page") do + nil -> + 1 + + page -> + case Integer.parse(page) do + {number, _} -> + if number < 1, do: 1, else: number + + :error -> + 1 + end + end + end +end diff --git a/explorer/lib/explorer_web/live/pages/agg_proofs/index.html.heex b/explorer/lib/explorer_web/live/pages/agg_proofs/index.html.heex new file mode 100644 index 0000000000..1da4493967 --- /dev/null +++ b/explorer/lib/explorer_web/live/pages/agg_proofs/index.html.heex @@ -0,0 +1,64 @@ +
+ <.card_preheding>Aggregation + <%= if @proofs != :empty and @proofs != [] do %> + <.card_background class="w-full overflow-x-auto sm:col-span-2"> + <.agg_proofs_table proofs={@proofs} /> + + <% else %> + <.empty_card_background text="No aggregated proofs To Display." class="sm:col-span-2" /> + <% end %> +
+ <%= if @current_page >= 2 do %> + <.link navigate={~p"/batches?page=#{1}"}> + <.button class="text-muted-foreground group"> + First + + + <% end %> + <%= if @current_page > 1 do %> + <.link navigate={~p"/aggregated_proofs?page=#{@current_page - 1}"}> + <.button + icon="arrow-right-solid" + icon_class="group-hover:translate-x-1 transition-all duration-150" + class="text-muted-foreground size-10 group rotate-180" + > + Previous Page + + + <% end %> +
+ + +
+ <%= if @current_page != @last_page do %> + <.link navigate={~p"/aggregated_proofs?page=#{@current_page + 1}"}> + <.button + icon="arrow-right-solid" + icon_class="group-hover:translate-x-1 transition-all duration-150" + class="text-muted-foreground size-10 group" + > + Next Page + + + <.link navigate={~p"/batches?page=#{@last_page}"}> + <.button class="text-muted-foreground group"> + Last + + + <% end %> +
+
diff --git a/explorer/lib/explorer_web/live/pages/home/index.ex b/explorer/lib/explorer_web/live/pages/home/index.ex index 727f213d2d..b561d832c8 100644 --- a/explorer/lib/explorer_web/live/pages/home/index.ex +++ b/explorer/lib/explorer_web/live/pages/home/index.ex @@ -6,6 +6,7 @@ defmodule ExplorerWeb.Home.Index do def get_stats() do verified_batches = Batches.get_amount_of_verified_batches() + aggregated_proofs = AggregatedProofs.get_number_of_agg_proofs() avg_fee_per_proof = Batches.get_avg_fee_per_proof() avg_fee_per_proof_usd = @@ -59,6 +60,12 @@ defmodule ExplorerWeb.Home.Index do end, link: nil }, + %{ + title: "Aggregated proofs", + value: aggregated_proofs, + tooltip_text: nil, + link: "/aggregated_proofs" + }, %{ title: "AVG proof cost", value: "#{avg_fee_per_proof_usd} USD", diff --git a/explorer/lib/explorer_web/live/utils.ex b/explorer/lib/explorer_web/live/utils.ex index c11aae9007..87545abfb2 100644 --- a/explorer/lib/explorer_web/live/utils.ex +++ b/explorer/lib/explorer_web/live/utils.ex @@ -155,10 +155,36 @@ defmodule ExplorerWeb.Helpers do end end + @doc """ + Get the Etherscan URL based on the environment. + - `holesky` -> https://holesky.etherscan.io + - `mainnet` -> https://etherscan.io + - `default` -> http://localhost:4000 + """ + def get_blobscan_url() do + prefix = System.get_env("ENVIRONMENT") + + case prefix do + "mainnet" -> "https://blobscan.com/" + "holesky" -> "https://holesky.blobscan.com/" + _ -> "http://localhost:4000" + end + end + def get_aligned_contracts_addresses() do + Map.merge(get_batcher_service_addresses(), get_proof_aggregation_addresses()) + end + + defp get_proof_aggregation_addresses() do + proof_agg_config_file = System.get_env("ALIGNED_PROOF_AGG_CONFIG_FILE") + {_, config_json_string} = File.read(proof_agg_config_file) + proof_agg_service_addresses = Jason.decode!(config_json_string) |> Map.get("addresses") + end + + defp get_batcher_service_addresses() do aligned_config_file = System.get_env("ALIGNED_CONFIG_FILE") {_, config_json_string} = File.read(aligned_config_file) - Jason.decode!(config_json_string) |> Map.get("addresses") + agg_service_addresses = Jason.decode!(config_json_string) |> Map.get("addresses") end def binary_to_hex_string(binary) do @@ -228,22 +254,36 @@ defmodule Utils do @batcher_submission_gas_cost Application.compile_env(:explorer, :batcher_submission_gas_cost) @aggregator_gas_cost Application.compile_env(:explorer, :aggregator_gas_cost) - @aggregator_fee_percentage_multiplier Application.compile_env(:explorer, :aggregator_fee_percentage_multiplier) + @aggregator_fee_percentage_multiplier Application.compile_env( + :explorer, + :aggregator_fee_percentage_multiplier + ) @percentage_divider Application.compile_env(:explorer, :percentage_divider) - @additional_submission_gas_cost_per_proof Application.compile_env(:explorer, :additional_submission_gas_cost_per_proof) + @additional_submission_gas_cost_per_proof Application.compile_env( + :explorer, + :additional_submission_gas_cost_per_proof + ) def scheduled_batch_interval() do default_value = 10 + case System.get_env("SCHEDULED_BATCH_INTERVAL_MINUTES") do nil -> - Logger.warning("SCHEDULED_BATCH_INTERVAL_MINUTES .env var is not set, using default value: #{default_value}") + Logger.warning( + "SCHEDULED_BATCH_INTERVAL_MINUTES .env var is not set, using default value: #{default_value}" + ) + default_value + value -> try do String.to_integer(value) rescue ArgumentError -> - Logger.warning("Invalid SCHEDULED_BATCH_INTERVAL_MINUTES .env var: #{value}, using default value: #{default_value}") + Logger.warning( + "Invalid SCHEDULED_BATCH_INTERVAL_MINUTES .env var: #{value}, using default value: #{default_value}" + ) + default_value end end @@ -457,8 +497,8 @@ defmodule Utils do def constant_batch_submission_gas_cost() do trunc( @aggregator_gas_cost * @aggregator_fee_percentage_multiplier / - @percentage_divider + - @batcher_submission_gas_cost + @percentage_divider + + @batcher_submission_gas_cost ) end diff --git a/explorer/lib/explorer_web/router.ex b/explorer/lib/explorer_web/router.ex index ca855d46ca..5a7c5381e7 100644 --- a/explorer/lib/explorer_web/router.ex +++ b/explorer/lib/explorer_web/router.ex @@ -47,6 +47,8 @@ defmodule ExplorerWeb.Router do live "/", Home.Index live "/batches/:merkle_root", Batch.Index live "/batches", Batches.Index + live "/aggregated_proofs", AggProofs.Index + live "/aggregated_proofs/:id", AggProof.Index live "/restaked", Restakes.Index live "/restaked/:address", Restake.Index live "/operators", Operators.Index