diff --git a/Cargo.toml b/Cargo.toml
index ed0f05ca..a0048d88 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -19,6 +19,7 @@ parking_lot = { version = "0.12.4", features = ["send_guard"] }
fxhash = "0.2.1"
static_assertions = "1.1.0"
rayon = "1.10.0"
+serde = { version = "1", features = ["derive"] }
[dev-dependencies]
criterion = "0.6.0"
diff --git a/explorer/.gitignore b/explorer/.gitignore
new file mode 100644
index 00000000..1b0fa1e3
--- /dev/null
+++ b/explorer/.gitignore
@@ -0,0 +1,3 @@
+
+# Built frontend assets
+explorer/frontend/dist/
diff --git a/explorer/Cargo.lock b/explorer/Cargo.lock
new file mode 100644
index 00000000..20d04e16
--- /dev/null
+++ b/explorer/Cargo.lock
@@ -0,0 +1,2607 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "ahash"
+version = "0.8.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "version_check",
+ "zerocopy",
+]
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "alloy-primitives"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "355bf68a433e0fd7f7d33d5a9fc2583fde70bf5c530f63b80845f8da5505cf28"
+dependencies = [
+ "alloy-rlp",
+ "arbitrary",
+ "bytes",
+ "cfg-if",
+ "const-hex",
+ "derive_more",
+ "foldhash",
+ "getrandom 0.3.4",
+ "hashbrown",
+ "indexmap",
+ "itoa",
+ "k256",
+ "keccak-asm",
+ "paste",
+ "proptest",
+ "proptest-derive 0.6.0",
+ "rand 0.9.2",
+ "ruint",
+ "rustc-hash",
+ "serde",
+ "sha3",
+ "tiny-keccak",
+]
+
+[[package]]
+name = "alloy-rlp"
+version = "0.3.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f70d83b765fdc080dbcd4f4db70d8d23fe4761f2f02ebfa9146b833900634b4"
+dependencies = [
+ "alloy-rlp-derive",
+ "arrayvec",
+ "bytes",
+]
+
+[[package]]
+name = "alloy-rlp-derive"
+version = "0.3.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "64b728d511962dda67c1bc7ea7c03736ec275ed2cf4c35d9585298ac9ccf3b73"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.111",
+]
+
+[[package]]
+name = "alloy-trie"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "983d99aa81f586cef9dae38443245e585840fcf0fc58b09aee0b1f27aed1d500"
+dependencies = [
+ "alloy-primitives",
+ "alloy-rlp",
+ "arbitrary",
+ "arrayvec",
+ "derive_arbitrary",
+ "derive_more",
+ "nybbles",
+ "proptest",
+ "proptest-derive 0.5.1",
+ "serde",
+ "smallvec",
+ "tracing",
+]
+
+[[package]]
+name = "anstream"
+version = "0.6.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "arbitrary"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
+dependencies = [
+ "derive_arbitrary",
+]
+
+[[package]]
+name = "ark-ff"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b3235cc41ee7a12aaaf2c575a2ad7b46713a8a50bda2fc3b003a04845c05dd6"
+dependencies = [
+ "ark-ff-asm 0.3.0",
+ "ark-ff-macros 0.3.0",
+ "ark-serialize 0.3.0",
+ "ark-std 0.3.0",
+ "derivative",
+ "num-bigint",
+ "num-traits",
+ "paste",
+ "rustc_version 0.3.3",
+ "zeroize",
+]
+
+[[package]]
+name = "ark-ff"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec847af850f44ad29048935519032c33da8aa03340876d351dfab5660d2966ba"
+dependencies = [
+ "ark-ff-asm 0.4.2",
+ "ark-ff-macros 0.4.2",
+ "ark-serialize 0.4.2",
+ "ark-std 0.4.0",
+ "derivative",
+ "digest 0.10.7",
+ "itertools 0.10.5",
+ "num-bigint",
+ "num-traits",
+ "paste",
+ "rustc_version 0.4.1",
+ "zeroize",
+]
+
+[[package]]
+name = "ark-ff"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a177aba0ed1e0fbb62aa9f6d0502e9b46dad8c2eab04c14258a1212d2557ea70"
+dependencies = [
+ "ark-ff-asm 0.5.0",
+ "ark-ff-macros 0.5.0",
+ "ark-serialize 0.5.0",
+ "ark-std 0.5.0",
+ "arrayvec",
+ "digest 0.10.7",
+ "educe",
+ "itertools 0.13.0",
+ "num-bigint",
+ "num-traits",
+ "paste",
+ "zeroize",
+]
+
+[[package]]
+name = "ark-ff-asm"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db02d390bf6643fb404d3d22d31aee1c4bc4459600aef9113833d17e786c6e44"
+dependencies = [
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "ark-ff-asm"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ed4aa4fe255d0bc6d79373f7e31d2ea147bcf486cba1be5ba7ea85abdb92348"
+dependencies = [
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "ark-ff-asm"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62945a2f7e6de02a31fe400aa489f0e0f5b2502e69f95f853adb82a96c7a6b60"
+dependencies = [
+ "quote",
+ "syn 2.0.111",
+]
+
+[[package]]
+name = "ark-ff-macros"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db2fd794a08ccb318058009eefdf15bcaaaaf6f8161eb3345f907222bac38b20"
+dependencies = [
+ "num-bigint",
+ "num-traits",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "ark-ff-macros"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7abe79b0e4288889c4574159ab790824d0033b9fdcb2a112a3182fac2e514565"
+dependencies = [
+ "num-bigint",
+ "num-traits",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "ark-ff-macros"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09be120733ee33f7693ceaa202ca41accd5653b779563608f1234f78ae07c4b3"
+dependencies = [
+ "num-bigint",
+ "num-traits",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.111",
+]
+
+[[package]]
+name = "ark-serialize"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d6c2b318ee6e10f8c2853e73a83adc0ccb88995aa978d8a3408d492ab2ee671"
+dependencies = [
+ "ark-std 0.3.0",
+ "digest 0.9.0",
+]
+
+[[package]]
+name = "ark-serialize"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5"
+dependencies = [
+ "ark-std 0.4.0",
+ "digest 0.10.7",
+ "num-bigint",
+]
+
+[[package]]
+name = "ark-serialize"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f4d068aaf107ebcd7dfb52bc748f8030e0fc930ac8e360146ca54c1203088f7"
+dependencies = [
+ "ark-std 0.5.0",
+ "arrayvec",
+ "digest 0.10.7",
+ "num-bigint",
+]
+
+[[package]]
+name = "ark-std"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1df2c09229cbc5a028b1d70e00fdb2acee28b1055dfb5ca73eea49c5a25c4e7c"
+dependencies = [
+ "num-traits",
+ "rand 0.8.5",
+]
+
+[[package]]
+name = "ark-std"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185"
+dependencies = [
+ "num-traits",
+ "rand 0.8.5",
+]
+
+[[package]]
+name = "ark-std"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "246a225cc6131e9ee4f24619af0f19d67761fff15d7ccc22e42b80846e69449a"
+dependencies = [
+ "num-traits",
+ "rand 0.8.5",
+]
+
+[[package]]
+name = "arrayvec"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "async-trait"
+version = "0.1.89"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.111",
+]
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
+name = "auto_impl"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.111",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+
+[[package]]
+name = "axum"
+version = "0.7.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"
+dependencies = [
+ "async-trait",
+ "axum-core",
+ "axum-macros",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "itoa",
+ "matchit",
+ "memchr",
+ "mime",
+ "percent-encoding",
+ "pin-project-lite",
+ "rustversion",
+ "serde",
+ "serde_json",
+ "serde_path_to_error",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "tokio",
+ "tower",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "axum-core"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199"
+dependencies = [
+ "async-trait",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "mime",
+ "pin-project-lite",
+ "rustversion",
+ "sync_wrapper",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "axum-macros"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.111",
+]
+
+[[package]]
+name = "base16ct"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
+
+[[package]]
+name = "base64ct"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba"
+
+[[package]]
+name = "bit-set"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
+dependencies = [
+ "bit-vec",
+]
+
+[[package]]
+name = "bit-vec"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
+
+[[package]]
+name = "bitflags"
+version = "2.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
+
+[[package]]
+name = "bitvec"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c"
+dependencies = [
+ "funty",
+ "radium",
+ "tap",
+ "wyz",
+]
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "byte-slice-cast"
+version = "1.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d"
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "bytes"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "cc"
+version = "1.2.48"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c481bdbf0ed3b892f6f806287d72acd515b352a4ec27a208489b8c1bc839633a"
+dependencies = [
+ "find-msvc-tools",
+ "shlex",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "clap"
+version = "4.5.53"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.5.53"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.5.49"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.111",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
+
+[[package]]
+name = "const-hex"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3bb320cac8a0750d7f25280aa97b09c26edfe161164238ecbbb31092b079e735"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "proptest",
+ "serde_core",
+]
+
+[[package]]
+name = "const-oid"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
+
+[[package]]
+name = "const_format"
+version = "0.2.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad"
+dependencies = [
+ "const_format_proc_macros",
+]
+
+[[package]]
+name = "const_format_proc_macros"
+version = "0.2.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-xid",
+]
+
+[[package]]
+name = "convert_case"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9"
+dependencies = [
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
+dependencies = [
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
+[[package]]
+name = "crunchy"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
+
+[[package]]
+name = "crypto-bigint"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
+dependencies = [
+ "generic-array",
+ "rand_core 0.6.4",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "der"
+version = "0.7.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
+dependencies = [
+ "const-oid",
+ "zeroize",
+]
+
+[[package]]
+name = "derivative"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "derive_arbitrary"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.111",
+]
+
+[[package]]
+name = "derive_more"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10b768e943bed7bf2cab53df09f4bc34bfd217cdb57d971e769874c9a6710618"
+dependencies = [
+ "derive_more-impl",
+]
+
+[[package]]
+name = "derive_more-impl"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d286bfdaf75e988b4a78e013ecd79c581e06399ab53fbacd2d916c2f904f30b"
+dependencies = [
+ "convert_case",
+ "proc-macro2",
+ "quote",
+ "rustc_version 0.4.1",
+ "syn 2.0.111",
+ "unicode-xid",
+]
+
+[[package]]
+name = "digest"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "const-oid",
+ "crypto-common",
+ "subtle",
+]
+
+[[package]]
+name = "ecdsa"
+version = "0.16.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca"
+dependencies = [
+ "der",
+ "digest 0.10.7",
+ "elliptic-curve",
+ "rfc6979",
+ "signature",
+ "spki",
+]
+
+[[package]]
+name = "educe"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417"
+dependencies = [
+ "enum-ordinalize",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.111",
+]
+
+[[package]]
+name = "either"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
+
+[[package]]
+name = "elliptic-curve"
+version = "0.13.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
+dependencies = [
+ "base16ct",
+ "crypto-bigint",
+ "digest 0.10.7",
+ "ff",
+ "generic-array",
+ "group",
+ "pkcs8",
+ "rand_core 0.6.4",
+ "sec1",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "enum-ordinalize"
+version = "4.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a1091a7bb1f8f2c4b28f1fe2cef4980ca2d410a3d727d67ecc3178c9b0800f0"
+dependencies = [
+ "enum-ordinalize-derive",
+]
+
+[[package]]
+name = "enum-ordinalize-derive"
+version = "4.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.111",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
+
+[[package]]
+name = "fastrlp"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "139834ddba373bbdd213dffe02c8d110508dcf1726c2be27e8d1f7d7e1856418"
+dependencies = [
+ "arrayvec",
+ "auto_impl",
+ "bytes",
+]
+
+[[package]]
+name = "fastrlp"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce8dba4714ef14b8274c371879b175aa55b16b30f269663f19d576f380018dc4"
+dependencies = [
+ "arrayvec",
+ "auto_impl",
+ "bytes",
+]
+
+[[package]]
+name = "ff"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393"
+dependencies = [
+ "rand_core 0.6.4",
+ "subtle",
+]
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844"
+
+[[package]]
+name = "fixed-hash"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534"
+dependencies = [
+ "byteorder",
+ "rand 0.8.5",
+ "rustc-hex",
+ "static_assertions",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foldhash"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "funty"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
+
+[[package]]
+name = "futures-channel"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
+
+[[package]]
+name = "futures-sink"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
+
+[[package]]
+name = "futures-task"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
+
+[[package]]
+name = "futures-util"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "pin-project-lite",
+ "pin-utils",
+]
+
+[[package]]
+name = "fxhash"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
+dependencies = [
+ "byteorder",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2"
+dependencies = [
+ "typenum",
+ "version_check",
+ "zeroize",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi",
+ "wasip2",
+]
+
+[[package]]
+name = "group"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
+dependencies = [
+ "ff",
+ "rand_core 0.6.4",
+ "subtle",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
+dependencies = [
+ "foldhash",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "hmac"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
+dependencies = [
+ "digest 0.10.7",
+]
+
+[[package]]
+name = "http"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
+dependencies = [
+ "bytes",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
+dependencies = [
+ "bytes",
+ "http",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "http-range-header"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c"
+
+[[package]]
+name = "httparse"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+
+[[package]]
+name = "httpdate"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
+
+[[package]]
+name = "hyper"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "http",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "pin-utils",
+ "smallvec",
+ "tokio",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "hyper",
+ "pin-project-lite",
+ "tokio",
+ "tower-service",
+]
+
+[[package]]
+name = "impl-codec"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba6a270039626615617f3f36d15fc827041df3b78c439da2cadfa47455a77f2f"
+dependencies = [
+ "parity-scale-codec",
+]
+
+[[package]]
+name = "impl-trait-for-tuples"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.111",
+]
+
+[[package]]
+name = "include_dir"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd"
+dependencies = [
+ "include_dir_macros",
+]
+
+[[package]]
+name = "include_dir_macros"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75"
+dependencies = [
+ "proc-macro2",
+ "quote",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2"
+dependencies = [
+ "arbitrary",
+ "equivalent",
+ "hashbrown",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
+
+[[package]]
+name = "itertools"
+version = "0.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itertools"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
+
+[[package]]
+name = "k256"
+version = "0.13.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b"
+dependencies = [
+ "cfg-if",
+ "ecdsa",
+ "elliptic-curve",
+ "once_cell",
+ "sha2",
+]
+
+[[package]]
+name = "keccak"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654"
+dependencies = [
+ "cpufeatures",
+]
+
+[[package]]
+name = "keccak-asm"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "505d1856a39b200489082f90d897c3f07c455563880bc5952e38eabf731c83b6"
+dependencies = [
+ "digest 0.10.7",
+ "sha3-asm",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.178"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091"
+
+[[package]]
+name = "libm"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
+
+[[package]]
+name = "lock_api"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
+dependencies = [
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
+
+[[package]]
+name = "matchit"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
+
+[[package]]
+name = "memchr"
+version = "2.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
+
+[[package]]
+name = "memmap2"
+version = "0.9.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "metrics"
+version = "0.24.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d5312e9ba3771cfa961b585728215e3d972c950a3eed9252aa093d6301277e8"
+dependencies = [
+ "ahash",
+ "portable-atomic",
+]
+
+[[package]]
+name = "metrics-derive"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3dbdd96ed57d565ec744cba02862d707acf373c5772d152abae6ec5c4e24f6c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "regex",
+ "syn 2.0.111",
+]
+
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[package]]
+name = "mime_guess"
+version = "2.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
+dependencies = [
+ "mime",
+ "unicase",
+]
+
+[[package]]
+name = "mio"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873"
+dependencies = [
+ "libc",
+ "wasi",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "num-bigint"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+ "libm",
+]
+
+[[package]]
+name = "nybbles"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8983bb634df7248924ee0c4c3a749609b5abcb082c28fffe3254b3eb3602b307"
+dependencies = [
+ "alloy-rlp",
+ "arbitrary",
+ "const-hex",
+ "proptest",
+ "serde",
+ "smallvec",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+
+[[package]]
+name = "once_cell_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
+
+[[package]]
+name = "parity-scale-codec"
+version = "3.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "799781ae679d79a948e13d4824a40970bfa500058d245760dd857301059810fa"
+dependencies = [
+ "arrayvec",
+ "bitvec",
+ "byte-slice-cast",
+ "const_format",
+ "impl-trait-for-tuples",
+ "parity-scale-codec-derive",
+ "rustversion",
+ "serde",
+]
+
+[[package]]
+name = "parity-scale-codec-derive"
+version = "3.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34b4653168b563151153c9e4c08ebed57fb8262bebfa79711552fa983c623e7a"
+dependencies = [
+ "proc-macro-crate",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.111",
+]
+
+[[package]]
+name = "parking_lot"
+version = "0.12.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-link",
+]
+
+[[package]]
+name = "paste"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
+
+[[package]]
+name = "pest"
+version = "2.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cbcfd20a6d4eeba40179f05735784ad32bdaef05ce8e8af05f180d45bb3e7e22"
+dependencies = [
+ "memchr",
+ "ucd-trie",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "pkcs8"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
+dependencies = [
+ "der",
+ "spki",
+]
+
+[[package]]
+name = "portable-atomic"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
+dependencies = [
+ "zerocopy",
+]
+
+[[package]]
+name = "primitive-types"
+version = "0.12.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b34d9fd68ae0b74a41b21c03c2f62847aa0ffea044eee893b4c140b37e244e2"
+dependencies = [
+ "fixed-hash",
+ "impl-codec",
+ "uint",
+]
+
+[[package]]
+name = "proc-macro-crate"
+version = "3.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983"
+dependencies = [
+ "toml_edit",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.103"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "proptest"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bee689443a2bd0a16ab0348b52ee43e3b2d1b1f931c8aa5c9f8de4c86fbe8c40"
+dependencies = [
+ "bit-set",
+ "bit-vec",
+ "bitflags",
+ "num-traits",
+ "rand 0.9.2",
+ "rand_chacha 0.9.0",
+ "rand_xorshift",
+ "regex-syntax",
+ "rusty-fork",
+ "tempfile",
+ "unarray",
+]
+
+[[package]]
+name = "proptest-derive"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ee1c9ac207483d5e7db4940700de86a9aae46ef90c48b57f99fe7edb8345e49"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.111",
+]
+
+[[package]]
+name = "proptest-derive"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "095a99f75c69734802359b682be8daaf8980296731f6470434ea2c652af1dd30"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.111",
+]
+
+[[package]]
+name = "quick-error"
+version = "1.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
+
+[[package]]
+name = "quote"
+version = "1.0.42"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+[[package]]
+name = "radium"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
+
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha 0.3.1",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
+dependencies = [
+ "rand_chacha 0.9.0",
+ "rand_core 0.9.3",
+ "serde",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.9.3",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom 0.2.16",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
+dependencies = [
+ "getrandom 0.3.4",
+ "serde",
+]
+
+[[package]]
+name = "rand_xorshift"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a"
+dependencies = [
+ "rand_core 0.9.3",
+]
+
+[[package]]
+name = "rayon"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f"
+dependencies = [
+ "either",
+ "rayon-core",
+]
+
+[[package]]
+name = "rayon-core"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
+dependencies = [
+ "crossbeam-deque",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "regex"
+version = "1.12.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
+
+[[package]]
+name = "rfc6979"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2"
+dependencies = [
+ "hmac",
+ "subtle",
+]
+
+[[package]]
+name = "rlp"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb919243f34364b6bd2fc10ef797edbfa75f33c252e7998527479c6d6b47e1ec"
+dependencies = [
+ "bytes",
+ "rustc-hex",
+]
+
+[[package]]
+name = "ruint"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a68df0380e5c9d20ce49534f292a36a7514ae21350726efe1865bdb1fa91d278"
+dependencies = [
+ "alloy-rlp",
+ "arbitrary",
+ "ark-ff 0.3.0",
+ "ark-ff 0.4.2",
+ "ark-ff 0.5.0",
+ "bytes",
+ "fastrlp 0.3.1",
+ "fastrlp 0.4.0",
+ "num-bigint",
+ "num-integer",
+ "num-traits",
+ "parity-scale-codec",
+ "primitive-types",
+ "proptest",
+ "rand 0.8.5",
+ "rand 0.9.2",
+ "rlp",
+ "ruint-macro",
+ "serde_core",
+ "valuable",
+ "zeroize",
+]
+
+[[package]]
+name = "ruint-macro"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18"
+
+[[package]]
+name = "rustc-hash"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
+dependencies = [
+ "rand 0.8.5",
+]
+
+[[package]]
+name = "rustc-hex"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6"
+
+[[package]]
+name = "rustc_version"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee"
+dependencies = [
+ "semver 0.11.0",
+]
+
+[[package]]
+name = "rustc_version"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
+dependencies = [
+ "semver 1.0.27",
+]
+
+[[package]]
+name = "rustix"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+
+[[package]]
+name = "rusty-fork"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2"
+dependencies = [
+ "fnv",
+ "quick-error",
+ "tempfile",
+ "wait-timeout",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "sealed"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22f968c5ea23d555e670b449c1c5e7b2fc399fdaec1d304a17cd48e288abc107"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.111",
+]
+
+[[package]]
+name = "sec1"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
+dependencies = [
+ "base16ct",
+ "der",
+ "generic-array",
+ "pkcs8",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "semver"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6"
+dependencies = [
+ "semver-parser",
+]
+
+[[package]]
+name = "semver"
+version = "1.0.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
+
+[[package]]
+name = "semver-parser"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9900206b54a3527fdc7b8a938bffd94a568bac4f4aa8113b209df75a09c0dec2"
+dependencies = [
+ "pest",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.111",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.145"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
+dependencies = [
+ "itoa",
+ "memchr",
+ "ryu",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "serde_path_to_error"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
+dependencies = [
+ "itoa",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "sha2"
+version = "0.10.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest 0.10.7",
+]
+
+[[package]]
+name = "sha3"
+version = "0.10.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60"
+dependencies = [
+ "digest 0.10.7",
+ "keccak",
+]
+
+[[package]]
+name = "sha3-asm"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c28efc5e327c837aa837c59eae585fc250715ef939ac32881bcc11677cd02d46"
+dependencies = [
+ "cc",
+ "cfg-if",
+]
+
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "signature"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
+dependencies = [
+ "digest 0.10.7",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
+dependencies = [
+ "arbitrary",
+ "serde",
+]
+
+[[package]]
+name = "socket2"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881"
+dependencies = [
+ "libc",
+ "windows-sys 0.60.2",
+]
+
+[[package]]
+name = "spki"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
+dependencies = [
+ "base64ct",
+ "der",
+]
+
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.111"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "sync_wrapper"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
+
+[[package]]
+name = "tap"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
+
+[[package]]
+name = "tempfile"
+version = "3.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16"
+dependencies = [
+ "fastrand",
+ "getrandom 0.3.4",
+ "once_cell",
+ "rustix",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "tiny-keccak"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
+dependencies = [
+ "crunchy",
+]
+
+[[package]]
+name = "tokio"
+version = "1.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408"
+dependencies = [
+ "bytes",
+ "libc",
+ "mio",
+ "parking_lot",
+ "pin-project-lite",
+ "signal-hook-registry",
+ "socket2",
+ "tokio-macros",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.111",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.23.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d"
+dependencies = [
+ "indexmap",
+ "toml_datetime",
+ "toml_parser",
+ "winnow",
+]
+
+[[package]]
+name = "toml_parser"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e"
+dependencies = [
+ "winnow",
+]
+
+[[package]]
+name = "tower"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "pin-project-lite",
+ "sync_wrapper",
+ "tokio",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tower-http"
+version = "0.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9cf146f99d442e8e68e585f5d798ccd3cad9a7835b917e09728880a862706456"
+dependencies = [
+ "bitflags",
+ "bytes",
+ "futures-core",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "http-range-header",
+ "httpdate",
+ "mime",
+ "mime_guess",
+ "percent-encoding",
+ "pin-project-lite",
+ "tokio",
+ "tokio-util",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
+
+[[package]]
+name = "tower-service"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
+
+[[package]]
+name = "tracing"
+version = "0.1.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647"
+dependencies = [
+ "log",
+ "pin-project-lite",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "triedb"
+version = "0.1.0"
+dependencies = [
+ "alloy-primitives",
+ "alloy-rlp",
+ "alloy-trie",
+ "arrayvec",
+ "fxhash",
+ "memmap2",
+ "metrics",
+ "metrics-derive",
+ "parking_lot",
+ "proptest",
+ "proptest-derive 0.6.0",
+ "rayon",
+ "sealed",
+ "serde",
+ "static_assertions",
+ "zerocopy",
+]
+
+[[package]]
+name = "triedb-explorer"
+version = "0.1.0"
+dependencies = [
+ "alloy-primitives",
+ "alloy-trie",
+ "axum",
+ "clap",
+ "include_dir",
+ "mime_guess",
+ "parking_lot",
+ "serde",
+ "serde_json",
+ "tokio",
+ "tower",
+ "tower-http",
+ "triedb",
+]
+
+[[package]]
+name = "typenum"
+version = "1.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
+
+[[package]]
+name = "ucd-trie"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
+
+[[package]]
+name = "uint"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52"
+dependencies = [
+ "byteorder",
+ "crunchy",
+ "hex",
+ "static_assertions",
+]
+
+[[package]]
+name = "unarray"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94"
+
+[[package]]
+name = "unicase"
+version = "2.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "valuable"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "wait-timeout"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "wasip2"
+version = "1.0.1+wasi-0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
+dependencies = [
+ "wit-bindgen",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-sys"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.53.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
+dependencies = [
+ "windows-link",
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
+
+[[package]]
+name = "winnow"
+version = "0.7.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "wit-bindgen"
+version = "0.46.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
+
+[[package]]
+name = "wyz"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
+dependencies = [
+ "tap",
+]
+
+[[package]]
+name = "zerocopy"
+version = "0.8.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.111",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
+dependencies = [
+ "zeroize_derive",
+]
+
+[[package]]
+name = "zeroize_derive"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.111",
+]
diff --git a/explorer/Cargo.toml b/explorer/Cargo.toml
new file mode 100644
index 00000000..8e54b350
--- /dev/null
+++ b/explorer/Cargo.toml
@@ -0,0 +1,23 @@
+[package]
+name = "triedb-explorer"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+triedb = { path = ".." }
+axum = { version = "0.7", features = ["macros"] }
+tokio = { version = "1", features = ["full"] }
+tower = "0.5"
+tower-http = { version = "0.6", features = ["fs", "cors"] }
+serde = { version = "1", features = ["derive"] }
+serde_json = "1"
+clap = { version = "4", features = ["derive"] }
+alloy-primitives = { version = "1.2.1", features = ["serde"] }
+alloy-trie = { version = "0.8.1", features = ["serde"] }
+parking_lot = "0.12"
+include_dir = "0.7"
+mime_guess = "2"
+
+[[bin]]
+name = "triedb-explorer"
+path = "src/main.rs"
diff --git a/explorer/frontend/index.html b/explorer/frontend/index.html
new file mode 100644
index 00000000..1117fd5e
--- /dev/null
+++ b/explorer/frontend/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ TrieDB Explorer
+
+
+
+
+
+
diff --git a/explorer/frontend/package-lock.json b/explorer/frontend/package-lock.json
new file mode 100644
index 00000000..e8fdf7da
--- /dev/null
+++ b/explorer/frontend/package-lock.json
@@ -0,0 +1,1681 @@
+{
+ "name": "triedb-explorer-frontend",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "triedb-explorer-frontend",
+ "version": "0.1.0",
+ "dependencies": {
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0"
+ },
+ "devDependencies": {
+ "@types/react": "^18.2.0",
+ "@types/react-dom": "^18.2.0",
+ "@vitejs/plugin-react": "^4.2.0",
+ "typescript": "^5.2.0",
+ "vite": "^5.0.0"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
+ "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz",
+ "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
+ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/generator": "^7.28.5",
+ "@babel/helper-compilation-targets": "^7.27.2",
+ "@babel/helper-module-transforms": "^7.28.3",
+ "@babel/helpers": "^7.28.4",
+ "@babel/parser": "^7.28.5",
+ "@babel/template": "^7.27.2",
+ "@babel/traverse": "^7.28.5",
+ "@babel/types": "^7.28.5",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz",
+ "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.28.5",
+ "@babel/types": "^7.28.5",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
+ "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.27.2",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
+ "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.27.1",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.28.3",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz",
+ "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "@babel/traverse": "^7.28.3"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz",
+ "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz",
+ "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.28.4"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz",
+ "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.5"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
+ "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
+ "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
+ "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/parser": "^7.27.2",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz",
+ "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/generator": "^7.28.5",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.28.5",
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.28.5",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz",
+ "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+ "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+ "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+ "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+ "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+ "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+ "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+ "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+ "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+ "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+ "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+ "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+ "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+ "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+ "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+ "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+ "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+ "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+ "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+ "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-beta.27",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
+ "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz",
+ "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz",
+ "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz",
+ "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz",
+ "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz",
+ "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz",
+ "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz",
+ "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz",
+ "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz",
+ "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz",
+ "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz",
+ "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz",
+ "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz",
+ "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz",
+ "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz",
+ "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz",
+ "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz",
+ "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz",
+ "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz",
+ "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz",
+ "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz",
+ "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz",
+ "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.2"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/prop-types": {
+ "version": "15.7.15",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
+ "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/react": {
+ "version": "18.3.27",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz",
+ "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/prop-types": "*",
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "18.3.7",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
+ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^18.0.0"
+ }
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
+ "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.28.0",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-beta.27",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.17.0"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
+ }
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.0.tgz",
+ "integrity": "sha512-Mh++g+2LPfzZToywfE1BUzvZbfOY52Nil0rn9H1CPC5DJ7fX+Vir7nToBeoiSbB1zTNeGYbELEvJESujgGrzXw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.js"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
+ "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.9.0",
+ "caniuse-lite": "^1.0.30001759",
+ "electron-to-chromium": "^1.5.263",
+ "node-releases": "^2.0.27",
+ "update-browserslist-db": "^1.2.0"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001759",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001759.tgz",
+ "integrity": "sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.263",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.263.tgz",
+ "integrity": "sha512-DrqJ11Knd+lo+dv+lltvfMDLU27g14LMdH2b0O3Pio4uk0x+z7OR+JrmyacTPN2M8w3BrZ7/RTwG3R9B7irPlg==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/esbuild": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
+ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.21.5",
+ "@esbuild/android-arm": "0.21.5",
+ "@esbuild/android-arm64": "0.21.5",
+ "@esbuild/android-x64": "0.21.5",
+ "@esbuild/darwin-arm64": "0.21.5",
+ "@esbuild/darwin-x64": "0.21.5",
+ "@esbuild/freebsd-arm64": "0.21.5",
+ "@esbuild/freebsd-x64": "0.21.5",
+ "@esbuild/linux-arm": "0.21.5",
+ "@esbuild/linux-arm64": "0.21.5",
+ "@esbuild/linux-ia32": "0.21.5",
+ "@esbuild/linux-loong64": "0.21.5",
+ "@esbuild/linux-mips64el": "0.21.5",
+ "@esbuild/linux-ppc64": "0.21.5",
+ "@esbuild/linux-riscv64": "0.21.5",
+ "@esbuild/linux-s390x": "0.21.5",
+ "@esbuild/linux-x64": "0.21.5",
+ "@esbuild/netbsd-x64": "0.21.5",
+ "@esbuild/openbsd-x64": "0.21.5",
+ "@esbuild/sunos-x64": "0.21.5",
+ "@esbuild/win32-arm64": "0.21.5",
+ "@esbuild/win32-ia32": "0.21.5",
+ "@esbuild/win32-x64": "0.21.5"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "license": "MIT"
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.27",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
+ "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/postcss": {
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/react": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
+ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.2"
+ },
+ "peerDependencies": {
+ "react": "^18.3.1"
+ }
+ },
+ "node_modules/react-refresh": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
+ "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz",
+ "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.53.3",
+ "@rollup/rollup-android-arm64": "4.53.3",
+ "@rollup/rollup-darwin-arm64": "4.53.3",
+ "@rollup/rollup-darwin-x64": "4.53.3",
+ "@rollup/rollup-freebsd-arm64": "4.53.3",
+ "@rollup/rollup-freebsd-x64": "4.53.3",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.53.3",
+ "@rollup/rollup-linux-arm-musleabihf": "4.53.3",
+ "@rollup/rollup-linux-arm64-gnu": "4.53.3",
+ "@rollup/rollup-linux-arm64-musl": "4.53.3",
+ "@rollup/rollup-linux-loong64-gnu": "4.53.3",
+ "@rollup/rollup-linux-ppc64-gnu": "4.53.3",
+ "@rollup/rollup-linux-riscv64-gnu": "4.53.3",
+ "@rollup/rollup-linux-riscv64-musl": "4.53.3",
+ "@rollup/rollup-linux-s390x-gnu": "4.53.3",
+ "@rollup/rollup-linux-x64-gnu": "4.53.3",
+ "@rollup/rollup-linux-x64-musl": "4.53.3",
+ "@rollup/rollup-openharmony-arm64": "4.53.3",
+ "@rollup/rollup-win32-arm64-msvc": "4.53.3",
+ "@rollup/rollup-win32-ia32-msvc": "4.53.3",
+ "@rollup/rollup-win32-x64-gnu": "4.53.3",
+ "@rollup/rollup-win32-x64-msvc": "4.53.3",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.23.2",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.0.tgz",
+ "integrity": "sha512-Dn+NlSF/7+0lVSEZ57SYQg6/E44arLzsVOGgrElBn/BlG1B8WKdbLppOocFrXwRNTkNlgdGNaBgH1o0lggDPiw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/vite": {
+ "version": "5.4.21",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
+ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.21.3",
+ "postcss": "^8.4.43",
+ "rollup": "^4.20.0"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || >=20.0.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ }
+ }
+}
diff --git a/explorer/frontend/package.json b/explorer/frontend/package.json
new file mode 100644
index 00000000..aed851a5
--- /dev/null
+++ b/explorer/frontend/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "triedb-explorer-frontend",
+ "private": true,
+ "version": "0.1.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0"
+ },
+ "devDependencies": {
+ "@types/react": "^18.2.0",
+ "@types/react-dom": "^18.2.0",
+ "@vitejs/plugin-react": "^4.2.0",
+ "typescript": "^5.2.0",
+ "vite": "^5.0.0"
+ }
+}
diff --git a/explorer/frontend/src/App.tsx b/explorer/frontend/src/App.tsx
new file mode 100644
index 00000000..c3ca9a9d
--- /dev/null
+++ b/explorer/frontend/src/App.tsx
@@ -0,0 +1,131 @@
+import { useEffect, useState } from 'react';
+import { useTrieData } from './hooks/useTrieData';
+import { PageContainer } from './components/PageContainer';
+import { SearchBar } from './components/SearchBar';
+import { MetadataModal } from './components/MetadataModal';
+import type { PathSearchResult } from './types';
+import './styles/index.css';
+
+export function App() {
+ const {
+ state,
+ loadRootPage,
+ loadPage,
+ toggleNode,
+ isPageLoaded,
+ getPageBasePath,
+ loadSearchPath,
+ toggleAutoExpand,
+ toggleShowEmptySlots,
+ toggleShowHashes,
+ expandAllOnPage,
+ collapseAllOnPage,
+ } = useTrieData();
+
+ const [isMetadataOpen, setIsMetadataOpen] = useState(false);
+ const [searchResult, setSearchResult] = useState(null);
+
+ useEffect(() => {
+ loadRootPage();
+ }, [loadRootPage]);
+
+ const handleNavigate = (pageId: number, basePath?: string, isChildPage: boolean = true) => {
+ if (!isPageLoaded(pageId)) {
+ loadPage(pageId, basePath, isChildPage);
+ }
+ };
+
+ const handleSearchResult = async (result: PathSearchResult | null) => {
+ setSearchResult(result);
+ if (result) {
+ // Load pages along the search path with proper hierarchy
+ await loadSearchPath(result);
+ }
+ };
+
+ // Sort pages by ID for consistent display, filtering out child pages
+ const sortedPages = Array.from(state.loadedPages.entries())
+ .filter(([pageId]) => !state.childPages.has(pageId))
+ .sort(([a], [b]) => a - b);
+
+ return (
+
+
+
+
+ {state.isLoading && Loading...
}
+ {state.error && {state.error}
}
+
+ {searchResult && (
+
+ Found: {searchResult.node.nodeType} at Page {searchResult.node.pageId},
+ Cell {searchResult.node.cellIndex}
+
+ )}
+
+
+ {sortedPages.map(([pageId, pageData]) => (
+
+ ))}
+
+
+ {!state.isLoading && sortedPages.length === 0 && !state.error && (
+
+ No data loaded. The database may be empty.
+
+ )}
+
+
+
setIsMetadataOpen(false)}
+ />
+
+ );
+}
diff --git a/explorer/frontend/src/api/client.ts b/explorer/frontend/src/api/client.ts
new file mode 100644
index 00000000..660c383f
--- /dev/null
+++ b/explorer/frontend/src/api/client.ts
@@ -0,0 +1,58 @@
+import type {
+ ApiResponse,
+ DatabaseInfo,
+ DescendantsResponse,
+ ExplorerNode,
+ PageInfo,
+ PageResponse,
+ PathSearchResult,
+ RootResponse,
+} from '../types';
+
+const API_BASE = '/api';
+
+async function fetchApi(url: string): Promise> {
+ const response = await fetch(`${API_BASE}${url}`);
+ return response.json();
+}
+
+export async function getRoot(): Promise> {
+ return fetchApi('/root');
+}
+
+export async function getPage(pageId: number): Promise> {
+ return fetchApi(`/pages/${pageId}`);
+}
+
+export async function getPageInfo(pageId: number): Promise> {
+ return fetchApi(`/pages/${pageId}/info`);
+}
+
+export async function getDescendants(pageId: number): Promise> {
+ return fetchApi(`/pages/${pageId}/descendants`);
+}
+
+export async function getNodeDescendants(
+ pageId: number,
+ cellIndex: number
+): Promise> {
+ return fetchApi(`/pages/${pageId}/nodes/${cellIndex}/descendants`);
+}
+
+export async function getNode(
+ pageId: number,
+ cellIndex: number
+): Promise> {
+ return fetchApi(`/nodes/${pageId}/${cellIndex}`);
+}
+
+export async function search(
+ query: string,
+ type: 'raw' | 'address' | 'storage' = 'raw'
+): Promise> {
+ return fetchApi(`/search?q=${encodeURIComponent(query)}&type=${type}`);
+}
+
+export async function getMetadata(): Promise> {
+ return fetchApi('/metadata');
+}
diff --git a/explorer/frontend/src/components/MetadataModal.tsx b/explorer/frontend/src/components/MetadataModal.tsx
new file mode 100644
index 00000000..350dec92
--- /dev/null
+++ b/explorer/frontend/src/components/MetadataModal.tsx
@@ -0,0 +1,89 @@
+import { useState, useEffect } from 'react';
+import type { DatabaseInfo } from '../types';
+import * as api from '../api/client';
+
+interface MetadataModalProps {
+ isOpen: boolean;
+ onClose: () => void;
+}
+
+export function MetadataModal({ isOpen, onClose }: MetadataModalProps) {
+ const [metadata, setMetadata] = useState(null);
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ if (isOpen && !metadata) {
+ loadMetadata();
+ }
+ }, [isOpen]);
+
+ const loadMetadata = async () => {
+ setIsLoading(true);
+ setError(null);
+ try {
+ const response = await api.getMetadata();
+ if (response.success && response.data) {
+ setMetadata(response.data);
+ } else {
+ setError(response.error || 'Failed to load metadata');
+ }
+ } catch (e) {
+ setError(e instanceof Error ? e.message : 'Unknown error');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ if (!isOpen) return null;
+
+ return (
+
+
e.stopPropagation()}>
+
+
Database Metadata
+
+
+
+ {isLoading &&
Loading metadata...
}
+ {error &&
{error}
}
+ {metadata && (
+ <>
+
+
Database Info
+
+ - Snapshot ID
+ - {metadata.snapshotId}
+ - State Root
+ - 0x{metadata.rootNodeHash}
+ - Root Page ID
+ - {metadata.rootNodePageId ?? 'None'}
+ - Total Pages
+ - {metadata.totalPageCount}
+
+
+
+
+
Orphaned Pages ({metadata.orphanedPages.length})
+ {metadata.orphanedPages.length > 0 ? (
+
+ {metadata.orphanedPages.map((orphan) => (
+
+ Page {orphan.pageId}
+ Orphaned at snapshot {orphan.orphanedAtSnapshot}
+
+ ))}
+
+ ) : (
+
No orphaned pages
+ )}
+
+ >
+ )}
+
+
+
+ );
+}
diff --git a/explorer/frontend/src/components/NodeComponent.tsx b/explorer/frontend/src/components/NodeComponent.tsx
new file mode 100644
index 00000000..9c0f6c91
--- /dev/null
+++ b/explorer/frontend/src/components/NodeComponent.tsx
@@ -0,0 +1,157 @@
+import type { ExplorerNode, ChildEntry } from '../types';
+
+interface NodeComponentProps {
+ node: ExplorerNode;
+ basePath: string;
+ isExpanded: boolean;
+ onToggle: () => void;
+ onNavigate: (pageId: number, basePath?: string) => void;
+}
+
+export function NodeComponent({ node, basePath, isExpanded, onToggle, onNavigate }: NodeComponentProps) {
+ const nodeTypeClass = node.nodeType.toLowerCase().replace('leaf', '-leaf');
+ const fullPath = basePath + (node.prefix || '');
+
+ return (
+
+
+ ▶
+ {node.nodeType}
+
+ {fullPath ? (
+ <>
+ 0x{basePath && {basePath}}
+ {node.prefix && {node.prefix}}
+ >
+ ) : (
+ '(empty path)'
+ )}
+
+
+ Cell {node.cellIndex} | {node.sizeBytes} bytes
+
+
+
+ {isExpanded && (
+ <>
+
+
+ {node.nodeType === 'AccountLeaf' && (
+ <>
+ - Nonce
+ - {node.nonce}
+ - Balance
+ - {node.balance}
+ - Code Hash
+ - 0x{node.codeHash}
+ {node.storageRoot && (
+ <>
+ - Storage Root
+ -
+
+
+ >
+ )}
+ >
+ )}
+ {node.nodeType === 'StorageLeaf' && (
+ <>
+ - Value
+ - {node.value}
+ >
+ )}
+
+
+
+ {node.nodeType === 'Branch' && node.children && node.children.length > 0 && (
+
+
+ {node.children.map((child) => (
+
+ ))}
+
+
+ )}
+ >
+ )}
+
+ );
+}
+
+interface PointerLinkProps {
+ pointer: { locationType: string; pageId?: number; cellIndex?: number; rlpHash?: string };
+ onNavigate: (pageId: number, basePath?: string) => void;
+}
+
+function PointerLink({ pointer, onNavigate }: PointerLinkProps) {
+ if (pointer.locationType === 'SamePage' && pointer.cellIndex !== undefined) {
+ return (
+ {
+ e.stopPropagation();
+ // Scroll to cell on same page - for now just highlight
+ }}
+ >
+ Cell {pointer.cellIndex}
+
+ );
+ } else if (pointer.locationType === 'OtherPage' && pointer.pageId !== undefined) {
+ return (
+ {
+ e.stopPropagation();
+ // Storage root pointers start a new trie, so empty base path
+ onNavigate(pointer.pageId!, '');
+ }}
+ >
+ Page {pointer.pageId}
+
+ );
+ }
+ return Unknown;
+}
+
+interface ChildPointerProps {
+ child: ChildEntry;
+ fullPath: string;
+ onNavigate: (pageId: number, basePath?: string) => void;
+}
+
+function ChildPointer({ child, fullPath, onNavigate }: ChildPointerProps) {
+ const nibble = child.index.toString(16).toUpperCase();
+ const pointer = child.pointer;
+ // The child's base path is the parent's full path + this nibble
+ const childBasePath = fullPath + nibble;
+
+ if (pointer.locationType === 'SamePage' && pointer.cellIndex !== undefined) {
+ return (
+ {
+ // For same-page navigation, could scroll to node
+ }}
+ >
+ [{nibble}] Cell {pointer.cellIndex}
+
+ );
+ } else if (pointer.locationType === 'OtherPage' && pointer.pageId !== undefined) {
+ return (
+ onNavigate(pointer.pageId!, childBasePath)}
+ >
+ [{nibble}] Page {pointer.pageId}
+
+ );
+ }
+ return (
+
+ [{nibble}] Unknown
+
+ );
+}
diff --git a/explorer/frontend/src/components/PageContainer.tsx b/explorer/frontend/src/components/PageContainer.tsx
new file mode 100644
index 00000000..f1642d00
--- /dev/null
+++ b/explorer/frontend/src/components/PageContainer.tsx
@@ -0,0 +1,81 @@
+import { useMemo } from 'react';
+import type { ExplorerNode, PageInfo } from '../types';
+import { TreeNodeComponent } from './TreeNodeComponent';
+import { buildTree } from '../utils/treeBuilder';
+
+interface PageContainerProps {
+ pageInfo: PageInfo;
+ nodes: ExplorerNode[];
+ basePath: string;
+ expandedNodes: Set;
+ loadedPages: Map;
+ pagePaths: Map;
+ showEmptySlots: boolean;
+ showHashes: boolean;
+ onToggleNode: (pageId: number, cellIndex: number) => void;
+ onNavigate: (pageId: number, basePath?: string, isChildPage?: boolean) => void;
+ onExpandAllOnPage: (pageId: number) => void;
+ onCollapseAllOnPage: (pageId: number) => void;
+}
+
+function getFullnessColor(percent: number): string {
+ // HSL: 120=green, 60=yellow, 0=red
+ const hue = Math.max(0, 120 - percent * 1.2);
+ return `hsl(${hue}, 60%, 92%)`;
+}
+
+export function PageContainer({
+ pageInfo,
+ nodes,
+ basePath,
+ expandedNodes,
+ loadedPages,
+ pagePaths,
+ showEmptySlots,
+ showHashes,
+ onToggleNode,
+ onNavigate,
+ onExpandAllOnPage,
+ onCollapseAllOnPage,
+}: PageContainerProps) {
+ const bgColor = getFullnessColor(pageInfo.occupancyPercent);
+
+ // Build tree structure from flat node list
+ const treeRoots = useMemo(() => buildTree(nodes, basePath), [nodes, basePath]);
+
+ return (
+
+
+ Page {pageInfo.pageId}
+ {pageInfo.cellCount} nodes
+
+ {pageInfo.usedBytes} / {pageInfo.totalBytes} bytes
+
+ {pageInfo.occupancyPercent.toFixed(1)}% full
+
+
+
+
+
+
+ {treeRoots.map((treeNode) => (
+
+ ))}
+
+
+ );
+}
diff --git a/explorer/frontend/src/components/SearchBar.tsx b/explorer/frontend/src/components/SearchBar.tsx
new file mode 100644
index 00000000..95142039
--- /dev/null
+++ b/explorer/frontend/src/components/SearchBar.tsx
@@ -0,0 +1,64 @@
+import React, { useState } from 'react';
+import * as api from '../api/client';
+import type { PathSearchResult } from '../types';
+
+interface SearchBarProps {
+ onSearchResult: (result: PathSearchResult | null) => void;
+}
+
+export function SearchBar({ onSearchResult }: SearchBarProps) {
+ const [query, setQuery] = useState('');
+ const [searchType, setSearchType] = useState<'raw' | 'address' | 'storage'>('raw');
+ const [isSearching, setIsSearching] = useState(false);
+
+ const handleSearch = async () => {
+ if (!query.trim()) return;
+
+ setIsSearching(true);
+ try {
+ const response = await api.search(query, searchType);
+ if (response.success && response.data) {
+ onSearchResult(response.data);
+ } else {
+ onSearchResult(null);
+ }
+ } catch (e) {
+ console.error('Search failed:', e);
+ onSearchResult(null);
+ } finally {
+ setIsSearching(false);
+ }
+ };
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === 'Enter') {
+ handleSearch();
+ }
+ };
+
+ return (
+
+
+ setQuery(e.target.value)}
+ onKeyDown={handleKeyDown}
+ placeholder={
+ searchType === 'address'
+ ? '0x... (Ethereum address)'
+ : searchType === 'storage'
+ ? 'address:slot (e.g., 0x...:0x0)'
+ : '0x... (raw path, or 0xAccount:0xStorage)'
+ }
+ />
+
+
+ );
+}
diff --git a/explorer/frontend/src/components/TreeNodeComponent.tsx b/explorer/frontend/src/components/TreeNodeComponent.tsx
new file mode 100644
index 00000000..0686c7e3
--- /dev/null
+++ b/explorer/frontend/src/components/TreeNodeComponent.tsx
@@ -0,0 +1,493 @@
+import { useMemo, useState } from 'react';
+import type { ExplorerNode, PageInfo, TreeNode, ChildEntry } from '../types';
+import { buildTree } from '../utils/treeBuilder';
+
+function getFullnessColor(percent: number): string {
+ const hue = Math.max(0, 120 - percent * 1.2);
+ return `hsl(${hue}, 60%, 92%)`;
+}
+
+function copyToClipboard(text: string): Promise {
+ return navigator.clipboard.writeText(text);
+}
+
+interface TreeNodeComponentProps {
+ treeNode: TreeNode;
+ depth: number;
+ accountPath: string; // Path of parent account (for storage nodes)
+ expandedNodes: Set;
+ loadedPages: Map;
+ pagePaths: Map;
+ showEmptySlots: boolean;
+ showHashes: boolean;
+ onToggleNode: (pageId: number, cellIndex: number) => void;
+ onNavigate: (pageId: number, basePath?: string, isChildPage?: boolean) => void;
+ onExpandAllOnPage: (pageId: number) => void;
+ onCollapseAllOnPage: (pageId: number) => void;
+}
+
+export function TreeNodeComponent({
+ treeNode,
+ depth,
+ accountPath,
+ expandedNodes,
+ loadedPages,
+ pagePaths,
+ showEmptySlots,
+ showHashes,
+ onToggleNode,
+ onNavigate,
+ onExpandAllOnPage,
+ onCollapseAllOnPage,
+}: TreeNodeComponentProps) {
+ const { node, localChildren, basePath } = treeNode;
+ const fullPath = basePath + (node.prefix || '');
+ // For storage nodes, prepend the account path
+ const displayPath = accountPath ? `${accountPath}:${fullPath}` : fullPath;
+ const isExpanded = expandedNodes.has(`${node.pageId}:${node.cellIndex}`);
+ const nodeTypeClass = node.nodeType.toLowerCase().replace('leaf', '-leaf');
+ const [copied, setCopied] = useState(false);
+
+ const handleCopyPath = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ const pathToCopy = displayPath ? `0x${displayPath}` : '';
+ if (pathToCopy) {
+ copyToClipboard(pathToCopy).then(() => {
+ setCopied(true);
+ setTimeout(() => setCopied(false), 1500);
+ });
+ }
+ };
+
+ // Check if this node has any expandable content
+ const hasLocalChildren = localChildren.length > 0;
+ const hasRemoteChildren = node.nodeType === 'Branch' && node.children?.some(
+ c => c.pointer.locationType === 'OtherPage'
+ );
+ const hasStorageRoot = node.nodeType === 'AccountLeaf' && node.storageRoot?.locationType === 'OtherPage';
+ const hasExpandableContent = hasLocalChildren || hasRemoteChildren || hasStorageRoot ||
+ node.nodeType === 'AccountLeaf' || node.nodeType === 'StorageLeaf';
+
+ // Build sorted list of all children (local + remote) by index for branches
+ const sortedBranchChildren = useMemo(() => {
+ if (node.nodeType !== 'Branch' || !node.children) return [];
+
+ // Create a map of index -> child info
+ const childMap = new Map();
+
+ // Initialize all 16 slots as empty if showing empty slots
+ if (showEmptySlots) {
+ for (let i = 0; i < 16; i++) {
+ childMap.set(i, { type: 'empty' });
+ }
+ }
+
+ // Add remote children
+ for (const child of node.children) {
+ if (child.pointer.locationType === 'OtherPage') {
+ childMap.set(child.index, { type: 'remote', child });
+ }
+ }
+
+ // Add local children (override if exists)
+ for (const treeChild of localChildren) {
+ if (!treeChild.isStorageChild) {
+ // Find the nibble for this child from the parent's children array
+ const childEntry = node.children.find(c =>
+ c.pointer.locationType === 'SamePage' &&
+ c.pointer.cellIndex === treeChild.node.cellIndex
+ );
+ if (childEntry) {
+ childMap.set(childEntry.index, { type: 'local', child: childEntry, treeNode: treeChild });
+ }
+ }
+ }
+
+ // Sort by index and return
+ return Array.from(childMap.entries())
+ .sort((a, b) => a[0] - b[0])
+ .map(([index, data]) => ({ index, ...data }));
+ }, [node, localChildren, showEmptySlots]);
+
+ // Get storage children (local) separately
+ const storageChildren = localChildren.filter(c => c.isStorageChild);
+
+ return (
+
+
onToggleNode(node.pageId, node.cellIndex)}>
+ {hasExpandableContent ? (
+ ▶
+ ) : (
+
+ )}
+ {node.nodeType}
+
+ {displayPath ? (
+ <>
+ 0x{accountPath && {accountPath}:}
+ {basePath && {basePath}}
+ {node.prefix && {node.prefix}}
+ {copied && Copied!}
+ >
+ ) : (
+ '(root)'
+ )}
+
+
+ Cell {node.cellIndex} | {node.sizeBytes} bytes
+
+
+
+ {isExpanded && (
+
+ {/* Node details (account/storage leaf values) */}
+ {(node.nodeType === 'AccountLeaf' || node.nodeType === 'StorageLeaf') && (
+
+
+ {node.nodeType === 'AccountLeaf' && (
+ <>
+ - Nonce
+ - {node.nonce}
+ - Balance
+ - {node.balance}
+ - Code Hash
+ - 0x{node.codeHash}
+ >
+ )}
+ {node.nodeType === 'StorageLeaf' && (
+ <>
+ - Value
+ - {node.value}
+ >
+ )}
+
+
+ )}
+
+ {/* Branch children - sorted by index */}
+ {node.nodeType === 'Branch' && sortedBranchChildren.length > 0 && (
+
+ {sortedBranchChildren.map(({ index, type, child, treeNode: childTreeNode }) => {
+ const nibble = index.toString(16).toUpperCase();
+
+ if (type === 'empty') {
+ return (
+
+ [{nibble}]
+ —
+
+ );
+ }
+
+ if (type === 'local' && childTreeNode) {
+ return (
+
+
+ [{nibble}]
+ {showHashes && child?.pointer.rlpHash && (
+
+ {child.pointer.rlpHash}
+
+ )}
+
+
+
+ );
+ }
+
+ if (type === 'remote' && child) {
+ return (
+
+ );
+ }
+
+ return null;
+ })}
+
+ )}
+
+ {/* Local storage children */}
+ {storageChildren.map((child) => (
+
+ ))}
+
+ {/* Remote storage root for account leaves */}
+ {node.nodeType === 'AccountLeaf' && node.storageRoot &&
+ node.storageRoot.locationType === 'OtherPage' &&
+ node.storageRoot.pageId !== undefined && (
+
+
+
+ )}
+
+ )}
+
+ );
+}
+
+interface RemoteChildPointerProps {
+ child: ChildEntry;
+ parentPath: string;
+ accountPath: string;
+ loadedPages: Map;
+ pagePaths: Map;
+ expandedNodes: Set;
+ showEmptySlots: boolean;
+ showHashes: boolean;
+ onNavigate: (pageId: number, basePath?: string, isChildPage?: boolean) => void;
+ onToggleNode: (pageId: number, cellIndex: number) => void;
+ onExpandAllOnPage: (pageId: number) => void;
+ onCollapseAllOnPage: (pageId: number) => void;
+ depth: number;
+}
+
+function RemoteChildPointer({
+ child,
+ parentPath,
+ accountPath,
+ loadedPages,
+ pagePaths,
+ expandedNodes,
+ showEmptySlots,
+ showHashes,
+ onNavigate,
+ onToggleNode,
+ onExpandAllOnPage,
+ onCollapseAllOnPage,
+ depth,
+}: RemoteChildPointerProps) {
+ const nibble = child.index.toString(16).toUpperCase();
+ const childBasePath = parentPath + nibble;
+ const pageId = child.pointer.pageId!;
+ const pageData = loadedPages.get(pageId);
+
+ const pageTree = useMemo(() => {
+ if (!pageData) return null;
+ return buildTree(pageData.nodes, childBasePath);
+ }, [pageData, childBasePath]);
+
+ if (!pageData) {
+ return (
+
+ [{nibble}]
+ onNavigate(pageId, childBasePath, true)}
+ >
+ Page {pageId} (click to load)
+
+ {showHashes && child.pointer.rlpHash && (
+
+ {child.pointer.rlpHash}
+
+ )}
+
+ );
+ }
+
+ const fullnessColor = getFullnessColor(pageData.info.occupancyPercent);
+
+ return (
+
+
+ [{nibble}]
+ Page {pageId}
+ {pageData.info.cellCount} nodes
+
+ {pageData.info.usedBytes} / {pageData.info.totalBytes} bytes
+
+ {pageData.info.occupancyPercent.toFixed(1)}% full
+ {showHashes && child.pointer.rlpHash && (
+
+ {child.pointer.rlpHash}
+
+ )}
+
+
+
+
+
+ {pageTree?.map((treeNode) => (
+
+ ))}
+
+ );
+}
+
+interface StorageRootPointerProps {
+ pageId: number;
+ rlpHash?: string;
+ accountPath: string;
+ loadedPages: Map;
+ pagePaths: Map;
+ expandedNodes: Set;
+ showEmptySlots: boolean;
+ showHashes: boolean;
+ onNavigate: (pageId: number, basePath?: string, isChildPage?: boolean) => void;
+ onToggleNode: (pageId: number, cellIndex: number) => void;
+ onExpandAllOnPage: (pageId: number) => void;
+ onCollapseAllOnPage: (pageId: number) => void;
+ depth: number;
+}
+
+function StorageRootPointer({
+ pageId,
+ rlpHash,
+ accountPath,
+ loadedPages,
+ pagePaths,
+ expandedNodes,
+ showEmptySlots,
+ showHashes,
+ onNavigate,
+ onToggleNode,
+ onExpandAllOnPage,
+ onCollapseAllOnPage,
+ depth,
+}: StorageRootPointerProps) {
+ const pageData = loadedPages.get(pageId);
+
+ const pageTree = useMemo(() => {
+ if (!pageData) return null;
+ return buildTree(pageData.nodes, '');
+ }, [pageData]);
+
+ if (!pageData) {
+ return (
+
+ Storage:
+ onNavigate(pageId, '', true)}
+ >
+ Page {pageId} (click to load)
+
+ {showHashes && rlpHash && (
+
+ {rlpHash}
+
+ )}
+
+ );
+ }
+
+ const fullnessColor = getFullnessColor(pageData.info.occupancyPercent);
+
+ return (
+
+
+ Storage
+ Page {pageId}
+ {pageData.info.cellCount} nodes
+
+ {pageData.info.usedBytes} / {pageData.info.totalBytes} bytes
+
+ {pageData.info.occupancyPercent.toFixed(1)}% full
+ {showHashes && rlpHash && (
+
+ {rlpHash}
+
+ )}
+
+
+
+
+
+ {pageTree?.map((treeNode) => (
+
+ ))}
+
+ );
+}
diff --git a/explorer/frontend/src/hooks/useTrieData.ts b/explorer/frontend/src/hooks/useTrieData.ts
new file mode 100644
index 00000000..1c7371f7
--- /dev/null
+++ b/explorer/frontend/src/hooks/useTrieData.ts
@@ -0,0 +1,274 @@
+import { useState, useCallback } from 'react';
+import type { ExplorerNode, PageInfo, PathSearchResult } from '../types';
+import * as api from '../api/client';
+
+export interface PageData {
+ info: PageInfo;
+ nodes: ExplorerNode[];
+}
+
+export interface TrieState {
+ loadedPages: Map;
+ pagePaths: Map; // Base path for each page's root
+ childPages: Set; // Pages loaded as children of other nodes (not shown at top level)
+ expandedNodes: Set;
+ selectedNode: { pageId: number; cellIndex: number } | null;
+ autoExpand: boolean; // Auto-expand nodes as pages are loaded
+ showEmptySlots: boolean; // Show empty child slots in branches
+ showHashes: boolean; // Show RLP hashes alongside pointers
+ isLoading: boolean;
+ error: string | null;
+}
+
+export function useTrieData() {
+ const [state, setState] = useState({
+ loadedPages: new Map(),
+ pagePaths: new Map(),
+ childPages: new Set(),
+ expandedNodes: new Set(),
+ selectedNode: null,
+ autoExpand: false,
+ showEmptySlots: false,
+ showHashes: false,
+ isLoading: false,
+ error: null,
+ });
+
+ const loadRootPage = useCallback(async () => {
+ setState(s => ({ ...s, isLoading: true, error: null }));
+ try {
+ const response = await api.getRoot();
+ if (!response.success) {
+ throw new Error(response.error || 'Failed to load root page');
+ }
+ if (response.data?.page) {
+ const pageId = response.data.rootPageId!;
+ setState(s => ({
+ ...s,
+ loadedPages: new Map([[pageId, response.data!.page!]]),
+ pagePaths: new Map([[pageId, '']]), // Root page has empty base path
+ isLoading: false,
+ }));
+ } else {
+ setState(s => ({ ...s, isLoading: false }));
+ }
+ } catch (e) {
+ setState(s => ({
+ ...s,
+ isLoading: false,
+ error: e instanceof Error ? e.message : 'Unknown error',
+ }));
+ }
+ }, []);
+
+ const loadPage = useCallback(async (pageId: number, basePath?: string, isChildPage: boolean = false) => {
+ setState(s => ({ ...s, isLoading: true, error: null }));
+ try {
+ const response = await api.getPage(pageId);
+ if (!response.success) {
+ throw new Error(response.error || 'Failed to load page');
+ }
+ if (response.data) {
+ setState(s => {
+ const newPages = new Map(s.loadedPages);
+ newPages.set(pageId, response.data!);
+ const newPaths = new Map(s.pagePaths);
+ if (basePath !== undefined) {
+ newPaths.set(pageId, basePath);
+ }
+ const newChildPages = new Set(s.childPages);
+ if (isChildPage) {
+ newChildPages.add(pageId);
+ }
+ // Auto-expand all nodes on this page if autoExpand is enabled
+ const newExpanded = new Set(s.expandedNodes);
+ if (s.autoExpand && response.data) {
+ for (const node of response.data.nodes) {
+ newExpanded.add(`${pageId}:${node.cellIndex}`);
+ }
+ }
+ return { ...s, loadedPages: newPages, pagePaths: newPaths, childPages: newChildPages, expandedNodes: newExpanded, isLoading: false };
+ });
+ }
+ } catch (e) {
+ setState(s => ({
+ ...s,
+ isLoading: false,
+ error: e instanceof Error ? e.message : 'Unknown error',
+ }));
+ }
+ }, []);
+
+ const toggleNode = useCallback((pageId: number, cellIndex: number) => {
+ const key = `${pageId}:${cellIndex}`;
+ setState(s => {
+ const newExpanded = new Set(s.expandedNodes);
+ if (newExpanded.has(key)) {
+ newExpanded.delete(key);
+ } else {
+ newExpanded.add(key);
+ }
+ return { ...s, expandedNodes: newExpanded };
+ });
+ }, []);
+
+ const selectNode = useCallback((pageId: number, cellIndex: number) => {
+ setState(s => ({
+ ...s,
+ selectedNode: { pageId, cellIndex },
+ }));
+ }, []);
+
+ const isNodeExpanded = useCallback(
+ (pageId: number, cellIndex: number) => {
+ return state.expandedNodes.has(`${pageId}:${cellIndex}`);
+ },
+ [state.expandedNodes]
+ );
+
+ const isPageLoaded = useCallback(
+ (pageId: number) => {
+ return state.loadedPages.has(pageId);
+ },
+ [state.loadedPages]
+ );
+
+ const getPageBasePath = useCallback(
+ (pageId: number) => {
+ return state.pagePaths.get(pageId) ?? '';
+ },
+ [state.pagePaths]
+ );
+
+ // Load pages along a search path, computing proper base paths and expanding nodes
+ const loadSearchPath = useCallback(async (result: PathSearchResult) => {
+ setState(s => ({ ...s, isLoading: true, error: null }));
+
+ try {
+ const newPages = new Map(state.loadedPages);
+ const newPaths = new Map(state.pagePaths);
+ const newChildPages = new Set(state.childPages);
+ const newExpanded = new Set(state.expandedNodes);
+
+ let currentPath = '';
+ let lastPageId: number | null = null;
+
+ // Process each node in the path
+ for (const node of result.pathToNode) {
+ const pageId = node.pageId;
+
+ // Load page if not already loaded
+ if (!newPages.has(pageId)) {
+ const response = await api.getPage(pageId);
+ if (response.success && response.data) {
+ newPages.set(pageId, response.data);
+ }
+ }
+
+ // Set base path for this page if it's a new page in the traversal
+ if (lastPageId !== null && pageId !== lastPageId) {
+ // This is a child page - mark it and set path
+ newPaths.set(pageId, currentPath);
+ newChildPages.add(pageId);
+ } else if (lastPageId === null) {
+ // First page (root) - set empty base path
+ newPaths.set(pageId, '');
+ }
+
+ // Expand this node
+ newExpanded.add(`${pageId}:${node.cellIndex}`);
+
+ // Update current path with this node's prefix
+ currentPath += node.prefix || '';
+
+ // If this is a branch, add the nibble for the next node
+ if (node.nodeType === 'Branch' && node.children) {
+ // Find which child leads to the next node in the path
+ const nodeIndex = result.pathToNode.indexOf(node);
+ if (nodeIndex < result.pathToNode.length - 1) {
+ const nextNode = result.pathToNode[nodeIndex + 1];
+ for (const child of node.children) {
+ if (child.pointer.locationType === 'SamePage' && child.pointer.cellIndex === nextNode.cellIndex) {
+ currentPath += child.index.toString(16).toUpperCase();
+ break;
+ } else if (child.pointer.locationType === 'OtherPage' && child.pointer.pageId === nextNode.pageId) {
+ currentPath += child.index.toString(16).toUpperCase();
+ break;
+ }
+ }
+ }
+ }
+
+ lastPageId = pageId;
+ }
+
+ setState(s => ({
+ ...s,
+ loadedPages: newPages,
+ pagePaths: newPaths,
+ childPages: newChildPages,
+ expandedNodes: newExpanded,
+ isLoading: false,
+ }));
+ } catch (e) {
+ setState(s => ({
+ ...s,
+ isLoading: false,
+ error: e instanceof Error ? e.message : 'Unknown error',
+ }));
+ }
+ }, [state.loadedPages, state.pagePaths, state.childPages, state.expandedNodes]);
+
+ const toggleAutoExpand = useCallback(() => {
+ setState(s => ({ ...s, autoExpand: !s.autoExpand }));
+ }, []);
+
+ const toggleShowEmptySlots = useCallback(() => {
+ setState(s => ({ ...s, showEmptySlots: !s.showEmptySlots }));
+ }, []);
+
+ const toggleShowHashes = useCallback(() => {
+ setState(s => ({ ...s, showHashes: !s.showHashes }));
+ }, []);
+
+ const expandAllOnPage = useCallback((pageId: number) => {
+ setState(s => {
+ const pageData = s.loadedPages.get(pageId);
+ if (!pageData) return s;
+ const newExpanded = new Set(s.expandedNodes);
+ for (const node of pageData.nodes) {
+ newExpanded.add(`${pageId}:${node.cellIndex}`);
+ }
+ return { ...s, expandedNodes: newExpanded };
+ });
+ }, []);
+
+ const collapseAllOnPage = useCallback((pageId: number) => {
+ setState(s => {
+ const pageData = s.loadedPages.get(pageId);
+ if (!pageData) return s;
+ const newExpanded = new Set(s.expandedNodes);
+ for (const node of pageData.nodes) {
+ newExpanded.delete(`${pageId}:${node.cellIndex}`);
+ }
+ return { ...s, expandedNodes: newExpanded };
+ });
+ }, []);
+
+ return {
+ state,
+ loadRootPage,
+ loadPage,
+ toggleNode,
+ selectNode,
+ isNodeExpanded,
+ isPageLoaded,
+ getPageBasePath,
+ loadSearchPath,
+ toggleAutoExpand,
+ toggleShowEmptySlots,
+ toggleShowHashes,
+ expandAllOnPage,
+ collapseAllOnPage,
+ };
+}
diff --git a/explorer/frontend/src/main.tsx b/explorer/frontend/src/main.tsx
new file mode 100644
index 00000000..6c14464c
--- /dev/null
+++ b/explorer/frontend/src/main.tsx
@@ -0,0 +1,9 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import { App } from './App';
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+
+);
diff --git a/explorer/frontend/src/styles/index.css b/explorer/frontend/src/styles/index.css
new file mode 100644
index 00000000..df0e0ac0
--- /dev/null
+++ b/explorer/frontend/src/styles/index.css
@@ -0,0 +1,578 @@
+* {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
+ font-size: 14px;
+ line-height: 1.5;
+ color: #333;
+ background: #f5f5f5;
+}
+
+.app {
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+}
+
+.header {
+ background: #1a1a2e;
+ color: white;
+ padding: 1rem 2rem;
+ display: flex;
+ align-items: center;
+ gap: 2rem;
+}
+
+.header h1 {
+ font-size: 1.5rem;
+ font-weight: 600;
+}
+
+.header-actions {
+ display: flex;
+ gap: 1rem;
+ margin-left: auto;
+}
+
+.search-bar {
+ display: flex;
+ gap: 0.5rem;
+}
+
+.search-bar input {
+ padding: 0.5rem 1rem;
+ border: none;
+ border-radius: 4px;
+ width: 400px;
+ font-size: 14px;
+}
+
+.search-bar select {
+ padding: 0.5rem;
+ border: none;
+ border-radius: 4px;
+}
+
+.search-bar button {
+ padding: 0.5rem 1rem;
+ border: none;
+ border-radius: 4px;
+ background: #4a90d9;
+ color: white;
+ cursor: pointer;
+ font-weight: 500;
+}
+
+.search-bar button:hover {
+ background: #3a7bc0;
+}
+
+.btn {
+ padding: 0.5rem 1rem;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ font-weight: 500;
+}
+
+.btn-secondary {
+ background: #6c757d;
+ color: white;
+}
+
+.btn-secondary:hover {
+ background: #5a6268;
+}
+
+.btn-primary {
+ background: #4a90d9;
+ color: white;
+}
+
+.btn-primary:hover {
+ background: #3a7bc0;
+}
+
+.btn-small {
+ padding: 0.15rem 0.4rem;
+ border: 1px solid #ccc;
+ border-radius: 3px;
+ background: white;
+ cursor: pointer;
+ font-size: 0.85rem;
+ font-weight: 600;
+ line-height: 1;
+}
+
+.btn-small:hover {
+ background: #f0f0f0;
+}
+
+.main-content {
+ flex: 1;
+ padding: 1.5rem;
+ overflow: auto;
+}
+
+.tree-view {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.page-container {
+ border: 1px solid #ddd;
+ border-radius: 8px;
+ overflow: hidden;
+ background: white;
+}
+
+.page-header {
+ padding: 0.75rem 1rem;
+ font-weight: 600;
+ display: flex;
+ gap: 1.5rem;
+ align-items: center;
+ border-bottom: 1px solid #eee;
+}
+
+.page-header .page-id {
+ font-size: 1rem;
+}
+
+.page-header .page-stats {
+ font-weight: normal;
+ color: #666;
+ font-size: 0.85rem;
+}
+
+.page-header .page-actions {
+ margin-left: auto;
+ display: flex;
+ gap: 0.25rem;
+}
+
+.page-nodes {
+ padding: 0.5rem;
+}
+
+.node-item {
+ border: 1px solid #e0e0e0;
+ border-radius: 6px;
+ margin: 0.5rem;
+ background: white;
+}
+
+.node-header {
+ padding: 0.75rem 1rem;
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ cursor: pointer;
+ user-select: none;
+}
+
+.node-header:hover {
+ background: #f8f9fa;
+}
+
+.node-type {
+ font-weight: 600;
+ padding: 0.2rem 0.5rem;
+ border-radius: 4px;
+ font-size: 0.8rem;
+}
+
+.node-type.branch {
+ background: #e3f2fd;
+ color: #1565c0;
+}
+
+.node-type.account-leaf {
+ background: #e8f5e9;
+ color: #2e7d32;
+}
+
+.node-type.storage-leaf {
+ background: #fff3e0;
+ color: #ef6c00;
+}
+
+.node-path {
+ font-family: monospace;
+ font-size: 0.85rem;
+ color: #666;
+ position: relative;
+}
+
+.node-path.clickable {
+ cursor: pointer;
+ padding: 0.1rem 0.3rem;
+ border-radius: 3px;
+ transition: background-color 0.15s;
+}
+
+.node-path.clickable:hover {
+ background-color: #e3f2fd;
+}
+
+.node-path.copied {
+ background-color: #c8e6c9;
+}
+
+.node-path .path-base {
+ color: #999;
+}
+
+.node-path .path-prefix {
+ font-weight: 600;
+ color: #333;
+}
+
+.node-path .copy-feedback {
+ margin-left: 0.5rem;
+ font-size: 0.75rem;
+ color: #2e7d32;
+ font-weight: 500;
+}
+
+.node-location {
+ color: #999;
+ font-size: 0.8rem;
+ margin-left: auto;
+}
+
+.expand-icon {
+ transition: transform 0.2s;
+}
+
+.expand-icon.expanded {
+ transform: rotate(90deg);
+}
+
+.node-details {
+ padding: 0.75rem 1rem;
+ border-top: 1px solid #eee;
+ background: #fafafa;
+}
+
+.node-details dl {
+ display: grid;
+ grid-template-columns: auto 1fr;
+ gap: 0.25rem 1rem;
+}
+
+.node-details dt {
+ color: #666;
+ font-weight: 500;
+}
+
+.node-details dd {
+ font-family: monospace;
+ word-break: break-all;
+}
+
+.node-children {
+ padding: 0.5rem 1rem;
+ border-top: 1px solid #eee;
+}
+
+.child-list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+}
+
+.child-pointer {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.25rem;
+ padding: 0.25rem 0.5rem;
+ border-radius: 4px;
+ font-family: monospace;
+ font-size: 0.8rem;
+ cursor: pointer;
+}
+
+.child-pointer.same-page {
+ background: #e8f5e9;
+ color: #2e7d32;
+}
+
+.child-pointer.other-page {
+ background: #e3f2fd;
+ color: #1565c0;
+}
+
+.child-pointer:hover {
+ filter: brightness(0.95);
+}
+
+.loading {
+ text-align: center;
+ padding: 2rem;
+ color: #666;
+}
+
+.error {
+ text-align: center;
+ padding: 2rem;
+ color: #d32f2f;
+ background: #ffebee;
+ border-radius: 8px;
+}
+
+/* Metadata Modal */
+.modal-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.5);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1000;
+}
+
+.modal {
+ background: white;
+ border-radius: 8px;
+ max-width: 700px;
+ max-height: 80vh;
+ overflow: auto;
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
+}
+
+.modal-header {
+ padding: 1rem 1.5rem;
+ border-bottom: 1px solid #eee;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.modal-header h2 {
+ font-size: 1.25rem;
+}
+
+.modal-close {
+ background: none;
+ border: none;
+ font-size: 1.5rem;
+ cursor: pointer;
+ color: #666;
+}
+
+.modal-body {
+ padding: 1.5rem;
+}
+
+.metadata-section {
+ margin-bottom: 1.5rem;
+}
+
+.metadata-section h3 {
+ font-size: 1rem;
+ margin-bottom: 0.75rem;
+ color: #333;
+}
+
+.metadata-grid {
+ display: grid;
+ grid-template-columns: auto 1fr;
+ gap: 0.5rem 1rem;
+}
+
+.metadata-grid dt {
+ color: #666;
+}
+
+.metadata-grid dd {
+ font-family: monospace;
+ word-break: break-all;
+}
+
+.orphan-list {
+ max-height: 200px;
+ overflow: auto;
+ border: 1px solid #eee;
+ border-radius: 4px;
+}
+
+.orphan-item {
+ padding: 0.5rem 1rem;
+ border-bottom: 1px solid #eee;
+ display: flex;
+ justify-content: space-between;
+}
+
+.orphan-item:last-child {
+ border-bottom: none;
+}
+
+.empty-message {
+ text-align: center;
+ padding: 2rem;
+ color: #666;
+}
+
+.search-result-banner {
+ padding: 0.75rem 1rem;
+ background: #e3f2fd;
+ border: 1px solid #90caf9;
+ border-radius: 4px;
+ margin-bottom: 1rem;
+ color: #1565c0;
+}
+
+/* Tree view styles */
+.tree-node {
+ position: relative;
+}
+
+.tree-node::before {
+ content: '';
+ position: absolute;
+ left: -10px;
+ top: 0;
+ bottom: 0;
+ width: 1px;
+ background: #ddd;
+}
+
+.tree-node:first-child::before {
+ top: 20px;
+}
+
+.tree-node:last-child::before {
+ height: 20px;
+}
+
+.tree-node::after {
+ content: '';
+ position: absolute;
+ left: -10px;
+ top: 20px;
+ width: 10px;
+ height: 1px;
+ background: #ddd;
+}
+
+.page-nodes > .tree-node::before,
+.page-nodes > .tree-node::after {
+ display: none;
+}
+
+.node-content {
+ padding-left: 0.5rem;
+}
+
+.expand-icon-placeholder {
+ width: 1rem;
+ display: inline-block;
+}
+
+.remote-pointer-box,
+.storage-pointer-box {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.5rem;
+ background: #f8f9fa;
+ border-radius: 4px;
+ margin: 0.25rem 0;
+}
+
+.remote-nibble {
+ font-family: monospace;
+ font-weight: 600;
+ color: #666;
+}
+
+.storage-label {
+ font-weight: 600;
+ color: #9c27b0;
+ font-size: 0.85rem;
+}
+
+.loaded-subtree,
+.loaded-storage-subtree {
+ margin: 0.25rem 0;
+ padding-left: 0.5rem;
+ border-left: 2px solid #e3f2fd;
+}
+
+.loaded-storage-subtree {
+ border-left-color: #f3e5f5;
+}
+
+.subtree-header {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ padding: 0.5rem 0.75rem;
+ font-size: 0.85rem;
+ border-radius: 4px;
+ margin-bottom: 0.25rem;
+}
+
+.subtree-page-id {
+ font-weight: 600;
+}
+
+.subtree-page-stats {
+ color: #666;
+ font-size: 0.8rem;
+}
+
+.storage-child-label {
+ padding: 0.25rem 0;
+}
+
+.slot-nibble {
+ font-family: monospace;
+ font-weight: 600;
+ color: #666;
+ min-width: 1.5rem;
+ display: inline-block;
+}
+
+.empty-slot {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.15rem 0;
+ opacity: 0.5;
+}
+
+.slot-empty {
+ color: #ccc;
+}
+
+.child-nibble-label {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.15rem 0;
+}
+
+.pointer-hash {
+ font-family: monospace;
+ font-size: 0.7rem;
+ color: #999;
+ background: #f5f5f5;
+ padding: 0.1rem 0.3rem;
+ border-radius: 3px;
+ word-break: break-all;
+}
+
+.path-account {
+ color: #9c27b0;
+ font-weight: 500;
+}
diff --git a/explorer/frontend/src/types/index.ts b/explorer/frontend/src/types/index.ts
new file mode 100644
index 00000000..52ee31b3
--- /dev/null
+++ b/explorer/frontend/src/types/index.ts
@@ -0,0 +1,83 @@
+export interface ExplorerPointer {
+ locationType: 'SamePage' | 'OtherPage' | 'Unknown';
+ cellIndex?: number;
+ pageId?: number;
+ rlpHash?: string;
+}
+
+export interface ChildEntry {
+ index: number;
+ pointer: ExplorerPointer;
+}
+
+export interface ExplorerNode {
+ pageId: number;
+ cellIndex: number;
+ prefix: string;
+ nodeType: 'AccountLeaf' | 'StorageLeaf' | 'Branch';
+ sizeBytes: number;
+ nonce?: number;
+ balance?: string;
+ codeHash?: string;
+ storageRoot?: ExplorerPointer;
+ value?: string;
+ children?: ChildEntry[];
+}
+
+// Tree node that wraps ExplorerNode with resolved children
+export interface TreeNode {
+ node: ExplorerNode;
+ localChildren: TreeNode[];
+ basePath: string;
+ isStorageChild?: boolean; // True if this subtree is storage for an account
+}
+
+export interface PageInfo {
+ pageId: number;
+ snapshotId: number;
+ totalBytes: number;
+ usedBytes: number;
+ freeBytes: number;
+ cellCount: number;
+ occupancyPercent: number;
+}
+
+export interface OrphanPageInfo {
+ pageId: number;
+ orphanedAtSnapshot: number;
+}
+
+export interface DatabaseInfo {
+ snapshotId: number;
+ rootNodeHash: string;
+ rootNodePageId: number | null;
+ totalPageCount: number;
+ orphanedPages: OrphanPageInfo[];
+}
+
+export interface PathSearchResult {
+ matchedPath: string;
+ node: ExplorerNode;
+ pathToNode: ExplorerNode[];
+}
+
+export interface ApiResponse {
+ success: boolean;
+ data?: T;
+ error?: string;
+}
+
+export interface PageResponse {
+ info: PageInfo;
+ nodes: ExplorerNode[];
+}
+
+export interface RootResponse {
+ rootPageId: number | null;
+ page: PageResponse | null;
+}
+
+export interface DescendantsResponse {
+ pageId: number;
+ nodes: ExplorerNode[];
+}
diff --git a/explorer/frontend/src/utils/treeBuilder.ts b/explorer/frontend/src/utils/treeBuilder.ts
new file mode 100644
index 00000000..26c09ffe
--- /dev/null
+++ b/explorer/frontend/src/utils/treeBuilder.ts
@@ -0,0 +1,60 @@
+import type { ExplorerNode, TreeNode } from '../types';
+
+/**
+ * Build a tree structure from a flat list of nodes.
+ * Cell 0 is always the canonical root of a page's subtrie.
+ * Also handles same-page storage root references for account leaves.
+ */
+export function buildTree(nodes: ExplorerNode[], basePath: string): TreeNode[] {
+ // Create a map from cellIndex -> node for quick lookup
+ const nodeMap = new Map();
+ for (const node of nodes) {
+ nodeMap.set(node.cellIndex, node);
+ }
+
+ // Cell 0 is always the root of a page's subtrie
+ const rootNode = nodeMap.get(0);
+ if (!rootNode) {
+ // No root node found - page may be empty or corrupted
+ return [];
+ }
+
+ // Recursively build tree nodes
+ function buildTreeNode(node: ExplorerNode, currentPath: string, isStorageSubtree: boolean = false): TreeNode {
+ const nodePath = currentPath + (node.prefix || '');
+ const localChildren: TreeNode[] = [];
+
+ if (node.nodeType === 'Branch' && node.children) {
+ for (const child of node.children) {
+ if (child.pointer.locationType === 'SamePage' && child.pointer.cellIndex !== undefined) {
+ const childNode = nodeMap.get(child.pointer.cellIndex);
+ if (childNode) {
+ const nibble = child.index.toString(16).toUpperCase();
+ localChildren.push(buildTreeNode(childNode, nodePath + nibble, isStorageSubtree));
+ }
+ }
+ }
+ }
+
+ // Handle same-page storage root for account leaves
+ if (node.nodeType === 'AccountLeaf' && node.storageRoot) {
+ if (node.storageRoot.locationType === 'SamePage' && node.storageRoot.cellIndex !== undefined) {
+ const storageNode = nodeMap.get(node.storageRoot.cellIndex);
+ if (storageNode) {
+ // Storage tries start fresh with empty path
+ const storageTreeNode = buildTreeNode(storageNode, '', true);
+ storageTreeNode.isStorageChild = true;
+ localChildren.push(storageTreeNode);
+ }
+ }
+ }
+
+ return {
+ node,
+ localChildren,
+ basePath: currentPath,
+ };
+ }
+
+ return [buildTreeNode(rootNode, basePath)];
+}
diff --git a/explorer/frontend/src/vite-env.d.ts b/explorer/frontend/src/vite-env.d.ts
new file mode 100644
index 00000000..11f02fe2
--- /dev/null
+++ b/explorer/frontend/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/explorer/frontend/tsconfig.json b/explorer/frontend/tsconfig.json
new file mode 100644
index 00000000..3934b8f6
--- /dev/null
+++ b/explorer/frontend/tsconfig.json
@@ -0,0 +1,21 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src"],
+ "references": [{ "path": "./tsconfig.node.json" }]
+}
diff --git a/explorer/frontend/vite.config.ts b/explorer/frontend/vite.config.ts
new file mode 100644
index 00000000..92fce032
--- /dev/null
+++ b/explorer/frontend/vite.config.ts
@@ -0,0 +1,15 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+export default defineConfig({
+ plugins: [react()],
+ build: {
+ outDir: 'dist',
+ emptyOutDir: true,
+ },
+ server: {
+ proxy: {
+ '/api': 'http://localhost:3000',
+ },
+ },
+})
diff --git a/explorer/src/handlers/metadata.rs b/explorer/src/handlers/metadata.rs
new file mode 100644
index 00000000..a6d0aa51
--- /dev/null
+++ b/explorer/src/handlers/metadata.rs
@@ -0,0 +1,13 @@
+use axum::{extract::State, Json};
+use std::sync::Arc;
+use triedb::storage::explorer::DatabaseInfo;
+
+use crate::{models::ApiResponse, state::AppState};
+
+/// GET /api/metadata - Get database metadata including orphaned pages
+pub async fn get_metadata(
+ State(state): State>,
+) -> Json> {
+ let data = state.with_explorer(|explorer, _context| explorer.get_database_info());
+ Json(ApiResponse::success(data))
+}
diff --git a/explorer/src/handlers/mod.rs b/explorer/src/handlers/mod.rs
new file mode 100644
index 00000000..776e97f9
--- /dev/null
+++ b/explorer/src/handlers/mod.rs
@@ -0,0 +1,4 @@
+pub mod metadata;
+pub mod nodes;
+pub mod pages;
+pub mod search;
diff --git a/explorer/src/handlers/nodes.rs b/explorer/src/handlers/nodes.rs
new file mode 100644
index 00000000..688b5ef9
--- /dev/null
+++ b/explorer/src/handlers/nodes.rs
@@ -0,0 +1,26 @@
+use axum::{
+ extract::{Path, State},
+ Json,
+};
+use std::sync::Arc;
+use triedb::{page::PageId, storage::explorer::ExplorerNode};
+
+use crate::{models::ApiResponse, state::AppState};
+
+/// GET /api/nodes/:page_id/:cell_index - Get a single node
+pub async fn get_node(
+ State(state): State>,
+ Path((page_id, cell_index)): Path<(u32, u8)>,
+) -> Json> {
+ let result = state.with_explorer(|explorer, context| {
+ let page_id = PageId::new(page_id).ok_or_else(|| "Invalid page ID".to_string())?;
+ explorer
+ .get_node_at(context, page_id, cell_index)
+ .map_err(|e| format!("Failed to get node: {}", e))
+ });
+
+ match result {
+ Ok(data) => Json(ApiResponse::success(data)),
+ Err(e) => Json(ApiResponse::error(e)),
+ }
+}
diff --git a/explorer/src/handlers/pages.rs b/explorer/src/handlers/pages.rs
new file mode 100644
index 00000000..64967a7f
--- /dev/null
+++ b/explorer/src/handlers/pages.rs
@@ -0,0 +1,129 @@
+use axum::{
+ extract::{Path, State},
+ Json,
+};
+use std::sync::Arc;
+use triedb::page::PageId;
+
+use crate::{
+ models::{ApiResponse, DescendantsResponse, PageResponse, RootResponse},
+ state::AppState,
+};
+
+/// GET /api/root - Get root page info and nodes
+pub async fn get_root(
+ State(state): State>,
+) -> Json> {
+ let result = state.with_explorer(|explorer, context| {
+ let root_page_id = explorer.get_root_page_id();
+
+ match root_page_id {
+ Some(page_id) => {
+ let info = explorer.get_page_info(context, page_id);
+ let nodes = explorer.get_page_nodes(context, page_id);
+
+ match (info, nodes) {
+ (Ok(info), Ok(nodes)) => Ok(RootResponse {
+ root_page_id: Some(page_id.as_u32()),
+ page: Some(PageResponse { info, nodes }),
+ }),
+ (Err(e), _) => Err(format!("Failed to get page info: {}", e)),
+ (_, Err(e)) => Err(format!("Failed to get page nodes: {}", e)),
+ }
+ }
+ None => Ok(RootResponse {
+ root_page_id: None,
+ page: None,
+ }),
+ }
+ });
+
+ match result {
+ Ok(data) => Json(ApiResponse::success(data)),
+ Err(e) => Json(ApiResponse::error(e)),
+ }
+}
+
+/// GET /api/pages/:page_id - Get all nodes on a page with page info
+pub async fn get_page(
+ State(state): State>,
+ Path(page_id): Path,
+) -> Json> {
+ let result = state.with_explorer(|explorer, context| {
+ let page_id = PageId::new(page_id).ok_or_else(|| "Invalid page ID".to_string())?;
+ let info = explorer
+ .get_page_info(context, page_id)
+ .map_err(|e| format!("Failed to get page info: {}", e))?;
+ let nodes = explorer
+ .get_page_nodes(context, page_id)
+ .map_err(|e| format!("Failed to get page nodes: {}", e))?;
+ Ok(PageResponse { info, nodes })
+ });
+
+ match result {
+ Ok(data) => Json(ApiResponse::success(data)),
+ Err(e) => Json(ApiResponse::error(e)),
+ }
+}
+
+/// GET /api/pages/:page_id/info - Get page metadata only
+pub async fn get_page_info(
+ State(state): State>,
+ Path(page_id): Path,
+) -> Json> {
+ let result = state.with_explorer(|explorer, context| {
+ let page_id = PageId::new(page_id).ok_or_else(|| "Invalid page ID".to_string())?;
+ explorer
+ .get_page_info(context, page_id)
+ .map_err(|e| format!("Failed to get page info: {}", e))
+ });
+
+ match result {
+ Ok(data) => Json(ApiResponse::success(data)),
+ Err(e) => Json(ApiResponse::error(e)),
+ }
+}
+
+/// GET /api/pages/:page_id/descendants - Get page-local descendants from cell 0
+pub async fn get_descendants(
+ State(state): State>,
+ Path(page_id): Path,
+) -> Json> {
+ let result = state.with_explorer(|explorer, context| {
+ let page_id = PageId::new(page_id).ok_or_else(|| "Invalid page ID".to_string())?;
+ let nodes = explorer
+ .walk_page_local_descendants(context, page_id, 0)
+ .map_err(|e| format!("Failed to walk descendants: {}", e))?;
+ Ok(DescendantsResponse {
+ page_id: page_id.as_u32(),
+ nodes,
+ })
+ });
+
+ match result {
+ Ok(data) => Json(ApiResponse::success(data)),
+ Err(e) => Json(ApiResponse::error(e)),
+ }
+}
+
+/// GET /api/pages/:page_id/nodes/:cell_index/descendants - Get descendants from specific node
+pub async fn get_node_descendants(
+ State(state): State>,
+ Path((page_id, cell_index)): Path<(u32, u8)>,
+) -> Json> {
+ let result = state.with_explorer(|explorer, context| {
+ let page_id = PageId::new(page_id).ok_or_else(|| "Invalid page ID".to_string())?;
+ let nodes = explorer
+ .walk_page_local_descendants(context, page_id, cell_index)
+ .map_err(|e| format!("Failed to walk descendants: {}", e))?;
+ Ok(DescendantsResponse {
+ page_id: page_id.as_u32(),
+ nodes,
+ })
+ });
+
+ match result {
+ Ok(data) => Json(ApiResponse::success(data)),
+ Err(e) => Json(ApiResponse::error(e)),
+ }
+}
diff --git a/explorer/src/handlers/search.rs b/explorer/src/handlers/search.rs
new file mode 100644
index 00000000..a7901266
--- /dev/null
+++ b/explorer/src/handlers/search.rs
@@ -0,0 +1,124 @@
+use alloy_primitives::{hex, Address, StorageKey};
+use alloy_trie::Nibbles;
+use axum::{
+ extract::{Query, State},
+ Json,
+};
+use serde::Deserialize;
+use std::sync::Arc;
+use triedb::storage::explorer::PathSearchResult;
+
+use crate::{models::ApiResponse, state::AppState};
+
+#[derive(Debug, Deserialize)]
+pub struct SearchQuery {
+ /// The search query (path in hex, address, or storage slot)
+ pub q: String,
+ /// Type of query: "raw", "address", or "storage"
+ #[serde(rename = "type", default = "default_query_type")]
+ pub query_type: String,
+}
+
+fn default_query_type() -> String {
+ "raw".to_string()
+}
+
+/// GET /api/search?q=&type=
+pub async fn search(
+ State(state): State>,
+ Query(params): Query,
+) -> Json>> {
+ let result = parse_search_query(¶ms.q, ¶ms.query_type);
+
+ let nibbles = match result {
+ Ok(n) => n,
+ Err(e) => return Json(ApiResponse::error(e)),
+ };
+
+ let search_result = state.with_explorer(|explorer, context| {
+ explorer
+ .search_by_path(context, &nibbles)
+ .map_err(|e| format!("Search failed: {}", e))
+ });
+
+ match search_result {
+ Ok(data) => Json(ApiResponse::success(data)),
+ Err(e) => Json(ApiResponse::error(e)),
+ }
+}
+
+fn parse_search_query(query: &str, query_type: &str) -> Result {
+ match query_type {
+ "raw" => {
+ // Raw nibbles path (hex encoded)
+ // Supports colon-separated format: "0xAccountPath:0xStoragePath" for account+storage paths
+ let hex_str = query.strip_prefix("0x").unwrap_or(query);
+
+ if let Some(colon_pos) = hex_str.find(':') {
+ // Account + storage path format
+ let account_hex = &hex_str[..colon_pos];
+ let storage_hex = hex_str[colon_pos + 1..]
+ .strip_prefix("0x")
+ .unwrap_or(&hex_str[colon_pos + 1..]);
+
+ // Combine account and storage nibbles
+ let combined_hex = format!("{}{}", account_hex, storage_hex);
+ let bytes =
+ hex::decode(&combined_hex).map_err(|e| format!("Invalid hex: {}", e))?;
+ Ok(Nibbles::unpack(&bytes))
+ } else {
+ // Simple path
+ let bytes = hex::decode(hex_str).map_err(|e| format!("Invalid hex: {}", e))?;
+ Ok(Nibbles::unpack(&bytes))
+ }
+ }
+ "address" => {
+ // Ethereum address -> keccak256 hash -> nibbles
+ let hex_str = query.strip_prefix("0x").unwrap_or(query);
+ if hex_str.len() != 40 {
+ return Err("Address must be 20 bytes (40 hex chars)".to_string());
+ }
+ let bytes: [u8; 20] = hex::decode(hex_str)
+ .map_err(|e| format!("Invalid hex: {}", e))?
+ .try_into()
+ .map_err(|_| "Invalid address length".to_string())?;
+ let address = Address::from(bytes);
+ let hash = alloy_primitives::keccak256(address);
+ Ok(Nibbles::unpack(hash))
+ }
+ "storage" => {
+ // Format: "address:slot" or "address slot"
+ let parts: Vec<&str> = query.split(|c| c == ':' || c == ' ').collect();
+ if parts.len() != 2 {
+ return Err(
+ "Storage query must be 'address:slot' or 'address slot'".to_string()
+ );
+ }
+
+ let address_hex = parts[0].strip_prefix("0x").unwrap_or(parts[0]);
+ if address_hex.len() != 40 {
+ return Err("Address must be 20 bytes (40 hex chars)".to_string());
+ }
+ let address_bytes: [u8; 20] = hex::decode(address_hex)
+ .map_err(|e| format!("Invalid address hex: {}", e))?
+ .try_into()
+ .map_err(|_| "Invalid address length".to_string())?;
+ let address = Address::from(address_bytes);
+
+ let slot_hex = parts[1].strip_prefix("0x").unwrap_or(parts[1]);
+ let slot_bytes = hex::decode(slot_hex).map_err(|e| format!("Invalid slot hex: {}", e))?;
+ let slot = StorageKey::left_padding_from(&slot_bytes);
+
+ // Hash address and slot
+ let address_hash = alloy_primitives::keccak256(address);
+ let slot_hash = alloy_primitives::keccak256(slot);
+
+ // Combine into 128 nibbles (64 for address hash + 64 for slot hash)
+ let mut combined = Vec::with_capacity(64);
+ combined.extend_from_slice(address_hash.as_slice());
+ combined.extend_from_slice(slot_hash.as_slice());
+ Ok(Nibbles::unpack(&combined))
+ }
+ _ => Err(format!("Unknown query type: {}", query_type)),
+ }
+}
diff --git a/explorer/src/main.rs b/explorer/src/main.rs
new file mode 100644
index 00000000..98c426ab
--- /dev/null
+++ b/explorer/src/main.rs
@@ -0,0 +1,115 @@
+mod handlers;
+mod models;
+mod state;
+
+use axum::{
+ http::Method,
+ routing::get,
+ Router,
+};
+use clap::Parser;
+use include_dir::{include_dir, Dir};
+use std::{net::SocketAddr, sync::Arc};
+use tower_http::cors::{Any, CorsLayer};
+
+use crate::state::AppState;
+
+static FRONTEND_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/frontend/dist");
+
+#[derive(Parser)]
+#[command(name = "triedb-explorer")]
+#[command(about = "Interactive web UI for exploring triedb")]
+struct Args {
+ /// Path to the database file
+ #[arg(short, long)]
+ database: String,
+
+ /// Port to run the server on
+ #[arg(short, long, default_value = "3000")]
+ port: u16,
+}
+
+#[tokio::main]
+async fn main() -> Result<(), Box> {
+ let args = Args::parse();
+
+ println!("Opening database: {}", args.database);
+ let state = Arc::new(
+ AppState::new(&args.database).expect("Failed to open database"),
+ );
+
+ // CORS layer for development
+ let cors = CorsLayer::new()
+ .allow_methods([Method::GET, Method::POST])
+ .allow_origin(Any)
+ .allow_headers(Any);
+
+ // API routes
+ let api_routes = Router::new()
+ .route("/root", get(handlers::pages::get_root))
+ .route("/pages/:page_id", get(handlers::pages::get_page))
+ .route("/pages/:page_id/info", get(handlers::pages::get_page_info))
+ .route(
+ "/pages/:page_id/descendants",
+ get(handlers::pages::get_descendants),
+ )
+ .route(
+ "/pages/:page_id/nodes/:cell_index/descendants",
+ get(handlers::pages::get_node_descendants),
+ )
+ .route(
+ "/nodes/:page_id/:cell_index",
+ get(handlers::nodes::get_node),
+ )
+ .route("/search", get(handlers::search::search))
+ .route("/metadata", get(handlers::metadata::get_metadata));
+
+ // Main router
+ let app = Router::new()
+ .nest("/api", api_routes)
+ .fallback(serve_frontend)
+ .layer(cors)
+ .with_state(state);
+
+ let addr = SocketAddr::from(([127, 0, 0, 1], args.port));
+ println!("Starting server at http://{}", addr);
+
+ let listener = tokio::net::TcpListener::bind(addr).await?;
+ axum::serve(listener, app).await?;
+
+ Ok(())
+}
+
+/// Serve static files from the embedded frontend directory.
+async fn serve_frontend(uri: axum::http::Uri) -> impl axum::response::IntoResponse {
+ let path = uri.path().trim_start_matches('/');
+ let path = if path.is_empty() { "index.html" } else { path };
+
+ match FRONTEND_DIR.get_file(path) {
+ Some(file) => {
+ let mime = mime_guess::from_path(path).first_or_octet_stream();
+ (
+ [(axum::http::header::CONTENT_TYPE, mime.as_ref())],
+ file.contents(),
+ )
+ .into_response()
+ }
+ None => {
+ // Fallback to index.html for SPA routing
+ match FRONTEND_DIR.get_file("index.html") {
+ Some(file) => (
+ [(axum::http::header::CONTENT_TYPE, "text/html")],
+ file.contents(),
+ )
+ .into_response(),
+ None => (
+ axum::http::StatusCode::NOT_FOUND,
+ "Frontend not found. Please build the frontend first.",
+ )
+ .into_response(),
+ }
+ }
+ }
+}
+
+use axum::response::IntoResponse;
diff --git a/explorer/src/models.rs b/explorer/src/models.rs
new file mode 100644
index 00000000..33eedb2c
--- /dev/null
+++ b/explorer/src/models.rs
@@ -0,0 +1,55 @@
+use serde::Serialize;
+use triedb::storage::explorer::{ExplorerNode, PageInfo};
+
+/// API response wrapper.
+#[derive(Debug, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ApiResponse {
+ pub success: bool,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub data: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub error: Option,
+}
+
+impl ApiResponse {
+ pub fn success(data: T) -> Self {
+ Self {
+ success: true,
+ data: Some(data),
+ error: None,
+ }
+ }
+
+ pub fn error(message: String) -> Self {
+ Self {
+ success: false,
+ data: None,
+ error: Some(message),
+ }
+ }
+}
+
+/// Response for page endpoints.
+#[derive(Debug, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct PageResponse {
+ pub info: PageInfo,
+ pub nodes: Vec,
+}
+
+/// Response for root endpoint.
+#[derive(Debug, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct RootResponse {
+ pub root_page_id: Option,
+ pub page: Option,
+}
+
+/// Response for descendants endpoint.
+#[derive(Debug, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct DescendantsResponse {
+ pub page_id: u32,
+ pub nodes: Vec,
+}
diff --git a/explorer/src/state.rs b/explorer/src/state.rs
new file mode 100644
index 00000000..1b10210f
--- /dev/null
+++ b/explorer/src/state.rs
@@ -0,0 +1,35 @@
+use parking_lot::Mutex;
+use std::sync::Arc;
+use triedb::{
+ context::TransactionContext,
+ storage::explorer::ExplorerService,
+ Database,
+};
+
+/// Application state that holds the database and a read-only transaction context.
+pub struct AppState {
+ pub database: Arc,
+ context: Mutex,
+}
+
+impl AppState {
+ /// Creates a new AppState from a database path.
+ pub fn new(db_path: &str) -> Result {
+ let database = Arc::new(Database::open(db_path)?);
+ let context = database.read_context();
+ Ok(Self {
+ database,
+ context: Mutex::new(context),
+ })
+ }
+
+ /// Executes a function with an ExplorerService and TransactionContext.
+ pub fn with_explorer(&self, f: F) -> T
+ where
+ F: FnOnce(&ExplorerService<'_>, &TransactionContext) -> T,
+ {
+ let context = self.context.lock();
+ let explorer = self.database.explorer();
+ f(&explorer, &context)
+ }
+}
diff --git a/src/database.rs b/src/database.rs
index ff241742..007d5c45 100644
--- a/src/database.rs
+++ b/src/database.rs
@@ -231,6 +231,16 @@ impl Database {
self.storage_engine.read_context().root_node_hash
}
+ /// Returns a read context for exploration.
+ pub fn read_context(&self) -> TransactionContext {
+ self.storage_engine.read_context()
+ }
+
+ /// Returns an ExplorerService for the interactive web UI.
+ pub fn explorer(&self) -> crate::storage::explorer::ExplorerService<'_> {
+ self.storage_engine.explorer()
+ }
+
pub fn size(&self) -> u32 {
self.storage_engine.size()
}
diff --git a/src/storage.rs b/src/storage.rs
index 2638784e..0640a234 100644
--- a/src/storage.rs
+++ b/src/storage.rs
@@ -1,5 +1,6 @@
pub mod debug;
pub mod engine;
+pub mod explorer;
pub mod overlay_root;
pub mod proofs;
mod test_utils;
diff --git a/src/storage/engine.rs b/src/storage/engine.rs
index 1e814ff9..3448b2e2 100644
--- a/src/storage/engine.rs
+++ b/src/storage/engine.rs
@@ -1328,6 +1328,11 @@ impl StorageEngine {
pub fn debugger(&self) -> crate::storage::debug::StorageDebugger<'_> {
crate::storage::debug::StorageDebugger::new(&self.page_manager)
}
+
+ /// Returns an ExplorerService for the interactive web UI.
+ pub fn explorer(&self) -> crate::storage::explorer::ExplorerService<'_> {
+ crate::storage::explorer::ExplorerService::new(self)
+ }
}
fn node_location(page_id: PageId, page_index: u8) -> Location {
diff --git a/src/storage/explorer.rs b/src/storage/explorer.rs
new file mode 100644
index 00000000..3395c3b7
--- /dev/null
+++ b/src/storage/explorer.rs
@@ -0,0 +1,504 @@
+//! Explorer module for the interactive web UI.
+//!
+//! This module provides query methods specifically designed for the triedb explorer,
+//! enabling interactive traversal of trie nodes and pages.
+
+use crate::{
+ context::TransactionContext,
+ node::{Node, NodeKind},
+ page::{Page, PageError, PageId, SlottedPage},
+ pointer::Pointer,
+ storage::{
+ engine::{Error, StorageEngine},
+ value::Value,
+ },
+};
+use alloy_primitives::hex;
+use alloy_trie::{nybbles, Nibbles};
+use serde::Serialize;
+use std::collections::HashSet;
+
+/// Error type for explorer operations.
+#[derive(Debug)]
+pub enum ExplorerError {
+ PageError(PageError),
+ StorageError(Error),
+ InvalidCellIndex(u8),
+ NodeNotFound,
+ InvalidSearchQuery(String),
+}
+
+impl From for ExplorerError {
+ fn from(e: PageError) -> Self {
+ Self::PageError(e)
+ }
+}
+
+impl From for ExplorerError {
+ fn from(e: Error) -> Self {
+ Self::StorageError(e)
+ }
+}
+
+impl std::fmt::Display for ExplorerError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ ExplorerError::PageError(e) => write!(f, "Page error: {:?}", e),
+ ExplorerError::StorageError(e) => write!(f, "Storage error: {:?}", e),
+ ExplorerError::InvalidCellIndex(i) => write!(f, "Invalid cell index: {}", i),
+ ExplorerError::NodeNotFound => write!(f, "Node not found"),
+ ExplorerError::InvalidSearchQuery(s) => write!(f, "Invalid search query: {}", s),
+ }
+ }
+}
+
+impl std::error::Error for ExplorerError {}
+
+/// Represents a child pointer entry for branches.
+#[derive(Debug, Clone, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ChildEntry {
+ pub index: u8,
+ pub pointer: ExplorerPointer,
+}
+
+/// Represents a pointer with location information for the UI.
+#[derive(Debug, Clone, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ExplorerPointer {
+ pub location_type: String,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub cell_index: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub page_id: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub rlp_hash: Option,
+}
+
+/// Represents a trie node with its location information for the UI.
+#[derive(Debug, Clone, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ExplorerNode {
+ pub page_id: u32,
+ pub cell_index: u8,
+ pub prefix: String,
+ pub node_type: String,
+ pub size_bytes: usize,
+
+ // AccountLeaf fields
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub nonce: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub balance: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub code_hash: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub storage_root: Option,
+
+ // StorageLeaf fields
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub value: Option,
+
+ // Branch fields
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub children: Option>,
+}
+
+/// Page information for the UI.
+#[derive(Debug, Clone, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct PageInfo {
+ pub page_id: u32,
+ pub snapshot_id: u64,
+ pub total_bytes: usize,
+ pub used_bytes: usize,
+ pub free_bytes: usize,
+ pub cell_count: usize,
+ pub occupancy_percent: f32,
+}
+
+/// Database metadata for the UI.
+#[derive(Debug, Clone, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct DatabaseInfo {
+ pub snapshot_id: u64,
+ pub root_node_hash: String,
+ pub root_node_page_id: Option,
+ pub total_page_count: u32,
+ pub orphaned_pages: Vec,
+}
+
+/// Orphan page information.
+#[derive(Debug, Clone, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct OrphanPageInfo {
+ pub page_id: u32,
+ pub orphaned_at_snapshot: u64,
+}
+
+/// Result of a path search.
+#[derive(Debug, Clone, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct PathSearchResult {
+ pub matched_path: String,
+ pub node: ExplorerNode,
+ pub path_to_node: Vec,
+}
+
+/// Explorer service for querying trie data.
+#[derive(Debug)]
+pub struct ExplorerService<'a> {
+ storage_engine: &'a StorageEngine,
+}
+
+impl<'a> ExplorerService<'a> {
+ /// Creates a new ExplorerService.
+ pub fn new(storage_engine: &'a StorageEngine) -> Self {
+ Self { storage_engine }
+ }
+
+ /// Gets a node at a specific page and cell index.
+ pub fn get_node_at(
+ &self,
+ context: &TransactionContext,
+ page_id: PageId,
+ cell_index: u8,
+ ) -> Result {
+ let slotted_page = self.storage_engine.get_slotted_page(context, page_id)?;
+ let node: Node = slotted_page
+ .get_value(cell_index)
+ .map_err(|_| ExplorerError::InvalidCellIndex(cell_index))?;
+ Ok(self.convert_node(page_id, cell_index, &node))
+ }
+
+ /// Gets all nodes on a page (including detecting orphaned cells).
+ pub fn get_page_nodes(
+ &self,
+ context: &TransactionContext,
+ page_id: PageId,
+ ) -> Result, ExplorerError> {
+ let slotted_page = self.storage_engine.get_slotted_page(context, page_id)?;
+ let num_cells = slotted_page.num_occupied_cells();
+ let mut nodes = Vec::new();
+ let mut found = 0;
+
+ // Iterate through all cell indices, skipping deleted cells
+ for cell_idx in 0..255u8 {
+ if found >= num_cells {
+ break;
+ }
+ match slotted_page.get_value::(cell_idx) {
+ Ok(node) => {
+ nodes.push(self.convert_node(page_id, cell_idx, &node));
+ found += 1;
+ }
+ Err(_) => {
+ // Deleted cell or invalid - continue to next
+ continue;
+ }
+ }
+ }
+ Ok(nodes)
+ }
+
+ /// Gets page metadata.
+ pub fn get_page_info(
+ &self,
+ context: &TransactionContext,
+ page_id: PageId,
+ ) -> Result {
+ let page = self.get_page(context, page_id)?;
+ let snapshot_id = page.snapshot_id();
+ let slotted_page = SlottedPage::try_from(page)?;
+
+ let used_bytes = slotted_page.num_occupied_bytes();
+ let free_bytes = slotted_page.num_free_bytes();
+ let total_bytes = Page::DATA_SIZE;
+ let cell_count = slotted_page.num_occupied_cells();
+
+ Ok(PageInfo {
+ page_id: page_id.as_u32(),
+ snapshot_id,
+ total_bytes,
+ used_bytes,
+ free_bytes,
+ cell_count,
+ occupancy_percent: (used_bytes as f32 / total_bytes as f32) * 100.0,
+ })
+ }
+
+ /// Walks all page-local descendants recursively from a starting cell.
+ pub fn walk_page_local_descendants(
+ &self,
+ context: &TransactionContext,
+ page_id: PageId,
+ start_cell_index: u8,
+ ) -> Result, ExplorerError> {
+ let slotted_page = self.storage_engine.get_slotted_page(context, page_id)?;
+ let mut result = Vec::new();
+ let mut stack = vec![start_cell_index];
+ let mut visited = HashSet::new();
+
+ while let Some(cell_idx) = stack.pop() {
+ if visited.contains(&cell_idx) {
+ continue;
+ }
+ visited.insert(cell_idx);
+
+ let node: Node = match slotted_page.get_value(cell_idx) {
+ Ok(n) => n,
+ Err(_) => continue,
+ };
+ result.push(self.convert_node(page_id, cell_idx, &node));
+
+ // Add same-page children to stack
+ match node.kind() {
+ NodeKind::Branch { children } => {
+ for child in children.iter().flatten() {
+ if let Some(child_cell_idx) = child.location().cell_index() {
+ stack.push(child_cell_idx);
+ }
+ }
+ }
+ NodeKind::AccountLeaf { storage_root, .. } => {
+ if let Some(ptr) = storage_root {
+ if let Some(child_cell_idx) = ptr.location().cell_index() {
+ stack.push(child_cell_idx);
+ }
+ }
+ }
+ NodeKind::StorageLeaf { .. } => {}
+ }
+ }
+ Ok(result)
+ }
+
+ /// Searches by path and returns the matched node along with ancestors.
+ pub fn search_by_path(
+ &self,
+ context: &TransactionContext,
+ path: &Nibbles,
+ ) -> Result