diff --git a/explorer/assets/css/tooltip.css b/explorer/assets/css/tooltip.css index 8ce7acdab..039017c72 100644 --- a/explorer/assets/css/tooltip.css +++ b/explorer/assets/css/tooltip.css @@ -8,16 +8,14 @@ .chart-tooltip-dot { height: 10px; width: 10px; - top: -5px; + bottom: -5px; border-radius: 100%; - background-color: hsl(var(--foreground)); - opacity: 0.2; + background-color: hsl(var(--accent)); position: absolute; transition: all 0.2s; } .chart-tooltip-dot:hover { - opacity: 0.5; transform: scale(1.2); cursor: pointer; } @@ -26,7 +24,7 @@ min-height: 50px; min-width: 250px; padding: 20px; - margin-top: 8px; + margin-bottom: 8px; background-color: hsl(var(--card)); border-radius: 8px; border-width: 1px; @@ -49,6 +47,7 @@ .chart-tooltip-item { display: flex; justify-content: space-between; + gap: 5px; width: 100%; } @@ -59,3 +58,4 @@ .chart-tooltip-item-value { color: hsl(var(--foreground)); } + diff --git a/explorer/assets/vendor/charts/batch_size.js b/explorer/assets/vendor/charts/batch_size.js index 91027dbee..9abbe0b5a 100644 --- a/explorer/assets/vendor/charts/batch_size.js +++ b/explorer/assets/vendor/charts/batch_size.js @@ -1,15 +1,12 @@ +import { yTickCallbackShowMinAndMaxValues } from "./helpers"; import { alignedTooltip } from "./tooltip"; export const batchSizeCustomOptions = (options, data) => { // show only min and max values - options.scales.y.ticks.callback = (_value, index, values) => { - const dataY = data.datasets[0].data.map((point) => parseFloat(point.y)); - if (index === 0) return `${Math.min(...dataY)} proofs`; - if (index === values.length - 1) { - return `${Math.max(...dataY)} proofs`; - } - return ""; - }; + options.scales.y.ticks.callback = yTickCallbackShowMinAndMaxValues( + data, + (val) => `${val} proofs` + ); // show age min, mean and max age in x axis options.scales.x.ticks.callback = (_value, index, values) => { @@ -23,9 +20,10 @@ export const batchSizeCustomOptions = (options, data) => { options.plugins.tooltip.external = (context) => alignedTooltip(context, { + name: "batch-size", title: "Batch size", items: [ - { title: "Cost", id: "cost" }, + { title: "Fee per proof", id: "cost" }, { title: "Age", id: "age" }, { title: "Merkle root", id: "merkle_root" }, { title: "Block number", id: "block_number" }, @@ -40,8 +38,7 @@ export const batchSizeCustomOptions = (options, data) => { onTooltipUpdate: (tooltipModel) => { const dataset = tooltipModel.dataPoints[0].dataset; const idx = tooltipModel.dataPoints[0].dataIndex; - - const cost = `${dataset.data[idx].y} USD`; + const amount_of_proofs = dataset.data[idx].y; const age = dataset.age[idx]; const merkleRootHash = dataset.merkle_root[idx]; const merkle_root = `${merkleRootHash.slice( @@ -49,7 +46,7 @@ export const batchSizeCustomOptions = (options, data) => { 6 )}...${merkleRootHash.slice(merkleRootHash.length - 4)}`; const block_number = dataset.data[idx].x; - const amount_of_proofs = dataset.amount_of_proofs[idx]; + const cost = `${dataset.fee_per_proof[idx]} USD`; return { cost, diff --git a/explorer/assets/vendor/charts/cost_per_proof.js b/explorer/assets/vendor/charts/cost_per_proof.js index e38ab1039..e60438b13 100644 --- a/explorer/assets/vendor/charts/cost_per_proof.js +++ b/explorer/assets/vendor/charts/cost_per_proof.js @@ -1,15 +1,12 @@ +import { yTickCallbackShowMinAndMaxValues } from "./helpers"; import { alignedTooltip } from "./tooltip"; export const costPerProofCustomOptions = (options, data) => { - // show only min and max values - options.scales.y.ticks.callback = (_value, index, values) => { - const dataY = data.datasets[0].data.map((point) => parseFloat(point.y)); - if (index === 0) return `${Math.min(...dataY)} USD`; - if (index === values.length - 1) { - return `${Math.max(...dataY)} USD`; - } - return ""; - }; + // show only 0, min and max values + options.scales.y.ticks.callback = yTickCallbackShowMinAndMaxValues( + data, + (val) => `${val} USD` + ); // show age min, mean and max age in x axis options.scales.x.ticks.callback = (_value, index, values) => { @@ -23,9 +20,10 @@ export const costPerProofCustomOptions = (options, data) => { options.plugins.tooltip.external = (context) => alignedTooltip(context, { + name: "cost-per-proof", title: "Cost per proof", items: [ - { title: "Cost", id: "cost" }, + { title: "Fee per proof", id: "cost" }, { title: "Age", id: "age" }, { title: "Merkle root", id: "merkle_root" }, { title: "Block number", id: "block_number" }, diff --git a/explorer/assets/vendor/charts/helpers.js b/explorer/assets/vendor/charts/helpers.js new file mode 100644 index 000000000..ee73d0d2d --- /dev/null +++ b/explorer/assets/vendor/charts/helpers.js @@ -0,0 +1,43 @@ +const findClosestIndex = (target, values) => { + let closestIndex = 0; + let smallestDiff = Math.abs(values[0] - target); + for (let i = 1; i < values.length; i++) { + const diff = Math.abs(values[i] - target); + if (diff < smallestDiff) { + closestIndex = i; + smallestDiff = diff; + } + } + return closestIndex; +}; + +/** + * A callback function to customize y-axis tick labels by showing only the zero, minimum and maximum data values. + * + * @param {Object} data - The chart data object containing datasets and their values. + * @param {Function} renderText - A function to format and render text for the tick labels. + * @returns {Function} - A callback function for Chart.js tick customization. + * + * The returned function compares the current tick index with the indices of the values closest + * to the minimum and maximum data points, and displays these values formatted using the + * `renderText` function. + * + * @example + * options.scales.y.ticks.callback = yTickCallbackShowMinAndMaxValues(data, (val) => `${val} USD`); + */ +export const yTickCallbackShowMinAndMaxValues = + (data, renderText) => (_value, index, values) => { + if (index === 0) return renderText(0); + + const dataY = data.datasets[0].data.map((point) => parseFloat(point.y)); + const sortedData = dataY.sort((a, b) => b - a); + const min = sortedData[0]; + const max = sortedData[sortedData.length - 1]; + const valsData = values.map((item) => item.value); + const idxClosestToMin = findClosestIndex(min, valsData); + const idxClosestToMax = findClosestIndex(max, valsData); + + if (index == idxClosestToMin) return renderText(min); + if (index == idxClosestToMax) return renderText(max); + return ""; + }; diff --git a/explorer/assets/vendor/charts/index.js b/explorer/assets/vendor/charts/index.js index faa2155f7..911230632 100644 --- a/explorer/assets/vendor/charts/index.js +++ b/explorer/assets/vendor/charts/index.js @@ -21,7 +21,7 @@ const applyOptionsByChartId = (id, options, data) => { export default { mounted() { this.initChart(); - window.addEventListener("theme-changed", this.reinitChart.bind(this)); + window.addEventListener("resize", this.resizeChart.bind(this)); }, updated() { @@ -31,9 +31,8 @@ export default { destroyed() { if (this.chart) { this.chart.destroy(); + window.removeEventListener("resize", this.resizeChart.bind(this)); } - - window.removeEventListener("theme-changed", this.reinitChart.bind(this)); }, initChart() { @@ -51,6 +50,8 @@ export default { data, options, }); + + this.resizeChart(); }, reinitChart() { @@ -59,4 +60,10 @@ export default { } this.initChart(); }, + + resizeChart() { + if (this.chart) { + this.chart.resize(); + } + }, }; diff --git a/explorer/assets/vendor/charts/tooltip.js b/explorer/assets/vendor/charts/tooltip.js index 3ab1d723e..57309fe99 100644 --- a/explorer/assets/vendor/charts/tooltip.js +++ b/explorer/assets/vendor/charts/tooltip.js @@ -26,6 +26,7 @@ const tooltipComponent = ({ title, isTooltipClickable, items }) => ` * * @param {Object} context - The chart.js context, typically passed as `this` within chart hooks. * @param {Object} params - An object containing configuration for the tooltip. + * @param {Object} params.name - A string that serves as an identifier for the tooltip. * @param {string} params.title - The title text to display in the tooltip. * @param {Array} params.items - An array of items (with ids) to be displayed inside the tooltip. * @param {Array} params.onTooltipClick - A callback that receives `tooltipModel` and gets triggered when the tooltip is clicked. @@ -36,6 +37,7 @@ const tooltipComponent = ({ title, isTooltipClickable, items }) => ` * * @example * alignedTooltip(context, { + * name: "my-chart", * title: "Tooltip Title", * items: [{ title: "Cost", id: "cost_id" }, { title: "Timestamp", id: "timestamp_id" }], * onTooltipClick: (tooltipModel) => { @@ -58,15 +60,15 @@ const tooltipComponent = ({ title, isTooltipClickable, items }) => ` */ export const alignedTooltip = ( context, - { title, items, onTooltipClick, onTooltipUpdate } + { name, title, items, onTooltipClick, onTooltipUpdate } ) => { const tooltipModel = context.tooltip; - let tooltipEl = document.getElementById("chartjs-tooltip"); + let tooltipEl = document.getElementById(`chartjs-tooltip-${name}`); if (!tooltipEl) { tooltipEl = document.createElement("div"); tooltipEl.style = "transition: opacity 0.3s;"; tooltipEl.style = "transition: left 0.1s;"; - tooltipEl.id = "chartjs-tooltip"; + tooltipEl.id = `chartjs-tooltip-${name}`; tooltipEl.innerHTML = tooltipComponent({ title, isTooltipClickable: !!onTooltipClick, @@ -81,6 +83,10 @@ export const alignedTooltip = ( tooltipEl.style.opacity = 0; tooltipEl.style.zIndex = -1; }; + // this is needed to maintain responsiveness + window.addEventListener("resize", () => { + tooltipEl.remove(); + }); if (onTooltipClick) tooltipEl.querySelector(".chart-tooltip-dot").onclick = () => onTooltipClick(tooltipModel); @@ -91,12 +97,6 @@ export const alignedTooltip = ( tooltipEl.style.opacity = 0; return; } - tooltipEl.classList.remove("above", "below", "no-transform"); - if (tooltipModel.yAlign) { - tooltipEl.classList.add(tooltipModel.yAlign); - } else { - tooltipEl.classList.add("no-transform"); - } const values = onTooltipUpdate(tooltipModel); items.forEach((item) => { @@ -115,5 +115,9 @@ export const alignedTooltip = ( tooltipModel.caretX + "px"; tooltipEl.style.top = - position.top + window.scrollY + tooltipModel.caretY + "px"; + position.top - + tooltipEl.offsetHeight + + window.scrollY + + tooltipModel.caretY + + "px"; }; diff --git a/explorer/assets/vendor/dark_mode.js b/explorer/assets/vendor/dark_mode.js index 9485082de..7c0ebc19b 100644 --- a/explorer/assets/vendor/dark_mode.js +++ b/explorer/assets/vendor/dark_mode.js @@ -41,8 +41,6 @@ const setupThemeToggle = () => { .getElementById("theme-toggle") .addEventListener("click", function () { toggleVisibility(!isDark()); - // chart.js listens for this event to re-render the chart and update its colors - window.dispatchEvent(new Event("theme-changed")); }); }; diff --git a/explorer/assets/vendor/tooltip.js b/explorer/assets/vendor/tooltip.js index 86bd1de22..80b29ba7a 100644 --- a/explorer/assets/vendor/tooltip.js +++ b/explorer/assets/vendor/tooltip.js @@ -41,7 +41,7 @@ class Tooltip { setupFloatingUI() { this.cleanup = autoUpdate(this.$parent, this.$tooltip, () => { computePosition(this.$parent, this.$tooltip, { - placement: "top", + placement: "bottom", middleware: [offset(5), flip(), shift({ padding: 5 })] }).then(({ x, y }) => { Object.assign(this.$tooltip.style, { diff --git a/explorer/lib/explorer/models/batches.ex b/explorer/lib/explorer/models/batches.ex index 27045ad6e..b22dd7b68 100644 --- a/explorer/lib/explorer/models/batches.ex +++ b/explorer/lib/explorer/models/batches.ex @@ -139,6 +139,19 @@ defmodule Batches do result -> result end end + + def get_avg_fee_per_proof() do + query = + from(b in Batches, + where: b.is_verified == true, + select: avg(b.fee_per_proof) + ) + + case Explorer.Repo.one(query) do + nil -> 0 + result -> result + end + end def get_amount_of_verified_proofs() do query = from(b in Batches, diff --git a/explorer/lib/explorer_web/components/assets_cta.ex b/explorer/lib/explorer_web/components/assets_cta.ex index 184885649..35cc452c6 100644 --- a/explorer/lib/explorer_web/components/assets_cta.ex +++ b/explorer/lib/explorer_web/components/assets_cta.ex @@ -25,7 +25,7 @@ defmodule AssetsCTAComponent do View all active operators - <.link navigate={~p"/restakes"} class="flex-1 flex flex-col justify-start gap-0.5 group"> + <.link navigate={~p"/restaked"} class="flex-1 flex flex-col justify-start gap-0.5 group">

Total Restaked diff --git a/explorer/lib/explorer_web/components/batches_table.ex b/explorer/lib/explorer_web/components/batches_table.ex new file mode 100644 index 000000000..3fc72b306 --- /dev/null +++ b/explorer/lib/explorer_web/components/batches_table.ex @@ -0,0 +1,51 @@ +defmodule ExplorerWeb.BatchesTable do + use Phoenix.Component + use ExplorerWeb, :live_component + + attr(:batches, :list, required: true) + + def batches_table(assigns) do + ~H""" + <.table id="batches" rows={@batches}> + <:col :let={batch} label="Batch Hash" class="text-left"> + <.link navigate={~p"/batches/#{batch.merkle_root}"}> + + <%= Helpers.shorten_hash(batch.merkle_root, 6) %> + <.right_arrow /> + <.tooltip> + <%= batch.merkle_root %> + + + + + <:col :let={batch} label="Status"> + <.dynamic_badge_for_batcher status={Helpers.get_batch_status(batch)} /> + + <:col :let={batch} label="Age"> + + <%= batch.submission_timestamp |> Helpers.parse_timeago() %> + + + <:col :let={batch} label="Block Number"> + <%= batch.submission_block_number |> Helpers.format_number() %> + + + <:col :let={batch} label="Fee per proof"> + <%= case EthConverter.wei_to_usd(batch.fee_per_proof, 6) do %> + <% {:ok, usd} -> %> + <%= "#{usd} USD" %> + <% {:error, _} -> %> + <%= "N/A" %> + <% end %> + <.tooltip> + ~= <%= EthConverter.wei_to_eth(batch.fee_per_proof, 6) %> ETH + + + + <:col :let={batch} label="Number of proofs"> + <%= batch.amount_of_proofs |> Helpers.format_number() %> + + + """ + end +end diff --git a/explorer/lib/explorer_web/components/charts.ex b/explorer/lib/explorer_web/components/charts.ex index 6603a6ebb..a5c1f5aa7 100644 --- a/explorer/lib/explorer_web/components/charts.ex +++ b/explorer/lib/explorer_web/components/charts.ex @@ -15,7 +15,7 @@ defmodule ExplorerWeb.ChartComponents do data-chart-type={@chart_type} data-chart-data={@chart_data} data-chart-options={@chart_options} - style="height: 100%; width: 100%;" + class="!w-full !h-full" >

@@ -23,9 +23,116 @@ defmodule ExplorerWeb.ChartComponents do end @doc """ - Renders a line chart with aligned style. + Renders a bar chart with aligned style. ## Examples - <.line + <.bar_chart + id="exchanges" + points={%{x: [1, 2, 3, 4], y: ["01-01-2024", "01-02-2024", "01-03-2024", "01-04-2024"]},} + show_ticks={%{x: true, y: true}} + extra_data={%{merkle_roots: [0x1, 0x2, 0x3, 0x4]}} + /> + !Note: + - id is used to reference the chart on javascript to apply custom styles, configurations, tooltip, that are possible only via javascript + - points: nil values are automatically ignored and not displayed + - extra_data: any other data you might want to retrieve via javascript later + """ + attr(:id, :string, required: true) + attr(:points, :map, required: true) + attr(:extra_data, :map, default: %{}) + attr(:show_ticks, :map, default: %{x: true, y: true}) + + def bar_chart(assigns) do + ~H""" + <.basic_chart + id={@id} + chart_type="bar" + chart_data={ + Jason.encode!(%{ + labels: @points, + datasets: [ + Map.merge( + %{ + data: @points, + fill: false, + tension: 0.1, + backgroundColor: "rgba(24, 255, 128, 0.3)", + hoverBackgroundColor: "rgba(24, 255, 128, 0.5)", + borderColor: "rgb(24, 255, 127)", + borderWidth: 1.5, + borderRadius: 4 + }, + @extra_data + ) + ] + }) + } + chart_options={ + Jason.encode!(%{ + animation: false, + responsive: false, + maintainAspectRatio: false, + interaction: %{ + mode: "index", + intersect: false + }, + plugins: %{ + legend: %{ + display: false + } + }, + elements: %{ + point: %{ + pointStyle: false + } + }, + scales: %{ + x: %{ + bounds: "data", + offset: false, + ticks: %{ + display: @show_ticks.x, + autoSkip: false, + maxRotation: 0, + font: %{ + weight: "700" + } + }, + grid: %{ + display: false + }, + border: %{ + display: false + } + }, + y: %{ + offset: false, + beginAtZero: true, + ticks: %{ + display: @show_ticks.y, + autoSkip: false, + maxRotation: 0, + font: %{ + weight: "700" + } + }, + grid: %{ + display: false + }, + border: %{ + display: false + } + } + } + }) + } + /> + """ + end + + @doc """ + Renders a linear chart with aligned style. + ## Examples + <.line_chart id="exchanges" points={%{x: [1, 2, 3, 4], y: ["01-01-2024", "01-02-2024", "01-03-2024", "01-04-2024"]},} show_ticks={%{x: true, y: true}} @@ -59,6 +166,8 @@ defmodule ExplorerWeb.ChartComponents do } chart_options={ Jason.encode!(%{ + animation: false, + responsive: false, maintainAspectRatio: false, interaction: %{ mode: "index", diff --git a/explorer/lib/explorer_web/components/contracts.ex b/explorer/lib/explorer_web/components/contracts.ex index 6f353d69a..48652bf21 100644 --- a/explorer/lib/explorer_web/components/contracts.ex +++ b/explorer/lib/explorer_web/components/contracts.ex @@ -1,17 +1,45 @@ defmodule ContractsComponent do use ExplorerWeb, :live_component - attr :class, :string, default: nil + attr(:class, :string, default: nil) + attr(:host, :string, default: nil) @impl true def mount(socket) do + addresses = Helpers.get_aligned_contracts_addresses() + {:ok, assign(socket, - service_manager_address: - AlignedLayerServiceManager.get_aligned_layer_service_manager_address(), - batcher_payment_service_address: - BatcherPaymentServiceManager.get_batcher_payment_service_address(), - network: System.get_env("ENVIRONMENT") + contracts: [ + %{ + contract_name: "AlignedServiceManager", + address: addresses["alignedLayerServiceManager"] + }, + %{ + contract_name: "BatcherPaymentService", + address: addresses["batcherPaymentService"] + }, + %{ + contract_name: "BlsApkRegistry", + address: addresses["blsApkRegistry"] + }, + %{ + contract_name: "IndexRegistry", + address: addresses["indexRegistry"] + }, + %{ + contract_name: "OperatorStateRetriever", + address: addresses["operatorStateRetriever"] + }, + %{ + contract_name: "RegistryCoordinator", + address: addresses["registryCoordinator"] + }, + %{ + contract_name: "StakeRegistry", + address: addresses["stakeRegistry"] + } + ] )} end @@ -22,6 +50,7 @@ defmodule ContractsComponent do <.card inner_class="text-base leading-9 flex flex-wrap sm:flex-row overflow-x-auto gap-x-2" title="Contract Addresses" + subtitle={"All Aligned contracts addresses on #{Helpers.get_current_network_from_host(@host)}"} > <.link href="https://docs.alignedlayer.com/guides/6_contract_addresses" @@ -29,32 +58,36 @@ defmodule ContractsComponent do target="_blank" rel="noopener noreferrer" > - View All <.icon name="hero-arrow-top-right-on-square-solid" class="size-3.5 mb-1" /> + See more <.icon name="hero-arrow-top-right-on-square-solid" class="size-3.5 mb-1" /> -

- <.icon name="hero-cpu-chip" class="size-4 mb-0.5" /> Service Manager: -

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

- <.icon name="hero-wallet" class="size-4 mb-0.5" /> Batcher Payment Service: -

- <.a - href={"#{Helpers.get_etherescan_url()}/address/#{@batcher_payment_service_address}"} - class="hover:text-foreground/80" - target="_blank" - rel="noopener noreferrer" - > - <%= @batcher_payment_service_address %> - +
+ <%= for %{contract_name: contract_name, address: address} <- @contracts do %> + <.contract contract_name={contract_name} address={address} /> + <% end %> +
""" end + + attr(:contract_name, :string) + attr(:address, :string) + + def contract(assigns) do + ~H""" +
+

+ <%= @contract_name %> +

+ <.a + href={"#{Helpers.get_etherescan_url()}/address/#{@address}"} + class="hover:text-foreground/80" + target="_blank" + rel="noopener noreferrer" + > + <%= @address %> + +
+ """ + end end diff --git a/explorer/lib/explorer_web/components/core_components.ex b/explorer/lib/explorer_web/components/core_components.ex index b77c5034b..e9b93e9c8 100644 --- a/explorer/lib/explorer_web/components/core_components.ex +++ b/explorer/lib/explorer_web/components/core_components.ex @@ -305,19 +305,23 @@ defmodule ExplorerWeb.CoreComponents do """ attr(:class, :string, default: nil) attr(:title, :string, default: nil) + attr(:subtitle, :string, default: nil) attr(:inner_class, :string, default: nil) - + attr(:header_container_class, :string, default: nil) slot(:inner_block, default: nil) def card(assigns) do ~H""" - <.card_background class={@class}> -

- <%= @title %> -

- + <.card_background class={classes(["px-10 py-8", @class])}> +
+

+ <%= @title %> +

+

<%= @subtitle %>

+
+
<%= render_slot(@inner_block) %> - +
""" end @@ -522,13 +526,26 @@ defmodule ExplorerWeb.CoreComponents do classes([ "px-3 py-1 rounded-full font-semibold relative group", case @variant do - "accent" -> "color-accent text-accent-foreground bg-accent group-hover:bg-accent/80" - "primary" -> "color-primary text-primary-foreground bg-primary group-hover:bg-primary/80" - "secondary" -> "color-secondary text-secondary-foreground bg-secondary group-hover:bg-secondary/80" - "destructive" -> "color-destructive text-destructive-foreground bg-destructive group-hover:bg-destructive/80" - "foreground" -> "color-foreground text-background bg-foreground group-hover:bg-foreground/80" - "card" -> "color-card text-card-foreground bg-card group-hover:bg-card/80" - _ -> "color-accent text-accent-foreground bg-accent group-hover:bg-accent/80" + "accent" -> + "color-accent text-accent-foreground bg-accent group-hover:bg-accent/80" + + "primary" -> + "color-primary text-primary-foreground bg-primary group-hover:bg-primary/80" + + "secondary" -> + "color-secondary text-secondary-foreground bg-secondary group-hover:bg-secondary/80" + + "destructive" -> + "color-destructive text-destructive-foreground bg-destructive group-hover:bg-destructive/80" + + "foreground" -> + "color-foreground text-background bg-foreground group-hover:bg-foreground/80" + + "card" -> + "color-card text-card-foreground bg-card group-hover:bg-card/80" + + _ -> + "color-accent text-accent-foreground bg-accent group-hover:bg-accent/80" end, @class ]) @@ -541,13 +558,16 @@ defmodule ExplorerWeb.CoreComponents do >
<%= for {value, on_click} <- @options do %> -