diff --git a/Cargo.lock b/Cargo.lock index 5716d80..cf32d53 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,18 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "once_cell", - "version_check", - "zerocopy", -] - [[package]] name = "aho-corasick" version = "1.1.4" @@ -31,9 +19,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "alloy" -version = "1.7.3" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4973038846323e4e69a433916522195dce2947770076c03078fc21c80ea0f1c4" +checksum = "d8010fc7e9e8643ef4e758cdccf3eef26734594aedf88a9d5ed35e51837d42ef" dependencies = [ "alloy-consensus", "alloy-contract", @@ -67,9 +55,9 @@ dependencies = [ [[package]] name = "alloy-consensus" -version = "1.7.3" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c0dc44157867da82c469c13186015b86abef209bf0e41625e4b68bac61d728" +checksum = "e3d64da86c616b5092ea64eea648f311bbd58630a0b384c42d699175d6f9122b" dependencies = [ "alloy-eips", "alloy-primitives", @@ -89,14 +77,14 @@ dependencies = [ "serde", "serde_json", "serde_with", - "thiserror 2.0.18", + "thiserror", ] [[package]] name = "alloy-consensus-any" -version = "1.7.3" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba4cdb42df3871cd6b346d6a938ec2ba69a9a0f49d1f82714bc5c48349268434" +checksum = "8fd98696ca3617d3a9ba1a6f2011880cbfd5618228dab6400c9f8bca457859a8" dependencies = [ "alloy-consensus", "alloy-eips", @@ -108,9 +96,9 @@ dependencies = [ [[package]] name = "alloy-contract" -version = "1.7.3" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca63b7125a981415898ffe2a2a696c83696c9c6bdb1671c8a912946bbd8e49e7" +checksum = "de3df0aadc569a8b277808a7d0ad0e421180654ea36a3c59e9ed2bb968c9a1cd" dependencies = [ "alloy-consensus", "alloy-dyn-abi", @@ -126,7 +114,8 @@ dependencies = [ "futures", "futures-util", "serde_json", - "thiserror 2.0.18", + "thiserror", + "tracing", ] [[package]] @@ -168,7 +157,7 @@ dependencies = [ "alloy-rlp", "crc", "serde", - "thiserror 2.0.18", + "thiserror", ] [[package]] @@ -193,7 +182,7 @@ dependencies = [ "alloy-rlp", "borsh", "serde", - "thiserror 2.0.18", + "thiserror", ] [[package]] @@ -210,9 +199,9 @@ dependencies = [ [[package]] name = "alloy-eips" -version = "1.7.3" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9f7ef09f21bd1e9cb8a686f168cb4a206646804567f0889eadb8dcc4c9288c8" +checksum = "64c0456f5f7a4497e9342d20f528e30f5288ddfa0d6a012bd5044afee46cd8a0" dependencies = [ "alloy-eip2124", "alloy-eip2930", @@ -229,14 +218,13 @@ dependencies = [ "serde", "serde_with", "sha2", - "thiserror 2.0.18", ] [[package]] name = "alloy-genesis" -version = "1.7.3" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c9cf3b99f46615fbf7dc1add0c96553abb7bf88fc9ec70dfbe7ad0b47ba7fe8" +checksum = "a71ff8b55d2b8aa05259f474cae7dea0e4991724dc18936b81cb23ec492a0c2a" dependencies = [ "alloy-eips", "alloy-primitives", @@ -261,24 +249,24 @@ dependencies = [ [[package]] name = "alloy-json-rpc" -version = "1.7.3" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff42cd777eea61f370c0b10f2648a1c81e0b783066cd7269228aa993afd487f7" +checksum = "19e352478b756bad5d7203148e4b461861282ea2ded3da406ba24868b52cd098" dependencies = [ "alloy-primitives", "alloy-sol-types", "http", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", "tracing", ] [[package]] name = "alloy-network" -version = "1.7.3" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cbca04f9b410fdc51aaaf88433cbac761213905a65fe832058bcf6690585762" +checksum = "ed08ae169869e08370ed121612e0d3dadac33d1a256e9f2465926b23f0bd7d95" dependencies = [ "alloy-consensus", "alloy-consensus-any", @@ -297,14 +285,14 @@ dependencies = [ "futures-utils-wasm", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", ] [[package]] name = "alloy-network-primitives" -version = "1.7.3" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d6d15e069a8b11f56bef2eccbad2a873c6dd4d4c81d04dda29710f5ea52f04" +checksum = "02e6c7ad28afe348a9a9c5624b67ee5b3607b8de98d5816b3056ecdfa6fa2697" dependencies = [ "alloy-consensus", "alloy-eips", @@ -325,6 +313,7 @@ dependencies = [ "const-hex", "derive_more", "foldhash 0.2.0", + "getrandom 0.4.2", "hashbrown 0.16.1", "indexmap 2.13.0", "itoa", @@ -342,9 +331,9 @@ dependencies = [ [[package]] name = "alloy-provider" -version = "1.7.3" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d181c8cc7cf4805d7e589bf4074d56d55064fa1a979f005a45a62b047616d870" +checksum = "93a7c17472b55482d4734154c2f5ed13f72e03f6752cebb927f6a2d8b52e646c" dependencies = [ "alloy-chains", "alloy-consensus", @@ -374,7 +363,7 @@ dependencies = [ "reqwest", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", "url", @@ -383,9 +372,9 @@ dependencies = [ [[package]] name = "alloy-pubsub" -version = "1.7.3" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8bd82953194dec221aa4cbbbb0b1e2df46066fe9d0333ac25b43a311e122d13" +checksum = "a8d86958b02bca85103d64fa60d7b364a8b017c6e40f2b02c3f50ca22964a738" dependencies = [ "alloy-json-rpc", "alloy-primitives", @@ -427,9 +416,9 @@ dependencies = [ [[package]] name = "alloy-rpc-client" -version = "1.7.3" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2792758a93ae32a32e9047c843d536e1448044f78422d71bf7d7c05149e103f" +checksum = "5beb5c2fe6b960c8e8b038e69fd502a90a2e930afa4770efb748b163b0767729" dependencies = [ "alloy-json-rpc", "alloy-primitives", @@ -452,9 +441,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types" -version = "1.7.3" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bdcbf9dfd5eea8bfeb078b1d906da8cd3a39c4d4dbe7a628025648e323611f6" +checksum = "4ee1257a278f6d293e05c5162c5940a1561b1aa85ded0028b464c81de37ebfa5" dependencies = [ "alloy-primitives", "alloy-rpc-types-eth", @@ -464,20 +453,24 @@ dependencies = [ [[package]] name = "alloy-rpc-types-any" -version = "1.7.3" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd720b63f82b457610f2eaaf1f32edf44efffe03ae25d537632e7d23e7929e1a" +checksum = "6a234bfbdf7a76c3d13808f729af5321852de3dedcaa6fc6d5f54787aaf54c6a" dependencies = [ "alloy-consensus-any", + "alloy-network-primitives", + "alloy-primitives", "alloy-rpc-types-eth", "alloy-serde", + "serde", + "serde_json", ] [[package]] name = "alloy-rpc-types-eth" -version = "1.7.3" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2dc411f13092f237d2bf6918caf80977fc2f51485f9b90cb2a2f956912c8c9" +checksum = "56a282daf869eeb7383d3d5c2deb35b0b3fb45ecb329513af4090fc61245ee18" dependencies = [ "alloy-consensus", "alloy-consensus-any", @@ -491,14 +484,14 @@ dependencies = [ "serde", "serde_json", "serde_with", - "thiserror 2.0.18", + "thiserror", ] [[package]] name = "alloy-serde" -version = "1.7.3" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2ce1e0dbf7720eee747700e300c99aac01b1a95bb93f493a01e78ee28bb1a37" +checksum = "a0eada2558e921b39dfcead33c487364df9b31374f5733c1c9d2c891c4529933" dependencies = [ "alloy-primitives", "serde", @@ -507,9 +500,9 @@ dependencies = [ [[package]] name = "alloy-signer" -version = "1.7.3" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2425c6f314522c78e8198979c8cbf6769362be4da381d4152ea8eefce383535d" +checksum = "41eb29f7a8adcd8941fbb8e134022a133e6f8dfd345f2e3b7109599f8a7dca08" dependencies = [ "alloy-primitives", "async-trait", @@ -517,14 +510,14 @@ dependencies = [ "either", "elliptic-curve", "k256", - "thiserror 2.0.18", + "thiserror", ] [[package]] name = "alloy-signer-local" -version = "1.7.3" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3ecb71ee53d8d9c3fa7bac17542c8116ebc7a9726c91b1bf333ec3d04f5a789" +checksum = "bef839e7ce9b59aa60fa9a175e97986c6145c888d643b0f1fb0a3e7b8e56a2e2" dependencies = [ "alloy-consensus", "alloy-network", @@ -533,7 +526,7 @@ dependencies = [ "async-trait", "k256", "rand 0.8.5", - "thiserror 2.0.18", + "thiserror", ] [[package]] @@ -611,9 +604,9 @@ dependencies = [ [[package]] name = "alloy-transport" -version = "1.7.3" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa186e560d523d196580c48bf00f1bf62e63041f28ecf276acc22f8b27bb9f53" +checksum = "3ac7a80c0bac3e44559d53d002e34c461dc2f23262b42cafec019bc70551abbe" dependencies = [ "alloy-json-rpc", "auto_impl", @@ -624,7 +617,7 @@ dependencies = [ "parking_lot", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", "tokio", "tower", "tracing", @@ -634,9 +627,9 @@ dependencies = [ [[package]] name = "alloy-transport-http" -version = "1.7.3" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa501ad58dd20acddbfebc65b52e60f05ebf97c52fa40d1b35e91f5e2da0ad0e" +checksum = "eed3ed3300a998f88639ed619fdbbd88bd82865e00c6a8ecb796c99eb12358f6" dependencies = [ "alloy-json-rpc", "alloy-transport", @@ -650,18 +643,20 @@ dependencies = [ [[package]] name = "alloy-transport-ws" -version = "1.7.3" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9f00445db69d63298e2b00a0ea1d859f00e6424a3144ffc5eba9c31da995e16" +checksum = "0e3bff84b2b2a46eb34cc522dc3f889a2867c70be90a377421429b662b3ec4ce" dependencies = [ "alloy-pubsub", "alloy-transport", "futures", "http", + "rustls", "serde_json", "tokio", "tokio-tungstenite", "tracing", + "url", "ws_stream_wasm", ] @@ -677,17 +672,17 @@ dependencies = [ "nybbles", "serde", "smallvec", - "thiserror 2.0.18", + "thiserror", "tracing", ] [[package]] name = "alloy-tx-macros" -version = "1.7.3" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fa0c53e8c1e1ef4d01066b01c737fb62fc9397ab52c6e7bb5669f97d281b9bc" +checksum = "99fce0350197dcd4ba4e9a7dd43915d908c0eb0e7352755791709a705e1c76b6" dependencies = [ - "darling 0.21.3", + "darling", "proc-macro2", "quote", "syn 2.0.117", @@ -953,6 +948,16 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "async-stream" version = "0.3.6" @@ -1022,9 +1027,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-lc-rs" -version = "1.16.1" +version = "1.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94bffc006df10ac2a68c83692d734a465f8ee6c5b384d8545a636f81d858f4bf" +checksum = "0ec6fb3fe69024a75fa7e1bfb48aa6cf59706a101658ea01bfd33b2b248a038f" dependencies = [ "aws-lc-sys", "zeroize", @@ -1032,9 +1037,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.38.0" +version = "0.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4321e568ed89bb5a7d291a7f37997c2c0df89809d7b6d12062c81ddb54aa782e" +checksum = "f50037ee5e1e41e7b8f9d161680a725bd1626cb6f8c7e901f91f942850852fe7" dependencies = [ "cc", "cmake", @@ -1044,9 +1049,9 @@ dependencies = [ [[package]] name = "axum" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" dependencies = [ "axum-core", "bytes", @@ -1116,9 +1121,11 @@ dependencies = [ "futures", "metrics", "metrics-exporter-prometheus", + "reqwest", "rustls", "serde", - "thiserror 2.0.18", + "serde_json", + "thiserror", "tokio", "tokio-stream", "tokio-util", @@ -1126,6 +1133,7 @@ dependencies = [ "tracing", "tracing-subscriber", "uuid", + "wiremock", ] [[package]] @@ -1325,9 +1333,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.0" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", @@ -1347,9 +1355,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.0" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ "heck", "proc-macro2", @@ -1378,6 +1386,16 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "const-hex" version = "1.18.1" @@ -1425,6 +1443,16 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -1508,39 +1536,14 @@ dependencies = [ "typenum", ] -[[package]] -name = "darling" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" -dependencies = [ - "darling_core 0.21.3", - "darling_macro 0.21.3", -] - [[package]] name = "darling" version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" dependencies = [ - "darling_core 0.23.0", - "darling_macro 0.23.0", -] - -[[package]] -name = "darling_core" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "serde", - "strsim", - "syn 2.0.117", + "darling_core", + "darling_macro", ] [[package]] @@ -1552,28 +1555,18 @@ dependencies = [ "ident_case", "proc-macro2", "quote", + "serde", "strsim", "syn 2.0.117", ] -[[package]] -name = "darling_macro" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" -dependencies = [ - "darling_core 0.21.3", - "quote", - "syn 2.0.117", -] - [[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", + "darling_core", "quote", "syn 2.0.117", ] @@ -1598,6 +1591,24 @@ version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" +[[package]] +name = "deadpool" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0be2b1d1d6ec8d846f05e137292d0b89133caf95ef33695424c09568bdd39b1b" +dependencies = [ + "deadpool-runtime", + "lazy_static", + "num_cpus", + "tokio", +] + +[[package]] +name = "deadpool-runtime" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" + [[package]] name = "der" version = "0.7.10" @@ -1752,6 +1763,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "enum-ordinalize" version = "4.3.2" @@ -1788,6 +1808,17 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "evmap" +version = "11.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8874945f036109c72242964c1174cf99434e30cfa45bf45fedc983f50046f8" +dependencies = [ + "hashbag", + "left-right", + "smallvec", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -1977,6 +2008,21 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42012b0f064e01aa58b545fe3727f90f7dd4020f4a3ea735b50344965f5a57e9" +[[package]] +name = "generator" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f04ae4152da20c76fe800fa48659201d5cf627c5149ca0b707b69d7eef6cf9" +dependencies = [ + "cc", + "cfg-if", + "libc", + "log", + "rustversion", + "windows-link", + "windows-result", +] + [[package]] name = "generic-array" version = "0.14.9" @@ -2076,6 +2122,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "hashbag" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7040a10f52cba493ddb09926e15d10a9d8a28043708a405931fe4c6f19fac064" + [[package]] name = "hashbrown" version = "0.12.3" @@ -2229,7 +2281,6 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots 1.0.6", ] [[package]] @@ -2250,9 +2301,11 @@ dependencies = [ "percent-encoding", "pin-project-lite", "socket2", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] @@ -2491,6 +2544,55 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys", + "log", + "simd_cesu8", + "thiserror", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version 0.4.1", + "simd_cesu8", + "syn 2.0.117", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.117", +] + [[package]] name = "jobserver" version = "0.1.34" @@ -2556,6 +2658,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" +[[package]] +name = "left-right" +version = "0.11.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f0c21e4c8ff95f487fb34e6f9182875f42c84cef966d29216bf115d9bba835a" +dependencies = [ + "crossbeam-utils", + "loom", + "slab", +] + [[package]] name = "libc" version = "0.2.183" @@ -2595,6 +2708,19 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "loom" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "tracing", + "tracing-subscriber", +] + [[package]] name = "lru" version = "0.16.3" @@ -2644,21 +2770,22 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "metrics" -version = "0.24.3" +version = "0.24.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d5312e9ba3771cfa961b585728215e3d972c950a3eed9252aa093d6301277e8" +checksum = "ff56c2e7dce6bd462e3b8919986a617027481b1dcc703175b58cf9dd98a2f071" dependencies = [ - "ahash", "portable-atomic", + "rapidhash", ] [[package]] name = "metrics-exporter-prometheus" -version = "0.16.2" +version = "0.18.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd7399781913e5393588a8d8c6a2867bf85fb38eaf2502fdce465aad2dc6f034" +checksum = "1db0d8f1fc9e62caebd0319e11eaec5822b0186c171568f0480b46a0137f9108" dependencies = [ "base64", + "evmap", "http-body-util", "hyper", "hyper-rustls", @@ -2668,24 +2795,26 @@ dependencies = [ "metrics", "metrics-util", "quanta", - "thiserror 1.0.69", + "rustls", + "thiserror", "tokio", "tracing", ] [[package]] name = "metrics-util" -version = "0.19.1" +version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8496cc523d1f94c1385dd8f0f0c2c480b2b8aeccb5b7e4485ad6365523ae376" +checksum = "9e56997f084e57b045edf17c3ed8ba7f9f779c670df8206dfd1c736f4c02dc4a" dependencies = [ "crossbeam-epoch", "crossbeam-utils", - "hashbrown 0.15.5", + "hashbrown 0.16.1", "metrics", "quanta", "rand 0.9.2", "rand_xoshiro", + "rapidhash", "sketches-ddsketch", ] @@ -2697,9 +2826,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "mio" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", "wasi", @@ -3083,7 +3212,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", "web-time", @@ -3095,6 +3224,7 @@ 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", @@ -3104,7 +3234,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.18", + "thiserror", "tinyvec", "tracing", "web-time", @@ -3278,6 +3408,18 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + [[package]] name = "regex-automata" version = "0.4.14" @@ -3297,13 +3439,15 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "reqwest" -version = "0.12.28" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" dependencies = [ "base64", "bytes", + "encoding_rs", "futures-core", + "h2", "http", "http-body", "http-body-util", @@ -3312,14 +3456,15 @@ dependencies = [ "hyper-util", "js-sys", "log", + "mime", "percent-encoding", "pin-project-lite", "quinn", "rustls", "rustls-pki-types", + "rustls-platform-verifier", "serde", "serde_json", - "serde_urlencoded", "sync_wrapper", "tokio", "tokio-rustls", @@ -3330,7 +3475,6 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 1.0.6", ] [[package]] @@ -3446,9 +3590,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.37" +version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ "aws-lc-rs", "once_cell", @@ -3481,11 +3625,38 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-platform-verifier" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + [[package]] name = "rustls-webpki" -version = "0.103.9" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ "aws-lc-rs", "ring", @@ -3517,6 +3688,15 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "schannel" version = "0.1.29" @@ -3550,6 +3730,12 @@ dependencies = [ "serde_json", ] +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" version = "1.2.0" @@ -3599,7 +3785,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ "bitflags", - "core-foundation", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -3736,7 +3922,7 @@ version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" dependencies = [ - "darling 0.23.0", + "darling", "proc-macro2", "quote", "syn 2.0.117", @@ -3829,6 +4015,22 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version 0.4.1", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "sketches-ddsketch" version = "0.3.1" @@ -3969,6 +4171,27 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tap" version = "1.0.1" @@ -3988,33 +4211,13 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - [[package]] name = "thiserror" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.18", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", + "thiserror-impl", ] [[package]] @@ -4104,9 +4307,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.50.0" +version = "1.52.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" dependencies = [ "bytes", "libc", @@ -4121,9 +4324,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.6.1" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", @@ -4154,9 +4357,9 @@ dependencies = [ [[package]] name = "tokio-tungstenite" -version = "0.26.2" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" +checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857" dependencies = [ "futures-util", "log", @@ -4340,9 +4543,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "tungstenite" -version = "0.26.2" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" +checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" dependencies = [ "bytes", "data-encoding", @@ -4353,7 +4556,7 @@ dependencies = [ "rustls", "rustls-pki-types", "sha1", - "thiserror 2.0.18", + "thiserror", "utf-8", ] @@ -4444,9 +4647,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.22.0" +version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" dependencies = [ "getrandom 0.4.2", "js-sys", @@ -4474,6 +4677,16 @@ dependencies = [ "libc", ] +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" @@ -4634,6 +4847,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-root-certs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webpki-roots" version = "0.26.11" @@ -4668,6 +4890,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -4715,6 +4946,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + [[package]] name = "windows-result" version = "0.4.1" @@ -4898,6 +5140,29 @@ dependencies = [ "memchr", ] +[[package]] +name = "wiremock" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08db1edfb05d9b3c1542e521aea074442088292f00b5f28e435c714a98f85031" +dependencies = [ + "assert-json-diff", + "base64", + "deadpool", + "futures", + "http", + "http-body-util", + "hyper", + "hyper-util", + "log", + "once_cell", + "regex", + "serde", + "serde_json", + "tokio", + "url", +] + [[package]] name = "wit-bindgen" version = "0.51.0" @@ -5005,7 +5270,7 @@ dependencies = [ "pharos", "rustc_version 0.4.1", "send_wrapper", - "thiserror 2.0.18", + "thiserror", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", diff --git a/Cargo.toml b/Cargo.toml index 92adb7d..ad6383c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,21 +4,26 @@ version = "0.1.0" edition = "2021" [dependencies] -axum = "0.8.6" -tokio = { version = "1.48.0", features = ["full"] } +axum = "0.8.9" +reqwest = { version = "0.13.3", features = ["json"] } +tokio = { version = "1.52.1", features = ["full"] } serde = { version = "1.0", features = ["derive"] } futures = "0.3" tokio-stream = "0.1" -clap = { version = "4.5.51", features = ["env", "derive"] } -alloy = { version = "1.7.3", features = ["provider-ws"] } -thiserror = "2.0.17" +clap = { version = "4.6.1", features = ["env", "derive"] } +alloy = { version = "2.0.4", features = ["provider-ws", "getrandom"] } +thiserror = "2.0.18" tracing = "0.1.43" tracing-subscriber = { version = "0.3.22", features = ["json", "env-filter"] } tokio-util = { version = "0.7.17" } tower-http = { version = "0.6", features = ["cors"] } -metrics = "0.24" -metrics-exporter-prometheus = "0.16" -rustls = { version = "0.23", features = ["ring"], default-features = false } +metrics = "0.24.5" +metrics-exporter-prometheus = "0.18.3" +rustls = { version = "0.23.40", features = ["ring"], default-features = false } backon = "1" -uuid = { version = "1.22.0", features = ["v4"] } \ No newline at end of file +uuid = { version = "1.23.1", features = ["v4"] } + +[dev-dependencies] +wiremock = "0.6.5" +serde_json="1.0.149" diff --git a/src/config/back_off_config.rs b/src/config/back_off_config.rs new file mode 100644 index 0000000..38349de --- /dev/null +++ b/src/config/back_off_config.rs @@ -0,0 +1,14 @@ +use backon::ExponentialBuilder; +use std::time::Duration; + +const TOKEN_LIST_FETCHER_BACKOFFS: usize = 3; + +// token list fetcher backoff configuration +// needed to handle errors when load token list +pub fn get_token_list_fetcher_backoff() -> ExponentialBuilder { + ExponentialBuilder::default() + .with_min_delay(Duration::from_secs(1)) + .with_max_delay(Duration::from_secs(3)) + .with_max_times(TOKEN_LIST_FETCHER_BACKOFFS) + .with_jitter() +} diff --git a/src/config/constants.rs b/src/config/constants.rs index 40a4e4f..9bc196f 100644 --- a/src/config/constants.rs +++ b/src/config/constants.rs @@ -1,8 +1,5 @@ use std::time::Duration; -/// Maximum number of concurrent HTTP requests when fetching token lists -pub const TOKEN_FETCH_CONCURRENCY: usize = 5; - /// Capacity of the broadcast channel for balance events per subscription pub const BROADCAST_CHANNEL_CAPACITY: usize = 256; diff --git a/src/config/mod.rs b/src/config/mod.rs index 6afaa37..e498ce2 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,2 +1,3 @@ +pub mod back_off_config; pub mod constants; pub mod network_config; diff --git a/src/domain/token.rs b/src/domain/token.rs index c76b71e..a19606f 100644 --- a/src/domain/token.rs +++ b/src/domain/token.rs @@ -5,7 +5,5 @@ use serde::{Deserialize, Serialize}; #[serde(rename_all = "camelCase")] pub struct Token { pub address: Address, - pub name: String, - pub decimals: u8, pub chain_id: u64, } diff --git a/src/services/session_manager.rs b/src/services/session_manager.rs index 72f591d..8805a71 100644 --- a/src/services/session_manager.rs +++ b/src/services/session_manager.rs @@ -1,3 +1,4 @@ +use crate::config::back_off_config::get_token_list_fetcher_backoff; use crate::domain::{BalanceEvent, EvmNetwork, Session}; use crate::services::balance_fetcher::BalanceFetcher; use crate::services::cleanup_stream; @@ -19,9 +20,11 @@ use serde::Serialize; use std::collections::{HashMap, HashSet}; use std::convert::Infallible; use std::sync::Arc; -use std::time::Instant; +use std::time::{Duration, Instant}; use tokio_stream::wrappers::BroadcastStream; +const TOKEN_LIST_CACHE_TTL: Duration = Duration::from_hours(5); + // handle subscriptions: fetch token lists, spawn watchers, update watched tokens pub struct SessionManager { sub_manager: Arc, @@ -95,7 +98,8 @@ impl SessionManager { snapshot_interval: usize, token_limit: usize, ) -> Self { - let token_list_fetcher = TokenListFetcher::new(); + let token_list_fetcher = + TokenListFetcher::new(TOKEN_LIST_CACHE_TTL, get_token_list_fetcher_backoff()); let sub_manager = Arc::new(SubscriptionManager::new()); Arc::clone(&sub_manager).spawn_cleanup(); diff --git a/src/services/token_list_fetcher.rs b/src/services/token_list_fetcher.rs index 77fdc2e..8e1ad24 100644 --- a/src/services/token_list_fetcher.rs +++ b/src/services/token_list_fetcher.rs @@ -1,8 +1,9 @@ -use alloy::transports::http::reqwest::Response; -use alloy::{primitives::Address, transports::http::Client}; +use alloy::primitives::Address; +use alloy::transports::BoxFuture; use backon::{ExponentialBuilder, Retryable}; -use futures::{stream, StreamExt}; +use futures::future::{try_join_all, FutureExt, Shared}; use metrics::{counter, histogram}; +use reqwest::{Client, Response}; use serde::Deserialize; use std::sync::Arc; use std::{ @@ -12,12 +13,13 @@ use std::{ use tokio::sync::{Mutex, RwLock}; use crate::{ - config::constants::TOKEN_FETCH_CONCURRENCY, domain::{EvmNetwork, Token}, services::errors::FetcherError, }; -const CACHE_TTL: Duration = Duration::from_secs(3600 * 5); // 5 hours +type SharedFetchTask = Shared>>; + +type ListUrl = String; // need to load token lists and save them to cache struct CachedTokenList { @@ -27,10 +29,17 @@ struct CachedTokenList { } pub struct TokenListFetcher { - cache: RwLock>, - locks: Mutex>>>, + // store cached lists by url + cache: RwLock>, + // store already fetched futures, need to share them between few requests with the same token lists + // to not load them, if they are already in flight + fetch_tasks: Mutex>, + // http client client: Client, + // cache duration ttl: Duration, + // backoff configuration + backoff_cfg: ExponentialBuilder, } #[derive(Debug, Deserialize)] @@ -39,172 +48,142 @@ struct ApiResponse { } impl TokenListFetcher { - pub fn new() -> Self { + pub fn new(cache_ttl: Duration, backoff_cfg: ExponentialBuilder) -> Self { Self { cache: RwLock::new(HashMap::new()), client: Client::new(), - ttl: CACHE_TTL, - locks: Mutex::new(HashMap::new()), + ttl: cache_ttl, + fetch_tasks: Mutex::new(HashMap::new()), + backoff_cfg, } } + // this function incapsulate fetching and cache logic + // if the list was cached and ttl is valid - return cache result per url + // otherwise - share or create a new future(if it doesnt exist) to fetch the list and share the result pub async fn get_tokens( - &self, + self: Arc, urls: &[String], network: EvmNetwork, ) -> Result, FetcherError> { - let mut normalized_urls: Vec = urls.to_owned(); - // sort urls to have the same order during all requests to not get deadlock - normalized_urls.sort(); - // remove possible duplicates for the same reason - normalized_urls.dedup(); - - self.fetch_with_locks(&normalized_urls).await?; + Arc::clone(&self).fetch_uncached(urls).await?; + let tokens = self.get_cached(urls, network).await; + Ok(tokens) + } - let cached = self.collect_from_cache(&normalized_urls, network).await; - Ok(cached) + // get cached token lists and filter them by network + async fn get_cached(&self, urls: &[String], network: EvmNetwork) -> HashSet
{ + let cache = self.cache.read().await; + urls.iter() + .filter_map(|url| cache.get(url)) + .filter_map(|cached_by_chain_id| cached_by_chain_id.list.get(&network.chain_id())) + .flatten() + .copied() + .collect() } - async fn fetch_with_locks(&self, normalized_urls: &[String]) -> Result<(), FetcherError> { - // create locks per each url if needed and gather them - let t0 = Instant::now(); - tracing::info!("fetch_with_locks: acquiring url lock map"); - let arcs: Vec<_> = { - let mut locks = self.locks.lock().await; - normalized_urls - .iter() - .map(|url| { - locks - .entry(url.clone()) - .or_insert_with(|| Arc::new(Mutex::new(()))) - .clone() - }) - .collect() - }; - let elapsed = t0.elapsed().as_millis() as f64; - histogram!("token_fetch_lock_map_ms").record(elapsed); - tracing::info!(time_ms = elapsed, "fetch_with_locks: url lock map acquired"); + async fn fetch_uncached(self: Arc, urls: &[String]) -> Result<(), FetcherError> { + let uncached = self.get_uncached(urls).await; - let t0 = Instant::now(); - tracing::info!( - count = arcs.len(), - "fetch_with_locks: acquiring per-url locks" - ); - let mut guards = Vec::with_capacity(arcs.len()); - for arc in arcs { - // lock url - guards.push(arc.lock_owned().await); - } - let elapsed = t0.elapsed().as_millis() as f64; - histogram!("token_fetch_per_url_locks_ms").record(elapsed); - tracing::info!( - time_ms = elapsed, - "fetch_with_locks: per-url locks acquired" - ); - - let uncached: Vec = { - let cache = self.cache.read().await; - normalized_urls - .iter() - .filter(|url| { - cache - .get(*url) - .map(|c| c.fetched_at.elapsed() > self.ttl) - .unwrap_or(true) - }) - .cloned() - .collect() - }; - - if !uncached.is_empty() { - let t0 = Instant::now(); - tracing::info!( - count = uncached.len(), - "fetch_with_locks: fetching uncached lists" - ); - self.fetch_and_cache(&uncached).await?; - let elapsed = t0.elapsed().as_millis() as f64; - histogram!("token_fetch_uncached_ms").record(elapsed); - tracing::info!( - time_ms = elapsed, - "fetch_with_locks: uncached lists fetched" - ); - } else { - tracing::info!("fetch_with_locks: all lists cached"); - } + let fetch_tasks_iter = uncached.into_iter().map(|url| { + let this = Arc::clone(&self); + this.fetch_and_update_cache(url) + }); + try_join_all(fetch_tasks_iter).await?; Ok(()) } - async fn fetch_and_cache(&self, urls: &[String]) -> Result<(), FetcherError> { - let t0 = Instant::now(); - // make request parallel - let result: Vec<(String, Result)> = - stream::iter(urls.iter().cloned()) - .map(move |url| { - let client = self.client.clone(); - async move { - let response = Self::fetch_list(&client, &url).await; - (url, response) - } - }) - .buffer_unordered(TOKEN_FETCH_CONCURRENCY) - .collect() - .await; - tracing::info!( - time_ms = t0.elapsed().as_millis(), - count = result.len(), - "all tokens lists loaded" - ); - - for (_, response) in &result { - if let Err(err) = response { - return Err(err.clone()); - } - } + // check if urls were already cached + // returns a list of uncached urls (if the url is not in the cache or the cache is invalid) + async fn get_uncached<'a>(&self, urls: &'a [String]) -> Vec<&'a String> { + let cache = self.cache.read().await; - let mut mapped_by_url: HashMap>> = HashMap::new(); - for (url, response) in result { - if let Ok(api_resp) = response { - let mut map_by_chain: HashMap> = HashMap::new(); + urls.iter() + .filter(|url| { + cache + .get(*url) + .is_none_or(|cached| cached.fetched_at.elapsed() > self.ttl) + }) + .collect() + } - for token in api_resp.tokens { - map_by_chain - .entry(token.chain_id) - .or_default() - .insert(token.address); - } + // check if the future to fetch token list was already created + // if it was - just clone it, otherwise - create a new one + // new future fetch data and store it in cache directly to avoid deduplication + async fn fetch_and_update_cache(self: Arc, url: &String) -> Result<(), FetcherError> { + let fetch_future = { + let mut fetch_guard = self.fetch_tasks.lock().await; + if let Some(future) = fetch_guard.get(url) { + future.clone() + } else { + let client = self.client.clone(); + let url_cloned = url.clone(); + let this = Arc::clone(&self); + + let new_future = async move { + // fetch data and update cache + let response = Arc::clone(&this).fetch_list(&client, &url_cloned).await?; - if !map_by_chain.is_empty() { - mapped_by_url.insert(url, map_by_chain); + this.store_response_in_cache(url_cloned, response).await; + + Ok(()) } + .boxed() + .shared(); + + fetch_guard.insert(url.clone(), new_future.clone()); + new_future + } + }; + + let result = fetch_future.await; + self.remove_fetch_task_if_resolved(url).await; + + result + } + + // check if the fetch task was already resolved + // if it was - removed it, otherwise - does nothing + // it's needed to protect removing task from new caller that wasn't resolved yet + async fn remove_fetch_task_if_resolved(&self, url: &String) { + let mut fetch_tasks_guard = self.fetch_tasks.lock().await; + if let Some(task) = fetch_tasks_guard.get(url) { + if task.peek().is_some() { + fetch_tasks_guard.remove(url); } } + } - let loaded_urls: Vec<&String> = mapped_by_url.keys().collect(); - tracing::info!(lists = ?loaded_urls, "token lists loaded"); - - let mut cache = self.cache.write().await; - for (url, result) in mapped_by_url { - cache.insert( - url, - CachedTokenList { - fetched_at: Instant::now(), - list: result, - }, - ); + // map response into CachedTokenList and save + async fn store_response_in_cache(&self, url: String, response: ApiResponse) { + let mut mapped_by_chain_id: HashMap> = HashMap::new(); + for token in response.tokens { + mapped_by_chain_id + .entry(token.chain_id) + .or_default() + .insert(token.address); } - Ok(()) + let cached = CachedTokenList { + list: mapped_by_chain_id, + fetched_at: Instant::now(), + }; + + self.cache.write().await.insert(url, cached); } - async fn fetch_list(client: &Client, url: &String) -> Result { + async fn fetch_list( + self: Arc, + client: &Client, + url: &String, + ) -> Result { let t0 = Instant::now(); - Self::fetch_with_backoff(client, url) + self.fetch_with_backoff(client, url) .await .inspect(move |_| { - counter!("token_list_load_total").increment(1); + counter!("token_list_loaded_total").increment(1); histogram!("token_list_loaded_time_in_ms").record(t0.elapsed().as_millis() as f64); tracing::info!( time_ms = ?t0.elapsed().as_millis(), @@ -217,10 +196,13 @@ impl TokenListFetcher { .map_err(|err| FetcherError::UnableToLoadList(url.clone(), err.to_string())) } - async fn fetch_with_backoff(client: &Client, url: &String) -> Result { - let backoff = Self::get_backoff(); - let resp = (|| async { client.get(url).send().await }) - .retry(backoff) + async fn fetch_with_backoff( + &self, + client: &Client, + url: &String, + ) -> Result { + let resp = (|| async { client.get(url).send().await?.error_for_status() }) + .retry(self.backoff_cfg) .await .map_err(|err| { counter!("token_list_load_failed_total").increment(1); @@ -229,27 +211,187 @@ impl TokenListFetcher { Ok(resp) } +} + +#[cfg(test)] +mod token_list_fetcher_tests { + use std::thread::sleep; + + use super::*; + use wiremock::matchers::method; + use wiremock::{Mock, MockServer, ResponseTemplate}; + + const BACK_OFFS: u64 = 3; + + fn make_token_list_resp_template(tokens: Vec<(u64, Address)>) -> ResponseTemplate { + let token_list = serde_json::json!({ + "tokens": tokens.iter().map(|(chain_id, address)| { + serde_json::json!({ "chainId": chain_id, "address": address }) + }).collect::>() + }); + + ResponseTemplate::new(200).set_body_json(token_list) + } - fn get_backoff() -> ExponentialBuilder { - ExponentialBuilder::default() - .with_min_delay(Duration::from_secs(1)) - .with_max_delay(Duration::from_secs(3)) - .with_max_times(3) - .with_jitter() + fn make_token_list(chain_ids: Vec, len: usize) -> Vec<(u64, Address)> { + chain_ids + .into_iter() + .flat_map(|chain_id| (0..len).map(move |_| (chain_id, Address::random()))) + .collect() } - async fn collect_from_cache(&self, urls: &[String], network: EvmNetwork) -> HashSet
{ - let cached_lists = self.cache.read().await; + fn make_error_resp_template() -> ResponseTemplate { + let error = serde_json::json!({ + "message": "unavailable" + }); - let mut result: HashSet
= HashSet::new(); - for url in urls { - if let Some(cached) = cached_lists.get(url) { - if let Some(cached_by_chain) = cached.list.get(&network.chain_id()) { - result.extend(cached_by_chain.iter().cloned()); + ResponseTemplate::new(500).set_body_json(error) + } + + fn make_fetcher() -> Arc { + let back_off = ExponentialBuilder::default() + .with_min_delay(Duration::from_millis(1)) + .with_max_delay(Duration::from_millis(20)) + .with_max_times(BACK_OFFS as usize) + .with_jitter(); + + Arc::new(TokenListFetcher::new(Duration::from_millis(300), back_off)) + } + + #[tokio::test] + async fn test_fail_backoffs() { + let server = MockServer::start().await; + + let resp_template = make_error_resp_template(); + let retries = BACK_OFFS + 1; + Mock::given(method("GET")) + .respond_with(resp_template) + .expect(retries) + .mount(&server) + .await; + + let fetcher = make_fetcher(); + let result = Arc::clone(&fetcher) + .get_tokens(&[server.uri()], EvmNetwork::Eth) + .await; + + assert!(result.is_err()); + } + + #[tokio::test] + async fn test_fail_and_success_after() { + let server = MockServer::start().await; + + // fail case + let resp_template = make_error_resp_template(); + let retries = BACK_OFFS + 1; + Mock::given(method("GET")) + .respond_with(resp_template) + .up_to_n_times(retries) + .with_priority(1) + .mount(&server) + .await; + + let fetcher = make_fetcher(); + let result = Arc::clone(&fetcher) + .get_tokens(&[server.uri()], EvmNetwork::Eth) + .await; + + assert!(result.is_err()); + + // success after if there is a new client + let resp_template = make_token_list_resp_template(vec![]); + Mock::given(method("GET")) + .respond_with(resp_template) + .with_priority(2) + .expect(1) + .mount(&server) + .await; + + let response = Arc::clone(&fetcher) + .get_tokens(&[server.uri()], EvmNetwork::Eth) + .await; + assert!(response.is_ok()); + } + + #[tokio::test] + async fn test_cache() { + let server = MockServer::start().await; + let token_list = make_token_list(vec![1, 2, 100], 3); + let network = EvmNetwork::Eth; + let expected_list_by_chain: HashSet<_> = token_list + .clone() + .into_iter() + .filter_map(|(chain_id, address)| { + if chain_id == network.chain_id() { + Some(address) + } else { + None } - } - } + }) + .collect(); - result + let resp_template = make_token_list_resp_template(token_list.clone()); + Mock::given(method("GET")) + .respond_with(resp_template) + .expect(2) + .mount(&server) + .await; + + let fetcher = make_fetcher(); + // warm up cache + let _ = Arc::clone(&fetcher) + .get_tokens(&[server.uri()], EvmNetwork::Gnosis) + .await + .unwrap(); + + // cache is still valid + sleep(Duration::from_millis(100)); + + let tokens = Arc::clone(&fetcher) + .get_tokens(&[server.uri()], network) + .await + .unwrap(); + + assert_eq!(expected_list_by_chain, tokens); + + // invalidate cache + sleep(Duration::from_millis(200)); + + let tokens = Arc::clone(&fetcher) + .get_tokens(&[server.uri()], network) + .await + .unwrap(); + + assert_eq!(expected_list_by_chain, tokens); + } + + #[tokio::test] + async fn test_concurrent_request_deduplication() { + let server = MockServer::start().await; + let token_list = make_token_list(vec![1, 2, 100], 3); + let network = EvmNetwork::Eth; + + let resp_template = make_token_list_resp_template(token_list.clone()); + Mock::given(method("GET")) + .respond_with(resp_template) + .expect(1) + .mount(&server) + .await; + + let fetcher = make_fetcher(); + + let handlers: Vec<_> = (0..10) + .map(|_| { + let urls = [server.uri()]; + let fetcher = Arc::clone(&fetcher); + tokio::spawn(async move { fetcher.get_tokens(&urls, network).await }) + }) + .collect(); + + for handler in handlers { + let result = handler.await; + assert!(result.is_ok()); + } } }