Skip to content

Commit 80cbc1a

Browse files
authored
feat(explorer): add search by proof hash (yetanotherco#766)
1 parent 43b266e commit 80cbc1a

7 files changed

Lines changed: 213 additions & 19 deletions

File tree

explorer/lib/explorer/models/proofs.ex

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,49 @@ defmodule Proofs do
4242
end
4343
end
4444

45+
def get_number_of_batches_containing_proof(proof_hash_hex) do
46+
proof_hash_hex = String.replace_prefix(proof_hash_hex, "0x", "")
47+
48+
{:ok, proof_hash_binary} = Base.decode16(proof_hash_hex, case: :mixed)
49+
50+
query = from p in Proofs,
51+
where: p.proof_hash == ^proof_hash_binary,
52+
select: %{
53+
count: count(p.batch_merkle_root, :distinct)
54+
}
55+
56+
case Explorer.Repo.one(query) do
57+
%{count: count} -> count
58+
nil -> 0
59+
end
60+
end
61+
62+
def get_batches_containing_proof(proof_hash_hex, page \\ 1, page_size \\ 10) do
63+
proof_hash_hex = String.replace_prefix(proof_hash_hex, "0x", "")
64+
65+
{:ok, proof_hash_binary} = Base.decode16(proof_hash_hex, case: :mixed)
66+
67+
offset = (page - 1) * page_size
68+
69+
query = from(p in Proofs,
70+
where: p.proof_hash == ^proof_hash_binary,
71+
order_by: [desc: p.id],
72+
limit: ^page_size,
73+
offset: ^offset,
74+
distinct: p.batch_merkle_root,
75+
select: p.batch_merkle_root)
76+
77+
case Explorer.Repo.all(query) do
78+
[] ->
79+
[]
80+
results ->
81+
results
82+
|> case do
83+
[] -> []
84+
[root] -> [root]
85+
roots -> roots
86+
end
87+
end
88+
end
89+
4590
end

explorer/lib/explorer_web/components/clipboard.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ defmodule CopyToClipboardButtonComponent do
2424
}
2525
phx-hook="CopyToClipboard"
2626
data-clipboard-text={@text_to_copy}
27-
id={"copy-to-clipboard-" <> @text_to_copy}
27+
id={"copy-to-clipboard-" <> Utils.random_id(@text_to_copy)}
2828
phx-target={@myself}
2929
phx-click="copied"
3030
>

explorer/lib/explorer_web/components/search.ex

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,23 @@ defmodule SearchComponent do
22
use ExplorerWeb, :live_component
33

44
@impl true
5-
def handle_event("search_batch", %{"batch" => batch_params}, socket) do
6-
batch_merkle_root = Map.get(batch_params, "merkle_root")
7-
is_batch_merkle_root_valid = String.match?(batch_merkle_root, ~r/^0x[a-fA-F0-9]+$/)
5+
def handle_event("search_batch", %{"batch" => %{"merkle_root" => input_hash}}, socket) do
6+
input_hash
7+
|> (fn hash ->
8+
if String.match?(hash, ~r/^0x[a-fA-F0-9]+$/), do: {:ok, hash}, else: :invalid_hash
9+
end).()
10+
|> case do
11+
{:ok, hash} ->
12+
case Proofs.get_number_of_batches_containing_proof(hash) do
13+
0 -> {:noreply, push_navigate(socket, to: ~p"/batches/#{hash}")}
14+
_ -> {:noreply, push_navigate(socket, to: ~p"/search?q=#{hash}")}
15+
end
816

9-
if not is_batch_merkle_root_valid do
10-
{:noreply,
11-
socket
12-
|> assign(batch_merkle_root: batch_merkle_root)
13-
|> put_flash!(
14-
:error,
15-
"Please enter a valid proof batch hash, these should be hex values (0x69...)."
16-
)}
17-
else
18-
{:noreply, push_navigate(socket, to: ~p"/batches/#{batch_merkle_root}")}
17+
:invalid_hash ->
18+
{:noreply,
19+
socket
20+
|> assign(batch_merkle_root: input_hash)
21+
|> put_flash!(:error, "Please enter a valid proof batch hash (0x69...).")}
1922
end
2023
end
2124

