diff --git a/Cargo.lock b/Cargo.lock index 50bbf3a8..21b4fe10 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -361,6 +361,18 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "any_spawner" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1384d3fe1eecb464229fcf6eebb72306591c56bf27b373561489458a7c73027d" +dependencies = [ + "futures", + "thiserror 2.0.17", + "tokio", + "wasm-bindgen-futures", +] + [[package]] name = "anyhow" version = "1.0.100" @@ -782,6 +794,23 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-once-cell" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288f83726785267c6f2ef073a3d83dc3f9b81464e9f99898240cced85fce35a" + [[package]] name = "async-stream" version = "0.3.6" @@ -821,6 +850,36 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "attribute-derive" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05832cdddc8f2650cc2cc187cc2e952b8c133a48eb055f35211f61ee81502d77" +dependencies = [ + "attribute-derive-macro", + "derive-where", + "manyhow", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "attribute-derive-macro" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a7cdbbd4bd005c5d3e2e9c885e6fa575db4f4a3572335b974d8db853b6beb61" +dependencies = [ + "collection_literals", + "interpolator", + "manyhow", + "proc-macro-utils", + "proc-macro2", + "quote", + "quote-use", + "syn 2.0.111", +] + [[package]] name = "autocfg" version = "1.5.0" @@ -834,7 +893,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" dependencies = [ "async-trait", - "axum-core", + "axum-core 0.4.5", "bytes", "futures-util", "http 1.4.0", @@ -843,7 +902,7 @@ dependencies = [ "hyper", "hyper-util", "itoa", - "matchit", + "matchit 0.7.3", "memchr", "mime", "percent-encoding", @@ -861,6 +920,43 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" +dependencies = [ + "axum-core 0.5.6", + "base64", + "bytes", + "form_urlencoded", + "futures-util", + "http 1.4.0", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit 0.8.4", + "memchr", + "mime", + "multer", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sha1", + "sync_wrapper", + "tokio", + "tokio-tungstenite", + "tower 0.5.2", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "axum-core" version = "0.4.5" @@ -882,12 +978,37 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http 1.4.0", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "base-x" version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" +[[package]] +name = "base16" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27c3610c36aee21ce8ac510e6224498de4228ad772a171ed65643a24693a5a8" + [[package]] name = "base16ct" version = "0.2.0" @@ -900,7 +1021,7 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e9430d9a245a77c92176e649af6e275f20839a48389859d1661e9a128d077c" dependencies = [ - "const-str", + "const-str 0.4.3", "match-lookup", ] @@ -1112,9 +1233,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" dependencies = [ "serde", ] @@ -1186,7 +1307,7 @@ dependencies = [ "serde_json", "syn 2.0.111", "tempfile", - "toml 0.9.9+spec-1.0.0", + "toml 0.9.11+spec-1.1.0", ] [[package]] @@ -1334,6 +1455,23 @@ dependencies = [ "thiserror 2.0.17", ] +[[package]] +name = "codee" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9dbbdc4b4d349732bc6690de10a9de952bd39ba6a065c586e26600b6b0b91f5" +dependencies = [ + "serde", + "serde_json", + "thiserror 2.0.17", +] + +[[package]] +name = "collection_literals" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2550f75b8cfac212855f6b1885455df8eaee8fe8e246b647d69146142e016084" + [[package]] name = "colorchoice" version = "1.0.4" @@ -1370,6 +1508,28 @@ dependencies = [ "url", ] +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "config" +version = "0.15.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b30fa8254caad766fc03cb0ccae691e14bf3bd72bfff27f72802ce729551b3d6" +dependencies = [ + "convert_case 0.6.0", + "pathdiff", + "serde_core", + "toml 0.9.11+spec-1.1.0", + "winnow", +] + [[package]] name = "console" version = "0.16.2" @@ -1383,6 +1543,26 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "console_log" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be8aed40e4edbf4d3b4431ab260b63fdc40f5780a4766824329ea0f1eefe3c0f" +dependencies = [ + "log", + "web-sys", +] + [[package]] name = "const-hex" version = "1.17.0" @@ -1407,12 +1587,53 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f421161cb492475f1661ddc9815a745a1c894592070661180fdec3d4872e9c3" +[[package]] +name = "const-str" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0664d2867b4a32697dfe655557f5c3b187e9b605b38612a748e5ec99811d160" + +[[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 = "const_str_slice_concat" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f67855af358fcb20fac58f9d714c94e2b228fe5694c1c9b4ead4a366343eda1b" + [[package]] name = "convert_case" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "convert_case" version = "0.8.0" @@ -1650,6 +1871,20 @@ dependencies = [ "parking_lot_core", ] +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "data-encoding" version = "2.9.0" @@ -1673,7 +1908,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976" dependencies = [ "data-encoding", - "syn 2.0.111", + "syn 1.0.109", ] [[package]] @@ -1708,6 +1943,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "derive-where" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef941ded77d15ca19b40374869ac6000af1c9f2a4c0f3d4c70926287e6364a8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "derive_builder" version = "0.20.2" @@ -1831,6 +2077,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" +[[package]] +name = "drain_filter_polyfill" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "669a445ee724c5c69b1b06fe0b63e70a1c84bc9bb7d9696cd4f4e3ec45050408" + [[package]] name = "duplicate" version = "2.0.1" @@ -1906,6 +2158,16 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "either_of" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216d23e0ec69759a17f05e1c553f3a6870e5ec73420fbb07807a6f34d5d1d5a4" +dependencies = [ + "paste", + "pin-project-lite", +] + [[package]] name = "elf" version = "0.7.4" @@ -1999,6 +2261,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "erased" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1731451909bde27714eacba19c2566362a7f35224f52b153d3f42cf60f72472" + [[package]] name = "errno" version = "0.3.14" @@ -2009,6 +2277,27 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + [[package]] name = "example_program_deployment_methods" version = "0.1.0" @@ -2026,6 +2315,33 @@ dependencies = [ "risc0-zkvm", ] +[[package]] +name = "explorer_service" +version = "0.1.0" +dependencies = [ + "axum 0.8.8", + "chrono", + "clap", + "console_error_panic_hook", + "console_log", + "env_logger", + "hex", + "indexer_service_protocol", + "indexer_service_rpc", + "jsonrpsee", + "leptos", + "leptos_axum", + "leptos_meta", + "leptos_router", + "log", + "serde", + "tokio", + "url", + "urlencoding", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -2167,6 +2483,7 @@ dependencies = [ "futures-core", "futures-task", "futures-util", + "num_cpus", ] [[package]] @@ -2205,7 +2522,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" dependencies = [ "gloo-timers", - "send_wrapper", + "send_wrapper 0.4.0", ] [[package]] @@ -2344,7 +2661,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68a7f542ee6b35af73b06abc0dad1c1bae89964e4e253bc4b587b91c9637867b" dependencies = [ "cfg-if", - "dashmap", + "dashmap 5.5.3", "futures", "futures-timer", "no-std-compat", @@ -2368,6 +2685,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "guardian" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17e2ac29387b1aa07a1e448f7bb4f35b500787971e965b02842b900afa5c8f6f" + [[package]] name = "h2" version = "0.3.27" @@ -2515,6 +2838,15 @@ version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89e8d20b3799fa526152a5301a771eaaad80857f83e01b23216ceaafb2d9280" +[[package]] +name = "html-escape" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476" +dependencies = [ + "utf8-width", +] + [[package]] name = "http" version = "0.2.12" @@ -2559,6 +2891,12 @@ dependencies = [ "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" @@ -2577,6 +2915,22 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" +[[package]] +name = "hydration_context" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8714ae4adeaa846d838f380fbd72f049197de629948f91bf045329e0cf0a283" +dependencies = [ + "futures", + "js-sys", + "once_cell", + "or_poisoned", + "pin-project-lite", + "serde", + "throw_error", + "wasm-bindgen", +] + [[package]] name = "hyper" version = "1.8.1" @@ -2928,6 +3282,21 @@ dependencies = [ "wallet-ffi", ] +[[package]] +name = "interpolator" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71dd52191aae121e8611f1e8dc3e324dd0dd1dee1e6dd91d10ee07a3cfb4d9d8" + +[[package]] +name = "inventory" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc61209c082fbeb19919bee74b176221b27223e27b65d781eb91af24eb1fb46e" +dependencies = [ + "rustversion", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -3316,6 +3685,228 @@ dependencies = [ "spin", ] +[[package]] +name = "leptos" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f9569fc37575a5d64c0512145af7630bf651007237ef67a8a77328199d315bb" +dependencies = [ + "any_spawner", + "base64", + "cfg-if", + "either_of", + "futures", + "getrandom 0.3.4", + "hydration_context", + "leptos_config", + "leptos_dom", + "leptos_hot_reload", + "leptos_macro", + "leptos_server", + "oco_ref", + "or_poisoned", + "paste", + "rand 0.9.2", + "reactive_graph", + "rustc-hash", + "rustc_version", + "send_wrapper 0.6.0", + "serde", + "serde_json", + "serde_qs", + "server_fn", + "slotmap", + "tachys", + "thiserror 2.0.17", + "throw_error", + "typed-builder 0.23.2", + "typed-builder-macro 0.23.2", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm_split_helpers", + "web-sys", +] + +[[package]] +name = "leptos_axum" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0caa95760f87f3067e05025140becefdbdfd36cbc2adac4519f06e1f1edf4af" +dependencies = [ + "any_spawner", + "axum 0.8.8", + "dashmap 6.1.0", + "futures", + "hydration_context", + "leptos", + "leptos_integration_utils", + "leptos_macro", + "leptos_meta", + "leptos_router", + "parking_lot", + "server_fn", + "tachys", + "tokio", + "tower 0.5.2", + "tower-http", +] + +[[package]] +name = "leptos_config" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071fc40aeb9fcab885965bad1887990477253ad51f926cd19068f45a44c59e89" +dependencies = [ + "config", + "regex", + "serde", + "thiserror 2.0.17", + "typed-builder 0.21.2", +] + +[[package]] +name = "leptos_dom" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78f4330c88694c5575e0bfe4eecf81b045d14e76a4f8b00d5fd2a63f8779f895" +dependencies = [ + "js-sys", + "or_poisoned", + "reactive_graph", + "send_wrapper 0.6.0", + "tachys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "leptos_hot_reload" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d61ec3e1ff8aaee8c5151688550c0363f85bc37845450764c31ff7584a33f38" +dependencies = [ + "anyhow", + "camino", + "indexmap 2.12.1", + "parking_lot", + "proc-macro2", + "quote", + "rstml", + "serde", + "syn 2.0.111", + "walkdir", +] + +[[package]] +name = "leptos_integration_utils" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13cccc9305df53757bae61bf15641bfa6a667b5f78456ace4879dfe0591ae0e8" +dependencies = [ + "futures", + "hydration_context", + "leptos", + "leptos_config", + "leptos_meta", + "leptos_router", + "reactive_graph", +] + +[[package]] +name = "leptos_macro" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c86ffd2e9cf3e264e9b3e16bdb086cefa26bd0fa7bc6a26b0cc5f6c1fd3178ed" +dependencies = [ + "attribute-derive", + "cfg-if", + "convert_case 0.10.0", + "html-escape", + "itertools 0.14.0", + "leptos_hot_reload", + "prettyplease", + "proc-macro-error2", + "proc-macro2", + "quote", + "rstml", + "rustc_version", + "server_fn_macro", + "syn 2.0.111", + "uuid", +] + +[[package]] +name = "leptos_meta" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d489e38d3f541e9e43ecc2e3a815527840345a2afca629b3e23fcc1dd254578" +dependencies = [ + "futures", + "indexmap 2.12.1", + "leptos", + "or_poisoned", + "send_wrapper 0.6.0", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "leptos_router" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01e573711f2fb9ab5d655ec38115220d359eaaf1dcb93cc0ea624543b6dba959" +dependencies = [ + "any_spawner", + "either_of", + "futures", + "gloo-net", + "js-sys", + "leptos", + "leptos_router_macro", + "or_poisoned", + "percent-encoding", + "reactive_graph", + "rustc_version", + "send_wrapper 0.6.0", + "tachys", + "thiserror 2.0.17", + "url", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "leptos_router_macro" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "409c0bd99f986c3cfa1a4db2443c835bc602ded1a12784e22ecb28c3ed5a2ae2" +dependencies = [ + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "leptos_server" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbf1045af93050bf3388d1c138426393fc131f6d9e46a65519da884c033ed730" +dependencies = [ + "any_spawner", + "base64", + "codee", + "futures", + "hydration_context", + "or_poisoned", + "reactive_graph", + "send_wrapper 0.6.0", + "serde", + "serde_json", + "server_fn", + "tachys", +] + [[package]] name = "libc" version = "0.2.178" @@ -3398,6 +3989,12 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "linear-map" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfae20f6b19ad527b550c223fddc3077a547fc70cda94b9b566575423fd303ee" + [[package]] name = "linux-raw-sys" version = "0.11.0" @@ -3657,7 +4254,7 @@ name = "logos-blockchain-http-api-common" version = "0.1.0" source = "git+https://github.com/logos-blockchain/logos-blockchain.git#451df112f8574aea2840d04fffb7e16e76d24f42" dependencies = [ - "axum", + "axum 0.7.9", "governor", "logos-blockchain-core", "logos-blockchain-key-management-system-keys", @@ -3934,6 +4531,29 @@ dependencies = [ "libc", ] +[[package]] +name = "manyhow" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b33efb3ca6d3b07393750d4030418d594ab1139cee518f0dc88db70fec873587" +dependencies = [ + "manyhow-macros", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "manyhow-macros" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46fce34d199b78b6e6073abf984c9cf5fd3e9330145a93ee0738a7443e371495" +dependencies = [ + "proc-macro-utils", + "proc-macro2", + "quote", +] + [[package]] name = "match-lookup" version = "0.1.1" @@ -3951,6 +4571,12 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "maybe-async" version = "0.2.10" @@ -4008,6 +4634,16 @@ 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 = "minimal-lexical" version = "0.2.1" @@ -4026,6 +4662,23 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "multer" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http 1.4.0", + "httparse", + "memchr", + "mime", + "spin", + "version_check", +] + [[package]] name = "multiaddr" version = "0.18.2" @@ -4083,6 +4736,12 @@ dependencies = [ "tempfile", ] +[[package]] +name = "next_tuple" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60993920e071b0c9b66f14e2b32740a4e27ffc82854dcd72035887f336a09a28" + [[package]] name = "nimue" version = "0.1.1" @@ -4212,9 +4871,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" [[package]] name = "num-integer" @@ -4246,6 +4905,16 @@ dependencies = [ "libm", ] +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "num_enum" version = "0.7.5" @@ -4276,6 +4945,16 @@ dependencies = [ "malloc_buf", ] +[[package]] +name = "oco_ref" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed0423ff9973dea4d6bd075934fdda86ebb8c05bdf9d6b0507067d4a1226371d" +dependencies = [ + "serde", + "thiserror 2.0.17", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -4361,6 +5040,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "or_poisoned" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c04f5d74368e4d0dfe06c45c8627c81bd7c317d52762d118fb9b3076f6420fd" + [[package]] name = "overwatch" version = "0.1.0" @@ -4388,6 +5073,12 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.5" @@ -4417,6 +5108,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -4545,6 +5242,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.111", +] + [[package]] name = "proc-macro-crate" version = "3.4.0" @@ -4576,6 +5283,17 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "proc-macro-utils" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeaf08a13de400bc215877b5bdc088f241b12eb42f0a548d3390dc1c56bb7071" +dependencies = [ + "proc-macro2", + "quote", + "smallvec", +] + [[package]] name = "proc-macro2" version = "1.0.103" @@ -4595,6 +5313,7 @@ dependencies = [ "quote", "syn 2.0.111", "version_check", + "yansi", ] [[package]] @@ -4742,6 +5461,28 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "quote-use" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9619db1197b497a36178cfc736dc96b271fe918875fbf1344c436a7e93d0321e" +dependencies = [ + "quote", + "quote-use-macros", +] + +[[package]] +name = "quote-use-macros" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82ebfb7faafadc06a7ab141a6f67bcfb24cb8beb158c6fe933f2f035afa99f35" +dependencies = [ + "proc-macro-utils", + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "r-efi" version = "5.3.0" @@ -4825,6 +5566,60 @@ dependencies = [ "bitflags 2.10.0", ] +[[package]] +name = "reactive_graph" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17f0df355582937223ea403e52490201d65295bd6981383c69bfae5a1f8730c2" +dependencies = [ + "any_spawner", + "async-lock", + "futures", + "guardian", + "hydration_context", + "indexmap 2.12.1", + "or_poisoned", + "paste", + "pin-project-lite", + "rustc-hash", + "rustc_version", + "send_wrapper 0.6.0", + "serde", + "slotmap", + "thiserror 2.0.17", + "web-sys", +] + +[[package]] +name = "reactive_stores" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35372f05664a62a3dd389503371a15b8feb3396f99f6ec000de651fddb030942" +dependencies = [ + "dashmap 6.1.0", + "guardian", + "itertools 0.14.0", + "or_poisoned", + "paste", + "reactive_graph", + "reactive_stores_macro", + "rustc-hash", + "send_wrapper 0.6.0", +] + +[[package]] +name = "reactive_stores_macro" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fa40919eb2975100283b2a70e68eafce1e8bcf81f0622ff168e4c2b3f8d46bb" +dependencies = [ + "convert_case 0.8.0", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -5261,11 +6056,26 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rstml" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61cf4616de7499fc5164570d40ca4e1b24d231c6833a88bff0fe00725080fd56" +dependencies = [ + "derive-where", + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn 2.0.111", + "syn_derive", + "thiserror 2.0.17", +] + [[package]] name = "ruint" -version = "1.17.0" +version = "1.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a68df0380e5c9d20ce49534f292a36a7514ae21350726efe1865bdb1fa91d278" +checksum = "c141e807189ad38a07276942c6623032d3753c8859c146104ac2e4d68865945a" dependencies = [ "borsh", "proptest", @@ -5565,6 +6375,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f638d531eccd6e23b980caf34876660d38e265409d8e99b397ab71eb3612fad0" +[[package]] +name = "send_wrapper" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" +dependencies = [ + "futures-core", +] + [[package]] name = "sequencer_core" version = "0.1.0" @@ -5705,6 +6524,17 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_qs" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3faaf9e727533a19351a43cc5a8de957372163c7d35cc48c90b75cdda13c352" +dependencies = [ + "percent-encoding", + "serde", + "thiserror 2.0.17", +] + [[package]] name = "serde_spanned" version = "0.6.9" @@ -5776,6 +6606,71 @@ dependencies = [ "serde", ] +[[package]] +name = "server_fn" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353d02fa2886cd8dae0b8da0965289fa8f2ecc7df633d1ce965f62fdf9644d29" +dependencies = [ + "axum 0.8.8", + "base64", + "bytes", + "const-str 0.7.1", + "const_format", + "dashmap 6.1.0", + "futures", + "gloo-net", + "http 1.4.0", + "http-body-util", + "hyper", + "inventory", + "js-sys", + "pin-project-lite", + "rustc_version", + "rustversion", + "send_wrapper 0.6.0", + "serde", + "serde_json", + "serde_qs", + "server_fn_macro_default", + "thiserror 2.0.17", + "throw_error", + "tokio", + "tower 0.5.2", + "tower-layer", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "xxhash-rust", +] + +[[package]] +name = "server_fn_macro" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "950b8cfc9ff5f39ca879c5a7c5e640de2695a199e18e424c3289d0964cabe642" +dependencies = [ + "const_format", + "convert_case 0.8.0", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.111", + "xxhash-rust", +] + +[[package]] +name = "server_fn_macro_default" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63eb08f80db903d3c42f64e60ebb3875e0305be502bdc064ec0a0eab42207f00" +dependencies = [ + "server_fn_macro", + "syn 2.0.111", +] + [[package]] name = "sha1" version = "0.10.6" @@ -5829,6 +6724,15 @@ version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + [[package]] name = "smallvec" version = "1.15.1" @@ -6004,6 +6908,18 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb066a04799e45f5d582e8fc6ec8e6d6896040d00898eb4e6a835196815b219" +dependencies = [ + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "sync_wrapper" version = "1.0.2" @@ -6045,6 +6961,40 @@ dependencies = [ "libc", ] +[[package]] +name = "tachys" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b2db11e455f7e84e2cc3e76f8a3f3843f7956096265d5ecff781eabe235077" +dependencies = [ + "any_spawner", + "async-trait", + "const_str_slice_concat", + "drain_filter_polyfill", + "either_of", + "erased", + "futures", + "html-escape", + "indexmap 2.12.1", + "itertools 0.14.0", + "js-sys", + "linear-map", + "next_tuple", + "oco_ref", + "or_poisoned", + "parking_lot", + "paste", + "reactive_graph", + "reactive_stores", + "rustc-hash", + "rustc_version", + "send_wrapper 0.6.0", + "slotmap", + "throw_error", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "tempfile" version = "3.23.0" @@ -6155,32 +7105,41 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "throw_error" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc0ed6038fcbc0795aca7c92963ddda636573b956679204e044492d2b13c8f64" +dependencies = [ + "pin-project-lite", +] + [[package]] name = "time" -version = "0.3.44" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", "num-conv", "powerfmt", - "serde", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.24" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", @@ -6308,6 +7267,18 @@ dependencies = [ "tokio-util", ] +[[package]] +name = "tokio-tungstenite" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + [[package]] name = "tokio-util" version = "0.7.18" @@ -6336,14 +7307,14 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.9+spec-1.0.0" +version = "0.9.11+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb5238e643fc34a1d5d7e753e1532a91912d74b63b92b3ea51fde8d1b7bc79dd" +checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46" dependencies = [ "indexmap 2.12.1", "serde_core", "serde_spanned 1.0.4", - "toml_datetime 0.7.4+spec-1.0.0", + "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", "toml_writer", "winnow", @@ -6360,9 +7331,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.4+spec-1.0.0" +version = "0.7.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe3cea6b2aa3b910092f6abd4053ea464fab5f9c170ba5e9a6aead16ec4af2b6" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" dependencies = [ "serde_core", ] @@ -6388,16 +7359,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" dependencies = [ "indexmap 2.12.1", - "toml_datetime 0.7.4+spec-1.0.0", + "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", "winnow", ] [[package]] name = "toml_parser" -version = "1.0.5+spec-1.0.0" +version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c03bee5ce3696f31250db0bbaff18bc43301ce0e8db2ed1f07cbb2acf89984c" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" dependencies = [ "winnow", ] @@ -6449,14 +7420,24 @@ checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ "bitflags 2.10.0", "bytes", + "futures-core", "futures-util", "http 1.4.0", "http-body", + "http-body-util", + "http-range-header", + "httpdate", "iri-string", + "mime", + "mime_guess", + "percent-encoding", "pin-project-lite", + "tokio", + "tokio-util", "tower 0.5.2", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -6477,7 +7458,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3790eac6ad3fb8d9d96c2b040ae06e2517aa24b067545d1078b96ae72f7bb9a7" dependencies = [ - "axum", + "axum 0.7.9", "forwarded-header-value", "governor", "http 1.4.0", @@ -6551,6 +7532,63 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "tungstenite" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" +dependencies = [ + "bytes", + "data-encoding", + "http 1.4.0", + "httparse", + "log", + "rand 0.9.2", + "sha1", + "thiserror 2.0.17", + "utf-8", +] + +[[package]] +name = "typed-builder" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fef81aec2ca29576f9f6ae8755108640d0a86dd3161b2e8bca6cfa554e98f77d" +dependencies = [ + "typed-builder-macro 0.21.2", +] + +[[package]] +name = "typed-builder" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31aa81521b70f94402501d848ccc0ecaa8f93c8eb6999eb9747e72287757ffda" +dependencies = [ + "typed-builder-macro 0.23.2", +] + +[[package]] +name = "typed-builder-macro" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ecb9ecf7799210407c14a8cfdfe0173365780968dc57973ed082211958e0b18" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "typed-builder-macro" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "076a02dc54dd46795c2e9c8282ed40bcfb1e22747e955de9389a1de28190fb26" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "typenum" version = "1.19.0" @@ -6563,6 +7601,12 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + [[package]] name = "unicode-ident" version = "1.0.22" @@ -6636,6 +7680,24 @@ dependencies = [ "serde", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -6648,6 +7710,17 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" +dependencies = [ + "getrandom 0.3.4", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "valuable" version = "0.1.1" @@ -6816,6 +7889,28 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wasm_split_helpers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a114b3073258dd5de3d812cdd048cca6842342755e828a14dbf15f843f2d1b84" +dependencies = [ + "async-once-cell", + "wasm_split_macros", +] + +[[package]] +name = "wasm_split_macros" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56481f8ed1a9f9ae97ea7b08a5e2b12e8adf9a7818a6ba952b918e09c7be8bf0" +dependencies = [ + "base16", + "quote", + "sha2", + "syn 2.0.111", +] + [[package]] name = "web-sys" version = "0.3.83" @@ -7219,6 +8314,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "xxhash-rust" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" + [[package]] name = "yaml-rust2" version = "0.10.4" @@ -7230,6 +8331,12 @@ dependencies = [ "hashlink", ] +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + [[package]] name = "yoke" version = "0.8.1" diff --git a/Cargo.toml b/Cargo.toml index ad2d9276..bf17e885 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ members = [ "indexer_service", "indexer_service/protocol", "indexer_service/rpc", + "explorer_service", "programs/token/core", "programs/token", "program_methods", @@ -123,3 +124,10 @@ actix-web = { version = "=4.1.0", default-features = false, features = [ ] } clap = { version = "4.5.42", features = ["derive", "env"] } reqwest = { version = "0.12", features = ["json", "rustls-tls", "stream"] } + +# Profile for leptos WASM release builds +[profile.wasm-release] +inherits = "release" +opt-level = 'z' +lto = true +codegen-units = 1 diff --git a/artifacts/program_methods/amm.bin b/artifacts/program_methods/amm.bin index e7f89d53..a9b6a4c8 100644 Binary files a/artifacts/program_methods/amm.bin and b/artifacts/program_methods/amm.bin differ diff --git a/artifacts/program_methods/authenticated_transfer.bin b/artifacts/program_methods/authenticated_transfer.bin index 480ea8d0..3a032598 100644 Binary files a/artifacts/program_methods/authenticated_transfer.bin and b/artifacts/program_methods/authenticated_transfer.bin differ diff --git a/artifacts/program_methods/pinata.bin b/artifacts/program_methods/pinata.bin index 87f779f8..34e19e61 100644 Binary files a/artifacts/program_methods/pinata.bin and b/artifacts/program_methods/pinata.bin differ diff --git a/artifacts/program_methods/pinata_token.bin b/artifacts/program_methods/pinata_token.bin index 460dbee0..d0dbbb97 100644 Binary files a/artifacts/program_methods/pinata_token.bin and b/artifacts/program_methods/pinata_token.bin differ diff --git a/artifacts/program_methods/privacy_preserving_circuit.bin b/artifacts/program_methods/privacy_preserving_circuit.bin index edd0f126..68837350 100644 Binary files a/artifacts/program_methods/privacy_preserving_circuit.bin and b/artifacts/program_methods/privacy_preserving_circuit.bin differ diff --git a/artifacts/program_methods/token.bin b/artifacts/program_methods/token.bin index f091c55e..1f71d2eb 100644 Binary files a/artifacts/program_methods/token.bin and b/artifacts/program_methods/token.bin differ diff --git a/artifacts/test_program_methods/burner.bin b/artifacts/test_program_methods/burner.bin index bce7959d..f9102842 100644 Binary files a/artifacts/test_program_methods/burner.bin and b/artifacts/test_program_methods/burner.bin differ diff --git a/artifacts/test_program_methods/chain_caller.bin b/artifacts/test_program_methods/chain_caller.bin index 2f35f7f8..f7cab3a8 100644 Binary files a/artifacts/test_program_methods/chain_caller.bin and b/artifacts/test_program_methods/chain_caller.bin differ diff --git a/artifacts/test_program_methods/changer_claimer.bin b/artifacts/test_program_methods/changer_claimer.bin index baf09a17..391ec799 100644 Binary files a/artifacts/test_program_methods/changer_claimer.bin and b/artifacts/test_program_methods/changer_claimer.bin differ diff --git a/artifacts/test_program_methods/claimer.bin b/artifacts/test_program_methods/claimer.bin index 37eb35d0..6f3980f9 100644 Binary files a/artifacts/test_program_methods/claimer.bin and b/artifacts/test_program_methods/claimer.bin differ diff --git a/artifacts/test_program_methods/data_changer.bin b/artifacts/test_program_methods/data_changer.bin index 457e1fd1..55e7bc6c 100644 Binary files a/artifacts/test_program_methods/data_changer.bin and b/artifacts/test_program_methods/data_changer.bin differ diff --git a/artifacts/test_program_methods/extra_output.bin b/artifacts/test_program_methods/extra_output.bin index cccc75dd..0c17c48a 100644 Binary files a/artifacts/test_program_methods/extra_output.bin and b/artifacts/test_program_methods/extra_output.bin differ diff --git a/artifacts/test_program_methods/malicious_authorization_changer.bin b/artifacts/test_program_methods/malicious_authorization_changer.bin index 791c749f..ba7db365 100644 Binary files a/artifacts/test_program_methods/malicious_authorization_changer.bin and b/artifacts/test_program_methods/malicious_authorization_changer.bin differ diff --git a/artifacts/test_program_methods/minter.bin b/artifacts/test_program_methods/minter.bin index 81a79926..81c99134 100644 Binary files a/artifacts/test_program_methods/minter.bin and b/artifacts/test_program_methods/minter.bin differ diff --git a/artifacts/test_program_methods/missing_output.bin b/artifacts/test_program_methods/missing_output.bin index 6c62c017..f572be63 100644 Binary files a/artifacts/test_program_methods/missing_output.bin and b/artifacts/test_program_methods/missing_output.bin differ diff --git a/artifacts/test_program_methods/modified_transfer.bin b/artifacts/test_program_methods/modified_transfer.bin index 801761d8..a2d93354 100644 Binary files a/artifacts/test_program_methods/modified_transfer.bin and b/artifacts/test_program_methods/modified_transfer.bin differ diff --git a/artifacts/test_program_methods/nonce_changer.bin b/artifacts/test_program_methods/nonce_changer.bin index 305a0d0e..a89074ed 100644 Binary files a/artifacts/test_program_methods/nonce_changer.bin and b/artifacts/test_program_methods/nonce_changer.bin differ diff --git a/artifacts/test_program_methods/noop.bin b/artifacts/test_program_methods/noop.bin index 667e575d..a5eb6e69 100644 Binary files a/artifacts/test_program_methods/noop.bin and b/artifacts/test_program_methods/noop.bin differ diff --git a/artifacts/test_program_methods/program_owner_changer.bin b/artifacts/test_program_methods/program_owner_changer.bin index 8c0c80b6..73ca6039 100644 Binary files a/artifacts/test_program_methods/program_owner_changer.bin and b/artifacts/test_program_methods/program_owner_changer.bin differ diff --git a/artifacts/test_program_methods/simple_balance_transfer.bin b/artifacts/test_program_methods/simple_balance_transfer.bin index e0e1ffed..e712af38 100644 Binary files a/artifacts/test_program_methods/simple_balance_transfer.bin and b/artifacts/test_program_methods/simple_balance_transfer.bin differ diff --git a/explorer_service/.gitignore b/explorer_service/.gitignore new file mode 100644 index 00000000..49015de2 --- /dev/null +++ b/explorer_service/.gitignore @@ -0,0 +1,11 @@ +# Leptos build outputs +/target +/pkg +/site + +# WASM artifacts +*.wasm + +# Environment +.env +.env.local diff --git a/explorer_service/Cargo.toml b/explorer_service/Cargo.toml new file mode 100644 index 00000000..49d1ddce --- /dev/null +++ b/explorer_service/Cargo.toml @@ -0,0 +1,73 @@ +[package] +name = "explorer_service" +version = "0.1.0" +edition = "2024" +license.workspace = true + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +indexer_service_protocol.workspace = true + +# Leptos framework +leptos = "0.8.15" +leptos_meta = "0.8.5" +leptos_router = "0.8.11" + +# Serialization +serde.workspace = true + +# Logging +log.workspace = true +console_error_panic_hook = "0.1" +console_log = "1.0" + +# Date/Time +chrono.workspace = true + +# Hex encoding/decoding +hex.workspace = true + +# URL encoding +urlencoding = "2.1" + +# WASM-specific +wasm-bindgen = "0.2" +web-sys = { version = "0.3", features = [ + "Window", + "Document", + "Location", + "HtmlInputElement", +] } + +# Server-side dependencies (optional, enabled by features) +indexer_service_rpc = { workspace = true, features = [ + "client", +], optional = true } +jsonrpsee = { workspace = true, features = ["http-client"], optional = true } +tokio = { workspace = true, optional = true } +axum = { version = "0.8.8", optional = true } +leptos_axum = { version = "0.8.7", optional = true } +clap = { workspace = true, features = ["derive"], optional = true } +url = { workspace = true, optional = true } +env_logger = { workspace = true, optional = true } + +[features] +hydrate = ["leptos/hydrate"] +ssr = [ + "leptos/ssr", + "dep:indexer_service_rpc", + "dep:jsonrpsee", + "dep:tokio", + "dep:axum", + "dep:leptos_axum", + "dep:clap", + "dep:url", + "dep:env_logger", +] + +[package.metadata.leptos] +bin-features = ["ssr"] +lib-features = ["hydrate"] +assets-dir = "public" diff --git a/explorer_service/Dockerfile b/explorer_service/Dockerfile new file mode 100644 index 00000000..e10c5ebe --- /dev/null +++ b/explorer_service/Dockerfile @@ -0,0 +1,52 @@ +FROM rust:1.91.1-trixie AS builder + +# Install cargo-binstall, which makes it easier to install other +# cargo extensions like cargo-leptos +RUN wget https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-unknown-linux-musl.tgz +RUN tar -xvf cargo-binstall-x86_64-unknown-linux-musl.tgz +RUN cp cargo-binstall /usr/local/cargo/bin + +# Install required tools +RUN apt-get update -y \ + && apt-get install -y --no-install-recommends clang + +# Install cargo-leptos +RUN cargo binstall cargo-leptos -y + +# Add the WASM target +RUN rustup target add wasm32-unknown-unknown + +# Make an /explorer_service dir, which everything will eventually live in +RUN mkdir -p /explorer_service +WORKDIR /explorer_service +COPY . . + +# Build the app +RUN cargo leptos build --release -vv + +FROM debian:trixie-slim AS runtime +WORKDIR /explorer_service +RUN apt-get update -y \ + && apt-get install -y --no-install-recommends openssl ca-certificates \ + && apt-get autoremove -y \ + && apt-get clean -y \ + && rm -rf /var/lib/apt/lists/* + +# Copy the server binary to the /explorer_service directory +COPY --from=builder /explorer_service/target/release/explorer_service /explorer_service/ + +# /target/site contains our JS/WASM/CSS, etc. +COPY --from=builder /explorer_service/target/site /explorer_service/site + +# Copy Cargo.toml as it’s needed at runtime +COPY --from=builder /explorer_service/Cargo.toml /explorer_service/ + +# Set any required env variables +ENV RUST_LOG="info" +ENV LEPTOS_SITE_ADDR="0.0.0.0:8080" +ENV LEPTOS_SITE_ROOT="site" +ENV INDEXER_RPC_URL="http://localhost:8779" +EXPOSE 8080 + +# Run the server +CMD ["/explorer_service/explorer_service"] diff --git a/explorer_service/README.md b/explorer_service/README.md new file mode 100644 index 00000000..6f118dbb --- /dev/null +++ b/explorer_service/README.md @@ -0,0 +1,71 @@ +# LEE Blockchain Explorer + +A web-based UI for exploring the blockchain state, built with Rust and Leptos framework. + +## Features + +- **Main Page**: Search for blocks, transactions, or accounts by hash/ID. View recent blocks. +- **Block Page**: View detailed block information and all transactions within a block. +- **Transaction Page**: View transaction details including type, accounts involved, and proofs. +- **Account Page**: View account state and transaction history. + +## Architecture + +- **Framework**: Leptos 0.8 with SSR (Server-Side Rendering) and hydration +- **Data Source**: Indexer Service JSON-RPC API +- **Components**: Reusable BlockPreview, TransactionPreview, and AccountPreview components +- **Styling**: Custom CSS with responsive design + +## Development + +### Prerequisites + +- Rust (stable or nightly) +- `cargo-leptos` tool: `cargo install cargo-leptos` +- Running indexer service at `http://localhost:8080/rpc` (or configure via `INDEXER_RPC_URL`) + +### Build and Run + +```bash +# Development mode (with hot-reload) +cargo leptos watch + +# Production build +cargo leptos build --release + +# Run production build +cargo leptos serve --release +``` + +The explorer will be available at `http://localhost:3000` by default. + +### Configuration + +Set the `INDEXER_RPC_URL` environment variable to point to your indexer service: + +```bash +export INDEXER_RPC_URL=http://localhost:8080/rpc +cargo leptos watch +``` + +## Features + +### Search + +The search bar supports: +- Block IDs (numeric) +- Block hashes (64-character hex) +- Transaction hashes (64-character hex) +- Account IDs (64-character hex) + +### Real-time Updates + +The main page loads recent blocks and can be extended to subscribe to new blocks via WebSocket. + +### Responsive Design + +The UI is mobile-friendly and adapts to different screen sizes. + +## License + +See LICENSE file in the repository root. diff --git a/explorer_service/docker-compose.yml b/explorer_service/docker-compose.yml new file mode 100644 index 00000000..28c4c9c7 --- /dev/null +++ b/explorer_service/docker-compose.yml @@ -0,0 +1,11 @@ +services: + explorer_service: + image: lssa/explorer_service + build: + context: .. + dockerfile: explorer_service/Dockerfile + container_name: explorer_service + environment: + INDEXER_RPC_URL: ${INDEXER_RPC_URL:-http://localhost:8779} + ports: + - "8080:8080" diff --git a/explorer_service/public/explorer.css b/explorer_service/public/explorer.css new file mode 100644 index 00000000..a6415ed5 --- /dev/null +++ b/explorer_service/public/explorer.css @@ -0,0 +1,516 @@ +/* Reset and base styles */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + background-color: #f5f7fa; + color: #2c3e50; + line-height: 1.6; +} + +/* App layout */ +.app { + display: flex; + flex-direction: column; + min-height: 100vh; +} + +.app-header { + background-color: #2c3e50; + color: white; + padding: 1rem 2rem; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.app-nav { + max-width: 1200px; + margin: 0 auto; +} + +.nav-logo { + color: white; + text-decoration: none; + font-size: 1.5rem; + font-weight: bold; +} + +.nav-logo:hover { + color: #3498db; +} + +.app-main { + flex: 1; + max-width: 1200px; + width: 100%; + margin: 0 auto; + padding: 2rem; +} + +.app-footer { + background-color: #34495e; + color: white; + text-align: center; + padding: 1.5rem; + margin-top: 2rem; +} + +/* Page headers */ +.page-header h1 { + font-size: 2rem; + margin-bottom: 1.5rem; + color: #2c3e50; +} + +/* Search section */ +.search-section { + margin-bottom: 3rem; +} + +.search-form { + display: flex; + gap: 0.5rem; + margin-bottom: 1.5rem; +} + +.search-input { + flex: 1; + padding: 0.75rem 1rem; + border: 2px solid #dde4ed; + border-radius: 8px; + font-size: 1rem; + transition: border-color 0.3s; +} + +.search-input:focus { + outline: none; + border-color: #3498db; +} + +.search-button { + padding: 0.75rem 2rem; + background-color: #3498db; + color: white; + border: none; + border-radius: 8px; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: background-color 0.3s; +} + +.search-button:hover { + background-color: #2980b9; +} + +/* Block preview */ +.block-preview { + background-color: white; + border-radius: 8px; + padding: 1.5rem; + margin-bottom: 1rem; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + transition: transform 0.2s, box-shadow 0.2s; +} + +.block-preview:hover { + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); +} + +.block-preview-link { + text-decoration: none; + color: inherit; +} + +.block-preview-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; + padding-bottom: 1rem; + border-bottom: 1px solid #eee; +} + +.block-id .label { + color: #7f8c8d; + font-size: 0.9rem; +} + +.block-id .value { + font-size: 1.5rem; + font-weight: bold; + color: #2c3e50; +} + +.block-status { + padding: 0.5rem 1rem; + border-radius: 20px; + font-size: 0.85rem; + font-weight: 600; +} + +.status-pending { + background-color: #fff3cd; + color: #856404; +} + +.status-safe { + background-color: #d1ecf1; + color: #0c5460; +} + +.status-finalized { + background-color: #d4edda; + color: #155724; +} + +.block-preview-body { + display: grid; + gap: 0.5rem; +} + +.block-field { + display: flex; + gap: 0.5rem; +} + +.field-label { + color: #7f8c8d; + font-weight: 500; +} + +.field-value { + color: #2c3e50; +} + +.hash { + font-family: "Courier New", monospace; + font-size: 0.9rem; + word-break: break-all; +} + +/* Transaction preview */ +.transaction-preview { + background-color: white; + border-radius: 8px; + padding: 1.5rem; + margin-bottom: 1rem; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + transition: transform 0.2s, box-shadow 0.2s; +} + +.transaction-preview:hover { + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); +} + +.transaction-preview-link { + text-decoration: none; + color: inherit; +} + +.transaction-preview-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.75rem; + padding-bottom: 0.75rem; + border-bottom: 1px solid #eee; +} + +.tx-type { + padding: 0.4rem 0.8rem; + border-radius: 16px; + font-size: 0.85rem; + font-weight: 600; + border: 2px solid; +} + +.tx-type-public { + background-color: #e3f2fd; + color: #0d47a1; + border-color: #1976d2; + border-style: solid; +} + +.tx-type-private { + background-color: #ffe0f0; + color: #880e4f; + border-color: #c2185b; + border-style: dashed; + font-style: italic; +} + +.tx-type-deployment { + background-color: #fff3e0; + color: #e65100; + border-color: #ff9800; + border-style: dotted; +} + +.tx-hash { + display: flex; + gap: 0.5rem; + align-items: center; +} + +.transaction-preview-body { + color: #7f8c8d; + font-size: 0.9rem; +} + +/* Account preview */ +.account-preview { + background-color: white; + border-radius: 8px; + padding: 1.5rem; + margin-bottom: 1rem; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + transition: transform 0.2s, box-shadow 0.2s; +} + +.account-preview:hover { + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); +} + +.account-preview-link { + text-decoration: none; + color: inherit; +} + +.account-preview-header { + margin-bottom: 1rem; + padding-bottom: 1rem; + border-bottom: 1px solid #eee; +} + +.account-id .label { + color: #7f8c8d; + font-size: 0.9rem; +} + +.account-id .value { + font-size: 1.2rem; + font-weight: 600; + color: #2c3e50; +} + +.account-preview-body { + display: grid; + gap: 0.5rem; +} + +.account-field { + display: flex; + gap: 0.5rem; +} + +.account-not-found { + color: #e74c3c; + font-style: italic; +} + +/* Detail pages */ +.block-detail, +.transaction-detail, +.account-detail { + background-color: white; + border-radius: 8px; + padding: 2rem; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.block-info, +.transaction-info, +.account-info, +.transaction-details { + margin-bottom: 2rem; +} + +.block-info h2, +.transaction-info h2, +.account-info h2, +.transaction-details h2 { + font-size: 1.5rem; + margin-bottom: 1rem; + color: #2c3e50; +} + +.info-grid { + display: grid; + gap: 1rem; +} + +.info-row { + display: flex; + gap: 1rem; + padding: 0.75rem; + background-color: #f8f9fa; + border-radius: 4px; +} + +.info-label { + color: #7f8c8d; + font-weight: 600; + min-width: 150px; +} + +.info-value { + color: #2c3e50; + word-break: break-all; +} + +.signature { + font-size: 0.75rem; +} + +/* Transactions list */ +.block-transactions, +.account-transactions { + margin-top: 2rem; +} + +.block-transactions h2, +.account-transactions h2 { + font-size: 1.5rem; + margin-bottom: 1rem; + color: #2c3e50; +} + +.transactions-list { + display: grid; + gap: 1rem; +} + +.no-transactions { + padding: 2rem; + text-align: center; + color: #7f8c8d; + background-color: #f8f9fa; + border-radius: 8px; +} + +/* Accounts list */ +.accounts-list { + display: grid; + gap: 0.5rem; + margin-top: 1rem; +} + +.account-item { + padding: 0.75rem; + background-color: #f8f9fa; + border-radius: 4px; +} + +.account-item a { + color: #3498db; + text-decoration: none; +} + +.account-item a:hover { + text-decoration: underline; +} + +.nonce { + color: #7f8c8d; + font-size: 0.9rem; + margin-left: 0.5rem; +} + +/* Loading and error states */ +.loading, +.loading-more { + text-align: center; + padding: 2rem; + color: #7f8c8d; + font-style: italic; +} + +.error, +.error-page { + background-color: #f8d7da; + color: #721c24; + padding: 1rem; + border-radius: 8px; + margin: 1rem 0; +} + +.not-found, +.not-found-page { + text-align: center; + padding: 3rem; + color: #7f8c8d; +} + +.not-found-page h1 { + font-size: 4rem; + color: #e74c3c; + margin-bottom: 1rem; +} + +.not-found-page a { + color: #3498db; + text-decoration: none; + font-weight: 600; +} + +.not-found-page a:hover { + text-decoration: underline; +} + +/* Load more button */ +.load-more-button { + display: block; + width: 100%; + padding: 1rem; + margin-top: 1rem; + background-color: #3498db; + color: white; + border: none; + border-radius: 8px; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: background-color 0.3s; +} + +.load-more-button:hover { + background-color: #2980b9; +} + +/* Responsive design */ +@media (max-width: 768px) { + .app-main { + padding: 1rem; + } + + .search-form { + flex-direction: column; + } + + .search-button { + width: 100%; + } + + .block-preview-header, + .transaction-preview-header { + flex-direction: column; + align-items: flex-start; + gap: 0.5rem; + } + + .info-row { + flex-direction: column; + gap: 0.25rem; + } + + .info-label { + min-width: auto; + } +} diff --git a/explorer_service/src/api.rs b/explorer_service/src/api.rs new file mode 100644 index 00000000..fe84033f --- /dev/null +++ b/explorer_service/src/api.rs @@ -0,0 +1,158 @@ +use indexer_service_protocol::{Account, AccountId, Block, BlockId, Hash, Transaction}; +use leptos::prelude::*; +use serde::{Deserialize, Serialize}; + +/// Search results structure +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct SearchResults { + pub blocks: Vec, + pub transactions: Vec, + pub accounts: Vec<(AccountId, Option)>, +} + +/// RPC client type +#[cfg(feature = "ssr")] +pub type IndexerRpcClient = jsonrpsee::http_client::HttpClient; + +/// Get account information by ID +#[server] +pub async fn get_account(account_id: AccountId) -> Result { + use indexer_service_rpc::RpcClient as _; + let client = expect_context::(); + client + .get_account(account_id) + .await + .map_err(|e| ServerFnError::ServerError(format!("RPC error: {}", e))) +} + +/// Parse hex string to bytes +#[cfg(feature = "ssr")] +fn parse_hex(s: &str) -> Option> { + let s = s.trim().trim_start_matches("0x"); + hex::decode(s).ok() +} + +/// Search for a block, transaction, or account by query string +#[server] +pub async fn search(query: String) -> Result { + use indexer_service_rpc::RpcClient as _; + let client = expect_context::(); + + let mut blocks = Vec::new(); + let mut transactions = Vec::new(); + let mut accounts = Vec::new(); + + // Try to parse as hash (32 bytes) + if let Some(bytes) = parse_hex(&query) + && let Ok(hash_array) = <[u8; 32]>::try_from(bytes) + { + let hash = Hash(hash_array); + + // Try as block hash + if let Ok(block) = client.get_block_by_hash(hash).await { + blocks.push(block); + } + + // Try as transaction hash + if let Ok(tx) = client.get_transaction(hash).await { + transactions.push(tx); + } + + // Try as account ID + let account_id = AccountId { value: hash_array }; + match client.get_account(account_id).await { + Ok(account) => { + accounts.push((account_id, Some(account))); + } + Err(_) => { + // Account might not exist yet, still add it to results + accounts.push((account_id, None)); + } + } + } + + // Try as block ID + if let Ok(block_id) = query.parse::() + && let Ok(block) = client.get_block_by_id(block_id).await + { + blocks.push(block); + } + + Ok(SearchResults { + blocks, + transactions, + accounts, + }) +} + +/// Get block by ID +#[server] +pub async fn get_block_by_id(block_id: BlockId) -> Result { + use indexer_service_rpc::RpcClient as _; + let client = expect_context::(); + client + .get_block_by_id(block_id) + .await + .map_err(|e| ServerFnError::ServerError(format!("RPC error: {}", e))) +} + +/// Get block by hash +#[server] +pub async fn get_block_by_hash(block_hash: Hash) -> Result { + use indexer_service_rpc::RpcClient as _; + let client = expect_context::(); + client + .get_block_by_hash(block_hash) + .await + .map_err(|e| ServerFnError::ServerError(format!("RPC error: {}", e))) +} + +/// Get transaction by hash +#[server] +pub async fn get_transaction(tx_hash: Hash) -> Result { + use indexer_service_rpc::RpcClient as _; + let client = expect_context::(); + client + .get_transaction(tx_hash) + .await + .map_err(|e| ServerFnError::ServerError(format!("RPC error: {}", e))) +} + +/// Get blocks with pagination +#[server] +pub async fn get_blocks(offset: u32, limit: u32) -> Result, ServerFnError> { + use indexer_service_rpc::RpcClient as _; + let client = expect_context::(); + client + .get_blocks(offset, limit) + .await + .map_err(|e| ServerFnError::ServerError(format!("RPC error: {}", e))) +} + +/// Get transactions by account +#[server] +pub async fn get_transactions_by_account( + account_id: AccountId, + limit: u32, + offset: u32, +) -> Result, ServerFnError> { + use indexer_service_rpc::RpcClient as _; + let client = expect_context::(); + client + .get_transactions_by_account(account_id, limit, offset) + .await + .map_err(|e| ServerFnError::ServerError(format!("RPC error: {}", e))) +} + +/// Create the RPC client for the indexer service (server-side only) +#[cfg(feature = "ssr")] +pub fn create_indexer_rpc_client(url: &url::Url) -> Result { + use jsonrpsee::http_client::HttpClientBuilder; + use log::info; + + info!("Connecting to Indexer RPC on URL: {url}"); + + HttpClientBuilder::default() + .build(url.as_str()) + .map_err(|e| format!("Failed to create RPC client: {e}")) +} diff --git a/explorer_service/src/components/account_preview.rs b/explorer_service/src/components/account_preview.rs new file mode 100644 index 00000000..30bbae5b --- /dev/null +++ b/explorer_service/src/components/account_preview.rs @@ -0,0 +1,63 @@ +use indexer_service_protocol::{Account, AccountId}; +use leptos::prelude::*; +use leptos_router::components::A; + +use crate::format_utils; + +/// Account preview component +#[component] +pub fn AccountPreview(account_id: AccountId, account: Option) -> impl IntoView { + let account_id_str = format_utils::format_account_id(&account_id); + + view! { + + } +} diff --git a/explorer_service/src/components/block_preview.rs b/explorer_service/src/components/block_preview.rs new file mode 100644 index 00000000..b577cceb --- /dev/null +++ b/explorer_service/src/components/block_preview.rs @@ -0,0 +1,77 @@ +use indexer_service_protocol::{BedrockStatus, Block, BlockBody, BlockHeader}; +use leptos::prelude::*; +use leptos_router::components::A; + +use crate::format_utils; + +/// Get CSS class for bedrock status +fn status_class(status: &BedrockStatus) -> &'static str { + match status { + BedrockStatus::Pending => "status-pending", + BedrockStatus::Safe => "status-safe", + BedrockStatus::Finalized => "status-finalized", + } +} + +/// Block preview component +#[component] +pub fn BlockPreview(block: Block) -> impl IntoView { + let Block { + header: + BlockHeader { + block_id, + prev_block_hash, + hash, + timestamp, + signature: _, + }, + body: BlockBody { transactions }, + bedrock_status, + bedrock_parent_id: _, + } = block; + + let tx_count = transactions.len(); + + let hash_str = hex::encode(hash.0); + let prev_hash_str = hex::encode(prev_block_hash.0); + let time_str = format_utils::format_timestamp(timestamp); + let status_str = match &bedrock_status { + BedrockStatus::Pending => "Pending", + BedrockStatus::Safe => "Safe", + BedrockStatus::Finalized => "Finalized", + }; + + view! { + + } +} diff --git a/explorer_service/src/components/mod.rs b/explorer_service/src/components/mod.rs new file mode 100644 index 00000000..a0032b10 --- /dev/null +++ b/explorer_service/src/components/mod.rs @@ -0,0 +1,7 @@ +pub mod account_preview; +pub mod block_preview; +pub mod transaction_preview; + +pub use account_preview::AccountPreview; +pub use block_preview::BlockPreview; +pub use transaction_preview::TransactionPreview; diff --git a/explorer_service/src/components/transaction_preview.rs b/explorer_service/src/components/transaction_preview.rs new file mode 100644 index 00000000..a08abb30 --- /dev/null +++ b/explorer_service/src/components/transaction_preview.rs @@ -0,0 +1,72 @@ +use indexer_service_protocol::Transaction; +use leptos::prelude::*; +use leptos_router::components::A; + +/// Get transaction type name and CSS class +fn transaction_type_info(tx: &Transaction) -> (&'static str, &'static str) { + match tx { + Transaction::Public(_) => ("Public", "tx-type-public"), + Transaction::PrivacyPreserving(_) => ("Privacy-Preserving", "tx-type-private"), + Transaction::ProgramDeployment(_) => ("Program Deployment", "tx-type-deployment"), + } +} + +/// Transaction preview component +#[component] +pub fn TransactionPreview(transaction: Transaction) -> impl IntoView { + let hash = transaction.hash(); + let hash_str = hex::encode(hash.0); + let (type_name, type_class) = transaction_type_info(&transaction); + + // Get additional metadata based on transaction type + let metadata = match &transaction { + Transaction::Public(tx) => { + let indexer_service_protocol::PublicTransaction { + hash: _, + message, + witness_set: _, + } = tx; + format!("{} accounts involved", message.account_ids.len()) + } + Transaction::PrivacyPreserving(tx) => { + let indexer_service_protocol::PrivacyPreservingTransaction { + hash: _, + message, + witness_set: _, + } = tx; + format!( + "{} public accounts, {} commitments", + message.public_account_ids.len(), + message.new_commitments.len() + ) + } + Transaction::ProgramDeployment(tx) => { + let indexer_service_protocol::ProgramDeploymentTransaction { hash: _, message } = tx; + format!("{} bytes", message.bytecode.len()) + } + }; + + view! { + + } +} diff --git a/explorer_service/src/format_utils.rs b/explorer_service/src/format_utils.rs new file mode 100644 index 00000000..6f5378de --- /dev/null +++ b/explorer_service/src/format_utils.rs @@ -0,0 +1,33 @@ +//! Formatting utilities for the explorer + +use indexer_service_protocol::{AccountId, ProgramId}; + +/// Format timestamp to human-readable string +pub fn format_timestamp(timestamp: u64) -> String { + let seconds = timestamp / 1000; + let datetime = chrono::DateTime::from_timestamp(seconds as i64, 0) + .unwrap_or_else(|| chrono::DateTime::from_timestamp(0, 0).unwrap()); + datetime.format("%Y-%m-%d %H:%M:%S UTC").to_string() +} + +/// Format hash (32 bytes) to hex string +pub fn format_hash(hash: &[u8; 32]) -> String { + hex::encode(hash) +} + +/// Format account ID to hex string +pub fn format_account_id(account_id: &AccountId) -> String { + hex::encode(account_id.value) +} + +/// Format program ID to hex string +pub fn format_program_id(program_id: &ProgramId) -> String { + let bytes: Vec = program_id.iter().flat_map(|n| n.to_be_bytes()).collect(); + hex::encode(bytes) +} + +/// Parse hex string to bytes +pub fn parse_hex(s: &str) -> Option> { + let s = s.trim().trim_start_matches("0x"); + hex::decode(s).ok() +} diff --git a/explorer_service/src/lib.rs b/explorer_service/src/lib.rs new file mode 100644 index 00000000..489636fd --- /dev/null +++ b/explorer_service/src/lib.rs @@ -0,0 +1,102 @@ +use leptos::prelude::*; +use leptos_meta::{Meta, Stylesheet, Title, provide_meta_context}; +use leptos_router::{ + ParamSegment, StaticSegment, + components::{Route, Router, Routes}, +}; +use pages::{AccountPage, BlockPage, MainPage, TransactionPage}; + +pub mod api; +mod components; +mod format_utils; +mod pages; + +/// Main application component with routing setup. +/// +/// # Routes +/// +/// - `/` - Main page with search and recent blocks +/// - `/block/:id` - Block detail page (`:id` is the numeric block ID) +/// - `/transaction/:hash` - Transaction detail page (`:hash` is the hex-encoded transaction hash) +/// - `/account/:id` - Account detail page (`:id` is the hex-encoded account ID) +/// +/// All other routes will show a 404 Not Found page. +#[component] +pub fn App() -> impl IntoView { + // Provides context that manages stylesheets, titles, meta tags, etc. + provide_meta_context(); + + view! { + + + <Meta name="description" content="Explore the blockchain - view blocks, transactions, and accounts" /> + + <Router> + <div class="app"> + <header class="app-header"> + <nav class="app-nav"> + <a href="/" class="nav-logo"> + "LEE Blockchain Explorer" + </a> + </nav> + </header> + + <main class="app-main"> + // Route definitions: + // - MainPage: Home with search and recent blocks + // - BlockPage: Detailed block view with all transactions + // - TransactionPage: Detailed transaction view + // - AccountPage: Account state and transaction history + <Routes fallback=|| view! { <NotFound /> }> + // Main page - search and recent blocks + <Route path=StaticSegment("") view=MainPage /> + + // Block detail page - /block/123 + <Route path=(StaticSegment("block"), ParamSegment("id")) view=BlockPage /> + + // Transaction detail page - /transaction/0abc123... + <Route + path=(StaticSegment("transaction"), ParamSegment("hash")) + view=TransactionPage + /> + + // Account detail page - /account/0def456... + <Route + path=(StaticSegment("account"), ParamSegment("id")) + view=AccountPage + /> + </Routes> + </main> + + <footer class="app-footer"> + <p>"LEE Blockchain Explorer © 2026"</p> + </footer> + </div> + </Router> + } +} + +/// 404 Not Found page component. +/// +/// Displayed when a user navigates to a route that doesn't exist. +#[component] +fn NotFound() -> impl IntoView { + view! { + <div class="not-found-page"> + <h1>"404"</h1> + <p>"Page not found"</p> + <a href="/">"Go back to home"</a> + </div> + } +} + +#[cfg(feature = "hydrate")] +#[wasm_bindgen::prelude::wasm_bindgen] +pub fn hydrate() { + use leptos::mount::hydrate_body; + + console_error_panic_hook::set_once(); + console_log::init_with_level(log::Level::Debug).expect("error initializing logger"); + + hydrate_body(App); +} diff --git a/explorer_service/src/main.rs b/explorer_service/src/main.rs new file mode 100644 index 00000000..63d54d70 --- /dev/null +++ b/explorer_service/src/main.rs @@ -0,0 +1,79 @@ +#[cfg(feature = "ssr")] +#[tokio::main] +async fn main() { + use axum::Router; + use clap::Parser; + use explorer_service::App; + use leptos::prelude::*; + use leptos_axum::{LeptosRoutes, generate_route_list}; + use leptos_meta::MetaTags; + + env_logger::init(); + + /// LEE Blockchain Explorer Server CLI arguments. + #[derive(Parser, Debug)] + #[command(version, about, long_about = None)] + struct Args { + /// Indexer RPC URL + #[arg(long, env = "INDEXER_RPC_URL", default_value = "http://localhost:8779")] + indexer_rpc_url: url::Url, + } + + let args = Args::parse(); + + let conf = get_configuration(None).unwrap(); + let leptos_options = conf.leptos_options; + let addr = leptos_options.site_addr; + let routes = generate_route_list(App); + + // Create RPC client once + let rpc_client = explorer_service::api::create_indexer_rpc_client(&args.indexer_rpc_url) + .expect("Failed to create RPC client"); + + // Build our application with routes + let app = Router::new() + .leptos_routes_with_context( + &leptos_options, + routes, + { + let rpc_client = rpc_client.clone(); + move || provide_context(rpc_client.clone()) + }, + { + let leptos_options = leptos_options.clone(); + move || { + view! { + <!DOCTYPE html> + <html lang="en"> + <head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <AutoReload options=leptos_options.clone() /> + <HydrationScripts options=leptos_options.clone() /> + <MetaTags /> + </head> + <body> + <App /> + </body> + </html> + } + } + }, + ) + .fallback(leptos_axum::file_and_error_handler(|_| { + view! { "Page not found" } + })) + .with_state(leptos_options); + + // Run the server + let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); + println!("Listening on http://{}", &addr); + axum::serve(listener, app.into_make_service()) + .await + .unwrap(); +} + +#[cfg(not(feature = "ssr"))] +fn main() { + // Client-only main - no-op since hydration is done via wasm_bindgen +} diff --git a/explorer_service/src/pages/account_page.rs b/explorer_service/src/pages/account_page.rs new file mode 100644 index 00000000..efd9ae81 --- /dev/null +++ b/explorer_service/src/pages/account_page.rs @@ -0,0 +1,229 @@ +use indexer_service_protocol::{Account, AccountId}; +use leptos::prelude::*; +use leptos_router::hooks::use_params_map; + +use crate::{api, components::TransactionPreview, format_utils}; + +/// Account page component +#[component] +pub fn AccountPage() -> impl IntoView { + let params = use_params_map(); + let (tx_offset, set_tx_offset) = signal(0u32); + let (all_transactions, set_all_transactions) = signal(Vec::new()); + let (is_loading, set_is_loading) = signal(false); + let (has_more, set_has_more) = signal(true); + let tx_limit = 10u32; + + // Parse account ID from URL params + let account_id = move || { + let account_id_str = params.read().get("id").unwrap_or_default(); + format_utils::parse_hex(&account_id_str).and_then(|bytes| { + if bytes.len() == 32 { + let account_id_array: [u8; 32] = bytes.try_into().ok()?; + Some(AccountId { + value: account_id_array, + }) + } else { + None + } + }) + }; + + // Load account data + let account_resource = Resource::new(account_id, |acc_id_opt| async move { + match acc_id_opt { + Some(acc_id) => api::get_account(acc_id).await, + None => Err(leptos::prelude::ServerFnError::ServerError( + "Invalid account ID".to_string(), + )), + } + }); + + // Load initial transactions + let transactions_resource = Resource::new(account_id, move |acc_id_opt| async move { + match acc_id_opt { + Some(acc_id) => api::get_transactions_by_account(acc_id, tx_limit, 0).await, + None => Err(leptos::prelude::ServerFnError::ServerError( + "Invalid account ID".to_string(), + )), + } + }); + + // Update all_transactions when initial load completes + Effect::new(move || { + if let Some(Ok(txs)) = transactions_resource.get() { + set_all_transactions.set(txs.clone()); + set_has_more.set(txs.len() as u32 == tx_limit); + } + }); + + // Load more transactions handler + let load_more = move |_| { + let Some(acc_id) = account_id() else { + return; + }; + + set_is_loading.set(true); + let current_offset = tx_offset.get() + tx_limit; + set_tx_offset.set(current_offset); + + leptos::task::spawn_local(async move { + match api::get_transactions_by_account(acc_id, tx_limit, current_offset).await { + Ok(new_txs) => { + let txs_count = new_txs.len() as u32; + set_all_transactions.update(|txs| txs.extend(new_txs)); + set_has_more.set(txs_count == tx_limit); + } + Err(e) => { + log::error!("Failed to load more transactions: {}", e); + } + } + set_is_loading.set(false); + }); + }; + + view! { + <div class="account-page"> + <Suspense fallback=move || view! { <div class="loading">"Loading account..."</div> }> + {move || { + account_resource + .get() + .map(|result| match result { + Ok(acc) => { + let Account { + program_owner, + balance, + data, + nonce, + } = acc; + + let acc_id = account_id().expect("Account ID should be set"); + let account_id_str = format_utils::format_account_id(&acc_id); + let program_id = format_utils::format_program_id(&program_owner); + let balance_str = balance.to_string(); + let nonce_str = nonce.to_string(); + let data_len = data.0.len(); + view! { + <div class="account-detail"> + <div class="page-header"> + <h1>"Account"</h1> + </div> + + <div class="account-info"> + <h2>"Account Information"</h2> + <div class="info-grid"> + <div class="info-row"> + <span class="info-label">"Account ID:"</span> + <span class="info-value hash">{account_id_str}</span> + </div> + <div class="info-row"> + <span class="info-label">"Balance:"</span> + <span class="info-value">{balance_str}</span> + </div> + <div class="info-row"> + <span class="info-label">"Program Owner:"</span> + <span class="info-value hash">{program_id}</span> + </div> + <div class="info-row"> + <span class="info-label">"Nonce:"</span> + <span class="info-value">{nonce_str}</span> + </div> + <div class="info-row"> + <span class="info-label">"Data:"</span> + <span class="info-value">{format!("{} bytes", data_len)}</span> + </div> + </div> + </div> + + <div class="account-transactions"> + <h2>"Transactions"</h2> + <Suspense fallback=move || { + view! { <div class="loading">"Loading transactions..."</div> } + }> + + {move || { + transactions_resource + .get() + .map(|result| match result { + Ok(_) => { + let txs = all_transactions.get(); + if txs.is_empty() { + view! { + <div class="no-transactions"> + "No transactions found" + </div> + } + .into_any() + } else { + view! { + <div> + <div class="transactions-list"> + {txs + .into_iter() + .map(|tx| { + view! { <TransactionPreview transaction=tx /> } + }) + .collect::<Vec<_>>()} + </div> + {move || { + if has_more.get() { + view! { + <button + class="load-more-button" + on:click=load_more + disabled=move || is_loading.get() + > + {move || { + if is_loading.get() { + "Loading..." + } else { + "Load More" + } + }} + + </button> + } + .into_any() + } else { + ().into_any() + } + }} + + </div> + } + .into_any() + } + } + Err(e) => { + view! { + <div class="error"> + {format!("Failed to load transactions: {}", e)} + </div> + } + .into_any() + } + }) + }} + + </Suspense> + </div> + </div> + } + .into_any() + } + Err(e) => { + view! { + <div class="error-page"> + <h1>"Error"</h1> + <p>{format!("Failed to load account: {}", e)}</p> + </div> + } + .into_any() + } + }) + }} + + </Suspense> + </div> + } +} diff --git a/explorer_service/src/pages/block_page.rs b/explorer_service/src/pages/block_page.rs new file mode 100644 index 00000000..988bc5e3 --- /dev/null +++ b/explorer_service/src/pages/block_page.rs @@ -0,0 +1,159 @@ +use indexer_service_protocol::{BedrockStatus, Block, BlockBody, BlockHeader, BlockId, Hash}; +use leptos::prelude::*; +use leptos_router::{components::A, hooks::use_params_map}; + +use crate::{api, components::TransactionPreview, format_utils}; + +#[derive(Clone, PartialEq, Eq)] +enum BlockIdOrHash { + BlockId(BlockId), + Hash(Hash), +} + +/// Block page component +#[component] +pub fn BlockPage() -> impl IntoView { + let params = use_params_map(); + + let block_resource = Resource::new( + move || { + let id_str = params.read().get("id").unwrap_or_default(); + + // Try to parse as block ID (number) + if let Ok(block_id) = id_str.parse::<BlockId>() { + return Some(BlockIdOrHash::BlockId(block_id)); + } + + // Try to parse as block hash (hex string) + let id_str = id_str.trim().trim_start_matches("0x"); + if let Some(bytes) = format_utils::parse_hex(id_str) + && let Ok(hash_array) = <[u8; 32]>::try_from(bytes) + { + return Some(BlockIdOrHash::Hash(Hash(hash_array))); + } + + None + }, + |block_id_or_hash| async move { + match block_id_or_hash { + Some(BlockIdOrHash::BlockId(id)) => api::get_block_by_id(id).await, + Some(BlockIdOrHash::Hash(hash)) => api::get_block_by_hash(hash).await, + None => Err(leptos::prelude::ServerFnError::ServerError( + "Invalid block ID or hash".to_string(), + )), + } + }, + ); + + view! { + <div class="block-page"> + <Suspense fallback=move || view! { <div class="loading">"Loading block..."</div> }> + {move || { + block_resource + .get() + .map(|result| match result { + Ok(blk) => { + let Block { + header: BlockHeader { + block_id, + prev_block_hash, + hash, + timestamp, + signature, + }, + body: BlockBody { + transactions, + }, + bedrock_status, + bedrock_parent_id: _, + } = blk; + + let hash_str = format_utils::format_hash(&hash.0); + let prev_hash = format_utils::format_hash(&prev_block_hash.0); + let timestamp_str = format_utils::format_timestamp(timestamp); + let signature_str = hex::encode(signature.0); + let status = match &bedrock_status { + BedrockStatus::Pending => "Pending", + BedrockStatus::Safe => "Safe", + BedrockStatus::Finalized => "Finalized", + }; + view! { + <div class="block-detail"> + <div class="page-header"> + <h1>"Block " {block_id.to_string()}</h1> + </div> + + <div class="block-info"> + <h2>"Block Information"</h2> + <div class="info-grid"> + <div class="info-row"> + <span class="info-label">"Block ID: "</span> + <span class="info-value">{block_id.to_string()}</span> + </div> + <div class="info-row"> + <span class="info-label">"Hash: "</span> + <span class="info-value hash">{hash_str}</span> + </div> + <div class="info-row"> + <span class="info-label">"Previous Block Hash: "</span> + <A href=format!("/block/{}", prev_hash) attr:class="info-value hash"> + {prev_hash} + </A> + </div> + <div class="info-row"> + <span class="info-label">"Timestamp: "</span> + <span class="info-value">{timestamp_str}</span> + </div> + <div class="info-row"> + <span class="info-label">"Status: "</span> + <span class="info-value">{status}</span> + </div> + <div class="info-row"> + <span class="info-label">"Signature: "</span> + <span class="info-value hash signature">{signature_str}</span> + </div> + <div class="info-row"> + <span class="info-label">"Transaction Count: "</span> + <span class="info-value">{transactions.len().to_string()}</span> + </div> + </div> + </div> + + <div class="block-transactions"> + <h2>"Transactions"</h2> + {if transactions.is_empty() { + view! { <div class="no-transactions">"No transactions"</div> } + .into_any() + } else { + view! { + <div class="transactions-list"> + {transactions + .into_iter() + .map(|tx| view! { <TransactionPreview transaction=tx /> }) + .collect::<Vec<_>>()} + </div> + } + .into_any() + }} + + </div> + </div> + } + .into_any() + } + Err(e) => { + view! { + <div class="error-page"> + <h1>"Error"</h1> + <p>{format!("Failed to load block: {}", e)}</p> + </div> + } + .into_any() + } + }) + }} + + </Suspense> + </div> + } +} diff --git a/explorer_service/src/pages/main_page.rs b/explorer_service/src/pages/main_page.rs new file mode 100644 index 00000000..ffd625c8 --- /dev/null +++ b/explorer_service/src/pages/main_page.rs @@ -0,0 +1,208 @@ +use leptos::prelude::*; +use leptos_router::hooks::{use_navigate, use_query_map}; +use web_sys::SubmitEvent; + +use crate::{ + api::{self, SearchResults}, + components::{AccountPreview, BlockPreview, TransactionPreview}, +}; + +/// Main page component +#[component] +pub fn MainPage() -> impl IntoView { + let query_map = use_query_map(); + let navigate = use_navigate(); + + // Read search query from URL parameter + let url_query = move || query_map.read().get("q").unwrap_or_default(); + + let (search_query, set_search_query) = signal(url_query()); + + // Sync search input with URL parameter + Effect::new(move || { + set_search_query.set(url_query()); + }); + + // Search results resource based on URL query parameter + let search_resource = Resource::new(url_query, |query| async move { + if query.is_empty() { + return None; + } + match api::search(query).await { + Ok(result) => Some(result), + Err(e) => { + log::error!("Search error: {}", e); + None + } + } + }); + + // Load recent blocks on mount + let recent_blocks_resource = Resource::new(|| (), |_| async { api::get_blocks(0, 10).await }); + + // Handle search - update URL parameter + let on_search = move |ev: SubmitEvent| { + ev.prevent_default(); + let query = search_query.get(); + if query.is_empty() { + navigate("?", Default::default()); + return; + } + + navigate( + &format!("?q={}", urlencoding::encode(&query)), + Default::default(), + ); + }; + + view! { + <div class="main-page"> + <div class="page-header"> + <h1>"LEE Blockchain Explorer"</h1> + </div> + + <div class="search-section"> + <form on:submit=on_search class="search-form"> + <input + type="text" + class="search-input" + placeholder="Search by block ID, block hash, transaction hash, or account ID..." + prop:value=move || search_query.get() + on:input=move |ev| set_search_query.set(event_target_value(&ev)) + /> + <button type="submit" class="search-button"> + "Search" + </button> + </form> + + <Suspense fallback=move || view! { <div class="loading">"Searching..."</div> }> + {move || { + search_resource + .get() + .and_then(|opt_results| opt_results) + .map(|results| { + let SearchResults { + blocks, + transactions, + accounts, + } = results; + let has_results = !blocks.is_empty() + || !transactions.is_empty() + || !accounts.is_empty(); + view! { + <div class="search-results"> + <h2>"Search Results"</h2> + {if !has_results { + view! { <div class="not-found">"No results found"</div> } + .into_any() + } else { + view! { + <div class="results-container"> + {if !blocks.is_empty() { + view! { + <div class="results-section"> + <h3>"Blocks"</h3> + <div class="results-list"> + {blocks + .into_iter() + .map(|block| { + view! { <BlockPreview block=block /> } + }) + .collect::<Vec<_>>()} + </div> + </div> + } + .into_any() + } else { + ().into_any() + }} + + {if !transactions.is_empty() { + view! { + <div class="results-section"> + <h3>"Transactions"</h3> + <div class="results-list"> + {transactions + .into_iter() + .map(|tx| { + view! { <TransactionPreview transaction=tx /> } + }) + .collect::<Vec<_>>()} + </div> + </div> + } + .into_any() + } else { + ().into_any() + }} + + {if !accounts.is_empty() { + view! { + <div class="results-section"> + <h3>"Accounts"</h3> + <div class="results-list"> + {accounts + .into_iter() + .map(|(id, account)| { + view! { + <AccountPreview + account_id=id + account=account + /> + } + }) + .collect::<Vec<_>>()} + </div> + </div> + } + .into_any() + } else { + ().into_any() + }} + + </div> + } + .into_any() + }} + </div> + } + .into_any() + }) + }} + + </Suspense> + </div> + + <div class="blocks-section"> + <h2>"Recent Blocks"</h2> + <Suspense fallback=move || view! { <div class="loading">"Loading blocks..."</div> }> + {move || { + recent_blocks_resource + .get() + .map(|result| match result { + Ok(blocks) if !blocks.is_empty() => { + view! { + <div class="blocks-list"> + {blocks + .into_iter() + .map(|block| view! { <BlockPreview block=block /> }) + .collect::<Vec<_>>()} + </div> + } + .into_any() + } + Ok(_) => { + view! { <div class="no-blocks">"No blocks found"</div> }.into_any() + } + Err(e) => { + view! { <div class="error">{format!("Error: {}", e)}</div> } + .into_any() + } + }) + }} + + </Suspense> + </div> + </div> + } +} diff --git a/explorer_service/src/pages/mod.rs b/explorer_service/src/pages/mod.rs new file mode 100644 index 00000000..f4220145 --- /dev/null +++ b/explorer_service/src/pages/mod.rs @@ -0,0 +1,9 @@ +pub mod account_page; +pub mod block_page; +pub mod main_page; +pub mod transaction_page; + +pub use account_page::AccountPage; +pub use block_page::BlockPage; +pub use main_page::MainPage; +pub use transaction_page::TransactionPage; diff --git a/explorer_service/src/pages/transaction_page.rs b/explorer_service/src/pages/transaction_page.rs new file mode 100644 index 00000000..a8571a7a --- /dev/null +++ b/explorer_service/src/pages/transaction_page.rs @@ -0,0 +1,262 @@ +use indexer_service_protocol::{ + Hash, PrivacyPreservingMessage, PrivacyPreservingTransaction, ProgramDeploymentMessage, + ProgramDeploymentTransaction, PublicMessage, PublicTransaction, Transaction, WitnessSet, +}; +use leptos::prelude::*; +use leptos_router::{components::A, hooks::use_params_map}; + +use crate::{api, format_utils}; + +/// Transaction page component +#[component] +pub fn TransactionPage() -> impl IntoView { + let params = use_params_map(); + + let transaction_resource = Resource::new( + move || { + let tx_hash_str = params.read().get("hash").unwrap_or_default(); + format_utils::parse_hex(&tx_hash_str).and_then(|bytes| { + if bytes.len() == 32 { + let hash_array: [u8; 32] = bytes.try_into().ok()?; + Some(Hash(hash_array)) + } else { + None + } + }) + }, + |hash_opt| async move { + match hash_opt { + Some(hash) => api::get_transaction(hash).await, + None => Err(leptos::prelude::ServerFnError::ServerError( + "Invalid transaction hash".to_string(), + )), + } + }, + ); + + view! { + <div class="transaction-page"> + <Suspense fallback=move || view! { <div class="loading">"Loading transaction..."</div> }> + {move || { + transaction_resource + .get() + .map(|result| match result { + Ok(tx) => { + let tx_hash = format_utils::format_hash(&tx.hash().0); + let tx_type = match &tx { + Transaction::Public(_) => "Public Transaction", + Transaction::PrivacyPreserving(_) => "Privacy-Preserving Transaction", + Transaction::ProgramDeployment(_) => "Program Deployment Transaction", + }; + view! { + <div class="transaction-detail"> + <div class="page-header"> + <h1>"Transaction"</h1> + </div> + + <div class="transaction-info"> + <h2>"Transaction Information"</h2> + <div class="info-grid"> + <div class="info-row"> + <span class="info-label">"Hash:"</span> + <span class="info-value hash">{tx_hash}</span> + </div> + <div class="info-row"> + <span class="info-label">"Type:"</span> + <span class="info-value">{tx_type}</span> + </div> + </div> + </div> + + {match tx { + Transaction::Public(ptx) => { + let PublicTransaction { + hash: _, + message, + witness_set, + } = ptx; + let PublicMessage { + program_id, + account_ids, + nonces, + instruction_data, + } = message; + let WitnessSet { + signatures_and_public_keys, + proof, + } = witness_set; + + let program_id_str = program_id + .iter() + .map(|n| format!("{:08x}", n)) + .collect::<String>(); + let proof_len = proof.0.len(); + let signatures_count = signatures_and_public_keys.len(); + + view! { + <div class="transaction-details"> + <h2>"Public Transaction Details"</h2> + <div class="info-grid"> + <div class="info-row"> + <span class="info-label">"Program ID:"</span> + <span class="info-value hash">{program_id_str}</span> + </div> + <div class="info-row"> + <span class="info-label">"Instruction Data:"</span> + <span class="info-value"> + {format!("{} u32 values", instruction_data.len())} + </span> + </div> + <div class="info-row"> + <span class="info-label">"Proof Size:"</span> + <span class="info-value">{format!("{} bytes", proof_len)}</span> + </div> + <div class="info-row"> + <span class="info-label">"Signatures:"</span> + <span class="info-value">{signatures_count.to_string()}</span> + </div> + </div> + + <h3>"Accounts"</h3> + <div class="accounts-list"> + {account_ids + .into_iter() + .zip(nonces.into_iter()) + .map(|(account_id, nonce)| { + let account_id_str = format_utils::format_account_id(&account_id); + view! { + <div class="account-item"> + <A href=format!("/account/{}", account_id_str)> + <span class="hash">{account_id_str}</span> + </A> + <span class="nonce"> + " (nonce: " {nonce.to_string()} ")" + </span> + </div> + } + }) + .collect::<Vec<_>>()} + </div> + </div> + } + .into_any() + } + Transaction::PrivacyPreserving(pptx) => { + let PrivacyPreservingTransaction { + hash: _, + message, + witness_set, + } = pptx; + let PrivacyPreservingMessage { + public_account_ids, + nonces, + public_post_states: _, + encrypted_private_post_states, + new_commitments, + new_nullifiers, + } = message; + let WitnessSet { + signatures_and_public_keys: _, + proof, + } = witness_set; + + let proof_len = proof.0.len(); + view! { + <div class="transaction-details"> + <h2>"Privacy-Preserving Transaction Details"</h2> + <div class="info-grid"> + <div class="info-row"> + <span class="info-label">"Public Accounts:"</span> + <span class="info-value"> + {public_account_ids.len().to_string()} + </span> + </div> + <div class="info-row"> + <span class="info-label">"New Commitments:"</span> + <span class="info-value">{new_commitments.len().to_string()}</span> + </div> + <div class="info-row"> + <span class="info-label">"Nullifiers:"</span> + <span class="info-value">{new_nullifiers.len().to_string()}</span> + </div> + <div class="info-row"> + <span class="info-label">"Encrypted States:"</span> + <span class="info-value"> + {encrypted_private_post_states.len().to_string()} + </span> + </div> + <div class="info-row"> + <span class="info-label">"Proof Size:"</span> + <span class="info-value">{format!("{} bytes", proof_len)}</span> + </div> + </div> + + <h3>"Public Accounts"</h3> + <div class="accounts-list"> + {public_account_ids + .into_iter() + .zip(nonces.into_iter()) + .map(|(account_id, nonce)| { + let account_id_str = format_utils::format_account_id(&account_id); + view! { + <div class="account-item"> + <A href=format!("/account/{}", account_id_str)> + <span class="hash">{account_id_str}</span> + </A> + <span class="nonce"> + " (nonce: " {nonce.to_string()} ")" + </span> + </div> + } + }) + .collect::<Vec<_>>()} + </div> + </div> + } + .into_any() + } + Transaction::ProgramDeployment(pdtx) => { + let ProgramDeploymentTransaction { + hash: _, + message, + } = pdtx; + let ProgramDeploymentMessage { bytecode } = message; + + let bytecode_len = bytecode.len(); + view! { + <div class="transaction-details"> + <h2>"Program Deployment Transaction Details"</h2> + <div class="info-grid"> + <div class="info-row"> + <span class="info-label">"Bytecode Size:"</span> + <span class="info-value"> + {format!("{} bytes", bytecode_len)} + </span> + </div> + </div> + </div> + } + .into_any() + } + }} + + </div> + } + .into_any() + } + Err(e) => { + view! { + <div class="error-page"> + <h1>"Error"</h1> + <p>{format!("Failed to load transaction: {}", e)}</p> + </div> + } + .into_any() + } + }) + }} + + </Suspense> + </div> + } +} diff --git a/indexer_service/Cargo.toml b/indexer_service/Cargo.toml index 361328cf..6148d544 100644 --- a/indexer_service/Cargo.toml +++ b/indexer_service/Cargo.toml @@ -10,9 +10,13 @@ indexer_service_rpc = { workspace = true, features = ["server"] } clap = { workspace = true, features = ["derive"] } anyhow.workspace = true -tokio.workspace = true +tokio = { workspace = true, features = ["macros", "rt-multi-thread", "signal"] } tokio-util.workspace = true env_logger.workspace = true log.workspace = true jsonrpsee.workspace = true async-trait = "0.1.89" + +[features] +# Return mock responses with generated data for testing purposes +mock-responses = [] diff --git a/indexer_service/protocol/src/convert.rs b/indexer_service/protocol/src/convert.rs index 8c6de2f4..179a25ec 100644 --- a/indexer_service/protocol/src/convert.rs +++ b/indexer_service/protocol/src/convert.rs @@ -381,11 +381,17 @@ impl TryFrom<WitnessSet> for nssa::privacy_preserving_transaction::witness_set:: impl From<nssa::PublicTransaction> for PublicTransaction { fn from(value: nssa::PublicTransaction) -> Self { + let hash = Hash(value.hash()); + let nssa::PublicTransaction { + message, + witness_set, + } = value; + Self { - message: value.message().clone().into(), + hash, + message: message.into(), witness_set: WitnessSet { - signatures_and_public_keys: value - .witness_set() + signatures_and_public_keys: witness_set .signatures_and_public_keys() .iter() .map(|(sig, pk)| (sig.clone().into(), pk.clone().into())) @@ -401,6 +407,7 @@ impl TryFrom<PublicTransaction> for nssa::PublicTransaction { fn try_from(value: PublicTransaction) -> Result<Self, Self::Error> { let PublicTransaction { + hash: _, message, witness_set, } = value; @@ -408,6 +415,7 @@ impl TryFrom<PublicTransaction> for nssa::PublicTransaction { signatures_and_public_keys, proof: _, } = witness_set; + Ok(Self::new( message.into(), nssa::public_transaction::WitnessSet::from_raw_parts( @@ -422,9 +430,16 @@ impl TryFrom<PublicTransaction> for nssa::PublicTransaction { impl From<nssa::PrivacyPreservingTransaction> for PrivacyPreservingTransaction { fn from(value: nssa::PrivacyPreservingTransaction) -> Self { + let hash = Hash(value.hash()); + let nssa::PrivacyPreservingTransaction { + message, + witness_set, + } = value; + Self { - message: value.message().clone().into(), - witness_set: value.witness_set().clone().into(), + hash, + message: message.into(), + witness_set: witness_set.into(), } } } @@ -434,13 +449,17 @@ impl TryFrom<PrivacyPreservingTransaction> for nssa::PrivacyPreservingTransactio fn try_from(value: PrivacyPreservingTransaction) -> Result<Self, Self::Error> { let PrivacyPreservingTransaction { + hash: _, message, witness_set, } = value; + Ok(Self::new( - message.try_into().map_err(|_| { - nssa::error::NssaError::InvalidInput("Data too big error".to_string()) - })?, + message + .try_into() + .map_err(|err: nssa_core::account::data::DataTooBigError| { + nssa::error::NssaError::InvalidInput(err.to_string()) + })?, witness_set.try_into()?, )) } @@ -448,15 +467,19 @@ impl TryFrom<PrivacyPreservingTransaction> for nssa::PrivacyPreservingTransactio impl From<nssa::ProgramDeploymentTransaction> for ProgramDeploymentTransaction { fn from(value: nssa::ProgramDeploymentTransaction) -> Self { + let hash = Hash(value.hash()); + let nssa::ProgramDeploymentTransaction { message } = value; + Self { - message: value.into_message().into(), + hash, + message: message.into(), } } } impl From<ProgramDeploymentTransaction> for nssa::ProgramDeploymentTransaction { fn from(value: ProgramDeploymentTransaction) -> Self { - let ProgramDeploymentTransaction { message } = value; + let ProgramDeploymentTransaction { hash: _, message } = value; Self::new(message.into()) } } diff --git a/indexer_service/protocol/src/lib.rs b/indexer_service/protocol/src/lib.rs index f12bdf5b..c354aa59 100644 --- a/indexer_service/protocol/src/lib.rs +++ b/indexer_service/protocol/src/lib.rs @@ -67,14 +67,27 @@ pub enum Transaction { ProgramDeployment(ProgramDeploymentTransaction), } +impl Transaction { + /// Get the hash of the transaction + pub fn hash(&self) -> &self::Hash { + match self { + Transaction::Public(tx) => &tx.hash, + Transaction::PrivacyPreserving(tx) => &tx.hash, + Transaction::ProgramDeployment(tx) => &tx.hash, + } + } +} + #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] pub struct PublicTransaction { + pub hash: Hash, pub message: PublicMessage, pub witness_set: WitnessSet, } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] pub struct PrivacyPreservingTransaction { + pub hash: Hash, pub message: PrivacyPreservingMessage, pub witness_set: WitnessSet, } @@ -121,6 +134,7 @@ pub struct EncryptedAccountData { #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] pub struct ProgramDeploymentTransaction { + pub hash: Hash, pub message: ProgramDeploymentMessage, } @@ -133,7 +147,7 @@ pub struct Ciphertext( pub Vec<u8>, ); -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] pub struct PublicKey( #[serde(with = "base64::arr")] #[schemars(with = "String", description = "base64-encoded public key")] @@ -147,21 +161,21 @@ pub struct EphemeralPublicKey( pub Vec<u8>, ); -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] pub struct Commitment( #[serde(with = "base64::arr")] #[schemars(with = "String", description = "base64-encoded commitment")] pub [u8; 32], ); -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] pub struct Nullifier( #[serde(with = "base64::arr")] #[schemars(with = "String", description = "base64-encoded nullifier")] pub [u8; 32], ); -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] pub struct CommitmentSetDigest( #[serde(with = "base64::arr")] #[schemars(with = "String", description = "base64-encoded commitment set digest")] @@ -182,7 +196,7 @@ pub struct Data( pub Vec<u8>, ); -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] pub struct Hash( #[serde(with = "base64::arr")] #[schemars(with = "String", description = "base64-encoded hash")] diff --git a/indexer_service/rpc/src/lib.rs b/indexer_service/rpc/src/lib.rs index c1c4a560..def20ad5 100644 --- a/indexer_service/rpc/src/lib.rs +++ b/indexer_service/rpc/src/lib.rs @@ -1,11 +1,14 @@ use indexer_service_protocol::{Account, AccountId, Block, BlockId, Hash, Transaction}; -use jsonrpsee::{core::SubscriptionResult, proc_macros::rpc, types::ErrorObjectOwned}; +use jsonrpsee::proc_macros::rpc; +#[cfg(feature = "server")] +use jsonrpsee::{core::SubscriptionResult, types::ErrorObjectOwned}; #[cfg(all(not(feature = "server"), not(feature = "client")))] compile_error!("At least one of `server` or `client` features must be enabled."); -#[cfg_attr(feature = "server", rpc(server))] -#[cfg_attr(feature = "client", rpc(client))] +#[cfg_attr(all(feature = "server", not(feature = "client")), rpc(server))] +#[cfg_attr(all(feature = "client", not(feature = "server")), rpc(client))] +#[cfg_attr(all(feature = "server", feature = "client"), rpc(server, client))] pub trait Rpc { #[method(name = "get_schema")] fn get_schema(&self) -> Result<serde_json::Value, ErrorObjectOwned> { @@ -37,4 +40,15 @@ pub trait Rpc { #[method(name = "getTransaction")] async fn get_transaction(&self, tx_hash: Hash) -> Result<Transaction, ErrorObjectOwned>; + + #[method(name = "getBlocks")] + async fn get_blocks(&self, offset: u32, limit: u32) -> Result<Vec<Block>, ErrorObjectOwned>; + + #[method(name = "getTransactionsByAccount")] + async fn get_transactions_by_account( + &self, + account_id: AccountId, + limit: u32, + offset: u32, + ) -> Result<Vec<Transaction>, ErrorObjectOwned>; } diff --git a/indexer_service/src/lib.rs b/indexer_service/src/lib.rs index 1f278a4d..0c18410e 100644 --- a/indexer_service/src/lib.rs +++ b/indexer_service/src/lib.rs @@ -1 +1,4 @@ pub mod service; + +#[cfg(feature = "mock-responses")] +pub mod mock_service; diff --git a/indexer_service/src/main.rs b/indexer_service/src/main.rs index bfdd3259..1c6856ec 100644 --- a/indexer_service/src/main.rs +++ b/indexer_service/src/main.rs @@ -51,7 +51,13 @@ async fn run_server(port: u16) -> Result<jsonrpsee::server::ServerHandle> { info!("Starting Indexer Service RPC server on {addr}"); + #[cfg(not(feature = "mock-responses"))] let handle = server.start(indexer_service::service::IndexerService.into_rpc()); + #[cfg(feature = "mock-responses")] + let handle = server.start( + indexer_service::mock_service::MockIndexerService::new_with_mock_blocks().into_rpc(), + ); + Ok(handle) } diff --git a/indexer_service/src/mock_service.rs b/indexer_service/src/mock_service.rs new file mode 100644 index 00000000..907bdd0e --- /dev/null +++ b/indexer_service/src/mock_service.rs @@ -0,0 +1,271 @@ +use std::collections::HashMap; + +use indexer_service_protocol::{ + Account, AccountId, BedrockStatus, Block, BlockBody, BlockHeader, BlockId, Commitment, + CommitmentSetDigest, Data, EncryptedAccountData, Hash, MantleMsgId, PrivacyPreservingMessage, + PrivacyPreservingTransaction, ProgramDeploymentMessage, ProgramDeploymentTransaction, + PublicMessage, PublicTransaction, Signature, Transaction, WitnessSet, +}; +use jsonrpsee::{core::SubscriptionResult, types::ErrorObjectOwned}; + +/// A mock implementation of the IndexerService RPC for testing purposes. +pub struct MockIndexerService { + blocks: Vec<Block>, + accounts: HashMap<AccountId, Account>, + transactions: HashMap<Hash, (Transaction, BlockId)>, +} + +impl MockIndexerService { + pub fn new_with_mock_blocks() -> Self { + let mut blocks = Vec::new(); + let mut accounts = HashMap::new(); + let mut transactions = HashMap::new(); + + // Create some mock accounts + let account_ids: Vec<AccountId> = (0..5) + .map(|i| { + let mut value = [0u8; 32]; + value[0] = i; + AccountId { value } + }) + .collect(); + + for (i, account_id) in account_ids.iter().enumerate() { + accounts.insert( + *account_id, + Account { + program_owner: [i as u32; 8], + balance: 1000 * (i as u128 + 1), + data: Data(vec![0xaa, 0xbb, 0xcc]), + nonce: i as u128, + }, + ); + } + + // Create 10 blocks with transactions + let mut prev_hash = Hash([0u8; 32]); + + for block_id in 0..10 { + let block_hash = { + let mut hash = [0u8; 32]; + hash[0] = block_id as u8; + hash[1] = 0xff; + Hash(hash) + }; + + // Create 2-4 transactions per block (mix of Public, PrivacyPreserving, and + // ProgramDeployment) + let num_txs = 2 + (block_id % 3); + let mut block_transactions = Vec::new(); + + for tx_idx in 0..num_txs { + let tx_hash = { + let mut hash = [0u8; 32]; + hash[0] = block_id as u8; + hash[1] = tx_idx as u8; + Hash(hash) + }; + + // Vary transaction types: Public, PrivacyPreserving, or ProgramDeployment + let tx = match (block_id + tx_idx) % 5 { + // Public transactions (most common) + 0 | 1 => Transaction::Public(PublicTransaction { + hash: tx_hash, + message: PublicMessage { + program_id: [1u32; 8], + account_ids: vec![ + account_ids[tx_idx as usize % account_ids.len()], + account_ids[(tx_idx as usize + 1) % account_ids.len()], + ], + nonces: vec![block_id as u128, (block_id + 1) as u128], + instruction_data: vec![1, 2, 3, 4], + }, + witness_set: WitnessSet { + signatures_and_public_keys: vec![], + proof: indexer_service_protocol::Proof(vec![0; 32]), + }, + }), + // PrivacyPreserving transactions + 2 | 3 => Transaction::PrivacyPreserving(PrivacyPreservingTransaction { + hash: tx_hash, + message: PrivacyPreservingMessage { + public_account_ids: vec![ + account_ids[tx_idx as usize % account_ids.len()], + ], + nonces: vec![block_id as u128], + public_post_states: vec![Account { + program_owner: [1u32; 8], + balance: 500, + data: Data(vec![0xdd, 0xee]), + nonce: block_id as u128, + }], + encrypted_private_post_states: vec![EncryptedAccountData { + ciphertext: indexer_service_protocol::Ciphertext(vec![ + 0x01, 0x02, 0x03, 0x04, + ]), + epk: indexer_service_protocol::EphemeralPublicKey(vec![0xaa; 32]), + view_tag: 42, + }], + new_commitments: vec![Commitment([block_id as u8; 32])], + new_nullifiers: vec![( + indexer_service_protocol::Nullifier([tx_idx as u8; 32]), + CommitmentSetDigest([0xff; 32]), + )], + }, + witness_set: WitnessSet { + signatures_and_public_keys: vec![], + proof: indexer_service_protocol::Proof(vec![0; 32]), + }, + }), + // ProgramDeployment transactions (rare) + _ => Transaction::ProgramDeployment(ProgramDeploymentTransaction { + hash: tx_hash, + message: ProgramDeploymentMessage { + bytecode: vec![0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00], /* WASM magic number */ + }, + }), + }; + + transactions.insert(tx_hash, (tx.clone(), block_id)); + block_transactions.push(tx); + } + + let block = Block { + header: BlockHeader { + block_id, + prev_block_hash: prev_hash, + hash: block_hash, + timestamp: 1704067200000 + (block_id * 12000), // ~12 seconds per block + signature: Signature([0u8; 64]), + }, + body: BlockBody { + transactions: block_transactions, + }, + bedrock_status: match block_id { + 0..=5 => BedrockStatus::Finalized, + 6..=8 => BedrockStatus::Safe, + _ => BedrockStatus::Pending, + }, + bedrock_parent_id: MantleMsgId([0; 32]), + }; + + prev_hash = block_hash; + blocks.push(block); + } + + Self { + blocks, + accounts, + transactions, + } + } +} + +// `async_trait` is required by `jsonrpsee` +#[async_trait::async_trait] +impl indexer_service_rpc::RpcServer for MockIndexerService { + async fn subscribe_to_blocks( + &self, + _subscription_sink: jsonrpsee::PendingSubscriptionSink, + _from: BlockId, + ) -> SubscriptionResult { + // Subscription not implemented for mock service + Err("Subscriptions not supported in mock service".into()) + } + + async fn get_block_by_id(&self, block_id: BlockId) -> Result<Block, ErrorObjectOwned> { + self.blocks + .iter() + .find(|b| b.header.block_id == block_id) + .cloned() + .ok_or_else(|| { + ErrorObjectOwned::owned( + -32001, + format!("Block with ID {} not found", block_id), + None::<()>, + ) + }) + } + + async fn get_block_by_hash(&self, block_hash: Hash) -> Result<Block, ErrorObjectOwned> { + self.blocks + .iter() + .find(|b| b.header.hash == block_hash) + .cloned() + .ok_or_else(|| ErrorObjectOwned::owned(-32001, "Block with hash not found", None::<()>)) + } + + async fn get_last_block_id(&self) -> Result<BlockId, ErrorObjectOwned> { + self.blocks + .last() + .map(|b| b.header.block_id) + .ok_or_else(|| ErrorObjectOwned::owned(-32001, "No blocks available", None::<()>)) + } + + async fn get_account(&self, account_id: AccountId) -> Result<Account, ErrorObjectOwned> { + self.accounts + .get(&account_id) + .cloned() + .ok_or_else(|| ErrorObjectOwned::owned(-32001, "Account not found", None::<()>)) + } + + async fn get_transaction(&self, tx_hash: Hash) -> Result<Transaction, ErrorObjectOwned> { + self.transactions + .get(&tx_hash) + .map(|(tx, _)| tx.clone()) + .ok_or_else(|| ErrorObjectOwned::owned(-32001, "Transaction not found", None::<()>)) + } + + async fn get_blocks(&self, offset: u32, limit: u32) -> Result<Vec<Block>, ErrorObjectOwned> { + let offset = offset as usize; + let limit = limit as usize; + let total = self.blocks.len(); + + // Return blocks in reverse order (newest first), with pagination + let start = offset.min(total); + let end = (offset + limit).min(total); + + Ok(self + .blocks + .iter() + .rev() + .skip(start) + .take(end - start) + .cloned() + .collect()) + } + + async fn get_transactions_by_account( + &self, + account_id: AccountId, + limit: u32, + offset: u32, + ) -> Result<Vec<Transaction>, ErrorObjectOwned> { + let mut account_txs: Vec<_> = self + .transactions + .values() + .filter(|(tx, _)| match tx { + Transaction::Public(pub_tx) => pub_tx.message.account_ids.contains(&account_id), + Transaction::PrivacyPreserving(priv_tx) => { + priv_tx.message.public_account_ids.contains(&account_id) + } + Transaction::ProgramDeployment(_) => false, + }) + .collect(); + + // Sort by block ID descending (most recent first) + account_txs.sort_by(|a, b| b.1.cmp(&a.1)); + + let start = offset as usize; + if start >= account_txs.len() { + return Ok(Vec::new()); + } + + let end = (start + limit as usize).min(account_txs.len()); + + Ok(account_txs[start..end] + .iter() + .map(|(tx, _)| tx.clone()) + .collect()) + } +} diff --git a/indexer_service/src/service.rs b/indexer_service/src/service.rs index 46c5fb2d..432dcc24 100644 --- a/indexer_service/src/service.rs +++ b/indexer_service/src/service.rs @@ -33,4 +33,17 @@ impl indexer_service_rpc::RpcServer for IndexerService { async fn get_transaction(&self, _tx_hash: Hash) -> Result<Transaction, ErrorObjectOwned> { todo!() } + + async fn get_blocks(&self, _offset: u32, _limit: u32) -> Result<Vec<Block>, ErrorObjectOwned> { + todo!() + } + + async fn get_transactions_by_account( + &self, + _account_id: AccountId, + _limit: u32, + _offset: u32, + ) -> Result<Vec<Transaction>, ErrorObjectOwned> { + todo!() + } } diff --git a/nssa/src/privacy_preserving_transaction/transaction.rs b/nssa/src/privacy_preserving_transaction/transaction.rs index 34649d2d..8eb4236e 100644 --- a/nssa/src/privacy_preserving_transaction/transaction.rs +++ b/nssa/src/privacy_preserving_transaction/transaction.rs @@ -5,6 +5,7 @@ use nssa_core::{ Commitment, CommitmentSetDigest, Nullifier, PrivacyPreservingCircuitOutput, account::{Account, AccountWithMetadata}, }; +use sha2::{Digest as _, digest::FixedOutput as _}; use super::{message::Message, witness_set::WitnessSet}; use crate::{ @@ -131,6 +132,13 @@ impl PrivacyPreservingTransaction { &self.witness_set } + pub fn hash(&self) -> [u8; 32] { + let bytes = self.to_bytes(); + let mut hasher = sha2::Sha256::new(); + hasher.update(&bytes); + hasher.finalize_fixed().into() + } + pub(crate) fn signer_account_ids(&self) -> Vec<AccountId> { self.witness_set .signatures_and_public_keys() diff --git a/nssa/src/program_deployment_transaction/transaction.rs b/nssa/src/program_deployment_transaction/transaction.rs index 6002aded..188b73ea 100644 --- a/nssa/src/program_deployment_transaction/transaction.rs +++ b/nssa/src/program_deployment_transaction/transaction.rs @@ -1,4 +1,5 @@ use borsh::{BorshDeserialize, BorshSerialize}; +use sha2::{Digest as _, digest::FixedOutput as _}; use crate::{ V02State, error::NssaError, program::Program, program_deployment_transaction::message::Message, @@ -6,7 +7,7 @@ use crate::{ #[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] pub struct ProgramDeploymentTransaction { - pub(crate) message: Message, + pub message: Message, } impl ProgramDeploymentTransaction { @@ -30,4 +31,11 @@ impl ProgramDeploymentTransaction { Ok(program) } } + + pub fn hash(&self) -> [u8; 32] { + let bytes = self.to_bytes(); + let mut hasher = sha2::Sha256::new(); + hasher.update(&bytes); + hasher.finalize_fixed().into() + } } diff --git a/nssa/src/public_transaction/transaction.rs b/nssa/src/public_transaction/transaction.rs index f5badb6a..7d42dccc 100644 --- a/nssa/src/public_transaction/transaction.rs +++ b/nssa/src/public_transaction/transaction.rs @@ -17,8 +17,8 @@ use crate::{ #[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] pub struct PublicTransaction { - message: Message, - witness_set: WitnessSet, + pub message: Message, + pub witness_set: WitnessSet, } impl PublicTransaction {