diff --git a/Cargo.lock b/Cargo.lock index 4caa85d..8bdbf47 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -25,14 +40,14 @@ checksum = "90f374d3c6d729268bbe2d0e0ff992bb97898b2df756691a62ee1d5f0506bc39" dependencies = [ "alloy-primitives", "num_enum", - "strum", + "strum 0.27.2", ] [[package]] name = "alloy-consensus" -version = "1.5.2" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed1958f0294ecc05ebe7b3c9a8662a3e221c2523b7f2bcd94c7a651efbd510bf" +checksum = "86debde32d8dbb0ab29e7cc75ae1a98688ac7a4c9da54b3a9b14593b9b3c46d3" dependencies = [ "alloy-eips", "alloy-primitives", @@ -57,9 +72,9 @@ dependencies = [ [[package]] name = "alloy-consensus-any" -version = "1.5.2" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f752e99497ddc39e22d547d7dfe516af10c979405a034ed90e69b914b7dddeae" +checksum = "8d6cb2e7efd385b333f5a77b71baaa2605f7e22f1d583f2879543b54cbce777c" dependencies = [ "alloy-consensus", "alloy-eips", @@ -69,6 +84,44 @@ dependencies = [ "serde", ] +[[package]] +name = "alloy-contract" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668859fcdb42eee289de22a9d01758c910955bb6ecda675b97276f99ce2e16b0" +dependencies = [ + "alloy-consensus", + "alloy-dyn-abi", + "alloy-json-abi", + "alloy-network", + "alloy-network-primitives", + "alloy-primitives", + "alloy-provider", + "alloy-rpc-types-eth", + "alloy-sol-types", + "alloy-transport", + "futures", + "futures-util", + "serde_json", + "thiserror", +] + +[[package]] +name = "alloy-dyn-abi" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14ff5ee5f27aa305bda825c735f686ad71bb65508158f059f513895abe69b8c3" +dependencies = [ + "alloy-json-abi", + "alloy-primitives", + "alloy-sol-type-parser", + "alloy-sol-types", + "itoa", + "serde", + "serde_json", + "winnow", +] + [[package]] name = "alloy-eip2124" version = "0.2.0" @@ -121,9 +174,9 @@ dependencies = [ [[package]] name = "alloy-eips" -version = "1.5.2" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "813a67f87e56b38554d18b182616ee5006e8e2bf9df96a0df8bf29dff1d52e3f" +checksum = "be47bf1b91674a5f394b9ed3c691d764fb58ba43937f1371550ff4bc8e59c295" dependencies = [ "alloy-eip2124", "alloy-eip2930", @@ -157,9 +210,9 @@ dependencies = [ [[package]] name = "alloy-json-rpc" -version = "1.5.2" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2dd146b3de349a6ffaa4e4e319ab3a90371fb159fb0bddeb1c7bbe8b1792eff" +checksum = "5a24c81a56d684f525cd1c012619815ad3a1dd13b0238f069356795d84647d3c" dependencies = [ "alloy-primitives", "alloy-sol-types", @@ -172,9 +225,9 @@ dependencies = [ [[package]] name = "alloy-network" -version = "1.5.2" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c12278ffbb8872dfba3b2f17d8ea5e8503c2df5155d9bc5ee342794bde505c3" +checksum = "786c5b3ad530eaf43cda450f973fe7fb1c127b4c8990adf66709dafca25e3f6f" dependencies = [ "alloy-consensus", "alloy-consensus-any", @@ -198,9 +251,9 @@ dependencies = [ [[package]] name = "alloy-network-primitives" -version = "1.5.2" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "833037c04917bc2031541a60e8249e4ab5500e24c637c1c62e95e963a655d66f" +checksum = "c1ed40adf21ae4be786ef5eb62db9c692f6a30f86d34452ca3f849d6390ce319" dependencies = [ "alloy-consensus", "alloy-eips", @@ -220,7 +273,7 @@ dependencies = [ "cfg-if", "const-hex", "derive_more", - "foldhash", + "foldhash 0.2.0", "hashbrown 0.16.1", "indexmap 2.13.0", "itoa", @@ -238,9 +291,9 @@ dependencies = [ [[package]] name = "alloy-provider" -version = "1.5.2" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eafa840b0afe01c889a3012bb2fde770a544f74eab2e2870303eb0a5fb869c48" +checksum = "a3ca4c15818be7ac86208aff3a91b951d14c24e1426e66624e75f2215ba5e2cc" dependencies = [ "alloy-chains", "alloy-consensus", @@ -263,7 +316,7 @@ dependencies = [ "either", "futures", "futures-utils-wasm", - "lru", + "lru 0.16.3", "parking_lot", "pin-project", "reqwest", @@ -300,9 +353,9 @@ dependencies = [ [[package]] name = "alloy-rpc-client" -version = "1.5.2" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12768ae6303ec764905a8a7cd472aea9072f9f9c980d18151e26913da8ae0123" +checksum = "abe0addad5b8197e851062b49dc47157444bced173b601d91e3f9b561a060a50" dependencies = [ "alloy-json-rpc", "alloy-primitives", @@ -335,9 +388,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-any" -version = "1.5.2" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1cf5a093e437dfd62df48e480f24e1a3807632358aad6816d7a52875f1c04aa" +checksum = "d0e98aabb013a71a4b67b52825f7b503e5bb6057fb3b7b2290d514b0b0574b57" dependencies = [ "alloy-consensus-any", "alloy-rpc-types-eth", @@ -345,10 +398,28 @@ dependencies = [ ] [[package]] -name = "alloy-rpc-types-eth" +name = "alloy-rpc-types-engine" version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28e97603095020543a019ab133e0e3dc38cd0819f19f19bdd70c642404a54751" +checksum = "336ef381c7409f23c69f6e79bddc1917b6e832cff23e7a5cf84b9381d53582e6" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-rlp", + "alloy-serde", + "derive_more", + "jsonwebtoken", + "rand 0.8.5", + "serde", + "strum 0.27.2", +] + +[[package]] +name = "alloy-rpc-types-eth" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5899af8417dcf89f40f88fa3bdb2f3f172605d8e167234311ee34811bbfdb0bf" dependencies = [ "alloy-consensus", "alloy-consensus-any", @@ -367,9 +438,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-txpool" -version = "1.5.2" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25b73c1d6e4f1737a20d246dad5a0abd6c1b76ec4c3d153684ef8c6f1b6bb4f4" +checksum = "3a8074654c0292783d504bfa1f2691a69f420154ee9a7883f9212eaf611e60cd" dependencies = [ "alloy-primitives", "alloy-rpc-types-eth", @@ -379,9 +450,9 @@ dependencies = [ [[package]] name = "alloy-serde" -version = "1.5.2" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "946a0d413dbb5cd9adba0de5f8a1a34d5b77deda9b69c1d7feed8fc875a1aa26" +checksum = "feb73325ee881e42972a5a7bc85250f6af89f92c6ad1222285f74384a203abeb" dependencies = [ "alloy-primitives", "serde", @@ -390,9 +461,9 @@ dependencies = [ [[package]] name = "alloy-signer" -version = "1.5.2" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f7481dc8316768f042495eaf305d450c32defbc9bce09d8bf28afcd956895bb" +checksum = "1bea4c8f30eddb11d7ab56e83e49c814655daa78ca708df26c300c10d0189cbc" dependencies = [ "alloy-primitives", "async-trait", @@ -475,9 +546,9 @@ dependencies = [ [[package]] name = "alloy-transport" -version = "1.5.2" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78f169b85eb9334871db986e7eaf59c58a03d86a30cc68b846573d47ed0656bb" +checksum = "b321f506bd67a434aae8e8a7dfe5373bf66137c149a5f09c9e7dfb0ca43d7c91" dependencies = [ "alloy-json-rpc", "auto_impl", @@ -498,9 +569,9 @@ dependencies = [ [[package]] name = "alloy-transport-http" -version = "1.5.2" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "019821102e70603e2c141954418255bec539ef64ac4117f8e84fb493769acf73" +checksum = "30bf12879a20e1261cd39c3b101856f52d18886907a826e102538897f0d2b66e" dependencies = [ "alloy-json-rpc", "alloy-transport", @@ -532,11 +603,11 @@ dependencies = [ [[package]] name = "alloy-tx-macros" -version = "1.5.2" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45ceac797eb8a56bdf5ab1fab353072c17d472eab87645ca847afe720db3246d" +checksum = "6a91d6b4c2f6574fdbcb1611e460455c326667cf5b805c6bd1640dad8e8ee4d2" dependencies = [ - "darling", + "darling 0.21.3", "proc-macro2", "quote", "syn 2.0.114", @@ -861,6 +932,22 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "base-flashtypes" +version = "0.0.0" +source = "git+https://github.com/base/base.git#92d478196c8dbd8f81bec4ac542eec337a97039f" +dependencies = [ + "alloy-primitives", + "alloy-rpc-types-engine", + "alloy-rpc-types-eth", + "alloy-serde", + "brotli", + "bytes", + "serde", + "serde_json", + "thiserror", +] + [[package]] name = "base16ct" version = "0.2.0" @@ -879,6 +966,41 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" +[[package]] +name = "basectl" +version = "0.0.0" +dependencies = [ + "anyhow", + "basectl-cli", + "clap", + "tokio", +] + +[[package]] +name = "basectl-cli" +version = "0.0.0" +dependencies = [ + "alloy-contract", + "alloy-primitives", + "alloy-provider", + "alloy-rpc-types-eth", + "alloy-sol-types", + "anyhow", + "base-flashtypes", + "chrono", + "clap", + "crossterm", + "dirs", + "futures-util", + "ratatui", + "serde", + "serde_json", + "serde_yaml", + "tokio", + "tokio-tungstenite", + "url", +] + [[package]] name = "bit-set" version = "0.8.0" @@ -972,6 +1094,27 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "brotli" +version = "8.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + [[package]] name = "bumpalo" version = "3.19.1" @@ -992,9 +1135,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", ] @@ -1023,6 +1166,21 @@ dependencies = [ "crossbeam-channel", ] +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + [[package]] name = "cc" version = "1.2.54" @@ -1052,8 +1210,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" dependencies = [ "iana-time-zone", + "js-sys", "num-traits", "serde", + "wasm-bindgen", "windows-link", ] @@ -1103,6 +1263,20 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "compact_str" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + [[package]] name = "const-hex" version = "1.17.0" @@ -1205,6 +1379,32 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags", + "crossterm_winapi", + "futures-core", + "mio", + "parking_lot", + "rustix 0.38.44", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + [[package]] name = "crunchy" version = "0.2.4" @@ -1239,8 +1439,18 @@ version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.21.3", + "darling_macro 0.21.3", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core 0.23.0", + "darling_macro 0.23.0", ] [[package]] @@ -1258,13 +1468,37 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.114", +] + [[package]] name = "darling_macro" version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ - "darling_core", + "darling_core 0.21.3", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core 0.23.0", "quote", "syn 2.0.114", ] @@ -1283,6 +1517,12 @@ dependencies = [ "parking_lot_core", ] +[[package]] +name = "data-encoding" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" + [[package]] name = "der" version = "0.7.10" @@ -1358,6 +1598,27 @@ dependencies = [ "subtle", ] +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -1541,6 +1802,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "foldhash" version = "0.2.0" @@ -1690,8 +1957,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -1735,6 +2004,17 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.1.5", +] + [[package]] name = "hashbrown" version = "0.16.1" @@ -1743,7 +2023,7 @@ checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ "allocator-api2", "equivalent", - "foldhash", + "foldhash 0.2.0", "serde", "serde_core", ] @@ -2059,6 +2339,28 @@ dependencies = [ "serde_core", ] +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "instability" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357b7205c6cd18dd2c86ed312d1e70add149aea98e7ef72b9fdf0270e555c11d" +dependencies = [ + "darling 0.23.0", + "indoc", + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -2124,6 +2426,21 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonwebtoken" +version = "9.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" +dependencies = [ + "base64", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "k256" version = "0.13.4" @@ -2175,6 +2492,22 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" +[[package]] +name = "libredox" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +dependencies = [ + "bitflags", + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + [[package]] name = "linux-raw-sys" version = "0.11.0" @@ -2202,6 +2535,15 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown 0.15.5", +] + [[package]] name = "lru" version = "0.16.3" @@ -2239,7 +2581,7 @@ checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "mempool-rebroadcaster" -version = "0.1.0" +version = "0.0.0" dependencies = [ "alloy-consensus", "alloy-eips", @@ -2264,6 +2606,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", + "log", "wasi", "windows-sys 0.61.2", ] @@ -2456,6 +2799,12 @@ dependencies = [ "opentelemetry", ] +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "parity-scale-codec" version = "3.7.5" @@ -2513,6 +2862,16 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64", + "serde_core", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -2778,6 +3137,27 @@ dependencies = [ "rustversion", ] +[[package]] +name = "ratatui" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" +dependencies = [ + "bitflags", + "cassowary", + "compact_str", + "crossterm", + "indoc", + "instability", + "itertools 0.13.0", + "lru 0.12.5", + "paste", + "strum 0.26.3", + "unicode-segmentation", + "unicode-truncate", + "unicode-width 0.2.0", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -2787,6 +3167,17 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror", +] + [[package]] name = "ref-cast" version = "1.0.25" @@ -2870,6 +3261,20 @@ dependencies = [ "subtle", ] +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rlp" version = "0.5.2" @@ -2944,6 +3349,19 @@ dependencies = [ "semver 1.0.27", ] +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + [[package]] name = "rustix" version = "1.1.3" @@ -2953,7 +3371,7 @@ dependencies = [ "bitflags", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.11.0", "windows-sys 0.61.2", ] @@ -3192,12 +3610,25 @@ version = "3.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" dependencies = [ - "darling", + "darling 0.21.3", "proc-macro2", "quote", "syn 2.0.114", ] +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap 2.13.0", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "serdect" version = "0.2.0" @@ -3208,6 +3639,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + [[package]] name = "sha2" version = "0.10.9" @@ -3256,7 +3698,7 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "sidecrush" -version = "0.1.0" +version = "0.0.0" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -3272,6 +3714,27 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + [[package]] name = "signal-hook-registry" version = "1.4.8" @@ -3292,6 +3755,18 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simple_asn1" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time", +] + [[package]] name = "slab" version = "0.4.11" @@ -3345,13 +3820,35 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros 0.26.4", +] + [[package]] name = "strum" version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" dependencies = [ - "strum_macros", + "strum_macros 0.27.2", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.114", ] [[package]] @@ -3441,7 +3938,7 @@ dependencies = [ "fastrand", "getrandom 0.3.4", "once_cell", - "rustix", + "rustix 1.1.3", "windows-sys 0.61.2", ] @@ -3574,6 +4071,20 @@ dependencies = [ "tokio-util", ] +[[package]] +name = "tokio-tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" +dependencies = [ + "futures-util", + "log", + "native-tls", + "tokio", + "tokio-native-tls", + "tungstenite", +] + [[package]] name = "tokio-util" version = "0.7.18" @@ -3756,6 +4267,24 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "native-tls", + "rand 0.9.2", + "sha1", + "thiserror", + "utf-8", +] + [[package]] name = "typenum" version = "1.19.0" @@ -3798,12 +4327,47 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "unicode-truncate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +dependencies = [ + "itertools 0.13.0", + "unicode-segmentation", + "unicode-width 0.1.14", +] + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" + [[package]] name = "unicode-xid" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.8" @@ -3817,6 +4381,12 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -3973,6 +4543,28 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-core" version = "0.62.2" @@ -4032,13 +4624,31 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets", + "windows-targets 0.53.5", ] [[package]] @@ -4050,6 +4660,22 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + [[package]] name = "windows-targets" version = "0.53.5" @@ -4057,58 +4683,106 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ "windows-link", - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + [[package]] name = "windows_aarch64_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + [[package]] name = "windows_aarch64_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + [[package]] name = "windows_i686_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + [[package]] name = "windows_i686_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + [[package]] name = "windows_i686_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + [[package]] name = "windows_x86_64_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + [[package]] name = "windows_x86_64_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "windows_x86_64_msvc" version = "0.53.1" diff --git a/Cargo.toml b/Cargo.toml index 4e07355..2851da7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,60 @@ [workspace] resolver = "2" -members = ["crates/mempool-rebroadcaster", "crates/sidecrush"] +members = ["crates/*", "bin/*"] +exclude = [".github/"] + +[workspace.package] +version = "0.0.0" +edition = "2024" +license = "MIT" + +[workspace.lints.rust] +missing-debug-implementations = "warn" +unreachable-pub = "warn" +unused-must-use = "deny" +unnameable-types = "warn" + +[workspace.lints.clippy] +all = { level = "warn", priority = -1 } +missing-const-for-fn = "warn" +redundant-clone = "warn" +clone-on-ref-ptr = "warn" +unnecessary-to-owned = "warn" +cloned-instead-of-copied = "warn" +flat-map-option = "warn" +implicit-clone = "warn" +or-fun-call = "warn" +use-self = "warn" +option-if-let-else = "warn" +uninlined-format-args = "warn" +manual-string-new = "warn" +single-char-pattern = "warn" +redundant-else = "warn" +match-same-arms = "warn" +undocumented-unsafe-blocks = "warn" +doc-markdown = "warn" +dbg-macro = "warn" +branches-sharing-code = "warn" +derive-partial-eq-without-eq = "warn" +explicit-into-iter-loop = "warn" +explicit-iter-loop = "warn" +iter-with-drain = "warn" +needless-pass-by-ref-mut = "warn" +string-lit-as-bytes = "warn" [workspace.dependencies] clap = { version = "4.0", features = ["derive", "env"] } +tokio-tungstenite = { version = "0.26", features = ["native-tls"] } +futures-util = "0.3" +url = "2.5" +ratatui = "0.29" +crossterm = { version = "0.28", features = ["event-stream"] } +chrono = "0.4" +anyhow = "1.0" +serde_yaml = "0.9" +dirs = "6.0" +dotenvy = "0.15.7" +async-trait = "0.1" tracing = "0.1" tracing-subscriber = { version = "0.3", default-features = false, features = ["env-filter", "fmt", "ansi", "json"] } tokio = { version = "1.0", features = ["full"] } @@ -16,6 +67,7 @@ cadence = "1.4" # alloy alloy-primitives = { version = "1.5.2", default-features = false, features = [ "map-foldhash", + "serde", ] } alloy-genesis = { version = "1.5.2", default-features = false } alloy-eips = { version = "1.5.2", default-features = false } @@ -28,8 +80,16 @@ alloy-provider = { version = "1.5.2" } alloy-hardforks = { version = "0.5" } alloy-rpc-client = { version = "1.5.2" } alloy-transport-http = { version = "1.5.2" } +alloy-sol-types = { version = "1.5.2" } +alloy-contract = { version = "1.5.2" } # op-alloy op-alloy-rpc-types = { version = "0.22.0", default-features = false } op-alloy-rpc-types-engine = { version = "0.22.0", default-features = false } op-alloy-consensus = { version = "0.22.0", default-features = false } + +# base +base-flashtypes = { git = "https://github.com/base/base.git" } + +# internal +basectl-cli = { path = "crates/basectl" } diff --git a/README.md b/README.md index c5aaa0e..45cec1f 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,19 @@ ![Base](assets/logo.png) -# Base Infra \ No newline at end of file +# Base Infra + +## Installation + +### basectl + +Install `basectl` directly from the repository using cargo: + +```bash +cargo install --git https://github.com/base/infra --package basectl +``` + +Or from a specific branch: + +```bash +cargo install --git https://github.com/base/infra --branch main --package basectl +``` \ No newline at end of file diff --git a/bin/basectl/Cargo.toml b/bin/basectl/Cargo.toml new file mode 100644 index 0000000..83c73a3 --- /dev/null +++ b/bin/basectl/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "basectl" +version.workspace = true +edition.workspace = true +license.workspace = true + +[lints] +workspace = true + +[[bin]] +name = "basectl" +path = "main.rs" + +[dependencies] +basectl-cli = { workspace = true } +clap = { workspace = true } +tokio = { workspace = true } +anyhow = { workspace = true } diff --git a/bin/basectl/main.rs b/bin/basectl/main.rs new file mode 100644 index 0000000..0089529 --- /dev/null +++ b/bin/basectl/main.rs @@ -0,0 +1,62 @@ +use basectl_cli::{ + commands::{ + config::{ConfigCommand, default_view, run_config}, + flashblocks::{FlashblocksCommand, default_subscribe, run_flashblocks}, + }, + config::ChainConfig, + tui::{HomeSelection, NavResult, run_homescreen}, +}; +use clap::{Parser, Subcommand}; + +#[derive(Debug, Parser)] +#[command(name = "basectl")] +#[command(about = "Base infrastructure control CLI")] +struct Cli { + /// Chain configuration (mainnet, sepolia, or path to config file) + #[arg(short = 'c', long = "config", default_value = "mainnet", global = true)] + config: String, + + #[command(subcommand)] + command: Option, +} + +#[derive(Debug, Subcommand)] +enum Commands { + /// Chain configuration operations + #[command(visible_alias = "c")] + Config { + #[command(subcommand)] + command: ConfigCommand, + }, + /// Flashblocks operations + #[command(visible_alias = "f")] + Flashblocks { + #[command(subcommand)] + command: FlashblocksCommand, + }, +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let cli = Cli::parse(); + + let chain_config = ChainConfig::load(&cli.config)?; + + match cli.command { + Some(Commands::Config { command }) => run_config(command, &chain_config).await, + Some(Commands::Flashblocks { command }) => run_flashblocks(command, &chain_config).await, + None => { + // Show homescreen when no command provided + loop { + let next = match run_homescreen()? { + HomeSelection::Config => default_view(&chain_config).await?, + HomeSelection::Flashblocks => default_subscribe(&chain_config).await?, + HomeSelection::Quit => return Ok(()), + }; + if next == NavResult::Quit { + return Ok(()); + } + } + } + } +} diff --git a/crates/basectl/Cargo.toml b/crates/basectl/Cargo.toml new file mode 100644 index 0000000..703c463 --- /dev/null +++ b/crates/basectl/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "basectl-cli" +version.workspace = true +edition.workspace = true +license.workspace = true + +[lints] +workspace = true + +[dependencies] +clap = { workspace = true } +tokio = { workspace = true } +serde_json = { workspace = true } +tokio-tungstenite = { workspace = true } +futures-util = { workspace = true } +ratatui = { workspace = true } +crossterm = { workspace = true } +chrono = { workspace = true } +anyhow = { workspace = true } +serde = { workspace = true } +serde_yaml = { workspace = true } +dirs = { workspace = true } +alloy-provider = { workspace = true } +alloy-rpc-types-eth = { workspace = true } +base-flashtypes = { workspace = true } +url = { workspace = true } +alloy-primitives = { workspace = true } +alloy-sol-types = { workspace = true } +alloy-contract = { workspace = true } diff --git a/crates/basectl/src/commands/config.rs b/crates/basectl/src/commands/config.rs new file mode 100644 index 0000000..b32e2f0 --- /dev/null +++ b/crates/basectl/src/commands/config.rs @@ -0,0 +1,323 @@ +use std::{ + io::Stdout, + time::{Duration, Instant}, +}; + +use alloy_primitives::{Address, hex}; +use anyhow::Result; +use clap::Subcommand; +use crossterm::event::{Event, EventStream, KeyCode, KeyEventKind, KeyModifiers}; +use futures_util::StreamExt; +use ratatui::{ + layout::{Constraint, Rect}, + prelude::*, + widgets::{Block, Borders, Cell, Row, Table}, +}; +use tokio::time::interval; + +use crate::{ + config::ChainConfig, + l1_client::{FullSystemConfig, fetch_full_system_config}, + tui::{AppFrame, Keybinding, NavResult, StatusInfo, restore_terminal, setup_terminal}, +}; + +const REFRESH_INTERVAL: Duration = Duration::from_secs(12); + +const KEYBINDINGS: &[Keybinding] = &[ + Keybinding::new("r", "Refresh now"), + Keybinding::new("?", "Toggle help"), + Keybinding::new("h", "Home"), + Keybinding::new("q", "Quit"), +]; + +#[derive(Debug, Subcommand)] +pub enum ConfigCommand { + /// View the current chain configuration and L1 `SystemConfig` values + #[command(visible_alias = "v")] + View, +} + +pub async fn run_config(command: ConfigCommand, config: &ChainConfig) -> Result<()> { + match command { + ConfigCommand::View => run_view(config).await, + } +} + +/// Run the default config view (called from homescreen) +pub async fn default_view(config: &ChainConfig) -> Result { + let mut terminal = setup_terminal()?; + let result = run_view_loop(&mut terminal, config).await; + restore_terminal(&mut terminal)?; + result +} + +struct ConfigViewState { + chain_config: ChainConfig, + current: FullSystemConfig, + previous: Option, + last_fetch: Instant, + fetch_error: Option, + show_help: bool, + is_fetching: bool, +} + +impl ConfigViewState { + fn new(chain_config: ChainConfig) -> Self { + Self { + chain_config, + current: FullSystemConfig::default(), + previous: None, + last_fetch: Instant::now(), + fetch_error: None, + show_help: false, + is_fetching: true, + } + } + + fn seconds_until_refresh(&self) -> u64 { + REFRESH_INTERVAL.saturating_sub(self.last_fetch.elapsed()).as_secs() + } + + fn should_refresh(&self) -> bool { + self.last_fetch.elapsed() >= REFRESH_INTERVAL + } +} + +async fn run_view(config: &ChainConfig) -> Result<()> { + let mut terminal = setup_terminal()?; + let _ = run_view_loop(&mut terminal, config).await?; + restore_terminal(&mut terminal)?; + Ok(()) +} + +async fn run_view_loop( + terminal: &mut Terminal>, + config: &ChainConfig, +) -> Result { + let mut state = ConfigViewState::new(config.clone()); + let mut events = EventStream::new(); + let mut refresh_interval = interval(Duration::from_millis(100)); + + // Initial fetch + do_fetch(&mut state).await; + + loop { + terminal.draw(|f| draw_config_view(f, &state))?; + + tokio::select! { + _ = refresh_interval.tick() => { + if state.should_refresh() && !state.is_fetching { + do_fetch(&mut state).await; + } + } + Some(Ok(Event::Key(key))) = events.next() => { + if key.kind != KeyEventKind::Press { + continue; + } + if key.code == KeyCode::Char('c') && key.modifiers.contains(KeyModifiers::CONTROL) { + return Ok(NavResult::Quit); + } + match key.code { + KeyCode::Char('q') => return Ok(NavResult::Quit), + KeyCode::Char('h') => return Ok(NavResult::Home), + KeyCode::Char('?') => state.show_help = !state.show_help, + KeyCode::Char('r') if !state.is_fetching => do_fetch(&mut state).await, + _ => {} + } + } + } + } +} + +async fn do_fetch(state: &mut ConfigViewState) { + state.is_fetching = true; + + match fetch_full_system_config( + state.chain_config.l1_rpc.as_str(), + state.chain_config.system_config, + ) + .await + { + Ok(new_config) => { + // Only set previous if we had a successful fetch before + if state.current != FullSystemConfig::default() { + state.previous = Some(state.current.clone()); + } + state.current = new_config; + state.fetch_error = None; + } + Err(e) => { + state.fetch_error = Some(e.to_string()); + } + } + + state.last_fetch = Instant::now(); + state.is_fetching = false; +} + +fn draw_config_view(f: &mut Frame, state: &ConfigViewState) { + let layout = AppFrame::split_layout(f.area(), state.show_help); + + // Build status info for the status bar + let status_info = build_status_info(state); + + // Render the app frame (status bar + help sidebar) + AppFrame::render(f, &layout, &state.chain_config.name, KEYBINDINGS, Some(&status_info)); + + // Split content area for chain config and system config tables + let content_chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(8), // Chain config table + Constraint::Length(1), // Spacing + Constraint::Min(14), // System config table + ]) + .split(layout.content); + + draw_chain_config_table(f, content_chunks[0], &state.chain_config); + draw_system_config_table(f, content_chunks[2], state); +} + +fn draw_chain_config_table(f: &mut Frame, area: Rect, config: &ChainConfig) { + let label_style = Style::default().fg(Color::Cyan); + let value_style = Style::default().fg(Color::White); + + let rows = vec![ + Row::new(vec![ + Cell::from("Name").style(label_style), + Cell::from(config.name.clone()).style(value_style), + ]), + Row::new(vec![ + Cell::from("L2 RPC").style(label_style), + Cell::from(config.rpc.to_string()).style(value_style), + ]), + Row::new(vec![ + Cell::from("Flashblocks WS").style(label_style), + Cell::from(config.flashblocks_ws.to_string()).style(value_style), + ]), + Row::new(vec![ + Cell::from("L1 RPC").style(label_style), + Cell::from(config.l1_rpc.to_string()).style(value_style), + ]), + Row::new(vec![ + Cell::from("SystemConfig").style(label_style), + Cell::from(format!("{:#x}", config.system_config)).style(value_style), + ]), + ]; + + let table = Table::new(rows, [Constraint::Length(16), Constraint::Min(40)]).block( + Block::default() + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::DarkGray)) + .title(" Chain Configuration ") + .title_style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)), + ); + + f.render_widget(table, area); +} + +fn draw_system_config_table(f: &mut Frame, area: Rect, state: &ConfigViewState) { + let cur = &state.current; + let prev = state.previous.as_ref(); + + // Macro to check if a field changed between refreshes + macro_rules! changed { + ($field:ident) => { + prev.map_or(false, |p| cur.$field != p.$field) + }; + } + + let rows = vec![ + make_row("Gas Limit", format_gas_limit(cur.gas_limit), changed!(gas_limit)), + make_row( + "EIP-1559 Elasticity", + format_option(cur.eip1559_elasticity), + changed!(eip1559_elasticity), + ), + make_row( + "EIP-1559 Denominator", + format_option(cur.eip1559_denominator), + changed!(eip1559_denominator), + ), + make_row("Batcher Hash", format_batcher_hash(cur.batcher_hash), changed!(batcher_hash)), + make_row("Overhead", format_option(cur.overhead), changed!(overhead)), + make_row("Scalar", format_option(cur.scalar), changed!(scalar)), + make_row( + "Unsafe Block Signer", + format_address(cur.unsafe_block_signer), + changed!(unsafe_block_signer), + ), + make_row("Start Block", format_option(cur.start_block), changed!(start_block)), + make_row("Basefee Scalar", format_option(cur.basefee_scalar), changed!(basefee_scalar)), + make_row( + "Blobbasefee Scalar", + format_option(cur.blobbasefee_scalar), + changed!(blobbasefee_scalar), + ), + ]; + + let table = + Table::new(rows, [Constraint::Length(20), Constraint::Min(40), Constraint::Length(10)]) + .block( + Block::default() + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::DarkGray)) + .title(" L1 SystemConfig (auto-refresh) ") + .title_style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)), + ); + + f.render_widget(table, area); +} + +fn make_row(field: &str, value: String, changed: bool) -> Row<'static> { + let label_style = Style::default().fg(Color::Cyan); + let value_style = if changed { + Style::default().fg(Color::Green).add_modifier(Modifier::BOLD) + } else { + Style::default().fg(Color::White) + }; + + let status = if changed { "CHANGED" } else { "" }; + let status_style = Style::default().fg(Color::Green).add_modifier(Modifier::BOLD); + + Row::new(vec![ + Cell::from(field.to_string()).style(label_style), + Cell::from(value).style(value_style), + Cell::from(status).style(status_style), + ]) +} + +fn build_status_info(state: &ConfigViewState) -> StatusInfo { + if state.is_fetching { + StatusInfo::new("Refreshing...").with_style(Style::default().fg(Color::Yellow)) + } else if let Some(ref err) = state.fetch_error { + StatusInfo::new(format!("Error: {err}")).with_style(Style::default().fg(Color::Red)) + } else { + StatusInfo::new(format!("Next refresh in {}s", state.seconds_until_refresh())) + } +} + +// Formatting helpers + +const UNAVAILABLE: &str = "(unavailable)"; + +fn format_gas_limit(gas: Option) -> String { + match gas { + Some(g) if g >= 1_000_000 => format!("{g} ({:.0}M)", g as f64 / 1_000_000.0), + Some(g) if g >= 1_000 => format!("{g} ({:.1}K)", g as f64 / 1_000.0), + Some(g) => g.to_string(), + None => UNAVAILABLE.to_string(), + } +} + +fn format_option(val: Option) -> String { + val.map_or_else(|| UNAVAILABLE.to_string(), |v| v.to_string()) +} + +fn format_batcher_hash(hash: Option<[u8; 32]>) -> String { + hash.map_or_else(|| UNAVAILABLE.to_string(), |h| format!("0x{}", hex::encode(h))) +} + +fn format_address(addr: Option
) -> String { + addr.map_or_else(|| UNAVAILABLE.to_string(), |a| format!("{a:#x}")) +} diff --git a/crates/basectl/src/commands/flashblocks.rs b/crates/basectl/src/commands/flashblocks.rs new file mode 100644 index 0000000..8cf91ea --- /dev/null +++ b/crates/basectl/src/commands/flashblocks.rs @@ -0,0 +1,521 @@ +use std::{collections::VecDeque, io::Stdout}; + +use anyhow::Result; +use base_flashtypes::Flashblock; +use chrono::{DateTime, Local}; +use clap::{Args, Subcommand}; +use crossterm::event::{Event, EventStream, KeyCode, KeyEventKind, KeyModifiers}; +use futures_util::StreamExt; +use ratatui::{ + prelude::*, + widgets::{Block, Borders, Cell, Row, Table, TableState}, +}; +use tokio::sync::mpsc; +use tokio_tungstenite::connect_async; + +use crate::{ + config::ChainConfig, + rpc::{ChainParams, fetch_chain_params}, + tui::{AppFrame, Keybinding, NavResult, restore_terminal, setup_terminal}, +}; + +const MAX_FLASHBLOCKS: usize = 10_000; + +const KEYBINDINGS: &[Keybinding] = &[ + Keybinding::new("?", "Toggle help"), + Keybinding::new("Space", "Pause/Resume"), + Keybinding::new("Up/k", "Scroll up"), + Keybinding::new("Down/j", "Scroll down"), + Keybinding::new("PgUp", "Page up"), + Keybinding::new("PgDn", "Page down"), + Keybinding::new("Home/g", "Top (auto-scroll)"), + Keybinding::new("End/G", "Bottom"), + Keybinding::new("h", "Home"), + Keybinding::new("q", "Quit"), +]; + +#[derive(Debug, Subcommand)] +pub enum FlashblocksCommand { + /// Subscribe to flashblocks stream + #[command(visible_alias = "s")] + Subscribe(SubscribeArgs), +} + +#[derive(Debug, Args)] +pub struct SubscribeArgs { + /// WebSocket endpoint (overrides chain config) + #[arg(short = 'w', long = "websocket")] + websocket: Option, + + /// Output JSON lines instead of TUI + #[arg(long)] + json: bool, +} + +pub async fn run_flashblocks(command: FlashblocksCommand, config: &ChainConfig) -> Result<()> { + match command { + FlashblocksCommand::Subscribe(args) => run_subscribe(args, config).await, + } +} + +/// Run the default flashblocks subscribe with TUI mode (called from homescreen) +pub async fn default_subscribe(config: &ChainConfig) -> Result { + let mut terminal = setup_terminal()?; + let params = match fetch_chain_params(config).await { + Ok(params) => params, + Err(e) => { + restore_terminal(&mut terminal)?; + return Err(e); + } + }; + let result = + run_tui_loop(&mut terminal, config.flashblocks_ws.as_str(), &config.name, params).await; + restore_terminal(&mut terminal)?; + result +} + +struct FlashblockEntry { + block_number: u64, + index: u64, + tx_count: usize, + gas_used: u64, + gas_limit: u64, + base_fee: Option, + prev_base_fee: Option, + timestamp: DateTime, + time_diff_ms: Option, +} + +struct AppState { + chain_name: String, + elasticity_multiplier: u64, + flashblocks: VecDeque, + message_count: u64, + current_gas_limit: u64, + current_base_fee: Option, + show_help: bool, + table_state: TableState, + auto_scroll: bool, + paused: bool, +} + +impl AppState { + fn new(chain_name: String, params: ChainParams) -> Self { + let mut table_state = TableState::default(); + table_state.select(Some(0)); + Self { + chain_name, + elasticity_multiplier: params.elasticity, + flashblocks: VecDeque::with_capacity(MAX_FLASHBLOCKS), + message_count: 0, + current_gas_limit: params.gas_limit, + current_base_fee: None, + show_help: false, + table_state, + auto_scroll: true, + paused: false, + } + } + + fn add_flashblock(&mut self, fb: Flashblock) { + let block_number = fb.metadata.block_number; + let now = Local::now(); + + // Extract base fee from base payload (present in FB#0) + let base_fee = + fb.base.as_ref().map(|base| base.base_fee_per_gas.try_into().unwrap_or(u128::MAX)); + + // Capture previous base fee before updating + let prev_base_fee = self.current_base_fee; + + // Update gas limit and base fee from base payload (present in FB#0) + if let Some(ref base) = fb.base { + self.current_gas_limit = base.gas_limit; + self.current_base_fee = base_fee; + } + + // Calculate time diff from previous flashblock + let time_diff_ms = + self.flashblocks.front().map(|prev| (now - prev.timestamp).num_milliseconds()); + + let entry = FlashblockEntry { + block_number, + index: fb.index, + tx_count: fb.diff.transactions.len(), + gas_used: fb.diff.gas_used, + gas_limit: self.current_gas_limit, + base_fee, + prev_base_fee, + timestamp: now, + time_diff_ms, + }; + + self.flashblocks.push_front(entry); + if self.flashblocks.len() > MAX_FLASHBLOCKS { + self.flashblocks.pop_back(); + } + self.message_count += 1; + self.maintain_scroll_on_new_data(); + } + + fn scroll_up(&mut self) { + if let Some(selected) = self.table_state.selected() { + if selected > 0 { + self.table_state.select(Some(selected - 1)); + self.auto_scroll = false; + } else { + // At top, enable auto-scroll + self.auto_scroll = true; + } + } + } + + fn scroll_down(&mut self) { + if let Some(selected) = self.table_state.selected() { + let max = self.flashblocks.len().saturating_sub(1); + if selected < max { + self.table_state.select(Some(selected + 1)); + self.auto_scroll = false; + } + } + } + + fn page_up(&mut self, page_size: usize) { + if let Some(selected) = self.table_state.selected() { + let new_selected = selected.saturating_sub(page_size); + self.table_state.select(Some(new_selected)); + self.auto_scroll = new_selected == 0; + } + } + + fn page_down(&mut self, page_size: usize) { + if let Some(selected) = self.table_state.selected() { + let max = self.flashblocks.len().saturating_sub(1); + let new_selected = (selected + page_size).min(max); + self.table_state.select(Some(new_selected)); + self.auto_scroll = false; + } + } + + fn scroll_to_top(&mut self) { + self.table_state.select(Some(0)); + self.auto_scroll = true; + } + + fn scroll_to_bottom(&mut self) { + let max = self.flashblocks.len().saturating_sub(1); + self.table_state.select(Some(max)); + self.auto_scroll = false; + } + + fn maintain_scroll_on_new_data(&mut self) { + if self.auto_scroll { + // Keep at top when auto-scrolling + self.table_state.select(Some(0)); + } else if let Some(selected) = self.table_state.selected() { + // When not auto-scrolling, increment selection to stay on same row + // as new data pushes existing rows down + let max = self.flashblocks.len().saturating_sub(1); + self.table_state.select(Some((selected + 1).min(max))); + } + } +} + +async fn run_subscribe(args: SubscribeArgs, config: &ChainConfig) -> Result<()> { + let ws_url = args.websocket.as_deref().unwrap_or(config.flashblocks_ws.as_str()); + + if args.json { + run_json_mode(ws_url).await + } else { + // Fetch chain params from L1 SystemConfig (with L2 fallback for elasticity) + let params = fetch_chain_params(config).await?; + run_tui_mode(ws_url, &config.name, params).await + } +} + +async fn run_json_mode(url: &str) -> Result<()> { + let (ws_stream, _) = connect_async(url).await?; + let (_, mut read) = ws_stream.split(); + + while let Some(msg) = read.next().await { + match msg { + Ok(msg) => { + if msg.is_binary() || msg.is_text() { + let data = msg.into_data(); + match Flashblock::try_decode_message(data) { + Ok(fb) => { + println!("{}", serde_json::to_string(&fb)?); + } + Err(e) => { + eprintln!("Failed to decode flashblock: {e}"); + } + } + } + } + Err(e) => { + eprintln!("WebSocket error: {e}"); + break; + } + } + } + + Ok(()) +} + +async fn run_tui_mode(url: &str, chain_name: &str, params: ChainParams) -> Result<()> { + let mut terminal = setup_terminal()?; + let _ = run_tui_loop(&mut terminal, url, chain_name, params).await?; + restore_terminal(&mut terminal)?; + Ok(()) +} + +async fn run_ws_connection(url: String, tx: mpsc::Sender) -> Result<()> { + let (ws_stream, _) = connect_async(&url).await?; + let (_, mut read) = ws_stream.split(); + + while let Some(msg) = read.next().await { + let msg = msg?; + if !msg.is_binary() && !msg.is_text() { + continue; + } + let fb = Flashblock::try_decode_message(msg.into_data())?; + if tx.send(fb).await.is_err() { + break; + } + } + Ok(()) +} + +async fn run_tui_loop( + terminal: &mut Terminal>, + url: &str, + chain_name: &str, + params: ChainParams, +) -> Result { + let (tx, mut rx) = mpsc::channel::(100); + let mut state = AppState::new(chain_name.to_string(), params); + let mut events = EventStream::new(); + + let ws_url = url.to_string(); + tokio::spawn(async move { + if let Err(e) = run_ws_connection(ws_url, tx).await { + eprintln!("WebSocket error: {e}"); + } + }); + + loop { + let content_height = terminal.size()?.height.saturating_sub(5) as usize; + terminal.draw(|f| draw_ui(f, &mut state))?; + + tokio::select! { + Some(Ok(Event::Key(key))) = events.next() => { + if key.kind != KeyEventKind::Press { + continue; + } + if key.code == KeyCode::Char('c') && key.modifiers.contains(KeyModifiers::CONTROL) { + return Ok(NavResult::Quit); + } + match key.code { + KeyCode::Char('q') => return Ok(NavResult::Quit), + KeyCode::Char('h') => return Ok(NavResult::Home), + KeyCode::Char('?') => state.show_help = !state.show_help, + KeyCode::Char(' ') => state.paused = !state.paused, + KeyCode::Up | KeyCode::Char('k') => state.scroll_up(), + KeyCode::Down | KeyCode::Char('j') => state.scroll_down(), + KeyCode::PageUp => state.page_up(content_height), + KeyCode::PageDown => state.page_down(content_height), + KeyCode::Home | KeyCode::Char('g') => state.scroll_to_top(), + KeyCode::End | KeyCode::Char('G') => state.scroll_to_bottom(), + _ => {} + } + } + Some(fb) = rx.recv() => { + if !state.paused { + state.add_flashblock(fb); + } + } + } + } +} + +fn draw_ui(f: &mut Frame, state: &mut AppState) { + let layout = AppFrame::split_layout(f.area(), state.show_help); + draw_table(f, layout.content, state); + AppFrame::render(f, &layout, &state.chain_name, KEYBINDINGS, None); +} + +// Bar uses eighth-blocks for fine granularity (8 levels per character) +const BAR_CHARS: usize = 40; +const BAR_UNITS: usize = BAR_CHARS * 8; + +// Unicode eighth blocks: ▏▎▍▌▋▊▉█ (1/8 to 8/8) +const EIGHTH_BLOCKS: [char; 8] = ['▏', '▎', '▍', '▌', '▋', '▊', '▉', '█']; + +fn draw_table(f: &mut Frame, area: Rect, state: &mut AppState) { + let header_cells = + ["Block", "FB#", "Txns", "Gas Used", "Base Fee", "Delta", "Gas Fill", "Time"].iter().map( + |h| { + Cell::from(*h) + .style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)) + }, + ); + let header = Row::new(header_cells).height(1); + + let rows = state.flashblocks.iter().map(|fb| { + // Time delta with color: 150-250ms green, 100-150ms or 250-300ms yellow, else red + let delta_cell = fb.time_diff_ms.map_or_else( + || Cell::from("-".to_string()), + |ms| { + let color = if (150..=250).contains(&ms) { + Color::Green + } else if (100..150).contains(&ms) || (250..300).contains(&ms) { + Color::Yellow + } else { + Color::Red + }; + Cell::from(format!("+{ms}ms")).style(Style::default().fg(color)) + }, + ); + + // Base fee only shown on FB#0, colored based on change direction + let base_fee_cell = if fb.index == 0 { + let base_fee_str = fb.base_fee.map(format_gwei).unwrap_or_else(|| "-".to_string()); + + // Determine color based on change: green if up, red if down + let style = match (fb.base_fee, fb.prev_base_fee) { + (Some(current), Some(prev)) if current > prev => Style::default().fg(Color::Green), + (Some(current), Some(prev)) if current < prev => Style::default().fg(Color::Red), + _ => Style::default(), + }; + Cell::from(base_fee_str).style(style) + } else { + Cell::from(String::new()) + }; + + // Build inline bar chart for gas + let gas_bar = build_gas_bar(fb.gas_used, fb.gas_limit, state.elasticity_multiplier); + + let cells = [ + Cell::from(fb.block_number.to_string()), + Cell::from(fb.index.to_string()), + Cell::from(fb.tx_count.to_string()), + Cell::from(format_gas(fb.gas_used)), + base_fee_cell, + delta_cell, + Cell::from(gas_bar), + Cell::from(fb.timestamp.format("%H:%M:%S%.3f").to_string()), + ]; + let row = Row::new(cells); + // Highlight FB#0 - new block with deposit txn + if fb.index == 0 { + row.style(Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)) + } else { + row + } + }); + + let widths = [ + Constraint::Length(12), + Constraint::Length(5), + Constraint::Length(5), + Constraint::Length(10), + Constraint::Length(12), + Constraint::Length(9), + Constraint::Length(BAR_CHARS as u16), + Constraint::Min(14), + ]; + + // Build title with status indicator + let title = if state.paused { + " Recent Flashblocks [PAUSED] ".to_string() + } else if state.auto_scroll { + " Recent Flashblocks [AUTO] ".to_string() + } else { + let selected = state.table_state.selected().unwrap_or(0) + 1; + let total = state.flashblocks.len(); + format!(" Recent Flashblocks [{selected}/{total}] ") + }; + + let table = Table::new(rows, widths) + .header(header) + .block( + Block::default() + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::DarkGray)) + .title(title), + ) + .row_highlight_style(Style::default().bg(Color::Rgb(40, 40, 50))); + + f.render_stateful_widget(table, area, &mut state.table_state); +} + +fn build_gas_bar(gas_used: u64, gas_limit: u64, elasticity_multiplier: u64) -> Line<'static> { + if gas_limit == 0 { + return Line::from("-".to_string()); + } + + let limit = gas_limit; + + // Gas target based on elasticity multiplier + let gas_target = limit / elasticity_multiplier; + let target_char = ((gas_target as f64 / limit as f64) * BAR_CHARS as f64).round() as usize; + + // Calculate in eighth-block units + let filled_units = ((gas_used as f64 / limit as f64) * BAR_UNITS as f64).round() as usize; + let filled_units = filled_units.min(BAR_UNITS); + + let fill_color = Color::Rgb(100, 180, 255); // Nice blue + let target_color = Color::Rgb(255, 200, 100); // Orange/yellow for target marker + + let mut spans = Vec::new(); + + // Build character by character to insert target marker + let mut current_units = 0; + for char_idx in 0..BAR_CHARS { + let char_end_units = (char_idx + 1) * 8; + let is_target_char = char_idx == target_char; + + if is_target_char { + // Target marker - always use thin line, with background showing fill status + if current_units >= filled_units { + // Empty at target + spans.push(Span::styled("│", Style::default().fg(target_color))); + } else { + // Filled at target - show line with filled background + spans.push(Span::styled("│", Style::default().fg(target_color).bg(fill_color))); + } + } else if current_units >= filled_units { + // Empty portion + spans.push(Span::styled(" ", Style::default())); + } else if char_end_units <= filled_units { + // Full block + spans.push(Span::styled("█", Style::default().fg(fill_color))); + } else { + // Partial block + let units_in_char = filled_units - current_units; + spans.push(Span::styled( + EIGHTH_BLOCKS[units_in_char - 1].to_string(), + Style::default().fg(fill_color), + )); + } + + current_units = char_end_units; + } + + Line::from(spans) +} + +fn format_gas(gas: u64) -> String { + if gas >= 1_000_000 { + format!("{:.2}M", gas as f64 / 1_000_000.0) + } else if gas >= 1_000 { + format!("{:.1}K", gas as f64 / 1_000.0) + } else { + gas.to_string() + } +} + +fn format_gwei(wei: u128) -> String { + let gwei = wei as f64 / 1_000_000_000.0; + if gwei >= 1.0 { format!("{gwei:.2} gwei") } else { format!("{gwei:.4} gwei") } +} diff --git a/crates/basectl/src/commands/mod.rs b/crates/basectl/src/commands/mod.rs new file mode 100644 index 0000000..0bd0588 --- /dev/null +++ b/crates/basectl/src/commands/mod.rs @@ -0,0 +1,2 @@ +pub mod config; +pub mod flashblocks; diff --git a/crates/basectl/src/config.rs b/crates/basectl/src/config.rs new file mode 100644 index 0000000..881b4c2 --- /dev/null +++ b/crates/basectl/src/config.rs @@ -0,0 +1,109 @@ +use std::path::PathBuf; + +use alloy_primitives::Address; +use anyhow::{Context, Result}; +use serde::{Deserialize, Serialize}; +use url::Url; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ChainConfig { + pub name: String, + pub rpc: Url, + pub flashblocks_ws: Url, + pub l1_rpc: Url, + pub system_config: Address, +} + +impl ChainConfig { + /// Built-in mainnet configuration + pub fn mainnet() -> Self { + Self { + name: "mainnet".to_string(), + rpc: Url::parse("https://mainnet.base.org").unwrap(), + flashblocks_ws: Url::parse("wss://mainnet.flashblocks.base.org/ws").unwrap(), + l1_rpc: Url::parse("https://ethereum-rpc.publicnode.com").unwrap(), + system_config: "0x73a79Fab69143498Ed3712e519A88a918e1f4072".parse().unwrap(), + } + } + + /// Built-in sepolia configuration + pub fn sepolia() -> Self { + Self { + name: "sepolia".to_string(), + rpc: Url::parse("https://sepolia.base.org").unwrap(), + flashblocks_ws: Url::parse("wss://sepolia.flashblocks.base.org/ws").unwrap(), + l1_rpc: Url::parse("https://ethereum-sepolia-rpc.publicnode.com").unwrap(), + system_config: "0xf272670eb55e895584501d564AfEB048bEd26194".parse().unwrap(), + } + } + + /// Load config by name or path + /// + /// Resolution order: + /// 1. Built-in configs ("mainnet", "sepolia") + /// 2. User config at ~/.base/config/.yaml + /// 3. Treat as file path + pub fn load(name_or_path: &str) -> Result { + // Check built-in configs first + match name_or_path { + "mainnet" => return Ok(Self::mainnet()), + "sepolia" => return Ok(Self::sepolia()), + _ => {} + } + + // Check user config directory + if let Some(config_dir) = Self::config_dir() { + let user_config_path = config_dir.join(format!("{name_or_path}.yaml")); + if user_config_path.exists() { + return Self::load_from_file(&user_config_path); + } + } + + // Treat as file path + let path = PathBuf::from(name_or_path); + if path.exists() { + return Self::load_from_file(&path); + } + + anyhow::bail!( + "Config '{name_or_path}' not found. Expected built-in name (mainnet, sepolia), \ + user config at ~/.base/config/{name_or_path}.yaml, or a valid file path." + ) + } + + fn load_from_file(path: &PathBuf) -> Result { + let contents = std::fs::read_to_string(path) + .with_context(|| format!("Failed to read config file: {}", path.display()))?; + + let config: Self = serde_yaml::from_str(&contents) + .with_context(|| format!("Failed to parse config file: {}", path.display()))?; + + Ok(config) + } + + fn config_dir() -> Option { + dirs::home_dir().map(|h| h.join(".base").join("config")) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_builtin_configs() { + let mainnet = ChainConfig::load("mainnet").unwrap(); + assert_eq!(mainnet.name, "mainnet"); + assert!(mainnet.rpc.as_str().contains("mainnet")); + + let sepolia = ChainConfig::load("sepolia").unwrap(); + assert_eq!(sepolia.name, "sepolia"); + assert!(sepolia.rpc.as_str().contains("sepolia")); + } + + #[test] + fn test_unknown_config() { + let result = ChainConfig::load("nonexistent"); + assert!(result.is_err()); + } +} diff --git a/crates/basectl/src/l1_client.rs b/crates/basectl/src/l1_client.rs new file mode 100644 index 0000000..2061c9c --- /dev/null +++ b/crates/basectl/src/l1_client.rs @@ -0,0 +1,129 @@ +use std::time::Duration; + +use alloy_primitives::{Address, U256}; +use alloy_provider::{ProviderBuilder, layers::CallBatchLayer}; +use alloy_sol_types::sol; +use anyhow::Result; + +sol! { + #[sol(rpc)] + interface ISystemConfig { + function gasLimit() external view returns (uint64); + function eip1559Elasticity() external view returns (uint32); + function eip1559Denominator() external view returns (uint32); + function batcherHash() external view returns (bytes32); + function overhead() external view returns (uint256); + function scalar() external view returns (uint256); + function unsafeBlockSigner() external view returns (address); + function startBlock() external view returns (uint256); + function basefeeScalar() external view returns (uint32); + function blobbasefeeScalar() external view returns (uint32); + } +} + +#[derive(Debug, Clone)] +pub struct SystemConfigParams { + pub gas_limit: u64, + pub elasticity: Option, +} + +/// Full system configuration with all available fields. +/// Fields are `Option` because not all contracts have all functions (version differences). +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct FullSystemConfig { + pub gas_limit: Option, + pub eip1559_elasticity: Option, + pub eip1559_denominator: Option, + pub batcher_hash: Option<[u8; 32]>, + pub overhead: Option, + pub scalar: Option, + pub unsafe_block_signer: Option
, + pub start_block: Option, + pub basefee_scalar: Option, + pub blobbasefee_scalar: Option, +} + +/// Fetch gas limit and elasticity from the L1 `SystemConfig` contract. +/// +/// The `eip1559Elasticity()` function may not be available on older `SystemConfig` versions, +/// in which case `elasticity` will be `None`. +pub async fn fetch_system_config_params( + l1_rpc_url: &str, + system_config_address: Address, +) -> Result { + let provider = ProviderBuilder::new().connect(l1_rpc_url).await?; + let contract = ISystemConfig::new(system_config_address, provider); + + let gas_limit = contract.gasLimit().call().await?; + + // Try to fetch elasticity - may fail on older SystemConfig versions + let elasticity = contract.eip1559Elasticity().call().await.ok().map(|r| r as u64); + + Ok(SystemConfigParams { gas_limit, elasticity }) +} + +/// Fetch all available `SystemConfig` values from the L1 contract. +/// +/// Uses Multicall3 via `CallBatchLayer` to batch all calls into a single RPC request. +/// Each field is wrapped in `Option` to handle version differences in the `SystemConfig` contract. +pub async fn fetch_full_system_config( + l1_rpc_url: &str, + system_config_address: Address, +) -> Result { + // Use CallBatchLayer to batch all concurrent calls into a single Multicall3 call + let provider = ProviderBuilder::new() + .layer(CallBatchLayer::new().wait(Duration::from_millis(10))) + .connect(l1_rpc_url) + .await?; + let contract = ISystemConfig::new(system_config_address, provider); + + // Create call builders first to avoid temporary borrow issues + let gas_limit_call = contract.gasLimit(); + let eip1559_elasticity_call = contract.eip1559Elasticity(); + let eip1559_denominator_call = contract.eip1559Denominator(); + let batcher_hash_call = contract.batcherHash(); + let overhead_call = contract.overhead(); + let scalar_call = contract.scalar(); + let unsafe_block_signer_call = contract.unsafeBlockSigner(); + let start_block_call = contract.startBlock(); + let basefee_scalar_call = contract.basefeeScalar(); + let blobbasefee_scalar_call = contract.blobbasefeeScalar(); + + // Fetch all values concurrently - each may fail on older versions + let ( + gas_limit, + eip1559_elasticity, + eip1559_denominator, + batcher_hash, + overhead, + scalar, + unsafe_block_signer, + start_block, + basefee_scalar, + blobbasefee_scalar, + ) = tokio::join!( + gas_limit_call.call(), + eip1559_elasticity_call.call(), + eip1559_denominator_call.call(), + batcher_hash_call.call(), + overhead_call.call(), + scalar_call.call(), + unsafe_block_signer_call.call(), + start_block_call.call(), + basefee_scalar_call.call(), + blobbasefee_scalar_call.call(), + ); + + Ok(FullSystemConfig { + gas_limit: gas_limit.ok(), + eip1559_elasticity: eip1559_elasticity.ok(), + eip1559_denominator: eip1559_denominator.ok(), + batcher_hash: batcher_hash.ok().map(|h| h.0), + overhead: overhead.ok(), + scalar: scalar.ok(), + unsafe_block_signer: unsafe_block_signer.ok(), + start_block: start_block.ok(), + basefee_scalar: basefee_scalar.ok(), + blobbasefee_scalar: blobbasefee_scalar.ok(), + }) +} diff --git a/crates/basectl/src/lib.rs b/crates/basectl/src/lib.rs new file mode 100644 index 0000000..1b880cd --- /dev/null +++ b/crates/basectl/src/lib.rs @@ -0,0 +1,5 @@ +pub mod commands; +pub mod config; +pub mod l1_client; +pub mod rpc; +pub mod tui; diff --git a/crates/basectl/src/rpc.rs b/crates/basectl/src/rpc.rs new file mode 100644 index 0000000..957d3a1 --- /dev/null +++ b/crates/basectl/src/rpc.rs @@ -0,0 +1,54 @@ +use alloy_provider::{Provider, ProviderBuilder}; +use alloy_rpc_types_eth::BlockNumberOrTag; +use anyhow::Result; + +use crate::{config::ChainConfig, l1_client::fetch_system_config_params}; + +const DEFAULT_ELASTICITY: u64 = 6; + +/// Chain parameters needed for flashblocks display +#[derive(Debug, Clone, Copy)] +pub struct ChainParams { + pub gas_limit: u64, + pub elasticity: u64, +} + +/// Fetch chain parameters, trying L1 first and falling back to L2. +/// +/// This fetches `gas_limit` and elasticity from the L1 `SystemConfig` contract. +/// If elasticity is not available on L1 (older `SystemConfig`), it falls back +/// to fetching from L2 `extraData`. +pub async fn fetch_chain_params(config: &ChainConfig) -> Result { + let l1_params = + fetch_system_config_params(config.l1_rpc.as_str(), config.system_config).await?; + + let elasticity = match l1_params.elasticity { + Some(e) => e, + None => fetch_elasticity(config.rpc.as_str()).await?, + }; + + Ok(ChainParams { gas_limit: l1_params.gas_limit, elasticity }) +} + +/// Fetch the EIP-1559 elasticity multiplier from the L2 block extraData. +/// Falls back to default (6) if extraData is not in Holocene format. +pub async fn fetch_elasticity(rpc_url: &str) -> Result { + let provider = ProviderBuilder::new().connect(rpc_url).await?; + + let block = provider + .get_block_by_number(BlockNumberOrTag::Latest) + .await? + .ok_or_else(|| anyhow::anyhow!("No block found"))?; + + let extra_data = &block.header.extra_data; + + // Holocene format: version(1) + denominator(4) + elasticity(4) = 9 bytes + if extra_data.len() >= 9 && extra_data[0] == 0 { + let elasticity = + u32::from_be_bytes([extra_data[5], extra_data[6], extra_data[7], extra_data[8]]); + Ok(elasticity as u64) + } else { + // Pre-Holocene or invalid format, use default + Ok(DEFAULT_ELASTICITY) + } +} diff --git a/crates/basectl/src/tui/app_frame.rs b/crates/basectl/src/tui/app_frame.rs new file mode 100644 index 0000000..a765730 --- /dev/null +++ b/crates/basectl/src/tui/app_frame.rs @@ -0,0 +1,64 @@ +use ratatui::{layout::Rect, prelude::*}; + +use super::{HelpSidebar, Keybinding, StatusBar, StatusInfo}; + +/// Layout areas computed by `AppFrame`. +#[derive(Debug)] +pub struct AppLayout { + /// Main content area. + pub content: Rect, + /// Sidebar area (if help is shown). + pub sidebar: Option, + /// Status bar area at the bottom. + pub status_bar: Rect, +} + +/// App frame component that provides consistent layout for all views. +/// +/// Handles: +/// - Main content area +/// - Optional help sidebar (right) +/// - Status bar (bottom) +#[derive(Debug)] +pub struct AppFrame; + +impl AppFrame { + /// Splits the given area into content, optional sidebar, and status bar areas. + pub fn split_layout(area: Rect, show_help: bool) -> AppLayout { + // Reserve space for status bar at the bottom + let status_height = StatusBar::height(); + let main_height = area.height.saturating_sub(status_height); + + let main_area = Rect { x: area.x, y: area.y, width: area.width, height: main_height }; + + let status_bar = + Rect { x: area.x, y: area.y + main_height, width: area.width, height: status_height }; + + // Split main area for help sidebar if needed + let (content, sidebar) = if show_help { + let (content_area, sidebar_area) = HelpSidebar::split_layout(main_area); + (content_area, Some(sidebar_area)) + } else { + (main_area, None) + }; + + AppLayout { content, sidebar, status_bar } + } + + /// Renders the frame components (status bar and optional help sidebar). + pub fn render( + f: &mut Frame, + layout: &AppLayout, + config_name: &str, + keybindings: &[Keybinding], + status_info: Option<&StatusInfo>, + ) { + // Render status bar + StatusBar::render(f, layout.status_bar, config_name, status_info); + + // Render help sidebar if visible + if let Some(sidebar_area) = layout.sidebar { + HelpSidebar::render(f, sidebar_area, keybindings); + } + } +} diff --git a/crates/basectl/src/tui/help_sidebar.rs b/crates/basectl/src/tui/help_sidebar.rs new file mode 100644 index 0000000..2e82907 --- /dev/null +++ b/crates/basectl/src/tui/help_sidebar.rs @@ -0,0 +1,59 @@ +use ratatui::{ + layout::Rect, + prelude::*, + widgets::{Block, Borders, Paragraph}, +}; + +use super::Keybinding; + +/// Width of the help sidebar in characters. +const SIDEBAR_WIDTH: u16 = 28; + +/// Help sidebar component that displays keybindings on the right side of the screen. +#[derive(Debug)] +pub struct HelpSidebar; + +impl HelpSidebar { + /// Splits the given area into main content and sidebar areas. + /// + /// Returns `(main_area, sidebar_area)` where sidebar is on the right. + pub fn split_layout(area: Rect) -> (Rect, Rect) { + let sidebar_width = SIDEBAR_WIDTH.min(area.width); + let main_width = area.width.saturating_sub(sidebar_width); + + let main_area = Rect { x: area.x, y: area.y, width: main_width, height: area.height }; + + let sidebar_area = + Rect { x: area.x + main_width, y: area.y, width: sidebar_width, height: area.height }; + + (main_area, sidebar_area) + } + + /// Renders the help sidebar with the given keybindings. + pub fn render(f: &mut Frame, area: Rect, keybindings: &[Keybinding]) { + let mut lines = vec![Line::from("")]; + + for kb in keybindings { + lines.push(Line::from(vec![ + Span::styled( + format!(" {:<6}", kb.key), + Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD), + ), + Span::raw(kb.description), + ])); + } + + lines.push(Line::from("")); + + let help = Paragraph::new(lines) + .block( + Block::default() + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::Yellow)) + .title(" Help "), + ) + .style(Style::default().fg(Color::White)); + + f.render_widget(help, area); + } +} diff --git a/crates/basectl/src/tui/homescreen.rs b/crates/basectl/src/tui/homescreen.rs new file mode 100644 index 0000000..d6aead3 --- /dev/null +++ b/crates/basectl/src/tui/homescreen.rs @@ -0,0 +1,194 @@ +use std::io::Stdout; + +use anyhow::Result; +use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers}; +use ratatui::{ + layout::{Alignment, Constraint, Layout, Rect}, + prelude::*, + widgets::Paragraph, +}; + +use super::terminal::{restore_terminal, setup_terminal}; + +/// ASCII art for "basectl" logo - 3D block style +const LOGO: &str = "\ +██████╗ █████╗ ███████╗███████╗ ██████╗████████╗██╗ +██╔══██╗██╔══██╗██╔════╝██╔════╝██╔════╝╚══██╔══╝██║ +██████╔╝███████║███████╗█████╗ ██║ ██║ ██║ +██╔══██╗██╔══██║╚════██║██╔══╝ ██║ ██║ ██║ +██████╔╝██║ ██║███████║███████╗╚██████╗ ██║ ███████╗ +╚═════╝ ╚═╝ ╚═╝╚══════╝╚══════╝ ╚═════╝ ╚═╝ ╚══════╝"; + +/// Menu item definition +#[derive(Debug)] +pub struct MenuItem { + pub key: char, + pub label: &'static str, + pub description: &'static str, +} + +/// Available menu items +const MENU_ITEMS: &[MenuItem] = &[ + MenuItem { + key: 'c', + label: "Config", + description: "View chain configuration and L1 SystemConfig", + }, + MenuItem { key: 'f', label: "Flashblocks", description: "Subscribe to flashblocks stream" }, + MenuItem { key: 'q', label: "Quit", description: "Exit basectl" }, +]; + +/// Selected command from homescreen +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HomeSelection { + Config, + Flashblocks, + Quit, +} + +/// Run the homescreen TUI and return the user's selection +pub fn run_homescreen() -> Result { + let mut terminal = setup_terminal()?; + let result = run_homescreen_loop(&mut terminal); + restore_terminal(&mut terminal)?; + result +} + +fn run_homescreen_loop(terminal: &mut Terminal>) -> Result { + let mut selected_index = 0; + + loop { + terminal.draw(|f| draw_homescreen(f, selected_index))?; + + if let Event::Key(key) = event::read()? + && key.kind == KeyEventKind::Press + { + // Handle Ctrl+C to exit + if key.code == KeyCode::Char('c') && key.modifiers.contains(KeyModifiers::CONTROL) { + return Ok(HomeSelection::Quit); + } + match key.code { + KeyCode::Char('q') => return Ok(HomeSelection::Quit), + KeyCode::Char('c') => return Ok(HomeSelection::Config), + KeyCode::Char('f') => return Ok(HomeSelection::Flashblocks), + KeyCode::Up | KeyCode::Char('k') => { + selected_index = selected_index.saturating_sub(1); + } + KeyCode::Down | KeyCode::Char('j') => { + if selected_index < MENU_ITEMS.len() - 1 { + selected_index += 1; + } + } + KeyCode::Enter => { + return Ok(match selected_index { + 0 => HomeSelection::Config, + 1 => HomeSelection::Flashblocks, + _ => HomeSelection::Quit, + }); + } + _ => {} + } + } + } +} + +fn draw_homescreen(f: &mut Frame, selected_index: usize) { + let area = f.area(); + + // Calculate layout + let logo_height = LOGO.lines().count() as u16; + let menu_height = (MENU_ITEMS.len() * 2) as u16 + 2; + let total_content_height = logo_height + menu_height + 3; + + // Center vertically + let vertical_padding = area.height.saturating_sub(total_content_height) / 2; + + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(vertical_padding), + Constraint::Length(logo_height), + Constraint::Length(3), // spacing + Constraint::Length(menu_height), + Constraint::Min(0), + ]) + .split(area); + + // Draw logo + draw_logo(f, chunks[1]); + + // Draw menu + draw_menu(f, chunks[3], selected_index); +} + +fn draw_logo(f: &mut Frame, area: Rect) { + let base_blue = Color::Rgb(0, 82, 255); + + // Pad each line to same length for consistent centering + let max_len = LOGO.lines().map(|l| l.chars().count()).max().unwrap_or(0); + let padded_lines: Vec = LOGO + .lines() + .map(|line| { + let padding = max_len.saturating_sub(line.chars().count()); + let padded = format!("{}{}", line, " ".repeat(padding)); + Line::from(padded) + }) + .collect(); + + let logo = Paragraph::new(padded_lines) + .style(Style::default().fg(base_blue)) + .alignment(Alignment::Center); + + f.render_widget(logo, area); +} + +fn draw_menu(f: &mut Frame, area: Rect, selected_index: usize) { + let base_blue = Color::Rgb(0, 82, 255); + let mut lines = Vec::new(); + + for (i, item) in MENU_ITEMS.iter().enumerate() { + let is_selected = i == selected_index; + + // Key indicator + let key_style = Style::default().fg(base_blue).add_modifier(Modifier::BOLD); + + // Label style + let label_style = if is_selected { + Style::default().fg(Color::White).add_modifier(Modifier::BOLD) + } else { + Style::default().fg(Color::Gray) + }; + + // Description style + let desc_style = Style::default().fg(Color::DarkGray); + + // Selection indicator + let selector = if is_selected { "▸ " } else { " " }; + let selector_style = Style::default().fg(base_blue); + + lines.push(Line::from(vec![ + Span::styled(selector, selector_style), + Span::styled(format!("[{}]", item.key), key_style), + Span::raw(" "), + Span::styled(item.label, label_style), + Span::raw(" "), + Span::styled(item.description, desc_style), + ])); + + // Add spacing between items + if i < MENU_ITEMS.len() - 1 { + lines.push(Line::from("")); + } + } + + // Create a centered container for the menu with left-aligned text inside + let menu_width = 60u16.min(area.width); + let horizontal_padding = area.width.saturating_sub(menu_width) / 2; + + let centered_area = + Rect { x: area.x + horizontal_padding, y: area.y, width: menu_width, height: area.height }; + + let menu = Paragraph::new(lines).alignment(Alignment::Left); + + f.render_widget(menu, centered_area); +} diff --git a/crates/basectl/src/tui/keybinding.rs b/crates/basectl/src/tui/keybinding.rs new file mode 100644 index 0000000..51c724b --- /dev/null +++ b/crates/basectl/src/tui/keybinding.rs @@ -0,0 +1,12 @@ +/// A keybinding with its key and description for display in help. +#[derive(Debug, Clone, Copy)] +pub struct Keybinding { + pub key: &'static str, + pub description: &'static str, +} + +impl Keybinding { + pub const fn new(key: &'static str, description: &'static str) -> Self { + Self { key, description } + } +} diff --git a/crates/basectl/src/tui/mod.rs b/crates/basectl/src/tui/mod.rs new file mode 100644 index 0000000..673f6fd --- /dev/null +++ b/crates/basectl/src/tui/mod.rs @@ -0,0 +1,22 @@ +pub mod app_frame; +pub mod help_sidebar; +pub mod homescreen; +pub mod keybinding; +pub mod status_bar; +pub mod terminal; + +pub use app_frame::{AppFrame, AppLayout}; +pub use help_sidebar::HelpSidebar; +pub use homescreen::{HomeSelection, run_homescreen}; +pub use keybinding::Keybinding; +pub use status_bar::{StatusBar, StatusInfo}; +pub use terminal::{restore_terminal, setup_terminal}; + +/// Result of a view navigation action +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum NavResult { + /// Return to homescreen + Home, + /// Quit the application + Quit, +} diff --git a/crates/basectl/src/tui/status_bar.rs b/crates/basectl/src/tui/status_bar.rs new file mode 100644 index 0000000..af48065 --- /dev/null +++ b/crates/basectl/src/tui/status_bar.rs @@ -0,0 +1,63 @@ +use ratatui::{layout::Rect, prelude::*, widgets::Paragraph}; + +/// Optional status info to display in the center of the status bar. +#[derive(Debug, Clone, Default)] +pub struct StatusInfo { + pub message: String, + pub style: Style, +} + +impl StatusInfo { + pub fn new(message: impl Into) -> Self { + Self { message: message.into(), style: Style::default().fg(Color::DarkGray) } + } + + pub const fn with_style(mut self, style: Style) -> Self { + self.style = style; + self + } +} + +/// Status bar component that displays app info at the bottom of the screen. +#[derive(Debug)] +pub struct StatusBar; + +impl StatusBar { + /// Height of the status bar in lines. + pub const fn height() -> u16 { + 1 + } + + /// Renders the status bar. + /// + /// Left side: `basectl [config_name]` + /// Center: optional status info + /// Right side: `? (help)` + pub fn render(f: &mut Frame, area: Rect, config_name: &str, status_info: Option<&StatusInfo>) { + let left = format!("basectl [{config_name}]"); + let center = status_info.map(|s| s.message.as_str()).unwrap_or(""); + let center_style = status_info.map(|s| s.style).unwrap_or_default(); + let right = "? (help)"; + + let total_width = area.width as usize; + let left_len = left.len(); + let center_len = center.len(); + let right_len = right.len(); + + // Calculate padding for center and right alignment + let available = total_width.saturating_sub(left_len + center_len + right_len); + let left_padding = available / 2; + let right_padding = available.saturating_sub(left_padding); + + let line = Line::from(vec![ + Span::styled(left, Style::default().fg(Color::DarkGray)), + Span::raw(" ".repeat(left_padding)), + Span::styled(center, center_style), + Span::raw(" ".repeat(right_padding)), + Span::styled(right, Style::default().fg(Color::DarkGray)), + ]); + + let paragraph = Paragraph::new(line); + f.render_widget(paragraph, area); + } +} diff --git a/crates/basectl/src/tui/terminal.rs b/crates/basectl/src/tui/terminal.rs new file mode 100644 index 0000000..8956aa6 --- /dev/null +++ b/crates/basectl/src/tui/terminal.rs @@ -0,0 +1,24 @@ +use std::io::{self, Stdout}; + +use anyhow::Result; +use crossterm::{ + event::DisableMouseCapture, + execute, + terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode}, +}; +use ratatui::prelude::*; + +pub fn setup_terminal() -> Result>> { + enable_raw_mode()?; + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen, DisableMouseCapture)?; + let backend = CrosstermBackend::new(stdout); + let terminal = Terminal::new(backend)?; + Ok(terminal) +} + +pub fn restore_terminal(terminal: &mut Terminal>) -> Result<()> { + disable_raw_mode()?; + execute!(terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture)?; + Ok(()) +} diff --git a/crates/mempool-rebroadcaster/Cargo.toml b/crates/mempool-rebroadcaster/Cargo.toml index 9d7ff5e..1ad32e3 100644 --- a/crates/mempool-rebroadcaster/Cargo.toml +++ b/crates/mempool-rebroadcaster/Cargo.toml @@ -1,7 +1,11 @@ [package] name = "mempool-rebroadcaster" -version = "0.1.0" -edition = "2021" +version.workspace = true +edition.workspace = true +license.workspace = true + +[lints] +workspace = true [[bin]] name = "mempool-rebroadcaster" @@ -14,7 +18,7 @@ tracing-subscriber = { workspace = true } tokio = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } -dotenvy = "0.15.7" +dotenvy = { workspace = true } # alloy alloy-primitives.workspace = true diff --git a/crates/mempool-rebroadcaster/src/rebroadcaster.rs b/crates/mempool-rebroadcaster/src/rebroadcaster.rs index 32aea44..a4901c3 100644 --- a/crates/mempool-rebroadcaster/src/rebroadcaster.rs +++ b/crates/mempool-rebroadcaster/src/rebroadcaster.rs @@ -28,6 +28,7 @@ pub struct RebroadcasterResult { pub unexpected_failed_reth_to_geth: u32, } +#[derive(Debug)] pub struct TxpoolDiff { pub in_geth_not_in_reth: Vec, pub in_reth_not_in_geth: Vec, @@ -174,8 +175,8 @@ impl Rebroadcaster { ) -> TxpoolContent { let mut filtered_content = content.clone(); - for (account, nonce_txns) in content.pending.iter() { - for (nonce, txn) in nonce_txns.iter() { + for (account, nonce_txns) in &content.pending { + for (nonce, txn) in nonce_txns { if self.is_underpriced(txn, base_fee, gas_price) { filtered_content.pending.get_mut(account).unwrap().remove(nonce); } @@ -186,8 +187,8 @@ impl Rebroadcaster { } } - for (account, nonce_txns) in content.queued.iter() { - for (nonce, txn) in nonce_txns.iter() { + for (account, nonce_txns) in &content.queued { + for (nonce, txn) in nonce_txns { if self.is_underpriced(txn, base_fee, gas_price) { filtered_content.queued.get_mut(account).unwrap().remove(nonce); } @@ -229,11 +230,11 @@ impl Rebroadcaster { let mut pending_count = 0; let mut queued_count = 0; - for (_, nonce_txns) in mempool.pending.iter() { + for nonce_txns in mempool.pending.values() { pending_count += nonce_txns.len(); } - for (_, nonce_txns) in mempool.queued.iter() { + for nonce_txns in mempool.queued.values() { queued_count += nonce_txns.len(); } @@ -251,13 +252,13 @@ impl Rebroadcaster { let geth_hashes = self.txns_by_hash(geth_mempool); let reth_hashes = self.txns_by_hash(reth_mempool); - for (hash, txn) in geth_hashes.iter() { + for (hash, txn) in &geth_hashes { if !reth_hashes.contains_key(hash) { diff.in_geth_not_in_reth.push(txn.clone()); } } - for (hash, txn) in reth_hashes.iter() { + for (hash, txn) in &reth_hashes { if !geth_hashes.contains_key(hash) { diff.in_reth_not_in_geth.push(txn.clone()); } @@ -272,14 +273,14 @@ impl Rebroadcaster { fn txns_by_hash(&self, mempool: &TxpoolContent) -> HashMap { let mut txns_by_hash = HashMap::new(); - for (_, nonce_txns) in mempool.pending.iter() { - for (_, txn) in nonce_txns.iter() { + for nonce_txns in mempool.pending.values() { + for txn in nonce_txns.values() { txns_by_hash.insert(*txn.as_recovered().hash(), txn.clone()); } } - for (_, nonce_txns) in mempool.queued.iter() { - for (_, txn) in nonce_txns.iter() { + for nonce_txns in mempool.queued.values() { + for txn in nonce_txns.values() { txns_by_hash.insert(*txn.as_recovered().hash(), txn.clone()); } } diff --git a/crates/sidecrush/Cargo.toml b/crates/sidecrush/Cargo.toml index 1087d56..c91bc57 100644 --- a/crates/sidecrush/Cargo.toml +++ b/crates/sidecrush/Cargo.toml @@ -1,7 +1,11 @@ [package] name = "sidecrush" -version = "0.1.0" -edition = "2021" +version.workspace = true +edition.workspace = true +license.workspace = true + +[lints] +workspace = true [[bin]] name = "sidecrush" @@ -12,8 +16,8 @@ tracing = { workspace = true } tracing-subscriber = { workspace = true } tokio = { workspace = true } cadence = { workspace = true } -async-trait = "0.1" -anyhow = "1.0" +async-trait = { workspace = true } +anyhow = { workspace = true } clap = { workspace = true } # Ethereum client deps (will be used for header fetching) diff --git a/crates/sidecrush/src/bin/sidecrush.rs b/crates/sidecrush/src/bin/sidecrush.rs index c209756..fa64a05 100644 --- a/crates/sidecrush/src/bin/sidecrush.rs +++ b/crates/sidecrush/src/bin/sidecrush.rs @@ -56,7 +56,7 @@ async fn main() { // Initialize StatsD client (sends to Datadog agent) // Use DD_AGENT_HOST if set (Kubernetes), otherwise localhost let statsd_host = std::env::var("DD_AGENT_HOST").unwrap_or_else(|_| "127.0.0.1".to_string()); - let statsd_addr = format!("{}:8125", statsd_host); + let statsd_addr = format!("{statsd_host}:8125"); tracing::info!(address = %statsd_addr, "Connecting to StatsD agent"); let socket = UdpSocket::bind("0.0.0.0:0").expect("failed to bind UDP socket"); diff --git a/crates/sidecrush/src/blockbuilding_healthcheck/alloy_client.rs b/crates/sidecrush/src/blockbuilding_healthcheck/alloy_client.rs index 09bb853..863cb39 100644 --- a/crates/sidecrush/src/blockbuilding_healthcheck/alloy_client.rs +++ b/crates/sidecrush/src/blockbuilding_healthcheck/alloy_client.rs @@ -4,7 +4,7 @@ use async_trait::async_trait; use super::{EthClient, HeaderSummary}; -#[derive(Clone)] +#[derive(Debug, Clone)] pub struct AlloyEthClient { provider: RootProvider, } diff --git a/crates/sidecrush/src/blockbuilding_healthcheck/mod.rs b/crates/sidecrush/src/blockbuilding_healthcheck/mod.rs index 3a9bf77..a274ea4 100644 --- a/crates/sidecrush/src/blockbuilding_healthcheck/mod.rs +++ b/crates/sidecrush/src/blockbuilding_healthcheck/mod.rs @@ -23,12 +23,12 @@ enum HealthState { } impl HealthState { - fn code(&self) -> u8 { + const fn code(&self) -> u8 { match self { - HealthState::Healthy => 0, - HealthState::Delayed => 1, - HealthState::Unhealthy => 2, - HealthState::Error => 3, + Self::Healthy => 0, + Self::Delayed => 1, + Self::Unhealthy => 2, + Self::Error => 3, } } } @@ -41,7 +41,7 @@ pub struct HealthcheckConfig { } impl HealthcheckConfig { - pub fn new( + pub const fn new( poll_interval_ms: u64, grace_period_ms: u64, unhealthy_node_threshold_ms: u64, @@ -98,7 +98,7 @@ impl BlockProductionHealthChecker { } pub fn spawn_status_emitter(&self, period_ms: u64) -> tokio::task::JoinHandle<()> { - let status = self.status_code.clone(); + let status = Arc::clone(&self.status_code); let metrics = self.metrics.clone(); tokio::spawn(async move { let mut ticker = tokio::time::interval(Duration::from_millis(period_ms)); @@ -128,21 +128,19 @@ impl BlockProductionHealthChecker { Ok(Err(e)) => { if self.node.is_new_instance { debug!(sequencer = %url, error = %e, "waiting for node to become healthy"); - self.status_code.store(HealthState::Error.code(), Ordering::Relaxed); } else { error!(sequencer = %url, error = %e, "failed to fetch block"); - self.status_code.store(HealthState::Error.code(), Ordering::Relaxed); } + self.status_code.store(HealthState::Error.code(), Ordering::Relaxed); return; } Err(_elapsed) => { if self.node.is_new_instance { debug!(sequencer = %url, "waiting for node to become healthy (timeout)"); - self.status_code.store(HealthState::Error.code(), Ordering::Relaxed); } else { error!(sequencer = %url, "failed to fetch block (timeout)"); - self.status_code.store(HealthState::Error.code(), Ordering::Relaxed); } + self.status_code.store(HealthState::Error.code(), Ordering::Relaxed); return; } }; @@ -261,7 +259,7 @@ mod tests { timestamp_unix_seconds: start, transaction_count: 5, })); - let client = MockClient { header: shared_header.clone() }; + let client = MockClient { header: Arc::clone(&shared_header) }; let node = Node::new("http://localhost:8545", false); let metrics = mock_metrics(); let mut checker = BlockProductionHealthChecker::new(node, client, cfg, metrics); @@ -280,7 +278,7 @@ mod tests { timestamp_unix_seconds: start, transaction_count: 5, })); - let client = MockClient { header: shared_header.clone() }; + let client = MockClient { header: Arc::clone(&shared_header) }; let node = Node::new("http://localhost:8545", false); let metrics = mock_metrics(); let mut checker = BlockProductionHealthChecker::new(node, client, cfg, metrics); @@ -307,7 +305,7 @@ mod tests { timestamp_unix_seconds: start, transaction_count: 5, })); - let client = MockClient { header: shared_header.clone() }; + let client = MockClient { header: Arc::clone(&shared_header) }; let node = Node::new("http://localhost:8545", false); let metrics = mock_metrics(); let mut checker = BlockProductionHealthChecker::new(node, client, cfg, metrics); diff --git a/crates/sidecrush/src/metrics.rs b/crates/sidecrush/src/metrics.rs index 508f030..1d402a1 100644 --- a/crates/sidecrush/src/metrics.rs +++ b/crates/sidecrush/src/metrics.rs @@ -14,22 +14,22 @@ impl HealthcheckMetrics { Self { client: Arc::new(client) } } - /// Increment status_healthy counter (2s heartbeat). + /// Increment `status_healthy` counter (2s heartbeat). pub fn increment_status_healthy(&self) { let _ = self.client.incr("healthy"); } - /// Increment status_delayed counter (2s heartbeat). + /// Increment `status_delayed` counter (2s heartbeat). pub fn increment_status_delayed(&self) { let _ = self.client.incr("delayed"); } - /// Increment status_unhealthy counter (2s heartbeat). + /// Increment `status_unhealthy` counter (2s heartbeat). pub fn increment_status_unhealthy(&self) { let _ = self.client.incr("unhealthy"); } - /// Increment status_error counter (2s heartbeat). + /// Increment `status_error` counter (2s heartbeat). pub fn increment_status_error(&self) { let _ = self.client.incr("error"); }