@@ -37,9 +40,9 @@ defmodule SearchComponent do
3740
<input
3841
phx-hook="SearchFocus"
3942
id={"input_#{assigns.id}"}
40-
class="pr-10 shadow-md flex h-10 w-full md:min-w-72 file:border-0 text-foreground file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed flex-1 rounded-md border border-foreground/20 bg-card px-4 py-2 text-sm font-medium transition-colors hover:bg-muted focus:outline-none focus:ring-1 disabled:pointer-events-none disabled:opacity-50 hover:text-foreground"
43+
class="pr-10 shadow-md flex h-10 w-full md:min-w-80 file:border-0 text-foreground file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed flex-1 rounded-md border border-foreground/20 bg-card px-4 py-2 text-sm font-medium transition-colors hover:bg-muted focus:outline-none focus:ring-1 disabled:pointer-events-none disabled:opacity-50 hover:text-foreground"
4144
type="search"
42-
placeholder="Search by batch hash (cmd+K)"
45+
placeholder="Enter batch or proof hash (cmd+K)"
4346
name="batch[merkle_root]"
4447
/>
4548
<.button

explorer/lib/explorer_web/live/pages/batch/index.html.heex

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,15 @@
6464
<% else %>
6565
<div class="space-y-2 basis-3/4">
6666
<div class="h-36 overflow-y-auto text-foreground space-y-2">
67-
<p :for={proof <- @proof_hashes}><%= proof %></p>
67+
<p :for={{proof, index} <- Enum.with_index(@proof_hashes)}>
68+
<%= proof %>
69+
<.live_component
70+
module={CopyToClipboardButtonComponent}
71+
text_to_copy={proof}
72+
id={"copy_proof_batch_hash_#{proof}_#{Utils.random_id("cp_#{index}")}"}
73+
class="inline-flex"
74+
/>
75+
</p>
6876
</div>
6977
<.button class="w-fit text-foreground" phx-click="hide_proofs">
7078
<.icon name="hero-eye-slash" class="size-4" /> Hide Proofs
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
defmodule ExplorerWeb.Search.Index do
2+
use ExplorerWeb, :live_view
3+
4+
@page_size 15
5+
6+
@impl true
7+
def mount(%{"q" => hash}, _session, socket) do
8+
total_pages =
9+
Proofs.get_number_of_batches_containing_proof(hash)
10+
|> div(@page_size)
11+
|> Kernel.ceil()
12+
|> max(1)
13+
14+
{:ok,
15+
assign(socket,
16+
page_title: "Search Results For #{hash |> Helpers.shorten_hash()}",
17+
hash: hash,
18+
total_pages: total_pages
19+
)}
20+
end
21+
22+
@impl true
23+
def handle_params(params, _url, socket) do
24+
hash = params["q"]
25+
page_param = Integer.parse(params["page"] || "1")
26+
27+
current_page =
28+
case page_param do
29+
{page, _} when page > 0 -> page
30+
_ -> 1
31+
end
32+
33+
case Proofs.get_batches_containing_proof(hash, current_page, @page_size) do
34+
[] ->
35+
{:noreply, push_navigate(socket, to: ~p"/batches/#{hash}")}
36+
37+
results ->
38+
{:noreply,
39+
assign(socket,
40+
page_title: "Search Results For #{hash |> Helpers.shorten_hash()}",
41+
results: results,
42+
current_page: current_page
43+
)}
44+
end
45+
end
46+
47+
@impl true
48+
def render(assigns) do
49+
~H"""
50+
<div class="flex flex-col space-y-3 text-foreground px-1 sm:max-w-lg md:max-w-3xl lg:max-w-5xl mx-auto capitalize">
51+
<.card_preheding>
52+
Search Results for "<%= @hash |> Helpers.shorten_hash() %>"
53+
</.card_preheding>
54+
<%= if @results != nil or @results != [] do %>
55+
<.table id="results" rows={@results}>
56+
<:col :let={result} label="Batch Hash" class="text-left">
57+
<.link
58+
navigate={~p"/batches/#{result}"}
59+
class="flex justify-between group group-hover:text-foreground/80"
60+
>
61+
<span class="items-center group-hover:text-foreground/80 hidden md:inline">
62+
<%= result %>
63+
</span>
64+
<span class="items-center group-hover:text-foreground/80 md:hidden">
65+
<%= result |> Helpers.shorten_hash(12) %>
66+
</span>
67+
<.right_arrow />
68+
<.tooltip>
69+
<%= result %>
70+
</.tooltip>
71+
</.link>
72+
</:col>
73+
</.table>
74+
<div class="flex gap-x-2 justify-center items-center">
75+
<%= if @current_page != 1 do %>
76+
<.link patch={~p"/search?q=#{@hash}&page=#{@current_page - 1}"}>
77+
<.button
78+
icon="arrow-left-solid"
79+
icon_class="group-hover:-translate-x-1 transition-all duration-150"
80+
class="text-muted-foreground size-10 group"
81+
>
82+
<span class="sr-only">Previous Page</span>
83+
</.button>
84+
</.link>
85+
<% else %>
86+
<.button
87+
icon="arrow-left-solid"
88+
class="text-muted-foreground size-10 group pointer-events-none opacity-50"
89+
disabled
90+
>
91+
<span class="sr-only">Previous Page</span>
92+
</.button>
93+
<% end %>
94+
<p>
95+
<%= @current_page %> / <%= @total_pages %>
96+
</p>
97+
<%= if @current_page != @total_pages do %>
98+
<.link patch={~p"/search?q=#{@hash}&page=#{@current_page + 1}"}>
99+
<.button
100+
icon="arrow-right-solid"
101+
icon_class="group-hover:translate-x-1 transition-all duration-150"
102+
class="text-muted-foreground size-10 group"
103+
>
104+
<span class="sr-only">Next Page</span>
105+
</.button>
106+
</.link>
107+
<% else %>
108+
<.button
109+
icon="arrow-right-solid"
110+
class="text-muted-foreground size-10 group pointer-events-none opacity-50"
111+
disabled
112+
>
113+
<span class="sr-only">Next Page</span>
114+
</.button>
115+
<% end %>
116+
</div>
117+
<% else %>
118+
<.card_background class="overflow-x-auto min-h-[38.45rem] flex flex-col items-center justify-center gap-2">
119+
<p class="text-lg text-muted-foreground">No matching batches found.</p>
120+
</.card_background>
121+
<% end %>
122+
</div>
123+
"""
124+
end
125+
end

explorer/lib/explorer_web/live/utils.ex

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,9 @@ defmodule Utils do
152152
hex_string |> String.replace_prefix("0x", "") |> String.to_integer(16)
153153
end
154154

155+
def binary_to_hex_string(nil), do: "0x"
156+
def binary_to_hex_string(<<>>), do: "0x"
157+
155158
def binary_to_hex_string(binary) do
156159
hex_string = binary |> Base.encode16(case: :lower)
157160
"0x" <> hex_string
@@ -182,14 +185,18 @@ defmodule Utils do
182185
cond do
183186
is_json?(body) ->
184187
case Jason.decode(body) do
185-
{:ok, json} -> {:ok, json}
188+
{:ok, json} ->
189+
{:ok, json}
190+
186191
{:error, reason} ->
187192
{:error, {:json_decode, reason}}
188193
end
189194

190195
is_cbor?(body) ->
191196
case CBOR.decode(body) do
192-
{:ok, cbor_data, _} -> {:ok, cbor_data}
197+
{:ok, cbor_data, _} ->
198+
{:ok, cbor_data}
199+
193200
{:error, reason} ->
194201
{:error, {:cbor_decode, reason}}
195202
end
@@ -206,20 +213,25 @@ defmodule Utils do
206213
{:error, {:http_error, reason}}
207214
end
208215
end
216+
209217
defp is_json?(body) do
210218
case Jason.decode(body) do
211219
{:ok, _} ->
212220
true
221+
213222
{:error, _} ->
214223
false
215224
end
216225
end
226+
217227
defp is_cbor?(body) do
218228
case CBOR.decode(body) do
219229
{:ok, _, _} ->
220230
true
231+
221232
{:error, _} ->
222233
false
234+
223235
_other ->
224236
false
225237
end

explorer/lib/explorer_web/router.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ defmodule ExplorerWeb.Router do
4141
live "/restake/:address", Restake.Index
4242
live "/operators", Operators.Index
4343
live "/operators/:address", Operator.Index
44+
live "/search", Search.Index
4445
end
4546
end
4647

0 commit comments

Comments
 (0)