From bb9a34f796e8d1373b2b0c1dd2ef0d32314c6e52 Mon Sep 17 00:00:00 2001 From: Beast Date: Sat, 4 Apr 2026 18:05:20 +0800 Subject: [PATCH 01/11] feat: add risk checker endpoint --- Cargo.lock | 1641 +++++++++++++++++++++++++- Cargo.toml | 3 + config/default.toml | 7 + config/example.toml | 7 + config/test.toml | 7 + src/config.rs | 19 + src/errors.rs | 29 +- src/handlers/mod.rs | 1 + src/handlers/risk_checker.rs | 25 + src/http_server.rs | 4 +- src/routes/mod.rs | 3 + src/routes/risk_checker.rs | 7 + src/services/mod.rs | 1 + src/services/risk_checker_service.rs | 879 ++++++++++++++ src/utils/test_app_state.rs | 11 +- 15 files changed, 2579 insertions(+), 65 deletions(-) create mode 100644 src/handlers/risk_checker.rs create mode 100644 src/routes/risk_checker.rs create mode 100644 src/services/risk_checker_service.rs diff --git a/Cargo.lock b/Cargo.lock index 4619f6c..2bec34b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -90,6 +90,639 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "alloy" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50ab0cd8afe573d1f7dc2353698a51b1f93aec362c8211e28cfd3948c6adba39" +dependencies = [ + "alloy-consensus", + "alloy-contract", + "alloy-core", + "alloy-eips", + "alloy-ens", + "alloy-genesis", + "alloy-network", + "alloy-provider", + "alloy-rpc-client", + "alloy-rpc-types", + "alloy-serde", + "alloy-signer", + "alloy-signer-local", + "alloy-transport", + "alloy-transport-http", + "alloy-trie", +] + +[[package]] +name = "alloy-chains" +version = "0.2.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e9e31d834fe25fe991b8884e4b9f0e59db4a97d86e05d1464d6899c013cd62" +dependencies = [ + "alloy-primitives", + "num_enum", + "strum", +] + +[[package]] +name = "alloy-consensus" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f16daaf7e1f95f62c6c3bf8a3fc3d78b08ae9777810c0bb5e94966c7cd57ef0" +dependencies = [ + "alloy-eips", + "alloy-primitives", + "alloy-rlp", + "alloy-serde", + "alloy-trie", + "alloy-tx-macros", + "auto_impl", + "borsh", + "c-kzg", + "derive_more 2.0.1", + "either", + "k256", + "once_cell", + "rand 0.8.5", + "secp256k1 0.30.0", + "serde", + "serde_json", + "serde_with", + "thiserror 2.0.17", +] + +[[package]] +name = "alloy-consensus-any" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "118998d9015332ab1b4720ae1f1e3009491966a0349938a1f43ff45a8a4c6299" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-rlp", + "alloy-serde", + "serde", +] + +[[package]] +name = "alloy-contract" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ac9e0c34dc6bce643b182049cdfcca1b8ce7d9c260cbdd561f511873b7e26cd" +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 2.0.17", + "tracing", +] + +[[package]] +name = "alloy-core" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23e8604b0c092fabc80d075ede181c9b9e596249c70b99253082d7e689836529" +dependencies = [ + "alloy-dyn-abi", + "alloy-json-abi", + "alloy-primitives", + "alloy-rlp", + "alloy-sol-types", +] + +[[package]] +name = "alloy-dyn-abi" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2db5c583aaef0255aa63a4fe827f826090142528bba48d1bf4119b62780cad" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "741bdd7499908b3aa0b159bba11e71c8cddd009a2c2eb7a06e825f1ec87900a5" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "crc", + "serde", + "thiserror 2.0.17", +] + +[[package]] +name = "alloy-eip2930" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9441120fa82df73e8959ae0e4ab8ade03de2aaae61be313fbf5746277847ce25" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "borsh", + "serde", +] + +[[package]] +name = "alloy-eip7702" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2919c5a56a1007492da313e7a3b6d45ef5edc5d33416fdec63c0d7a2702a0d20" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "borsh", + "serde", + "thiserror 2.0.17", +] + +[[package]] +name = "alloy-eip7928" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8222b1d88f9a6d03be84b0f5e76bb60cd83991b43ad8ab6477f0e4a7809b98d" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "borsh", + "serde", +] + +[[package]] +name = "alloy-eips" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6ef28c9fdad22d4eec52d894f5f2673a0895f1e5ef196734568e68c0f6caca8" +dependencies = [ + "alloy-eip2124", + "alloy-eip2930", + "alloy-eip7702", + "alloy-eip7928", + "alloy-primitives", + "alloy-rlp", + "alloy-serde", + "auto_impl", + "borsh", + "c-kzg", + "derive_more 2.0.1", + "either", + "serde", + "serde_with", + "sha2 0.10.9", +] + +[[package]] +name = "alloy-ens" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0470701e0a9694953b9119906d44333197865ca422681d095b71152e12bd38ee" +dependencies = [ + "alloy-contract", + "alloy-primitives", + "alloy-provider", + "alloy-sol-types", + "async-trait", + "thiserror 2.0.17", +] + +[[package]] +name = "alloy-genesis" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbf9480307b09d22876efb67d30cadd9013134c21f3a17ec9f93fd7536d38024" +dependencies = [ + "alloy-eips", + "alloy-primitives", + "alloy-serde", + "alloy-trie", + "borsh", + "serde", + "serde_with", +] + +[[package]] +name = "alloy-json-abi" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9dbe713da0c737d9e5e387b0ba790eb98b14dd207fe53eef50e19a5a8ec3dac" +dependencies = [ + "alloy-primitives", + "alloy-sol-type-parser", + "serde", + "serde_json", +] + +[[package]] +name = "alloy-json-rpc" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "422d110f1c40f1f8d0e5562b0b649c35f345fccb7093d9f02729943dcd1eef71" +dependencies = [ + "alloy-primitives", + "alloy-sol-types", + "http 1.3.1", + "serde", + "serde_json", + "thiserror 2.0.17", + "tracing", +] + +[[package]] +name = "alloy-network" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7197a66d94c4de1591cdc16a9bcea5f8cccd0da81b865b49aef97b1b4016e0fa" +dependencies = [ + "alloy-consensus", + "alloy-consensus-any", + "alloy-eips", + "alloy-json-rpc", + "alloy-network-primitives", + "alloy-primitives", + "alloy-rpc-types-any", + "alloy-rpc-types-eth", + "alloy-serde", + "alloy-signer", + "alloy-sol-types", + "async-trait", + "auto_impl", + "derive_more 2.0.1", + "futures-utils-wasm", + "serde", + "serde_json", + "thiserror 2.0.17", +] + +[[package]] +name = "alloy-network-primitives" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb82711d59a43fdfd79727c99f270b974c784ec4eb5728a0d0d22f26716c87ef" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-serde", + "serde", +] + +[[package]] +name = "alloy-primitives" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3b431b4e72cd8bd0ec7a50b4be18e73dab74de0dba180eef171055e5d5926e" +dependencies = [ + "alloy-rlp", + "bytes", + "cfg-if", + "const-hex", + "derive_more 2.0.1", + "foldhash 0.2.0", + "hashbrown 0.16.0", + "indexmap 2.12.0", + "itoa", + "k256", + "keccak-asm", + "paste", + "proptest", + "rand 0.9.2", + "rapidhash", + "ruint", + "rustc-hash", + "serde", + "sha3", +] + +[[package]] +name = "alloy-provider" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf6b18b929ef1d078b834c3631e9c925177f3b23ddc6fa08a722d13047205876" +dependencies = [ + "alloy-chains", + "alloy-consensus", + "alloy-eips", + "alloy-json-rpc", + "alloy-network", + "alloy-network-primitives", + "alloy-primitives", + "alloy-rpc-client", + "alloy-rpc-types-eth", + "alloy-signer", + "alloy-sol-types", + "alloy-transport", + "alloy-transport-http", + "async-stream", + "async-trait", + "auto_impl", + "dashmap", + "either", + "futures", + "futures-utils-wasm", + "lru 0.16.3", + "parking_lot", + "pin-project", + "reqwest 0.13.2", + "serde", + "serde_json", + "thiserror 2.0.17", + "tokio", + "tracing", + "url", + "wasmtimer", +] + +[[package]] +name = "alloy-rlp" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc90b1e703d3c03f4ff7f48e82dd0bc1c8211ab7d079cd836a06fcfeb06651cb" +dependencies = [ + "alloy-rlp-derive", + "arrayvec 0.7.6", + "bytes", +] + +[[package]] +name = "alloy-rlp-derive" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36834a5c0a2fa56e171bf256c34d70fca07d0c0031583edea1c4946b7889c9e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.109", +] + +[[package]] +name = "alloy-rpc-client" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fcc9604042ca80bd37aa5e232ea1cd851f337e31e2babbbb345bc0b1c30de3" +dependencies = [ + "alloy-json-rpc", + "alloy-primitives", + "alloy-transport", + "alloy-transport-http", + "futures", + "pin-project", + "reqwest 0.13.2", + "serde", + "serde_json", + "tokio", + "tokio-stream", + "tower 0.5.2", + "tracing", + "url", + "wasmtimer", +] + +[[package]] +name = "alloy-rpc-types" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4faad925d3a669ffc15f43b3deec7fbdf2adeb28a4d6f9cf4bc661698c0f8f4b" +dependencies = [ + "alloy-primitives", + "alloy-rpc-types-eth", + "alloy-serde", + "serde", +] + +[[package]] +name = "alloy-rpc-types-any" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3823026d1ed239a40f12364fac50726c8daf1b6ab8077a97212c5123910429ed" +dependencies = [ + "alloy-consensus-any", + "alloy-rpc-types-eth", + "alloy-serde", +] + +[[package]] +name = "alloy-rpc-types-eth" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59c095f92c4e1ff4981d89e9aa02d5f98c762a1980ab66bec49c44be11349da2" +dependencies = [ + "alloy-consensus", + "alloy-consensus-any", + "alloy-eips", + "alloy-network-primitives", + "alloy-primitives", + "alloy-rlp", + "alloy-serde", + "alloy-sol-types", + "itertools 0.14.0", + "serde", + "serde_json", + "serde_with", + "thiserror 2.0.17", +] + +[[package]] +name = "alloy-serde" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ece63b89294b8614ab3f483560c08d016930f842bf36da56bf0b764a15c11e" +dependencies = [ + "alloy-primitives", + "serde", + "serde_json", +] + +[[package]] +name = "alloy-signer" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43f447aefab0f1c0649f71edc33f590992d4e122bc35fb9cdbbf67d4421ace85" +dependencies = [ + "alloy-primitives", + "async-trait", + "auto_impl", + "either", + "elliptic-curve", + "k256", + "thiserror 2.0.17", +] + +[[package]] +name = "alloy-signer-local" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f721f4bf2e4812e5505aaf5de16ef3065a8e26b9139ac885862d00b5a55a659a" +dependencies = [ + "alloy-consensus", + "alloy-network", + "alloy-primitives", + "alloy-signer", + "async-trait", + "k256", + "rand 0.8.5", + "thiserror 2.0.17", +] + +[[package]] +name = "alloy-sol-macro" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab81bab693da9bb79f7a95b64b394718259fdd7e41dceeced4cad57cb71c4f6a" +dependencies = [ + "alloy-sol-macro-expander", + "alloy-sol-macro-input", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.109", +] + +[[package]] +name = "alloy-sol-macro-expander" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "489f1620bb7e2483fb5819ed01ab6edc1d2f93939dce35a5695085a1afd1d699" +dependencies = [ + "alloy-json-abi", + "alloy-sol-macro-input", + "const-hex", + "heck 0.5.0", + "indexmap 2.12.0", + "proc-macro-error2", + "proc-macro2", + "quote", + "sha3", + "syn 2.0.109", + "syn-solidity", +] + +[[package]] +name = "alloy-sol-macro-input" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56cef806ad22d4392c5fc83cf8f2089f988eb99c7067b4e0c6f1971fc1cca318" +dependencies = [ + "alloy-json-abi", + "const-hex", + "dunce", + "heck 0.5.0", + "macro-string", + "proc-macro2", + "quote", + "serde_json", + "syn 2.0.109", + "syn-solidity", +] + +[[package]] +name = "alloy-sol-type-parser" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6df77fea9d6a2a75c0ef8d2acbdfd92286cc599983d3175ccdc170d3433d249" +dependencies = [ + "serde", + "winnow", +] + +[[package]] +name = "alloy-sol-types" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64612d29379782a5dde6f4b6570d9c756d734d760c0c94c254d361e678a6591f" +dependencies = [ + "alloy-json-abi", + "alloy-primitives", + "alloy-sol-macro", + "serde", +] + +[[package]] +name = "alloy-transport" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8098f965442a9feb620965ba4b4be5e2b320f4ec5a3fff6bfa9e1ff7ef42bed1" +dependencies = [ + "alloy-json-rpc", + "auto_impl", + "base64 0.22.1", + "derive_more 2.0.1", + "futures", + "futures-utils-wasm", + "parking_lot", + "serde", + "serde_json", + "thiserror 2.0.17", + "tokio", + "tower 0.5.2", + "tracing", + "url", + "wasmtimer", +] + +[[package]] +name = "alloy-transport-http" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8597d36d546e1dab822345ad563243ec3920e199322cb554ce56c8ef1a1e2e7" +dependencies = [ + "alloy-json-rpc", + "alloy-transport", + "itertools 0.14.0", + "reqwest 0.13.2", + "serde_json", + "tower 0.5.2", + "tracing", + "url", +] + +[[package]] +name = "alloy-trie" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f14b5d9b2c2173980202c6ff470d96e7c5e202c65a9f67884ad565226df7fbb" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "derive_more 2.0.1", + "nybbles", + "serde", + "smallvec", + "thiserror 2.0.17", + "tracing", +] + +[[package]] +name = "alloy-tx-macros" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d69722eddcdf1ce096c3ab66cf8116999363f734eb36fe94a148f4f71c85da84" +dependencies = [ + "darling 0.23.0", + "proc-macro2", + "quote", + "syn 2.0.109", +] + [[package]] name = "android_system_properties" version = "0.1.5" @@ -252,6 +885,24 @@ dependencies = [ "ark-std 0.5.0", ] +[[package]] +name = "ark-ff" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b3235cc41ee7a12aaaf2c575a2ad7b46713a8a50bda2fc3b003a04845c05dd6" +dependencies = [ + "ark-ff-asm 0.3.0", + "ark-ff-macros 0.3.0", + "ark-serialize 0.3.0", + "ark-std 0.3.0", + "derivative", + "num-bigint", + "num-traits", + "paste", + "rustc_version 0.3.3", + "zeroize", +] + [[package]] name = "ark-ff" version = "0.4.2" @@ -268,7 +919,7 @@ dependencies = [ "num-bigint", "num-traits", "paste", - "rustc_version", + "rustc_version 0.4.1", "zeroize", ] @@ -292,6 +943,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ark-ff-asm" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db02d390bf6643fb404d3d22d31aee1c4bc4459600aef9113833d17e786c6e44" +dependencies = [ + "quote", + "syn 1.0.109", +] + [[package]] name = "ark-ff-asm" version = "0.4.2" @@ -312,6 +973,18 @@ dependencies = [ "syn 2.0.109", ] +[[package]] +name = "ark-ff-macros" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fd794a08ccb318058009eefdf15bcaaaaf6f8161eb3345f907222bac38b20" +dependencies = [ + "num-bigint", + "num-traits", + "quote", + "syn 1.0.109", +] + [[package]] name = "ark-ff-macros" version = "0.4.2" @@ -366,6 +1039,16 @@ dependencies = [ "hashbrown 0.15.5", ] +[[package]] +name = "ark-serialize" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6c2b318ee6e10f8c2853e73a83adc0ccb88995aa978d8a3408d492ab2ee671" +dependencies = [ + "ark-std 0.3.0", + "digest 0.9.0", +] + [[package]] name = "ark-serialize" version = "0.4.2" @@ -413,6 +1096,16 @@ dependencies = [ "syn 2.0.109", ] +[[package]] +name = "ark-std" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1df2c09229cbc5a028b1d70e00fdb2acee28b1055dfb5ca73eea49c5a25c4e7c" +dependencies = [ + "num-traits", + "rand 0.8.5", +] + [[package]] name = "ark-std" version = "0.4.0" @@ -633,6 +1326,28 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.109", +] + [[package]] name = "async-task" version = "4.7.1" @@ -671,12 +1386,45 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "auto_impl" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.109", +] + [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "aws-lc-rs" +version = "1.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a25cf98105baa966497416dbd42565ce3a8cf8dbfd59803ec9ad46f3126399" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + [[package]] name = "axum" version = "0.7.9" @@ -800,15 +1548,36 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43d193de1f7487df1914d3a568b772458861d33f9c54249612cc2893d6915054" dependencies = [ - "bitcoin_hashes", + "bitcoin_hashes 0.13.0", ] +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bitcoin-internals" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9425c3bf7089c983facbae04de54513cce73b41c7f9ff8c845b54e7bc64ebbfb" +[[package]] +name = "bitcoin-io" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953" + [[package]] name = "bitcoin_hashes" version = "0.13.0" @@ -816,7 +1585,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1930a4dabfebb8d7d9992db18ebe3ae2876f0a305fab206fd168df931ede293b" dependencies = [ "bitcoin-internals", - "hex-conservative", + "hex-conservative 0.1.2", +] + +[[package]] +name = "bitcoin_hashes" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b" +dependencies = [ + "bitcoin-io", + "hex-conservative 0.2.2", ] [[package]] @@ -907,6 +1686,42 @@ dependencies = [ "piper", ] +[[package]] +name = "blst" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcdb4c7013139a150f9fc55d123186dbfaba0d912817466282c73ac49e71fb45" +dependencies = [ + "cc", + "glob", + "threadpool", + "zeroize", +] + +[[package]] +name = "borsh" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a" +dependencies = [ + "borsh-derive", + "bytes", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfcfdc083699101d5a7965e49925975f2f55060f94f9a05e7187be95d530ca59" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.109", +] + [[package]] name = "bounded-collections" version = "0.2.4" @@ -951,14 +1766,34 @@ name = "bytes" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +dependencies = [ + "serde", +] + +[[package]] +name = "c-kzg" +version = "2.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6648ed1e4ea8e8a1a4a2c78e1cda29a3fd500bc622899c340d8525ea9a76b24a" +dependencies = [ + "blst", + "cc", + "glob", + "hex", + "libc", + "once_cell", + "serde", +] [[package]] name = "cc" -version = "1.2.45" +version = "1.2.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35900b6c8d709fb1d854671ae27aeaa9eec2f8b01b364e1619a40da3e6fe2afe" +checksum = "b7a4d3ec6524d28a329fc53654bbadc9bdd7b0431f5d65f1a56ffb28a1ee5283" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -974,6 +1809,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chacha20" version = "0.9.1" @@ -1049,6 +1890,15 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + [[package]] name = "colorchoice" version = "1.0.4" @@ -1108,6 +1958,18 @@ dependencies = [ "yaml-rust2", ] +[[package]] +name = "const-hex" +version = "1.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "531185e432bb31db1ecda541e9e7ab21468d4d844ad7505e0546a49b4945d49b" +dependencies = [ + "cfg-if", + "cpufeatures", + "proptest", + "serde_core", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -1319,7 +2181,7 @@ dependencies = [ "curve25519-dalek-derive", "digest 0.10.7", "fiat-crypto", - "rustc_version", + "rustc_version 0.4.1", "subtle", "zeroize", ] @@ -1341,8 +2203,18 @@ version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + +[[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]] @@ -1359,17 +2231,56 @@ dependencies = [ "syn 2.0.109", ] +[[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", + "serde", + "strsim", + "syn 2.0.109", +] + [[package]] name = "darling_macro" version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ - "darling_core", + "darling_core 0.20.11", + "quote", + "syn 2.0.109", +] + +[[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.109", ] +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "deadpool" version = "0.9.5" @@ -1407,6 +2318,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" dependencies = [ "powerfmt", + "serde_core", ] [[package]] @@ -1591,6 +2503,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "dyn-clone" version = "1.0.20" @@ -1812,6 +2730,28 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "fastrlp" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139834ddba373bbdd213dffe02c8d110508dcf1726c2be27e8d1f7d7e1856418" +dependencies = [ + "arrayvec 0.7.6", + "auto_impl", + "bytes", +] + +[[package]] +name = "fastrlp" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce8dba4714ef14b8274c371879b175aa55b16b30f269663f19d576f380018dc4" +dependencies = [ + "arrayvec 0.7.6", + "auto_impl", + "bytes", +] + [[package]] name = "ff" version = "0.13.1" @@ -1840,9 +2780,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.4" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "fixed-hash" @@ -1879,6 +2819,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "foreign-types" version = "0.3.2" @@ -1944,6 +2890,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "fsevent-sys" version = "4.1.0" @@ -2098,6 +3050,12 @@ dependencies = [ "slab", ] +[[package]] +name = "futures-utils-wasm" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42012b0f064e01aa58b545fe3727f90f7dd4020f4a3ea735b50344965f5a57e9" + [[package]] name = "generic-array" version = "0.14.9" @@ -2140,9 +3098,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "wasip2", + "wasm-bindgen", ] [[package]] @@ -2171,6 +3131,12 @@ version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + [[package]] name = "gloo-net" version = "0.6.0" @@ -2240,7 +3206,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap", + "indexmap 2.12.0", "slab", "tokio", "tokio-util", @@ -2259,7 +3225,7 @@ dependencies = [ "futures-core", "futures-sink", "http 1.3.1", - "indexmap", + "indexmap 2.12.0", "slab", "tokio", "tokio-util", @@ -2281,6 +3247,12 @@ dependencies = [ "crunchy", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.13.2" @@ -2308,7 +3280,7 @@ checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", - "foldhash", + "foldhash 0.1.5", "serde", ] @@ -2317,6 +3289,12 @@ name = "hashbrown" version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", + "serde", +] [[package]] name = "hashlink" @@ -2360,6 +3338,15 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "212ab92002354b4819390025006c897e8140934349e8635c9b077f47b4dcbd20" +[[package]] +name = "hex-conservative" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" +dependencies = [ + "arrayvec 0.7.6", +] + [[package]] name = "hex-literal" version = "0.4.1" @@ -2768,6 +3755,15 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "impl-codec" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba6a270039626615617f3f36d15fc827041df3b78c439da2cadfa47455a77f2f" +dependencies = [ + "parity-scale-codec", +] + [[package]] name = "impl-codec" version = "0.7.1" @@ -2785,7 +3781,7 @@ checksum = "803d15461ab0dcc56706adf266158acbc44ccf719bf7d0af30705f58b90a4b8c" dependencies = [ "integer-sqrt", "num-traits", - "uint", + "uint 0.10.0", ] [[package]] @@ -2808,6 +3804,17 @@ dependencies = [ "syn 2.0.109", ] +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.12.0" @@ -2816,6 +3823,8 @@ checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" dependencies = [ "equivalent", "hashbrown 0.16.0", + "serde", + "serde_core", ] [[package]] @@ -2957,6 +3966,16 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + [[package]] name = "js-sys" version = "0.3.82" @@ -3007,7 +4026,7 @@ dependencies = [ "pin-project", "rustls 0.23.35", "rustls-pki-types", - "rustls-platform-verifier", + "rustls-platform-verifier 0.5.3", "soketto", "thiserror 1.0.69", "tokio", @@ -3057,7 +4076,7 @@ dependencies = [ "jsonrpsee-core", "jsonrpsee-types", "rustls 0.23.35", - "rustls-platform-verifier", + "rustls-platform-verifier 0.5.3", "serde", "serde_json", "thiserror 1.0.69", @@ -3142,13 +4161,23 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "keccak-asm" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa468878266ad91431012b3e5ef1bf9b170eab22883503a318d46857afa4579a" +dependencies = [ + "digest 0.10.7", + "sha3-asm", +] + [[package]] name = "keccak-hash" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e1b8590eb6148af2ea2d75f38e7d29f5ca970d5a4df456b3ef19b8b415d0264" dependencies = [ - "primitive-types", + "primitive-types 0.13.1", "tiny-keccak", ] @@ -3305,6 +4334,32 @@ dependencies = [ "hashbrown 0.15.5", ] +[[package]] +name = "lru" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" +dependencies = [ + "hashbrown 0.16.0", +] + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "macro-string" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b27834086c65ec3f9387b096d66e99f221cf081c2b738042aa252bcd41204e3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.109", +] + [[package]] name = "matchers" version = "0.2.0" @@ -3342,7 +4397,7 @@ version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e300c54e3239a86f9c61cc63ab0f03862eb40b1c6e065dc6fd6ceaeff6da93d" dependencies = [ - "foldhash", + "foldhash 0.1.5", "hash-db", "hashbrown 0.15.5", ] @@ -3576,9 +4631,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" [[package]] name = "num-format" @@ -3641,6 +4696,41 @@ dependencies = [ "libc", ] +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.109", +] + +[[package]] +name = "nybbles" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d49ff0c0d00d4a502b39df9af3a525e1efeb14b9dabb5bb83335284c1309210" +dependencies = [ + "alloy-rlp", + "cfg-if", + "proptest", + "ruint", + "serde", + "smallvec", +] + [[package]] name = "oauth2" version = "4.4.2" @@ -3870,7 +4960,7 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e69bf016dc406eff7d53a7d3f7cf1c2e72c82b9088aac1118591e36dd2cd3e9" dependencies = [ - "bitcoin_hashes", + "bitcoin_hashes 0.13.0", "rand 0.8.5", "rand_core 0.6.4", "serde", @@ -4241,6 +5331,17 @@ dependencies = [ "syn 2.0.109", ] +[[package]] +name = "primitive-types" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b34d9fd68ae0b74a41b21c03c2f62847aa0ffea044eee893b4c140b37e244e2" +dependencies = [ + "fixed-hash", + "impl-codec 0.6.0", + "uint 0.9.5", +] + [[package]] name = "primitive-types" version = "0.13.1" @@ -4248,11 +5349,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d15600a7d856470b7d278b3fe0e311fe28c2526348549f8ef2ff7db3299c87f5" dependencies = [ "fixed-hash", - "impl-codec", + "impl-codec 0.7.1", "impl-num-traits", "impl-serde", "scale-info", - "uint", + "uint 0.10.0", ] [[package]] @@ -4348,6 +5449,25 @@ dependencies = [ "thiserror 2.0.17", ] +[[package]] +name = "proptest" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags 2.10.0", + "num-traits", + "rand 0.9.2", + "rand_chacha 0.9.0", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + [[package]] name = "protobuf" version = "3.7.2" @@ -4542,6 +5662,68 @@ dependencies = [ "toml 0.9.8", ] +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls 0.23.35", + "socket2 0.6.1", + "thiserror 2.0.17", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "aws-lc-rs", + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls 0.23.35", + "rustls-pki-types", + "slab", + "thiserror 2.0.17", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2 0.6.1", + "tracing", + "windows-sys 0.60.2", +] + [[package]] name = "quote" version = "1.0.42" @@ -4585,6 +5767,7 @@ dependencies = [ "libc", "rand_chacha 0.3.1", "rand_core 0.6.4", + "serde", ] [[package]] @@ -4595,6 +5778,7 @@ checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.3", + "serde", ] [[package]] @@ -4649,18 +5833,37 @@ dependencies = [ name = "rand_core" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.4", + "serde", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" dependencies = [ - "getrandom 0.3.4", + "rand_core 0.9.3", ] [[package]] -name = "rand_hc" -version = "0.2.0" +name = "rapidhash" +version = "4.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +checksum = "b5e48930979c155e2f33aa36ab3119b5ee81332beb6482199a8ecd6029b80b59" dependencies = [ - "rand_core 0.5.1", + "rustversion", ] [[package]] @@ -4810,7 +6013,44 @@ dependencies = [ "tokio", "tokio-native-tls", "tower 0.5.2", - "tower-http 0.6.6", + "tower-http 0.6.8", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "reqwest" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "hyper 1.7.0", + "hyper-rustls 0.27.7", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls 0.23.35", + "rustls-pki-types", + "rustls-platform-verifier 0.6.2", + "serde", + "serde_json", + "sync_wrapper 1.0.2", + "tokio", + "tokio-rustls 0.26.4", + "tower 0.5.2", + "tower-http 0.6.8", "tower-service", "url", "wasm-bindgen", @@ -4848,6 +6088,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rlp" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb919243f34364b6bd2fc10ef797edbfa75f33c252e7998527479c6d6b47e1ec" +dependencies = [ + "bytes", + "rustc-hex", +] + [[package]] name = "ron" version = "0.8.1" @@ -4901,6 +6151,40 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "ruint" +version = "1.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c141e807189ad38a07276942c6623032d3753c8859c146104ac2e4d68865945a" +dependencies = [ + "alloy-rlp", + "ark-ff 0.3.0", + "ark-ff 0.4.2", + "ark-ff 0.5.0", + "bytes", + "fastrlp 0.3.1", + "fastrlp 0.4.0", + "num-bigint", + "num-integer", + "num-traits", + "parity-scale-codec", + "primitive-types 0.12.2", + "proptest", + "rand 0.8.5", + "rand 0.9.2", + "rlp", + "ruint-macro", + "serde_core", + "valuable", + "zeroize", +] + +[[package]] +name = "ruint-macro" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" + [[package]] name = "rust-ini" version = "0.20.0" @@ -4929,13 +6213,22 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" +[[package]] +name = "rustc_version" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" +dependencies = [ + "semver 0.11.0", +] + [[package]] name = "rustc_version" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ - "semver", + "semver 1.0.27", ] [[package]] @@ -4982,6 +6275,7 @@ version = "0.23.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" dependencies = [ + "aws-lc-rs", "log", "once_cell", "ring", @@ -5018,6 +6312,7 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" dependencies = [ + "web-time", "zeroize", ] @@ -5042,6 +6337,27 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "rustls-platform-verifier" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls 0.23.35", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki 0.103.8", + "security-framework 3.5.1", + "security-framework-sys", + "webpki-root-certs 1.0.4", + "windows-sys 0.61.2", +] + [[package]] name = "rustls-platform-verifier-android" version = "0.1.1" @@ -5064,6 +6380,7 @@ version = "0.103.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" dependencies = [ + "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -5075,6 +6392,18 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "rusty-fork" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + [[package]] name = "rusx" version = "0.6.1" @@ -5131,7 +6460,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d78196772d25b90a98046794ce0fe2588b39ebdfbdc1e45b4c6c85dd43bebad" dependencies = [ "parity-scale-codec", - "primitive-types", + "primitive-types 0.13.1", "scale-bits", "scale-decode-derive", "scale-type-resolver", @@ -5145,7 +6474,7 @@ version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f4b54a1211260718b92832b661025d1f1a4b6930fbadd6908e00edd265fa5f7" dependencies = [ - "darling", + "darling 0.20.11", "proc-macro2", "quote", "syn 2.0.109", @@ -5158,7 +6487,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64901733157f9d25ef86843bd783eda439fac7efb0ad5a615d12d2cf3a29464b" dependencies = [ "parity-scale-codec", - "primitive-types", + "primitive-types 0.13.1", "scale-bits", "scale-encode-derive", "scale-type-resolver", @@ -5172,7 +6501,7 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78a3993a13b4eafa89350604672c8757b7ea84c7c5947d4b3691e3169c96379b" dependencies = [ - "darling", + "darling 0.20.11", "proc-macro-crate", "proc-macro2", "quote", @@ -5256,6 +6585,30 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + [[package]] name = "schnellru" version = "0.2.4" @@ -5323,7 +6676,19 @@ version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d24b59d129cdadea20aea4fb2352fa053712e5d713eee47d700cd4b2bc002f10" dependencies = [ - "secp256k1-sys", + "secp256k1-sys 0.9.2", +] + +[[package]] +name = "secp256k1" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50c5943d326858130af85e049f2661ba3c78b26589b8ab98e65e80ae44a1252" +dependencies = [ + "bitcoin_hashes 0.14.1", + "rand 0.8.5", + "secp256k1-sys 0.10.1", + "serde", ] [[package]] @@ -5335,6 +6700,15 @@ dependencies = [ "cc", ] +[[package]] +name = "secp256k1-sys" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" +dependencies = [ + "cc", +] + [[package]] name = "secrecy" version = "0.8.0" @@ -5380,12 +6754,30 @@ dependencies = [ "libc", ] +[[package]] +name = "semver" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" +dependencies = [ + "semver-parser", +] + [[package]] name = "semver" version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +[[package]] +name = "semver-parser" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9900206b54a3527fdc7b8a938bffd94a568bac4f4aa8113b209df75a09c0dec2" +dependencies = [ + "pest", +] + [[package]] name = "send_wrapper" version = "0.4.0" @@ -5497,6 +6889,37 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.12.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" +dependencies = [ + "darling 0.23.0", + "proc-macro2", + "quote", + "syn 2.0.109", +] + [[package]] name = "serdect" version = "0.2.0" @@ -5552,6 +6975,16 @@ dependencies = [ "keccak", ] +[[package]] +name = "sha3-asm" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59cbb88c189d6352cc8ae96a39d19c7ecad8f7330b29461187f2587fdc2988d5" +dependencies = [ + "cc", + "cfg-if", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -5621,6 +7054,9 @@ name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] [[package]] name = "smol" @@ -5715,7 +7151,7 @@ dependencies = [ "hex", "itertools 0.14.0", "log", - "lru", + "lru 0.12.5", "parking_lot", "pin-project", "rand 0.8.5", @@ -5819,11 +7255,11 @@ dependencies = [ "parity-scale-codec", "parking_lot", "paste", - "primitive-types", + "primitive-types 0.13.1", "rand 0.8.5", "scale-info", "schnorrkel", - "secp256k1", + "secp256k1 0.28.2", "secrecy", "serde", "sha2 0.10.9", @@ -5891,7 +7327,7 @@ dependencies = [ "parity-scale-codec", "polkavm-derive", "rustversion", - "secp256k1", + "secp256k1 0.28.2", "sp-core", "sp-crypto-hashing", "sp-externalities", @@ -5966,7 +7402,7 @@ dependencies = [ "impl-trait-for-tuples", "parity-scale-codec", "polkavm-derive", - "primitive-types", + "primitive-types 0.13.1", "sp-externalities", "sp-runtime-interface-proc-macro", "sp-std", @@ -6049,7 +7485,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b2e157c9cf44a1a9d20f3c69322e302db70399bf3f218211387fe009dd4041c" dependencies = [ "ahash", - "foldhash", + "foldhash 0.1.5", "hash-db", "hashbrown 0.15.5", "memory-db", @@ -6159,7 +7595,7 @@ dependencies = [ "futures-util", "hashlink", "hex", - "indexmap", + "indexmap 2.12.0", "log", "memchr", "once_cell", @@ -6379,6 +7815,27 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.109", +] + [[package]] name = "substrate-bip39" version = "0.6.0" @@ -6427,7 +7884,7 @@ dependencies = [ "hex", "jsonrpsee", "parity-scale-codec", - "primitive-types", + "primitive-types 0.13.1", "scale-bits", "scale-decode", "scale-encode", @@ -6483,7 +7940,7 @@ dependencies = [ "impl-serde", "keccak-hash", "parity-scale-codec", - "primitive-types", + "primitive-types 0.13.1", "scale-bits", "scale-decode", "scale-encode", @@ -6520,7 +7977,7 @@ version = "0.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69516e8ff0e9340a0f21b8398da7f997571af4734ee81deada5150a2668c8443" dependencies = [ - "darling", + "darling 0.20.11", "parity-scale-codec", "proc-macro-error2", "quote", @@ -6559,7 +8016,7 @@ dependencies = [ "impl-serde", "jsonrpsee", "parity-scale-codec", - "primitive-types", + "primitive-types 0.13.1", "serde", "serde_json", "subxt-core", @@ -6603,6 +8060,18 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn-solidity" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53f425ae0b12e2f5ae65542e00898d500d4d318b4baf09f40fd0d410454e9947" +dependencies = [ + "paste", + "proc-macro2", + "quote", + "syn 2.0.109", +] + [[package]] name = "sync_wrapper" version = "0.1.2" @@ -6681,6 +8150,7 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" name = "task-master" version = "0.1.0" dependencies = [ + "alloy", "anyhow", "argon2", "axum", @@ -6796,32 +8266,41 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + [[package]] name = "time" -version = "0.3.44" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", "num-conv", "powerfmt", - "serde", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.24" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", @@ -6928,6 +8407,7 @@ dependencies = [ "futures-core", "pin-project-lite", "tokio", + "tokio-util", ] [[package]] @@ -6962,7 +8442,7 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" dependencies = [ - "indexmap", + "indexmap 2.12.0", "serde_core", "serde_spanned 1.0.3", "toml_datetime 0.7.3", @@ -6995,7 +8475,7 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap", + "indexmap 2.12.0", "serde", "serde_spanned 0.6.9", "toml_datetime 0.6.11", @@ -7009,7 +8489,7 @@ version = "0.23.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" dependencies = [ - "indexmap", + "indexmap 2.12.0", "toml_datetime 0.7.3", "toml_parser", "winnow", @@ -7103,9 +8583,9 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.6" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ "bitflags 2.10.0", "bytes", @@ -7267,6 +8747,18 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" +[[package]] +name = "uint" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + [[package]] name = "uint" version = "0.10.0" @@ -7279,6 +8771,12 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + [[package]] name = "unicode-bidi" version = "0.3.18" @@ -7468,6 +8966,15 @@ dependencies = [ "w3f-plonk-common", ] +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + [[package]] name = "waker-fn" version = "1.2.0" @@ -7628,6 +9135,20 @@ dependencies = [ "bitflags 2.10.0", ] +[[package]] +name = "wasmtimer" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c598d6b99ea013e35844697fc4670d08339d5cda15588f193c6beedd12f644b" +dependencies = [ + "futures", + "js-sys", + "parking_lot", + "pin-utils", + "slab", + "wasm-bindgen", +] + [[package]] name = "web-sys" version = "0.3.82" diff --git a/Cargo.toml b/Cargo.toml index 9b55db7..54ed578 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,6 +69,9 @@ uuid = {version = "1.6", features = ["v4", "serde"]} # HTTP client for GraphQL reqwest = {version = "0.11", features = ["json"]} +# Ethereum / ENS +alloy = {version = "1.8", features = ["providers", "provider-http", "ens"]} + # Logging tracing = "0.1" tracing-subscriber = {version = "0.3", features = ["env-filter"]} diff --git a/config/default.toml b/config/default.toml index 0778b60..32259cd 100644 --- a/config/default.toml +++ b/config/default.toml @@ -81,3 +81,10 @@ webhook_url = "https://www.webhook_url.com" [remote_configs] wallet_configs_file = "../wallet_configs/default_configs.json" + +[risk_checker] +etherscan_api_key = "change-me" +etherscan_base_url = "https://api.etherscan.io/v2/api?chainid=1" +infura_api_key = "change-me" +infura_base_url = "https://mainnet.infura.io/v3" +etherscan_calls_per_sec = 3 diff --git a/config/example.toml b/config/example.toml index a0884a3..a0d9354 100644 --- a/config/example.toml +++ b/config/example.toml @@ -92,6 +92,13 @@ webhook_url = "https://www.webhook_url.com" [remote_configs] wallet_configs_file = "../wallet_configs/default_configs.json" +[risk_checker] +etherscan_api_key = "change-me" +etherscan_base_url = "https://api.etherscan.io/v2/api?chainid=1" +infura_api_key = "change-me" +infura_base_url = "https://mainnet.infura.io/v3" +etherscan_calls_per_sec = 3 + # Example environment variable overrides: # TASKMASTER_BLOCKCHAIN__NODE_URL="ws://remote-node:9944" # TASKMASTER_BLOCKCHAIN__WALLET_PASSWORD="super_secure_password" diff --git a/config/test.toml b/config/test.toml index 303f748..3551829 100644 --- a/config/test.toml +++ b/config/test.toml @@ -81,3 +81,10 @@ webhook_url = "https://www.webhook_url.com" [remote_configs] wallet_configs_file = "../wallet_configs/test_configs.json" + +[risk_checker] +etherscan_api_key = "change-me" +etherscan_base_url = "https://api.etherscan.io/v2/api?chainid=1" +infura_api_key = "change-me" +infura_base_url = "https://mainnet.infura.io/v3" +etherscan_calls_per_sec = 3 diff --git a/src/config.rs b/src/config.rs index 908f4d3..5a9aef3 100644 --- a/src/config.rs +++ b/src/config.rs @@ -19,6 +19,7 @@ pub struct Config { pub alert: AlertConfig, pub x_association: XAssociationConfig, pub remote_configs: RemoteConfigsConfig, + pub risk_checker: RiskCheckerConfig, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -99,6 +100,17 @@ pub struct XAssociationConfig { pub keywords: String, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RiskCheckerConfig { + pub etherscan_api_key: String, + pub etherscan_base_url: String, + pub infura_api_key: String, + pub infura_base_url: String, + /// Maximum Etherscan API calls per second. Used to space out sequential + /// calls and stay within the free-tier limit (default: 3). + pub etherscan_calls_per_sec: u32, +} + impl Config { pub fn load(config_path: &str) -> Result { let settings = config::Config::builder() @@ -231,6 +243,13 @@ impl Default for Config { remote_configs: RemoteConfigsConfig { wallet_configs_file: "wallet_configs/default_configs.json".to_string(), }, + risk_checker: RiskCheckerConfig { + etherscan_api_key: "change-me".to_string(), + etherscan_base_url: "https://api.etherscan.io/api".to_string(), + infura_api_key: "change-me".to_string(), + infura_base_url: "https://mainnet.infura.io/v3".to_string(), + etherscan_calls_per_sec: 3, + }, } } } diff --git a/src/errors.rs b/src/errors.rs index dae622d..f3b1df9 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -11,7 +11,9 @@ use crate::{ db_persistence::DbError, handlers::{address::AddressHandlerError, auth::AuthHandlerError, referral::ReferralHandlerError, HandlerError}, models::ModelError, - services::{graphql_client::GraphqlError, wallet_config_service::WalletConfigsError}, + services::{ + graphql_client::GraphqlError, risk_checker_service::RiskCheckerError, wallet_config_service::WalletConfigsError, + }, }; #[derive(Debug, thiserror::Error)] @@ -38,6 +40,8 @@ pub enum AppError { Rusx(#[from] SdkError), #[error("Telegram API error: {1}")] Telegram(u16, String), + #[error("Risk checker error: {0}")] + RiskChecker(#[from] RiskCheckerError), } pub type AppResult = Result; @@ -66,6 +70,9 @@ impl IntoResponse for AppError { // --- Database --- AppError::Database(err) => map_db_error(err), + // --- Risk Checker --- + AppError::RiskChecker(err) => map_risk_checker_error(err), + // --- Everything else --- e @ (AppError::Join(_) | AppError::Graphql(_) @@ -175,3 +182,23 @@ fn map_db_error(err: DbError) -> (StatusCode, String) { fn map_wallet_configs_error(err: WalletConfigsError) -> (StatusCode, String) { (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()) } + +fn map_risk_checker_error(err: RiskCheckerError) -> (StatusCode, String) { + match err { + RiskCheckerError::InvalidInput => (StatusCode::BAD_REQUEST, err.to_string()), + RiskCheckerError::EnsNotFound(name) => ( + StatusCode::NOT_FOUND, + format!( + "The ENS name \"{}\" could not be resolved to an Ethereum address. Please verify the .eth name is correct.", + name + ), + ), + RiskCheckerError::AddressNotFound => (StatusCode::NOT_FOUND, err.to_string()), + RiskCheckerError::RateLimit => (StatusCode::TOO_MANY_REQUESTS, err.to_string()), + RiskCheckerError::NetworkError => (StatusCode::SERVICE_UNAVAILABLE, err.to_string()), + RiskCheckerError::Other(msg) => { + tracing::error!("Risk checker error: {}", msg); + (StatusCode::INTERNAL_SERVER_ERROR, "An internal server error occurred".to_string()) + } + } +} diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index feee07e..62ddd3a 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -13,6 +13,7 @@ pub mod config; pub mod raid_quest; pub mod referral; pub mod relevant_tweet; +pub mod risk_checker; pub mod tweet_author; #[derive(Debug, thiserror::Error)] diff --git a/src/handlers/risk_checker.rs b/src/handlers/risk_checker.rs new file mode 100644 index 0000000..061bef2 --- /dev/null +++ b/src/handlers/risk_checker.rs @@ -0,0 +1,25 @@ +use axum::{ + extract::{Path, State}, + Json, +}; +use serde_json::json; + +use crate::{ + handlers::SuccessResponse, + http_server::AppState, + services::risk_checker_service::{RiskCheckerError, RiskCheckerService}, + AppError, +}; + +pub async fn handle_get_risk_report( + State(state): State, + Path(address_or_ens): Path, +) -> Result>, AppError> { + if !RiskCheckerService::is_valid_eth_address(&address_or_ens) && !RiskCheckerService::is_ens_name(&address_or_ens) { + return Err(RiskCheckerError::InvalidInput.into()); + } + + let report = state.risk_checker_service.generate_report(&address_or_ens).await?; + + Ok(SuccessResponse::new(json!(report))) +} diff --git a/src/http_server.rs b/src/http_server.rs index 55df17b..15d5782 100644 --- a/src/http_server.rs +++ b/src/http_server.rs @@ -13,7 +13,7 @@ use crate::{ db_persistence::DbPersistence, metrics::{metrics_handler, track_metrics, Metrics}, routes::api_routes, - services::wallet_config_service::WalletConfigService, + services::{risk_checker_service::RiskCheckerService, wallet_config_service::WalletConfigService}, Config, GraphqlClient, }; use chrono::{DateTime, Utc}; @@ -25,6 +25,7 @@ pub struct AppState { pub metrics: Arc, pub graphql_client: Arc, pub wallet_config_service: Arc, + pub risk_checker_service: Arc, pub config: Arc, pub challenges: Arc>>, pub oauth_sessions: Arc>>, @@ -88,6 +89,7 @@ pub async fn start_server( wallet_config_service: Arc::new(WalletConfigService::new( config.remote_configs.wallet_configs_file.clone(), )?), + risk_checker_service: Arc::new(RiskCheckerService::new(&config.risk_checker)), config, twitter_gateway, challenges: Arc::new(RwLock::new(HashMap::new())), diff --git a/src/routes/mod.rs b/src/routes/mod.rs index acb6e1e..44d52dd 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -2,6 +2,7 @@ use auth::auth_routes; use axum::Router; use config::config_routes; use referral::referral_routes; +use risk_checker::risk_checker_routes; use crate::{ http_server::AppState, @@ -17,6 +18,7 @@ pub mod config; pub mod raid_quest; pub mod referral; pub mod relevant_tweet; +pub mod risk_checker; pub mod tweet_author; pub fn api_routes(state: AppState) -> Router { @@ -28,4 +30,5 @@ pub fn api_routes(state: AppState) -> Router { .merge(tweet_author_routes(state.clone())) .merge(config_routes()) .merge(raid_quest_routes(state)) + .merge(risk_checker_routes()) } diff --git a/src/routes/risk_checker.rs b/src/routes/risk_checker.rs new file mode 100644 index 0000000..ede8d23 --- /dev/null +++ b/src/routes/risk_checker.rs @@ -0,0 +1,7 @@ +use axum::{routing::get, Router}; + +use crate::{handlers::risk_checker::handle_get_risk_report, http_server::AppState}; + +pub fn risk_checker_routes() -> Router { + Router::new().route("/risk-checker/:address_or_ens", get(handle_get_risk_report)) +} diff --git a/src/services/mod.rs b/src/services/mod.rs index 7274e43..0186555 100644 --- a/src/services/mod.rs +++ b/src/services/mod.rs @@ -1,6 +1,7 @@ pub mod alert_service; pub mod graphql_client; pub mod raid_leaderboard_service; +pub mod risk_checker_service; pub mod signature_service; pub mod telegram_service; pub mod tweet_synchronizer_service; diff --git a/src/services/risk_checker_service.rs b/src/services/risk_checker_service.rs new file mode 100644 index 0000000..1dec2a8 --- /dev/null +++ b/src/services/risk_checker_service.rs @@ -0,0 +1,879 @@ +use alloy::{ens::ProviderEnsExt, primitives::Address, providers::ProviderBuilder}; +use reqwest::Client; +use serde::{Deserialize, Serialize}; +use std::{str::FromStr, time::Duration}; +use thiserror::Error; + +use crate::config::RiskCheckerConfig; + +#[derive(Debug, Error)] +pub enum RiskCheckerError { + #[error("Rate limit exceeded. Please try again later.")] + RateLimit, + #[error("The provided address could not be found on the Ethereum network. Please verify the address is correct.")] + AddressNotFound, + #[error("Unable to connect to blockchain data services. Please try again in a few moments.")] + NetworkError, + #[error("Invalid address or ENS name format")] + InvalidInput, + #[error("ENS name \"{0}\" could not be resolved to an Ethereum address")] + EnsNotFound(String), + #[error("{0}")] + Other(String), +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct RiskReport { + pub address: String, + pub ens_name: Option, + pub original_input: String, + pub balance: String, + pub balance_eth: f64, + pub has_outgoing_transactions: bool, + pub first_transaction_timestamp: Option, + pub days_since_first_transaction: Option, + pub is_smart_contract: bool, +} + +#[derive(Debug)] +pub enum AddressResolution { + Resolved { address: String, ens_name: Option }, + Invalid, + EnsNotFound, +} + +#[derive(Debug, Deserialize)] +struct EtherscanResponse { + status: String, + result: serde_json::Value, +} + +/// JSON-RPC envelope returned by Etherscan proxy endpoints. +/// Success: `{"jsonrpc":"2.0","id":1,"result":"0x5"}` +/// Error: `{"jsonrpc":"2.0","id":1,"error":{"code":-32005,"message":"..."}}` +#[derive(Debug, Deserialize)] +struct JsonRpcResponse { + result: Option, + error: Option, +} + +#[derive(Debug, Deserialize)] +struct JsonRpcError { + message: String, +} + +#[derive(Debug, Deserialize)] +struct EtherscanTx { + #[serde(rename = "timeStamp")] + time_stamp: String, +} + +#[derive(Debug)] +pub struct RiskCheckerService { + client: Client, + etherscan_api_key: String, + etherscan_base_url: String, + infura_rpc_url: String, + /// Minimum delay between consecutive Etherscan calls to stay within the rate limit. + etherscan_call_delay: Duration, +} + +impl RiskCheckerService { + pub fn new(config: &RiskCheckerConfig) -> Self { + let infura_rpc_url = format!( + "{}/{}", + config.infura_base_url.trim_end_matches('/'), + config.infura_api_key + ); + let calls_per_sec = config.etherscan_calls_per_sec.max(1) as u64; + // Add a 20% safety margin on top of the minimum inter-call interval. + let etherscan_call_delay = Duration::from_millis(300 / calls_per_sec); + Self { + client: Client::new(), + etherscan_api_key: config.etherscan_api_key.clone(), + etherscan_base_url: config.etherscan_base_url.clone(), + infura_rpc_url, + etherscan_call_delay, + } + } + + pub fn is_valid_eth_address(input: &str) -> bool { + let trimmed = input.trim(); + if !trimmed.starts_with("0x") || trimmed.len() != 42 { + return false; + } + trimmed[2..].chars().all(|c| c.is_ascii_hexdigit()) + } + + pub fn is_ens_name(input: &str) -> bool { + let trimmed = input.trim().to_lowercase(); + if !trimmed.ends_with(".eth") || trimmed.starts_with("0x") { + return false; + } + let label = &trimmed[..trimmed.len() - 4]; + !label.is_empty() && label.chars().all(|c| c.is_alphanumeric() || c == '-') + } + + pub async fn resolve_address_or_ens(&self, input: &str) -> Result { + let trimmed = input.trim(); + + if Self::is_valid_eth_address(trimmed) { + let ens_name = self.reverse_resolve_address(trimmed).await; + return Ok(AddressResolution::Resolved { + address: trimmed.to_lowercase(), + ens_name, + }); + } + + if Self::is_ens_name(trimmed) { + match self.resolve_ens_name(trimmed).await { + Some(address) => { + return Ok(AddressResolution::Resolved { + address, + ens_name: Some(trimmed.to_lowercase()), + }); + } + None => return Ok(AddressResolution::EnsNotFound), + } + } + + Ok(AddressResolution::Invalid) + } + + async fn resolve_ens_name(&self, ens_name: &str) -> Option { + let rpc_url = self.infura_rpc_url.parse().ok()?; + let provider = ProviderBuilder::new().connect_http(rpc_url); + match provider.resolve_name(ens_name).await { + Ok(address) => Some(format!("{:#x}", address)), + Err(e) => { + tracing::warn!("Failed to resolve ENS name {}: {}", ens_name, e); + None + } + } + } + + async fn reverse_resolve_address(&self, address: &str) -> Option { + let rpc_url = self.infura_rpc_url.parse().ok()?; + let provider = ProviderBuilder::new().connect_http(rpc_url); + let addr = Address::from_str(address).ok()?; + match provider.lookup_address(&addr).await { + Ok(name) => Some(name), + Err(e) => { + tracing::debug!("No ENS reverse record for {}: {}", address, e); + None + } + } + } + + async fn fetch_etherscan(&self, params: &[(&str, &str)]) -> Result { + let mut query: Vec<(&str, &str)> = params.to_vec(); + query.push(("apikey", &self.etherscan_api_key)); + + let response = self + .client + .get(&self.etherscan_base_url) + .query(&query) + .send() + .await + .map_err(|e| { + tracing::error!("Etherscan network error: {}", e); + RiskCheckerError::NetworkError + })?; + + if response.status() == reqwest::StatusCode::TOO_MANY_REQUESTS { + return Err(RiskCheckerError::RateLimit); + } + + if !response.status().is_success() { + return Err(RiskCheckerError::Other(format!( + "HTTP {}: {}", + response.status().as_u16(), + response.status().canonical_reason().unwrap_or("Unknown") + ))); + } + + let body = response + .bytes() + .await + .map_err(|e| RiskCheckerError::Other(format!("Failed to read Etherscan response body: {}", e)))?; + + let value: serde_json::Value = serde_json::from_slice(&body) + .map_err(|e| RiskCheckerError::Other(format!("Failed to parse Etherscan JSON: {}", e)))?; + + // Proxy endpoints (eth_getCode, eth_getTransactionCount, …) always use + // a JSON-RPC envelope — they never have a top-level "status" field. + // Detect by the presence of "jsonrpc" and normalise into EtherscanResponse. + if value.get("jsonrpc").is_some() { + let rpc: JsonRpcResponse = serde_json::from_value(value) + .map_err(|e| RiskCheckerError::Other(format!("Failed to parse JSON-RPC response: {}", e)))?; + + if let Some(err) = rpc.error { + let msg = err.message.to_lowercase(); + if msg.contains("rate limit") || msg.contains("max calls") { + return Err(RiskCheckerError::RateLimit); + } + return Err(RiskCheckerError::Other(err.message)); + } + + return Ok(EtherscanResponse { + status: "1".to_string(), + result: rpc.result.unwrap_or(serde_json::Value::Null), + }); + } + + // Standard account-module response shape: {"status":"1","message":"OK","result":...} + let data: EtherscanResponse = serde_json::from_value(value) + .map_err(|e| RiskCheckerError::Other(format!("Failed to parse Etherscan response: {}", e)))?; + + if data.status == "0" { + let result_str = data.result.as_str().unwrap_or("").to_lowercase(); + if result_str.contains("rate limit") || result_str.contains("max calls") { + return Err(RiskCheckerError::RateLimit); + } + return Err(RiskCheckerError::AddressNotFound); + } + + Ok(data) + } + + pub async fn get_balance(&self, address: &str) -> Result { + let data = self + .fetch_etherscan(&[ + ("module", "account"), + ("action", "balance"), + ("address", address), + ("tag", "latest"), + ]) + .await?; + + data.result + .as_str() + .map(|s| s.to_string()) + .ok_or_else(|| RiskCheckerError::Other("Unexpected balance response format".to_string())) + } + + pub async fn has_any_transactions(&self, address: &str) -> Result { + let data = self + .fetch_etherscan(&[ + ("module", "proxy"), + ("action", "eth_getTransactionCount"), + ("address", address), + ("tag", "latest"), + ]) + .await?; + + let hex = data + .result + .as_str() + .ok_or_else(|| RiskCheckerError::Other("Unexpected tx count response format".to_string()))?; + + let count = u64::from_str_radix(hex.trim_start_matches("0x"), 16) + .map_err(|e| RiskCheckerError::Other(format!("Failed to parse tx count: {}", e)))?; + + Ok(count > 0) + } + + pub async fn get_first_transaction_timestamp(&self, address: &str) -> Result, RiskCheckerError> { + let data = self + .fetch_etherscan(&[ + ("module", "account"), + ("action", "txlist"), + ("address", address), + ("startblock", "0"), + ("endblock", "99999999"), + ("page", "1"), + ("offset", "1"), + ("sort", "asc"), + ]) + .await; + + // A "no transactions found" response comes back as status "0" which + // fetch_etherscan maps to AddressNotFound — treat that as an empty result. + let data = match data { + Ok(d) => d, + Err(RiskCheckerError::AddressNotFound) => return Ok(None), + Err(e) => return Err(e), + }; + + let txs: Vec = serde_json::from_value(data.result) + .map_err(|e| RiskCheckerError::Other(format!("Failed to parse tx list: {}", e)))?; + + let timestamp = txs.first().and_then(|tx| tx.time_stamp.parse::().ok()); + Ok(timestamp) + } + + pub async fn is_smart_contract(&self, address: &str) -> bool { + match self + .fetch_etherscan(&[ + ("module", "proxy"), + ("action", "eth_getCode"), + ("address", address), + ("tag", "latest"), + ]) + .await + { + Ok(data) => data.result.as_str().map(|code| code.len() > 100).unwrap_or(false), + Err(_) => false, + } + } + + pub fn wei_to_eth(wei: &str) -> f64 { + wei.parse::().map(|w| w as f64 / 1e18).unwrap_or(0.0) + } + + #[cfg(test)] + pub fn new_with_base_url(etherscan_base_url: &str, infura_rpc_url: &str) -> Self { + Self { + client: Client::new(), + etherscan_api_key: "test-key".to_string(), + etherscan_base_url: etherscan_base_url.to_string(), + infura_rpc_url: infura_rpc_url.to_string(), + etherscan_call_delay: Duration::ZERO, + } + } + + pub async fn generate_report(&self, input: &str) -> Result { + let resolution = self.resolve_address_or_ens(input).await?; + + let (resolved_address, ens_name) = match resolution { + AddressResolution::Invalid => return Err(RiskCheckerError::InvalidInput), + AddressResolution::EnsNotFound => return Err(RiskCheckerError::EnsNotFound(input.to_string())), + AddressResolution::Resolved { address, ens_name } => (address, ens_name), + }; + + let balance = self.get_balance(&resolved_address).await?; + tokio::time::sleep(self.etherscan_call_delay).await; + + let has_outgoing_transactions = self.has_any_transactions(&resolved_address).await?; + tokio::time::sleep(self.etherscan_call_delay).await; + + let is_smart_contract = self.is_smart_contract(&resolved_address).await; + tokio::time::sleep(self.etherscan_call_delay).await; + + let balance_eth = Self::wei_to_eth(&balance); + + let first_transaction_timestamp = if has_outgoing_transactions { + let ts = self.get_first_transaction_timestamp(&resolved_address).await?; + tokio::time::sleep(self.etherscan_call_delay).await; + ts + } else { + None + }; + + let days_since_first_transaction = first_transaction_timestamp.map(|ts| { + let now = chrono::Utc::now().timestamp(); + let seconds = now - ts; + seconds / (24 * 60 * 60) + }); + + Ok(RiskReport { + address: resolved_address, + ens_name, + original_input: input.to_string(), + balance, + balance_eth, + has_outgoing_transactions, + first_transaction_timestamp, + days_since_first_transaction, + is_smart_contract, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use wiremock::{ + matchers::{method, query_param}, + Mock, MockServer, ResponseTemplate, + }; + + // ------------------------------------------------------------------------- + // Unit tests — pure functions, no I/O + // ------------------------------------------------------------------------- + + #[test] + fn test_is_valid_eth_address_valid() { + assert!(RiskCheckerService::is_valid_eth_address( + "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" + )); + } + + #[test] + fn test_is_valid_eth_address_lowercase() { + assert!(RiskCheckerService::is_valid_eth_address( + "0xd8da6bf26964af9d7eed9e03e53415d37aa96045" + )); + } + + #[test] + fn test_is_valid_eth_address_too_short() { + assert!(!RiskCheckerService::is_valid_eth_address("0xabc123")); + } + + #[test] + fn test_is_valid_eth_address_missing_prefix() { + assert!(!RiskCheckerService::is_valid_eth_address( + "d8da6bf26964af9d7eed9e03e53415d37aa96045" + )); + } + + #[test] + fn test_is_valid_eth_address_invalid_chars() { + // 'z' is not a hex digit + assert!(!RiskCheckerService::is_valid_eth_address( + "0xzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + )); + } + + #[test] + fn test_is_valid_eth_address_empty() { + assert!(!RiskCheckerService::is_valid_eth_address("")); + } + + #[test] + fn test_is_ens_name_valid() { + assert!(RiskCheckerService::is_ens_name("vitalik.eth")); + } + + #[test] + fn test_is_ens_name_with_numbers() { + assert!(RiskCheckerService::is_ens_name("wallet123.eth")); + } + + #[test] + fn test_is_ens_name_with_hyphen() { + assert!(RiskCheckerService::is_ens_name("my-wallet.eth")); + } + + #[test] + fn test_is_ens_name_uppercase_normalised() { + assert!(RiskCheckerService::is_ens_name("Vitalik.ETH")); + } + + #[test] + fn test_is_ens_name_wrong_tld() { + assert!(!RiskCheckerService::is_ens_name("vitalik.com")); + } + + #[test] + fn test_is_ens_name_eth_address_rejected() { + // Starts with 0x — must not be treated as ENS + assert!(!RiskCheckerService::is_ens_name( + "0xd8da6bf26964af9d7eed9e03e53415d37aa96045.eth" + )); + } + + #[test] + fn test_is_ens_name_empty_label() { + assert!(!RiskCheckerService::is_ens_name(".eth")); + } + + #[test] + fn test_is_ens_name_empty_string() { + assert!(!RiskCheckerService::is_ens_name("")); + } + + #[test] + fn test_wei_to_eth_one_eth() { + let result = RiskCheckerService::wei_to_eth("1000000000000000000"); + assert!((result - 1.0).abs() < f64::EPSILON); + } + + #[test] + fn test_wei_to_eth_zero() { + assert_eq!(RiskCheckerService::wei_to_eth("0"), 0.0); + } + + #[test] + fn test_wei_to_eth_fractional() { + // 0.5 ETH = 500_000_000_000_000_000 wei + let result = RiskCheckerService::wei_to_eth("500000000000000000"); + assert!((result - 0.5).abs() < 1e-9); + } + + #[test] + fn test_wei_to_eth_invalid_string() { + assert_eq!(RiskCheckerService::wei_to_eth("not-a-number"), 0.0); + } + + // ------------------------------------------------------------------------- + // Integration tests — Etherscan HTTP calls mocked with wiremock + // ------------------------------------------------------------------------- + + fn etherscan_ok(result: serde_json::Value) -> serde_json::Value { + serde_json::json!({ "status": "1", "message": "OK", "result": result }) + } + + fn etherscan_notok() -> serde_json::Value { + serde_json::json!({ "status": "0", "message": "NOTOK", "result": "" }) + } + + async fn setup_service(mock_server: &MockServer) -> RiskCheckerService { + RiskCheckerService::new_with_base_url(&mock_server.uri(), "http://unused-infura") + } + + #[tokio::test] + async fn test_get_balance_success() { + // Arrange + let mock_server = MockServer::start().await; + let address = "0xd8da6bf26964af9d7eed9e03e53415d37aa96045"; + + Mock::given(method("GET")) + .and(query_param("module", "account")) + .and(query_param("action", "balance")) + .and(query_param("address", address)) + .respond_with( + ResponseTemplate::new(200).set_body_json(etherscan_ok(serde_json::json!("1000000000000000000"))), + ) + .expect(1) + .mount(&mock_server) + .await; + + let service = setup_service(&mock_server).await; + + // Act + let result = service.get_balance(address).await; + + // Assert + assert!(result.is_ok()); + assert_eq!(result.unwrap(), "1000000000000000000"); + } + + #[tokio::test] + async fn test_get_balance_address_not_found() { + // Arrange + let mock_server = MockServer::start().await; + let address = "0xd8da6bf26964af9d7eed9e03e53415d37aa96045"; + + Mock::given(method("GET")) + .and(query_param("module", "account")) + .and(query_param("action", "balance")) + .respond_with(ResponseTemplate::new(200).set_body_json(etherscan_notok())) + .expect(1) + .mount(&mock_server) + .await; + + let service = setup_service(&mock_server).await; + + // Act + let result = service.get_balance(address).await; + + // Assert + assert!(matches!(result, Err(RiskCheckerError::AddressNotFound))); + } + + #[tokio::test] + async fn test_get_balance_rate_limit() { + // Arrange + let mock_server = MockServer::start().await; + let address = "0xd8da6bf26964af9d7eed9e03e53415d37aa96045"; + + Mock::given(method("GET")) + .and(query_param("module", "account")) + .and(query_param("action", "balance")) + .respond_with(ResponseTemplate::new(429)) + .expect(1) + .mount(&mock_server) + .await; + + let service = setup_service(&mock_server).await; + + // Act + let result = service.get_balance(address).await; + + // Assert + assert!(matches!(result, Err(RiskCheckerError::RateLimit))); + } + + #[tokio::test] + async fn test_has_any_transactions_true() { + // Arrange + let mock_server = MockServer::start().await; + let address = "0xd8da6bf26964af9d7eed9e03e53415d37aa96045"; + + Mock::given(method("GET")) + .and(query_param("module", "proxy")) + .and(query_param("action", "eth_getTransactionCount")) + .and(query_param("address", address)) + .respond_with(ResponseTemplate::new(200).set_body_json(etherscan_ok(serde_json::json!("0x5")))) + .expect(1) + .mount(&mock_server) + .await; + + let service = setup_service(&mock_server).await; + + // Act + let result = service.has_any_transactions(address).await; + + // Assert + assert!(result.is_ok()); + assert!(result.unwrap()); + } + + #[tokio::test] + async fn test_has_any_transactions_false() { + // Arrange + let mock_server = MockServer::start().await; + let address = "0xd8da6bf26964af9d7eed9e03e53415d37aa96045"; + + Mock::given(method("GET")) + .and(query_param("module", "proxy")) + .and(query_param("action", "eth_getTransactionCount")) + .respond_with(ResponseTemplate::new(200).set_body_json(etherscan_ok(serde_json::json!("0x0")))) + .expect(1) + .mount(&mock_server) + .await; + + let service = setup_service(&mock_server).await; + + // Act + let result = service.has_any_transactions(address).await; + + // Assert + assert!(result.is_ok()); + assert!(!result.unwrap()); + } + + #[tokio::test] + async fn test_is_smart_contract_true() { + // Arrange + let mock_server = MockServer::start().await; + let address = "0xd8da6bf26964af9d7eed9e03e53415d37aa96045"; + // A code string longer than 100 chars indicates a contract + let bytecode = "0x".to_string() + &"60".repeat(60); + + Mock::given(method("GET")) + .and(query_param("module", "proxy")) + .and(query_param("action", "eth_getCode")) + .and(query_param("address", address)) + .respond_with(ResponseTemplate::new(200).set_body_json(etherscan_ok(serde_json::json!(bytecode)))) + .expect(1) + .mount(&mock_server) + .await; + + let service = setup_service(&mock_server).await; + + // Act + let result = service.is_smart_contract(address).await; + + // Assert + assert!(result); + } + + #[tokio::test] + async fn test_is_smart_contract_false_eoa() { + // Arrange + let mock_server = MockServer::start().await; + let address = "0xd8da6bf26964af9d7eed9e03e53415d37aa96045"; + + Mock::given(method("GET")) + .and(query_param("module", "proxy")) + .and(query_param("action", "eth_getCode")) + .respond_with(ResponseTemplate::new(200).set_body_json(etherscan_ok(serde_json::json!("0x")))) + .expect(1) + .mount(&mock_server) + .await; + + let service = setup_service(&mock_server).await; + + // Act + let result = service.is_smart_contract(address).await; + + // Assert + assert!(!result); + } + + #[tokio::test] + async fn test_get_first_transaction_timestamp_success() { + // Arrange + let mock_server = MockServer::start().await; + let address = "0xd8da6bf26964af9d7eed9e03e53415d37aa96045"; + let expected_ts: i64 = 1438918233; + + Mock::given(method("GET")) + .and(query_param("module", "account")) + .and(query_param("action", "txlist")) + .and(query_param("address", address)) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "status": "1", + "message": "OK", + "result": [{ "timeStamp": expected_ts.to_string() }] + }))) + .expect(1) + .mount(&mock_server) + .await; + + let service = setup_service(&mock_server).await; + + // Act + let result = service.get_first_transaction_timestamp(address).await; + + // Assert + assert!(result.is_ok()); + assert_eq!(result.unwrap(), Some(expected_ts)); + } + + #[tokio::test] + async fn test_get_first_transaction_timestamp_no_transactions() { + // Arrange + let mock_server = MockServer::start().await; + let address = "0xd8da6bf26964af9d7eed9e03e53415d37aa96045"; + + // Etherscan returns status "0" / NOTOK when there are no transactions — + // our code treats this as an empty result, not an error. + Mock::given(method("GET")) + .and(query_param("module", "account")) + .and(query_param("action", "txlist")) + .respond_with(ResponseTemplate::new(200).set_body_json(etherscan_notok())) + .expect(1) + .mount(&mock_server) + .await; + + let service = setup_service(&mock_server).await; + + // Act + let result = service.get_first_transaction_timestamp(address).await; + + // Assert + assert!(result.is_ok()); + assert_eq!(result.unwrap(), None); + } + + #[tokio::test] + async fn test_generate_report_for_eoa_with_transactions() { + // Arrange + let mock_server = MockServer::start().await; + let address = "0xd8da6bf26964af9d7eed9e03e53415d37aa96045"; + + // balance + Mock::given(method("GET")) + .and(query_param("module", "account")) + .and(query_param("action", "balance")) + .respond_with( + ResponseTemplate::new(200).set_body_json(etherscan_ok(serde_json::json!("2000000000000000000"))), + ) + .mount(&mock_server) + .await; + + // tx count — has transactions + Mock::given(method("GET")) + .and(query_param("module", "proxy")) + .and(query_param("action", "eth_getTransactionCount")) + .respond_with(ResponseTemplate::new(200).set_body_json(etherscan_ok(serde_json::json!("0xa")))) + .mount(&mock_server) + .await; + + // eth_getCode — EOA (short result) + Mock::given(method("GET")) + .and(query_param("module", "proxy")) + .and(query_param("action", "eth_getCode")) + .respond_with(ResponseTemplate::new(200).set_body_json(etherscan_ok(serde_json::json!("0x")))) + .mount(&mock_server) + .await; + + // first tx timestamp + Mock::given(method("GET")) + .and(query_param("module", "account")) + .and(query_param("action", "txlist")) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "status": "1", + "message": "OK", + "result": [{ "timeStamp": "1438918233" }] + }))) + .mount(&mock_server) + .await; + + let service = setup_service(&mock_server).await; + + // Act + let result = service.generate_report(address).await; + + // Assert + assert!(result.is_ok()); + let report = result.unwrap(); + assert_eq!(report.address, address.to_lowercase()); + assert_eq!(report.original_input, address); + assert_eq!(report.balance, "2000000000000000000"); + assert!((report.balance_eth - 2.0).abs() < 1e-9); + assert!(report.has_outgoing_transactions); + assert_eq!(report.first_transaction_timestamp, Some(1438918233)); + assert!(report.days_since_first_transaction.is_some()); + assert!(!report.is_smart_contract); + assert!(report.ens_name.is_none()); + } + + #[tokio::test] + async fn test_generate_report_for_address_with_no_transactions() { + // Arrange + let mock_server = MockServer::start().await; + let address = "0xd8da6bf26964af9d7eed9e03e53415d37aa96045"; + + Mock::given(method("GET")) + .and(query_param("module", "account")) + .and(query_param("action", "balance")) + .respond_with(ResponseTemplate::new(200).set_body_json(etherscan_ok(serde_json::json!("0")))) + .mount(&mock_server) + .await; + + Mock::given(method("GET")) + .and(query_param("module", "proxy")) + .and(query_param("action", "eth_getTransactionCount")) + .respond_with(ResponseTemplate::new(200).set_body_json(etherscan_ok(serde_json::json!("0x0")))) + .mount(&mock_server) + .await; + + Mock::given(method("GET")) + .and(query_param("module", "proxy")) + .and(query_param("action", "eth_getCode")) + .respond_with(ResponseTemplate::new(200).set_body_json(etherscan_ok(serde_json::json!("0x")))) + .mount(&mock_server) + .await; + + let service = setup_service(&mock_server).await; + + // Act + let result = service.generate_report(address).await; + + // Assert + assert!(result.is_ok()); + let report = result.unwrap(); + assert!(!report.has_outgoing_transactions); + assert_eq!(report.first_transaction_timestamp, None); + assert_eq!(report.days_since_first_transaction, None); + } + + #[tokio::test] + async fn test_generate_report_invalid_input_returns_error() { + // Arrange — no mocks needed; validation is synchronous + let mock_server = MockServer::start().await; + let service = setup_service(&mock_server).await; + + // Act + let result = service.generate_report("not-valid-at-all").await; + + // Assert + assert!(matches!(result, Err(RiskCheckerError::InvalidInput))); + } + + #[tokio::test] + async fn test_generate_report_propagates_rate_limit_error() { + // Arrange + let mock_server = MockServer::start().await; + let address = "0xd8da6bf26964af9d7eed9e03e53415d37aa96045"; + + Mock::given(method("GET")) + .respond_with(ResponseTemplate::new(429)) + .mount(&mock_server) + .await; + + let service = setup_service(&mock_server).await; + + // Act + let result = service.generate_report(address).await; + + // Assert + assert!(matches!(result, Err(RiskCheckerError::RateLimit))); + } +} diff --git a/src/utils/test_app_state.rs b/src/utils/test_app_state.rs index d269593..8ce4e17 100644 --- a/src/utils/test_app_state.rs +++ b/src/utils/test_app_state.rs @@ -1,6 +1,10 @@ use crate::{ - db_persistence::DbPersistence, http_server::AppState, metrics::Metrics, models::auth::TokenClaims, - services::wallet_config_service::WalletConfigService, Config, GraphqlClient, + db_persistence::DbPersistence, + http_server::AppState, + metrics::Metrics, + models::auth::TokenClaims, + services::{risk_checker_service::RiskCheckerService, wallet_config_service::WalletConfigService}, + Config, GraphqlClient, }; use jsonwebtoken::{encode, EncodingKey, Header}; use rusx::RusxGateway; @@ -11,7 +15,7 @@ pub async fn create_test_app_state() -> AppState { let db = DbPersistence::new(config.get_database_url()).await.unwrap(); let twitter_gateway = RusxGateway::new(config.x_oauth.clone(), None).unwrap(); let graphql_client = GraphqlClient::new(db.clone(), config.candidates.graphql_url.clone()); - + let risk_checker_service = RiskCheckerService::new(&config.risk_checker); let db = Arc::new(db); AppState { @@ -21,6 +25,7 @@ pub async fn create_test_app_state() -> AppState { wallet_config_service: Arc::new( WalletConfigService::new(config.remote_configs.wallet_configs_file.clone()).unwrap(), ), + risk_checker_service: Arc::new(risk_checker_service), config: Arc::new(config), twitter_gateway: Arc::new(twitter_gateway), oauth_sessions: Arc::new(Mutex::new(std::collections::HashMap::new())), From 486d6882fce2d52978c5995dfe6c73db264f9603 Mon Sep 17 00:00:00 2001 From: Beast Date: Sat, 4 Apr 2026 21:11:35 +0800 Subject: [PATCH 02/11] fix: delay fetch --- src/services/risk_checker_service.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/risk_checker_service.rs b/src/services/risk_checker_service.rs index 1dec2a8..35909a9 100644 --- a/src/services/risk_checker_service.rs +++ b/src/services/risk_checker_service.rs @@ -88,7 +88,7 @@ impl RiskCheckerService { ); let calls_per_sec = config.etherscan_calls_per_sec.max(1) as u64; // Add a 20% safety margin on top of the minimum inter-call interval. - let etherscan_call_delay = Duration::from_millis(300 / calls_per_sec); + let etherscan_call_delay = Duration::from_millis(1000 / calls_per_sec); Self { client: Client::new(), etherscan_api_key: config.etherscan_api_key.clone(), From 341379746d218a010b7529835f3685ae1f950a3a Mon Sep 17 00:00:00 2001 From: Beast Date: Mon, 6 Apr 2026 17:30:01 +0800 Subject: [PATCH 03/11] feat: add cors config --- config/default.toml | 1 + config/example.toml | 1 + config/test.toml | 1 + src/config.rs | 11 +++++++++++ src/http_server.rs | 2 +- 5 files changed, 15 insertions(+), 1 deletion(-) diff --git a/config/default.toml b/config/default.toml index 32259cd..7832403 100644 --- a/config/default.toml +++ b/config/default.toml @@ -4,6 +4,7 @@ base_api_url = "http://localhost:3000/api" host = "127.0.0.1" port = 3000 +cors_allowed_origins = ["http://localhost:4321"] [blockchain] website_url = "https://www.quantus.com" diff --git a/config/example.toml b/config/example.toml index a0d9354..bfcaf1c 100644 --- a/config/example.toml +++ b/config/example.toml @@ -6,6 +6,7 @@ base_api_url = "http://127.0.0.1:3000/api" host = "127.0.0.1" port = 3000 +cors_allowed_origins = ["http://localhost:4321"] [blockchain] website_url = "http://localhost:3080" diff --git a/config/test.toml b/config/test.toml index 3551829..471e32d 100644 --- a/config/test.toml +++ b/config/test.toml @@ -4,6 +4,7 @@ base_api_url = "http://127.0.0.1:3000/api" host = "127.0.0.1" port = 3000 +cors_allowed_origins = ["http://localhost:4321"] [blockchain] website_url = "http://127.0.0.1:3080" diff --git a/src/config.rs b/src/config.rs index 5a9aef3..c43a1fe 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,5 +1,6 @@ use std::path::Path; +use axum::http::HeaderValue; use rusx::config::OauthConfig; use serde::{Deserialize, Serialize}; use tokio::time; @@ -32,6 +33,7 @@ pub struct ServerConfig { pub host: String, pub port: u16, pub base_api_url: String, + pub cors_allowed_origins: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -171,6 +173,14 @@ impl Config { &self.x_association.keywords } + pub fn get_cors_allowed_origins(&self) -> Vec { + self.server + .cors_allowed_origins + .iter() + .map(|o| o.parse().unwrap()) + .collect() + } + fn resolve_relative_paths(&mut self, config_path: &str) { let wallet_configs_path = Path::new(&self.remote_configs.wallet_configs_file); if wallet_configs_path.is_absolute() { @@ -188,6 +198,7 @@ impl Default for Config { host: "127.0.0.1".to_string(), port: 3000, base_api_url: "http://127.0.0.1:3000/api".to_string(), + cors_allowed_origins: vec!["http://localhost:3000".to_string()], }, blockchain: BlockchainConfig { website_url: "https://www.quantus.com".to_string(), diff --git a/src/http_server.rs b/src/http_server.rs index 15d5782..590eaa8 100644 --- a/src/http_server.rs +++ b/src/http_server.rs @@ -58,7 +58,7 @@ pub fn create_router(state: AppState) -> Router { .layer( ServiceBuilder::new() .layer(TraceLayer::new_for_http()) - .layer(CorsLayer::permissive()), + .layer(CorsLayer::permissive().allow_origin(state.config.get_cors_allowed_origins())), ) .layer(CookieManagerLayer::new()) // Enable Cookie support .with_state(state) From 2e8e85e745d45700f63b6548aeedf1fe9059abcf Mon Sep 17 00:00:00 2001 From: Beast Date: Mon, 6 Apr 2026 17:39:48 +0800 Subject: [PATCH 04/11] feat: add timeout to created client --- src/services/risk_checker_service.rs | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/services/risk_checker_service.rs b/src/services/risk_checker_service.rs index 35909a9..81a4903 100644 --- a/src/services/risk_checker_service.rs +++ b/src/services/risk_checker_service.rs @@ -87,10 +87,15 @@ impl RiskCheckerService { config.infura_api_key ); let calls_per_sec = config.etherscan_calls_per_sec.max(1) as u64; - // Add a 20% safety margin on top of the minimum inter-call interval. let etherscan_call_delay = Duration::from_millis(1000 / calls_per_sec); + + let client = Client::builder() + .timeout(Duration::from_secs(30)) + .build() + .expect("TLS backend should be initialized, or the resolver should load the system configuration."); + Self { - client: Client::new(), + client, etherscan_api_key: config.etherscan_api_key.clone(), etherscan_base_url: config.etherscan_base_url.clone(), infura_rpc_url, @@ -303,7 +308,7 @@ impl RiskCheckerService { Ok(timestamp) } - pub async fn is_smart_contract(&self, address: &str) -> bool { + pub async fn is_smart_contract(&self, address: &str) -> Result { match self .fetch_etherscan(&[ ("module", "proxy"), @@ -313,8 +318,8 @@ impl RiskCheckerService { ]) .await { - Ok(data) => data.result.as_str().map(|code| code.len() > 100).unwrap_or(false), - Err(_) => false, + Ok(data) => Ok(data.result.as_str().map(|code| code.len() > 100).unwrap_or(false)), + Err(e) => return Err(e), } } @@ -348,7 +353,7 @@ impl RiskCheckerService { let has_outgoing_transactions = self.has_any_transactions(&resolved_address).await?; tokio::time::sleep(self.etherscan_call_delay).await; - let is_smart_contract = self.is_smart_contract(&resolved_address).await; + let is_smart_contract = self.is_smart_contract(&resolved_address).await?; tokio::time::sleep(self.etherscan_call_delay).await; let balance_eth = Self::wei_to_eth(&balance); @@ -656,7 +661,7 @@ mod tests { let service = setup_service(&mock_server).await; // Act - let result = service.is_smart_contract(address).await; + let result = service.is_smart_contract(address).await.unwrap(); // Assert assert!(result); @@ -679,7 +684,7 @@ mod tests { let service = setup_service(&mock_server).await; // Act - let result = service.is_smart_contract(address).await; + let result = service.is_smart_contract(address).await.unwrap(); // Assert assert!(!result); From 607a575da331fe7aa04a23566a04986d7b7083f8 Mon Sep 17 00:00:00 2001 From: Beast Date: Mon, 6 Apr 2026 18:32:09 +0800 Subject: [PATCH 05/11] feat: finish adding rate limit --- config/default.toml | 1 + config/example.toml | 1 + config/test.toml | 1 + src/config.rs | 4 +- src/services/risk_checker_service.rs | 78 +++++++++++++++++++++++++++- 5 files changed, 82 insertions(+), 3 deletions(-) diff --git a/config/default.toml b/config/default.toml index 7832403..177037a 100644 --- a/config/default.toml +++ b/config/default.toml @@ -89,3 +89,4 @@ etherscan_base_url = "https://api.etherscan.io/v2/api?chainid=1" infura_api_key = "change-me" infura_base_url = "https://mainnet.infura.io/v3" etherscan_calls_per_sec = 3 +max_concurrent_requests = 1 diff --git a/config/example.toml b/config/example.toml index bfcaf1c..d2cbac8 100644 --- a/config/example.toml +++ b/config/example.toml @@ -99,6 +99,7 @@ etherscan_base_url = "https://api.etherscan.io/v2/api?chainid=1" infura_api_key = "change-me" infura_base_url = "https://mainnet.infura.io/v3" etherscan_calls_per_sec = 3 +max_concurrent_requests = 1 # Example environment variable overrides: # TASKMASTER_BLOCKCHAIN__NODE_URL="ws://remote-node:9944" diff --git a/config/test.toml b/config/test.toml index 471e32d..278e16c 100644 --- a/config/test.toml +++ b/config/test.toml @@ -89,3 +89,4 @@ etherscan_base_url = "https://api.etherscan.io/v2/api?chainid=1" infura_api_key = "change-me" infura_base_url = "https://mainnet.infura.io/v3" etherscan_calls_per_sec = 3 +max_concurrent_requests = 1 diff --git a/src/config.rs b/src/config.rs index c43a1fe..c129a65 100644 --- a/src/config.rs +++ b/src/config.rs @@ -108,9 +108,8 @@ pub struct RiskCheckerConfig { pub etherscan_base_url: String, pub infura_api_key: String, pub infura_base_url: String, - /// Maximum Etherscan API calls per second. Used to space out sequential - /// calls and stay within the free-tier limit (default: 3). pub etherscan_calls_per_sec: u32, + pub max_concurrent_requests: usize, } impl Config { @@ -260,6 +259,7 @@ impl Default for Config { infura_api_key: "change-me".to_string(), infura_base_url: "https://mainnet.infura.io/v3".to_string(), etherscan_calls_per_sec: 3, + max_concurrent_requests: 1, }, } } diff --git a/src/services/risk_checker_service.rs b/src/services/risk_checker_service.rs index 81a4903..b486424 100644 --- a/src/services/risk_checker_service.rs +++ b/src/services/risk_checker_service.rs @@ -1,8 +1,9 @@ use alloy::{ens::ProviderEnsExt, primitives::Address, providers::ProviderBuilder}; use reqwest::Client; use serde::{Deserialize, Serialize}; -use std::{str::FromStr, time::Duration}; +use std::{str::FromStr, sync::Arc, time::Duration}; use thiserror::Error; +use tokio::sync::Semaphore; use crate::config::RiskCheckerConfig; @@ -77,6 +78,10 @@ pub struct RiskCheckerService { infura_rpc_url: String, /// Minimum delay between consecutive Etherscan calls to stay within the rate limit. etherscan_call_delay: Duration, + /// Global semaphore that caps the number of concurrent `generate_report` + /// executions across all inbound requests, preventing outbound Etherscan + /// fan-out from overwhelming the API quota. + concurrency_limiter: Arc, } impl RiskCheckerService { @@ -94,12 +99,15 @@ impl RiskCheckerService { .build() .expect("TLS backend should be initialized, or the resolver should load the system configuration."); + let max_concurrent = config.max_concurrent_requests.max(1); + Self { client, etherscan_api_key: config.etherscan_api_key.clone(), etherscan_base_url: config.etherscan_base_url.clone(), infura_rpc_url, etherscan_call_delay, + concurrency_limiter: Arc::new(Semaphore::new(max_concurrent)), } } @@ -335,10 +343,19 @@ impl RiskCheckerService { etherscan_base_url: etherscan_base_url.to_string(), infura_rpc_url: infura_rpc_url.to_string(), etherscan_call_delay: Duration::ZERO, + concurrency_limiter: Arc::new(Semaphore::new(1)), } } pub async fn generate_report(&self, input: &str) -> Result { + // Acquire a concurrency permit before touching Etherscan. If all permits + // are taken the caller gets an immediate RateLimit error rather than + // queuing indefinitely, which protects the outbound API quota. + let _permit = self.concurrency_limiter.try_acquire().map_err(|_| { + tracing::warn!("Risk checker concurrency limit reached; rejecting request"); + RiskCheckerError::RateLimit + })?; + let resolution = self.resolve_address_or_ens(input).await?; let (resolved_address, ens_name) = match resolution { @@ -881,4 +898,63 @@ mod tests { // Assert assert!(matches!(result, Err(RiskCheckerError::RateLimit))); } + + /// Verify that requests beyond the concurrency limit are immediately + /// rejected with RateLimit rather than queuing and hammering Etherscan. + #[tokio::test] + async fn test_generate_report_concurrency_limit_rejects_excess_requests() { + use std::sync::Arc; + use tokio::sync::Barrier; + + let mock_server = MockServer::start().await; + let address = "0xd8da6bf26964af9d7eed9e03e53415d37aa96045"; + + // Respond slowly so the first request holds the permit long enough for + // the second to arrive and be rejected. + Mock::given(method("GET")) + .respond_with( + ResponseTemplate::new(200) + .set_delay(Duration::from_millis(200)) + .set_body_json(serde_json::json!({ "status": "0", "message": "NOTOK", "result": "" })), + ) + .mount(&mock_server) + .await; + + // Limit to exactly 1 concurrent request. + let service = Arc::new(setup_service(&mock_server).await); + + // Use a barrier so both tasks start at the same instant. + let barrier = Arc::new(Barrier::new(2)); + + let svc1 = service.clone(); + let b1 = barrier.clone(); + let addr = address.to_string(); + let t1 = tokio::spawn(async move { + b1.wait().await; + svc1.generate_report(&addr).await + }); + + let svc2 = service.clone(); + let b2 = barrier.clone(); + let addr = address.to_string(); + let t2 = tokio::spawn(async move { + b2.wait().await; + svc2.generate_report(&addr).await + }); + + let (r1, r2) = tokio::join!(t1, t2); + let r1 = r1.unwrap(); + let r2 = r2.unwrap(); + + // Exactly one of the two concurrent calls must be rejected with RateLimit. + let rate_limited = [&r1, &r2] + .iter() + .filter(|r| matches!(r, Err(RiskCheckerError::RateLimit))) + .count(); + + assert_eq!( + rate_limited, 1, + "expected exactly one request to be rate-limited; got r1={r1:?}, r2={r2:?}" + ); + } } From 0ed13b09beebfa74959c823a33a31394bd4e3f40 Mon Sep 17 00:00:00 2001 From: Beast Date: Tue, 7 Apr 2026 12:48:45 +0800 Subject: [PATCH 06/11] fix: CORS config --- src/config.rs | 8 +++++++- src/http_server.rs | 16 +++++++++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/config.rs b/src/config.rs index c129a65..d284348 100644 --- a/src/config.rs +++ b/src/config.rs @@ -176,7 +176,13 @@ impl Config { self.server .cors_allowed_origins .iter() - .map(|o| o.parse().unwrap()) + .filter_map(|o| match o.parse() { + Ok(v) => Some(v), + Err(e) => { + tracing::warn!("Skipping invalid CORS origin {:?}: {}", o, e); + None + } + }) .collect() } diff --git a/src/http_server.rs b/src/http_server.rs index 590eaa8..627fce3 100644 --- a/src/http_server.rs +++ b/src/http_server.rs @@ -1,4 +1,5 @@ use axum::{middleware, response::Json, routing::get, Router}; +use axum::http::Method; use rusx::{PkceCodeVerifier, TwitterGateway}; use serde::{Deserialize, Serialize}; use std::{ @@ -7,7 +8,10 @@ use std::{ }; use tower::ServiceBuilder; use tower_cookies::CookieManagerLayer; -use tower_http::{cors::CorsLayer, trace::TraceLayer}; +use tower_http::{ + cors::{AllowHeaders, CorsLayer}, + trace::TraceLayer, +}; use crate::{ db_persistence::DbPersistence, @@ -58,9 +62,15 @@ pub fn create_router(state: AppState) -> Router { .layer( ServiceBuilder::new() .layer(TraceLayer::new_for_http()) - .layer(CorsLayer::permissive().allow_origin(state.config.get_cors_allowed_origins())), + .layer( + CorsLayer::new() + .allow_origin(state.config.get_cors_allowed_origins()) + .allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE, Method::OPTIONS]) + .allow_headers(AllowHeaders::mirror_request()) + .allow_credentials(true), + ), ) - .layer(CookieManagerLayer::new()) // Enable Cookie support + .layer(CookieManagerLayer::new()) .with_state(state) } From a3617c37d553a95dda51cee6e861a27b19c4dd44 Mon Sep 17 00:00:00 2001 From: Beast Date: Tue, 7 Apr 2026 12:49:33 +0800 Subject: [PATCH 07/11] chore: formatting --- src/http_server.rs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/http_server.rs b/src/http_server.rs index 627fce3..ff67a7e 100644 --- a/src/http_server.rs +++ b/src/http_server.rs @@ -1,5 +1,5 @@ -use axum::{middleware, response::Json, routing::get, Router}; use axum::http::Method; +use axum::{middleware, response::Json, routing::get, Router}; use rusx::{PkceCodeVerifier, TwitterGateway}; use serde::{Deserialize, Serialize}; use std::{ @@ -60,15 +60,13 @@ pub fn create_router(state: AppState) -> Router { .nest("/api", api_routes(state.clone())) .layer(middleware::from_fn(track_metrics)) .layer( - ServiceBuilder::new() - .layer(TraceLayer::new_for_http()) - .layer( - CorsLayer::new() - .allow_origin(state.config.get_cors_allowed_origins()) - .allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE, Method::OPTIONS]) - .allow_headers(AllowHeaders::mirror_request()) - .allow_credentials(true), - ), + ServiceBuilder::new().layer(TraceLayer::new_for_http()).layer( + CorsLayer::new() + .allow_origin(state.config.get_cors_allowed_origins()) + .allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE, Method::OPTIONS]) + .allow_headers(AllowHeaders::mirror_request()) + .allow_credentials(true), + ), ) .layer(CookieManagerLayer::new()) .with_state(state) From 9bfd828786fd76080245d25db26918b1be963543 Mon Sep 17 00:00:00 2001 From: Beast Date: Tue, 7 Apr 2026 12:52:12 +0800 Subject: [PATCH 08/11] fix: clippy --- src/services/risk_checker_service.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/risk_checker_service.rs b/src/services/risk_checker_service.rs index b486424..254c33c 100644 --- a/src/services/risk_checker_service.rs +++ b/src/services/risk_checker_service.rs @@ -327,7 +327,7 @@ impl RiskCheckerService { .await { Ok(data) => Ok(data.result.as_str().map(|code| code.len() > 100).unwrap_or(false)), - Err(e) => return Err(e), + Err(e) => Err(e), } } From f045829f75464c07e37a75e7592997c8c6d58ffe Mon Sep 17 00:00:00 2001 From: Beast Date: Tue, 7 Apr 2026 13:04:30 +0800 Subject: [PATCH 09/11] fix: simplify pattern matching --- src/services/risk_checker_service.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/services/risk_checker_service.rs b/src/services/risk_checker_service.rs index 254c33c..2d1852d 100644 --- a/src/services/risk_checker_service.rs +++ b/src/services/risk_checker_service.rs @@ -317,18 +317,16 @@ impl RiskCheckerService { } pub async fn is_smart_contract(&self, address: &str) -> Result { - match self + let data = self .fetch_etherscan(&[ ("module", "proxy"), ("action", "eth_getCode"), ("address", address), ("tag", "latest"), ]) - .await - { - Ok(data) => Ok(data.result.as_str().map(|code| code.len() > 100).unwrap_or(false)), - Err(e) => Err(e), - } + .await?; + + Ok(data.result.as_str().map(|code| code.len() > 100).unwrap_or(false)) } pub fn wei_to_eth(wei: &str) -> f64 { From 8e21eb1826969ffde189a549b70c416d4f2396ab Mon Sep 17 00:00:00 2001 From: Beast Date: Tue, 7 Apr 2026 14:52:18 +0800 Subject: [PATCH 10/11] fix: ens check logic to allow dot concant, invalid if first char is dash --- src/services/risk_checker_service.rs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/services/risk_checker_service.rs b/src/services/risk_checker_service.rs index 2d1852d..0c60157 100644 --- a/src/services/risk_checker_service.rs +++ b/src/services/risk_checker_service.rs @@ -121,11 +121,11 @@ impl RiskCheckerService { pub fn is_ens_name(input: &str) -> bool { let trimmed = input.trim().to_lowercase(); - if !trimmed.ends_with(".eth") || trimmed.starts_with("0x") { + if !trimmed.ends_with(".eth") || trimmed.starts_with("0x") || trimmed.starts_with("-") { return false; } let label = &trimmed[..trimmed.len() - 4]; - !label.is_empty() && label.chars().all(|c| c.is_alphanumeric() || c == '-') + !label.is_empty() && label.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '.') } pub async fn resolve_address_or_ens(&self, input: &str) -> Result { @@ -457,6 +457,11 @@ mod tests { assert!(RiskCheckerService::is_ens_name("vitalik.eth")); } + #[test] + fn test_is_ens_name_with_sub_valid() { + assert!(RiskCheckerService::is_ens_name("pay.vitalik.eth")); + } + #[test] fn test_is_ens_name_with_numbers() { assert!(RiskCheckerService::is_ens_name("wallet123.eth")); @@ -485,6 +490,12 @@ mod tests { )); } + #[test] + fn test_is_ens_name_starts_with_hyphen_rejected() { + // Starts with 0x — must not be treated as ENS + assert!(!RiskCheckerService::is_ens_name("-alice.eth")); + } + #[test] fn test_is_ens_name_empty_label() { assert!(!RiskCheckerService::is_ens_name(".eth")); From 462d66529b323c0daf053e3ebf9b86f7ffb1dec1 Mon Sep 17 00:00:00 2001 From: Beast Date: Tue, 7 Apr 2026 16:16:36 +0800 Subject: [PATCH 11/11] fix: smart contract check --- src/services/risk_checker_service.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/services/risk_checker_service.rs b/src/services/risk_checker_service.rs index 0c60157..92ad3e7 100644 --- a/src/services/risk_checker_service.rs +++ b/src/services/risk_checker_service.rs @@ -326,7 +326,12 @@ impl RiskCheckerService { ]) .await?; - Ok(data.result.as_str().map(|code| code.len() > 100).unwrap_or(false)) + let code = data + .result + .as_str() + .ok_or(RiskCheckerError::Other("Unexpected code response format".to_string()))?; + + Ok(code != "0x") } pub fn wei_to_eth(wei: &str) -> f64 {