From bf5285f9179bab9e4bd4f1b8e749f37c91960b71 Mon Sep 17 00:00:00 2001 From: Bengineer Date: Thu, 2 Apr 2026 14:48:15 +0100 Subject: [PATCH 1/7] feat(pathfinder): add pathfinding utilities and SQLite integration - Introduced a new crate `pathfinder` for pathfinding utilities in the Torii project. - Updated `Cargo.toml` to include the new `pathfinder` crate and its dependencies. - Implemented event decoding and compression in `decoding.rs`. - Added error handling with custom error types in `error.rs`. - Created an extractor for fetching events from the database in `extractor.rs`. - Developed a fetcher for retrieving emitted events and block contexts in `fetcher.rs`. - Defined SQLite data structures and extension methods for database interactions in `sqlite.rs`. - Added utility functions for database connection in `utils.rs`. - Implemented tests for emitted events retrieval in `test.rs`. - Included binary dictionaries for event compression in `assets`. --- Cargo.lock | 608 ++++++++++++++-------- Cargo.toml | 8 +- crates/pathfinder/Cargo.toml | 27 + crates/pathfinder/src/assets/events.zdict | Bin 0 -> 112640 bytes crates/pathfinder/src/assets/txs.zdict | Bin 0 -> 112640 bytes crates/pathfinder/src/decoding.rs | 151 ++++++ crates/pathfinder/src/error.rs | 38 ++ crates/pathfinder/src/extractor.rs | 68 +++ crates/pathfinder/src/fetcher.rs | 178 +++++++ crates/pathfinder/src/lib.rs | 15 + crates/pathfinder/src/sqlite.rs | 123 +++++ crates/pathfinder/src/test.rs | 20 + crates/pathfinder/src/utils.rs | 10 + 13 files changed, 1039 insertions(+), 207 deletions(-) create mode 100644 crates/pathfinder/Cargo.toml create mode 100644 crates/pathfinder/src/assets/events.zdict create mode 100644 crates/pathfinder/src/assets/txs.zdict create mode 100644 crates/pathfinder/src/decoding.rs create mode 100644 crates/pathfinder/src/error.rs create mode 100644 crates/pathfinder/src/extractor.rs create mode 100644 crates/pathfinder/src/fetcher.rs create mode 100644 crates/pathfinder/src/lib.rs create mode 100644 crates/pathfinder/src/sqlite.rs create mode 100644 crates/pathfinder/src/test.rs create mode 100644 crates/pathfinder/src/utils.rs diff --git a/Cargo.lock b/Cargo.lock index 528244c8..1ec75777 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -25,7 +25,7 @@ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher", - "cpufeatures", + "cpufeatures 0.2.17", ] [[package]] @@ -79,9 +79,9 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -94,15 +94,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -212,9 +212,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-lc-rs" -version = "1.16.1" +version = "1.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94bffc006df10ac2a68c83692d734a465f8ee6c5b384d8545a636f81d858f4bf" +checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc" dependencies = [ "aws-lc-sys", "zeroize", @@ -222,9 +222,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.38.0" +version = "0.39.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4321e568ed89bb5a7d291a7f37997c2c0df89809d7b6d12062c81ddb54aa782e" +checksum = "83a25cf98105baa966497416dbd42565ce3a8cf8dbfd59803ec9ad46f3126399" dependencies = [ "cc", "cmake", @@ -336,6 +336,26 @@ dependencies = [ "num-traits", ] +[[package]] +name = "bincode" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" +dependencies = [ + "bincode_derive", + "serde", + "unty", +] + +[[package]] +name = "bincode_derive" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09" +dependencies = [ + "virtue", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -369,21 +389,21 @@ version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" dependencies = [ - "digest", + "digest 0.10.7", ] [[package]] name = "blake3" -version = "1.8.3" +version = "1.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2468ef7d57b3fb7e16b576e8377cdbde2320c60e1491e961d11da40fc4f02a2d" +checksum = "4d2d5991425dfd0785aed03aedcf0b321d61975c9b5b3689c774a2610ae0b51e" dependencies = [ "arrayref", "arrayvec", "cc", "cfg-if", "constant_time_eq", - "cpufeatures", + "cpufeatures 0.3.0", ] [[package]] @@ -395,6 +415,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +dependencies = [ + "hybrid-array", +] + [[package]] name = "bumpalo" version = "3.20.2" @@ -446,9 +475,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.56" +version = "1.2.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1" dependencies = [ "find-msvc-tools", "jobserver", @@ -468,6 +497,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.0", +] + [[package]] name = "chrono" version = "0.4.44" @@ -515,15 +555,15 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "crypto-common", + "crypto-common 0.1.7", "inout", ] [[package]] name = "clap" -version = "4.5.60" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" dependencies = [ "clap_builder", "clap_derive", @@ -531,9 +571,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.60" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -543,9 +583,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.55" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" dependencies = [ "heck", "proc-macro2", @@ -555,24 +595,30 @@ dependencies = [ [[package]] name = "clap_lex" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "cmake" -version = "0.1.57" +version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" dependencies = [ "cc", ] +[[package]] +name = "cmov" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de0758edba32d61d1fd9f4d69491b47604b91ee2f7e6b33de7e54ca4ebe55dc3" + [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "concurrent-queue" @@ -589,6 +635,12 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "const_format" version = "0.2.35" @@ -668,6 +720,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc" version = "3.4.0" @@ -791,6 +852,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" +dependencies = [ + "hybrid-array", +] + [[package]] name = "ctr" version = "0.9.2" @@ -800,11 +870,20 @@ dependencies = [ "cipher", ] +[[package]] +name = "ctutils" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1005a6d4446f5120ef475ad3d2af2b30c49c2c9c6904258e3bb30219bebed5e4" +dependencies = [ + "cmov", +] + [[package]] name = "darling" -version = "0.21.3" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" dependencies = [ "darling_core", "darling_macro", @@ -812,11 +891,10 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.21.3" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" dependencies = [ - "fnv", "ident_case", "proc-macro2", "quote", @@ -826,9 +904,9 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.21.3" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core", "quote", @@ -847,7 +925,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" dependencies = [ - "uuid 1.22.0", + "uuid 1.23.0", ] [[package]] @@ -856,7 +934,7 @@ version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ - "const-oid", + "const-oid 0.9.6", "pem-rfc7468", "zeroize", ] @@ -877,12 +955,24 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "const-oid", - "crypto-common", + "block-buffer 0.10.4", + "const-oid 0.9.6", + "crypto-common 0.1.7", "subtle", ] +[[package]] +name = "digest" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" +dependencies = [ + "block-buffer 0.12.0", + "const-oid 0.10.2", + "crypto-common 0.2.1", + "ctutils", +] + [[package]] name = "dirs" version = "4.0.0" @@ -1001,15 +1091,15 @@ checksum = "1fda3bf123be441da5260717e0661c25a2fd9cb2b2c1d20bf2e05580047158ab" dependencies = [ "aes", "ctr", - "digest", + "digest 0.10.7", "hex", - "hmac", + "hmac 0.12.1", "pbkdf2", "rand 0.8.5", "scrypt", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "sha3", "thiserror 1.0.69", "uuid 0.8.2", @@ -1339,6 +1429,7 @@ dependencies = [ "cfg-if", "libc", "r-efi 6.0.0", + "rand_core 0.10.0", "wasip2", "wasip3", ] @@ -1456,7 +1547,7 @@ version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" dependencies = [ - "hmac", + "hmac 0.12.1", ] [[package]] @@ -1465,7 +1556,16 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest", + "digest 0.10.7", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.2", ] [[package]] @@ -1528,11 +1628,20 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hybrid-array" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3944cf8cf766b40e2a1a333ee5e9b563f854d5fa49d6a8ca2764e97c6eddb214" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" dependencies = [ "atomic-waker", "bytes", @@ -1545,7 +1654,6 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "pin-utils", "smallvec", "tokio", "want", @@ -1649,12 +1757,13 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", + "utf8_iter", "yoke", "zerofrom", "zerovec", @@ -1662,9 +1771,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", @@ -1675,9 +1784,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -1689,15 +1798,15 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ "icu_collections", "icu_locale_core", @@ -1709,15 +1818,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", @@ -1918,9 +2027,9 @@ checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "iri-string" -version = "0.7.10" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" dependencies = [ "memchr", "serde", @@ -1972,9 +2081,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jobserver" @@ -1988,10 +2097,12 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.91" +version = "0.3.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] @@ -2002,7 +2113,7 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" dependencies = [ - "cpufeatures", + "cpufeatures 0.2.17", ] [[package]] @@ -2015,7 +2126,7 @@ dependencies = [ "rand 0.8.5", "rand_chacha 0.3.1", "serde", - "sha2", + "sha2 0.10.9", "sha3", ] @@ -2051,9 +2162,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.182" +version = "0.2.184" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" [[package]] name = "libm" @@ -2063,9 +2174,9 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.14" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" +checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08" dependencies = [ "bitflags 2.11.0", "libc", @@ -2092,9 +2203,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "lock_api" @@ -2139,7 +2250,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ "cfg-if", - "digest", + "digest 0.10.7", +] + +[[package]] +name = "md-5" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" +dependencies = [ + "cfg-if", + "digest 0.11.2", ] [[package]] @@ -2232,9 +2353,9 @@ dependencies = [ [[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 0.11.1+wasi-snapshot-preview1", @@ -2312,9 +2433,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" [[package]] name = "num-format" @@ -2385,9 +2506,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" @@ -2403,9 +2524,9 @@ checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" [[package]] name = "openssl" -version = "0.10.75" +version = "0.10.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" +checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" dependencies = [ "bitflags 2.11.0", "cfg-if", @@ -2435,9 +2556,9 @@ checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "openssl-sys" -version = "0.9.111" +version = "0.9.112" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" dependencies = [ "cc", "libc", @@ -2514,7 +2635,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" dependencies = [ - "digest", + "digest 0.10.7", ] [[package]] @@ -2597,12 +2718,6 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - [[package]] name = "pkcs1" version = "0.7.5" @@ -2672,27 +2787,27 @@ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "postgres-protocol" -version = "0.6.10" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ee9dd5fe15055d2b6806f4736aa0c9637217074e224bbec46d4041b91bb9491" +checksum = "56201207dac53e2f38e848e31b4b91616a6bb6e0c7205b77718994a7f49e70fc" dependencies = [ "base64 0.22.1", "byteorder", "bytes", "fallible-iterator 0.2.0", - "hmac", - "md-5", + "hmac 0.13.0", + "md-5 0.11.0", "memchr", - "rand 0.9.2", - "sha2", + "rand 0.10.0", + "sha2 0.11.0", "stringprep", ] [[package]] name = "postgres-types" -version = "0.2.12" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54b858f82211e84682fecd373f68e1ceae642d8d751a1ebd13f33de6257b3e20" +checksum = "8dc729a129e682e8d24170cd30ae1aa01b336b096cbb56df6d534ffec133d186" dependencies = [ "bytes", "fallible-iterator 0.2.0", @@ -2701,9 +2816,9 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ "zerovec", ] @@ -2732,7 +2847,7 @@ dependencies = [ "prost 0.12.6", "prost-build 0.12.6", "prost-derive 0.12.6", - "sha2", + "sha2 0.10.9", "smallvec", "symbolic-demangle", "tempfile", @@ -2952,9 +3067,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.13" +version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" dependencies = [ "bytes", "getrandom 0.3.4", @@ -3033,6 +3148,17 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "rand" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" +dependencies = [ + "chacha20", + "getrandom 0.4.2", + "rand_core 0.10.0", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -3071,6 +3197,12 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_core" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" + [[package]] name = "rand_xoshiro" version = "0.7.0" @@ -3246,7 +3378,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" dependencies = [ - "hmac", + "hmac 0.12.1", "subtle", ] @@ -3289,8 +3421,8 @@ version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" dependencies = [ - "const-oid", - "digest", + "const-oid 0.9.6", + "digest 0.10.7", "num-bigint-dig", "num-integer", "num-traits", @@ -3325,9 +3457,9 @@ checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" [[package]] name = "rustc-hash" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" [[package]] name = "rustc-hex" @@ -3397,9 +3529,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.9" +version = "0.103.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" dependencies = [ "aws-lc-rs", "ring", @@ -3439,9 +3571,9 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ "windows-sys 0.61.2", ] @@ -3482,10 +3614,10 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f9e24d2b632954ded8ab2ef9fea0a0c769ea56ea98bddbafbad22caeeadf45d" dependencies = [ - "hmac", + "hmac 0.12.1", "pbkdf2", "salsa20", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -3596,9 +3728,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.17.0" +version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "381b283ce7bc6b476d903296fb59d0d36633652b633b27f64db4fb46dcbfc3b9" +checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" dependencies = [ "base64 0.22.1", "chrono", @@ -3615,9 +3747,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.17.0" +version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6d4e30573c8cb306ed6ab1dca8423eec9a463ea0e155f45399455e0368b27e0" +checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" dependencies = [ "darling", "proc-macro2", @@ -3632,8 +3764,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", ] [[package]] @@ -3643,8 +3775,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.2", ] [[package]] @@ -3653,7 +3796,7 @@ version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" dependencies = [ - "digest", + "digest 0.10.7", "keccak", ] @@ -3688,15 +3831,15 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "digest", + "digest 0.10.7", "rand_core 0.6.4", ] [[package]] name = "simd-adler32" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] name = "siphasher" @@ -3804,7 +3947,7 @@ dependencies = [ "rustls", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "smallvec", "thiserror 2.0.18", "tokio", @@ -3842,7 +3985,7 @@ dependencies = [ "quote", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "sqlx-core", "sqlx-mysql", "sqlx-postgres", @@ -3865,7 +4008,7 @@ dependencies = [ "byteorder", "bytes", "crc", - "digest", + "digest 0.10.7", "dotenvy", "either", "futures-channel", @@ -3875,10 +4018,10 @@ dependencies = [ "generic-array", "hex", "hkdf", - "hmac", + "hmac 0.12.1", "itoa", "log", - "md-5", + "md-5 0.10.6", "memchr", "once_cell", "percent-encoding", @@ -3886,7 +4029,7 @@ dependencies = [ "rsa", "serde", "sha1", - "sha2", + "sha2 0.10.9", "smallvec", "sqlx-core", "stringprep", @@ -3914,18 +4057,18 @@ dependencies = [ "futures-util", "hex", "hkdf", - "hmac", + "hmac 0.12.1", "home", "itoa", "log", - "md-5", + "md-5 0.10.6", "memchr", "num-bigint", "once_cell", "rand 0.8.5", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "smallvec", "sqlx-core", "stringprep", @@ -4052,12 +4195,12 @@ checksum = "1004a16c25dc6113c19d4f9d0c19ff97d85804829894bba22c0d0e9e7b249812" dependencies = [ "crypto-bigint", "hex", - "hmac", + "hmac 0.12.1", "num-bigint", "num-integer", "num-traits", "rfc6979", - "sha2", + "sha2 0.10.9", "starknet-curve", "starknet-types-core", "zeroize", @@ -4127,7 +4270,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90d23b1bc014ee4cce40056ab3114bcbcdc2dbc1e845bbfb1f8bd0bab63507d4" dependencies = [ "blake2", - "digest", + "digest 0.10.7", "lambdaworks-crypto", "lambdaworks-math", "num-bigint", @@ -4175,21 +4318,21 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "symbolic-common" -version = "12.17.2" +version = "12.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "751a2823d606b5d0a7616499e4130a516ebd01a44f39811be2b9600936509c23" +checksum = "52ca086c1eb5c7ee74b151ba83c6487d5d33f8c08ad991b86f3f58f6629e68d5" dependencies = [ "debugid", "memmap2", "stable_deref_trait", - "uuid 1.22.0", + "uuid 1.23.0", ] [[package]] name = "symbolic-demangle" -version = "12.17.2" +version = "12.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79b237cfbe320601dd24b4ac817a5b68bb28f5508e33f08d42be0682cadc8ac9" +checksum = "baa911a28a62823aaf2cc2e074212492a3ee69d0d926cc8f5b12b4a108ff5c0c" dependencies = [ "cpp_demangle", "rustc-demangle", @@ -4256,9 +4399,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.26.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", "getrandom 0.4.2", @@ -4358,9 +4501,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "zerovec", @@ -4378,9 +4521,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ "tinyvec_macros", ] @@ -4431,9 +4574,9 @@ dependencies = [ [[package]] name = "tokio-postgres" -version = "0.7.16" +version = "0.7.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcea47c8f71744367793f16c2db1f11cb859d28f436bdb4ca9193eb1f787ee42" +checksum = "4dd8df5ef180f6364759a6f00f7aadda4fbbac86cdee37480826a6ff9f3574ce" dependencies = [ "async-trait", "byteorder", @@ -4448,7 +4591,7 @@ dependencies = [ "pin-project-lite", "postgres-protocol", "postgres-types", - "rand 0.9.2", + "rand 0.10.0", "socket2 0.6.3", "tokio", "tokio-util", @@ -4504,18 +4647,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "1.0.0+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" dependencies = [ "serde_core", ] [[package]] name = "toml_edit" -version = "0.25.4+spec-1.1.0" +version = "0.25.10+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2" +checksum = "a82418ca169e235e6c399a84e395ab6debeb3bc90edc959bf0f48647c6a32d1b" dependencies = [ "indexmap 2.13.0", "toml_datetime", @@ -4525,9 +4668,9 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.9+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ "winnow", ] @@ -5101,6 +5244,23 @@ dependencies = [ "tracing", ] +[[package]] +name = "torii-pathfinder" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "bincode", + "itertools 0.14.0", + "rand 0.8.5", + "rusqlite", + "serde", + "starknet", + "thiserror 2.0.18", + "torii", + "zstd", +] + [[package]] name = "torii-postgres" version = "0.1.0" @@ -5365,9 +5525,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.22" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "matchers", "nu-ansi-term", @@ -5470,9 +5630,9 @@ checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" [[package]] name = "unicode-segmentation" -version = "1.12.0" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" [[package]] name = "unicode-xid" @@ -5486,6 +5646,12 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "unty" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" + [[package]] name = "url" version = "2.5.8" @@ -5534,9 +5700,9 @@ dependencies = [ [[package]] name = "uuid" -version = "1.22.0" +version = "1.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" dependencies = [ "js-sys", "wasm-bindgen", @@ -5560,6 +5726,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "virtue" +version = "0.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" + [[package]] name = "walkdir" version = "2.5.0" @@ -5629,9 +5801,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.114" +version = "0.2.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0" dependencies = [ "cfg-if", "once_cell", @@ -5642,23 +5814,19 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.64" +version = "0.4.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +checksum = "03623de6905b7206edd0a75f69f747f134b7f0a2323392d664448bf2d3c5d87e" dependencies = [ - "cfg-if", - "futures-util", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.114" +version = "0.2.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5666,9 +5834,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.114" +version = "0.2.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2" dependencies = [ "bumpalo", "proc-macro2", @@ -5679,9 +5847,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.114" +version = "0.2.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b" dependencies = [ "unicode-ident", ] @@ -5722,9 +5890,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.91" +version = "0.3.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +checksum = "cd70027e39b12f0849461e08ffc50b9cd7688d942c1c8e3c7b22273236b4dd0a" dependencies = [ "js-sys", "wasm-bindgen", @@ -6106,9 +6274,9 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.15" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" dependencies = [ "memchr", ] @@ -6224,9 +6392,9 @@ checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" [[package]] name = "yoke" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -6235,9 +6403,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", @@ -6247,18 +6415,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.40" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.40" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", @@ -6267,18 +6435,18 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", @@ -6294,9 +6462,9 @@ checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zerotrie" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", @@ -6305,9 +6473,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "yoke", "zerofrom", @@ -6316,9 +6484,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", @@ -6330,3 +6498,31 @@ name = "zmij" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml index 41acb5e4..c9edc27f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ members = [ "crates/testing", "crates/postgres", "crates/sqlite", + "crates/pathfinder", "bins/torii-erc20", "bins/torii-erc20-synth", "bins/torii-introspect-bin", @@ -146,6 +147,7 @@ torii-introspect-sqlite-sink.path = "./crates/introspect-sqlite-sink" torii-test-utils.path = "crates/testing" torii-postgres.path = "crates/postgres" torii-sqlite.path = "crates/sqlite" +torii-pathfinder.path = "crates/pathfinder" [lib] @@ -198,7 +200,11 @@ serde.workspace = true sqlx.workspace = true starknet.workspace = true tokio-stream.workspace = true -tokio-rustls = { version = "0.26", default-features = false, features = ["logging", "tls12", "ring"] } +tokio-rustls = { version = "0.26", default-features = false, features = [ + "logging", + "tls12", + "ring", +] } tokio-util.workspace = true tokio.workspace = true tonic-reflection.workspace = true diff --git a/crates/pathfinder/Cargo.toml b/crates/pathfinder/Cargo.toml new file mode 100644 index 00000000..14c759dd --- /dev/null +++ b/crates/pathfinder/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "torii-pathfinder" +version = "0.1.0" +edition = "2021" +description = "Pathfinding utilities for Torii" + +[dependencies] +starknet.workspace = true +rusqlite = { version = "0.32.1", features = ["bundled"] } +zstd = { version = "0.13.2", features = ["experimental"] } +serde.workspace = true +itertools.workspace = true +bincode = { version = "2.0.0-rc.3", features = ["serde"] } +thiserror.workspace = true +rand = "0.8.5" + +# Etl dependencies +torii = { workspace = true, optional = true } +async-trait = { workspace = true, optional = true } +anyhow = { workspace = true, optional = true } + +[lints] +workspace = true + +[features] +default = ["etl"] +etl = ["torii", "async-trait", "anyhow"] diff --git a/crates/pathfinder/src/assets/events.zdict b/crates/pathfinder/src/assets/events.zdict new file mode 100644 index 0000000000000000000000000000000000000000..7f4fa28400969a219b32889909023ecbea657cd2 GIT binary patch literal 112640 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 literal 0 HcmV?d00001 diff --git a/crates/pathfinder/src/assets/txs.zdict b/crates/pathfinder/src/assets/txs.zdict new file mode 100644 index 0000000000000000000000000000000000000000..724448c6dd6feebfd249aa3c495ffcec0d412b17 GIT binary patch literal 112640 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

;rRB7A&2&P;-21{Aqro&_Wq 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 Wk@~ADIKSJh`Z~_sFTS&TBmV`4SPt9( literal 0 HcmV?d00001 diff --git a/crates/pathfinder/src/decoding.rs b/crates/pathfinder/src/decoding.rs new file mode 100644 index 00000000..fefb5cd7 --- /dev/null +++ b/crates/pathfinder/src/decoding.rs @@ -0,0 +1,151 @@ +use itertools::Itertools; +use serde::{Deserialize, Serialize}; +use starknet::core::types::Event; +use std::fmt::{Formatter, Result as FmtResult}; + +use crate::sqlite::BlockEventsRow; +use crate::{PFError, PFResult}; + +/// Mostly taken from pathfinder-common but with some optimizations + +/// Minimally encoded Felt value. +#[derive(Clone, Debug, PartialEq, Eq, Default)] +pub struct MinimalFelt([u8; 32]); + +impl serde::Serialize for MinimalFelt { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let bytes = self.0; + let zeros = bytes.iter().take_while(|&&x| x == 0).count(); + bytes[zeros..].serialize(serializer) + } +} + +impl<'de> serde::Deserialize<'de> for MinimalFelt { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct Visitor; + + impl<'de> serde::de::Visitor<'de> for Visitor { + type Value = MinimalFelt; + + fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult { + formatter.write_str("a sequence") + } + + fn visit_seq(self, mut seq: B) -> Result + where + B: serde::de::SeqAccess<'de>, + { + let len = seq.size_hint().unwrap(); + let mut bytes = [0; 32]; + let num_zeros = bytes.len() - len; + let mut i = num_zeros; + while let Some(value) = seq.next_element()? { + bytes[i] = value; + i += 1; + } + Ok(MinimalFelt(bytes)) + } + } + + deserializer.deserialize_seq(Visitor) + } +} + +impl From for starknet::core::types::Felt { + fn from(value: MinimalFelt) -> Self { + starknet::core::types::Felt::from_bytes_be(&value.0) + } +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub enum EventsForBlock { + V0 { events: Vec> }, +} + +impl EventsForBlock { + pub fn encoded_events(self) -> Vec> { + match self { + EventsForBlock::V0 { events } => events, + } + } + + pub fn events(self) -> Vec> { + self.encoded_events() + .into_iter() + .map(|evs| evs.into_iter().map_into().collect()) + .collect() + } +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct EncodedEvent { + pub data: Vec, + pub from_address: MinimalFelt, + pub keys: Vec, +} + +impl From for Event { + fn from(value: EncodedEvent) -> Self { + Self { + data: value.data.into_iter().map_into().collect(), + from_address: value.from_address.into(), + keys: value.keys.into_iter().map_into().collect(), + } + } +} + +pub(crate) mod compression { + use std::sync::LazyLock; + + const MAX_EVENTS_UNCOMPRESSED_SIZE: usize = 128usize * 1024 * 1024; + + static ZSTD_EVENTS_DECODER_DICTIONARY: LazyLock> = + LazyLock::new(|| zstd::dict::DecoderDictionary::new(include_bytes!("assets/events.zdict"))); + + fn new_events_decompressor() -> std::io::Result> { + zstd::bulk::Decompressor::with_prepared_dictionary(&ZSTD_EVENTS_DECODER_DICTIONARY) + } + pub(crate) fn decompress_events(input: &[u8]) -> std::io::Result> { + let mut decompressor = new_events_decompressor()?; + decompressor.decompress(input, MAX_EVENTS_UNCOMPRESSED_SIZE) + } +} + +impl BlockEventsRow { + pub fn decompress_events(&self) -> PFResult>> { + match self.events { + None => Ok(vec![]), + Some(ref events) => { + let events = compression::decompress_events(events)?; + let events: EventsForBlock = + bincode::serde::decode_from_slice(&events, bincode::config::standard())?.0; + Ok(events.events()) + } + } + } +} + +impl TryFrom for BlockEvents { + type Error = PFError; + + fn try_from(value: BlockEventsRow) -> Result { + Ok(Self { + block_number: value.block_number, + transactions: value.decompress_events()?, + }) + } +} + +#[derive(Debug)] +pub struct BlockEvents { + pub block_number: u64, + pub transactions: Vec>, +} diff --git a/crates/pathfinder/src/error.rs b/crates/pathfinder/src/error.rs new file mode 100644 index 00000000..feb5e039 --- /dev/null +++ b/crates/pathfinder/src/error.rs @@ -0,0 +1,38 @@ +use std::sync::PoisonError; + +use bincode::error::DecodeError; + +#[derive(Debug, thiserror::Error)] +pub enum PFError { + #[error(transparent)] + Decode(#[from] DecodeError), + #[error(transparent)] + Io(#[from] std::io::Error), + #[error(transparent)] + Sqlite(#[from] rusqlite::Error), + #[error("data mismatch: {0}")] + DataMismatch(String), + #[error("Block Hash missing for block {0}")] + BlockHashMissing(u64), + #[error("Lock poisoned: {0}")] + LockPoison(String), +} + +impl From> for PFError { + fn from(err: PoisonError) -> Self { + Self::LockPoison(err.to_string()) + } +} + +pub type PFResult = std::result::Result; + +impl PFError { + pub fn tx_hash_mismatch(block_number: u64) -> Self { + Self::DataMismatch(format!( + "Transactions hash missing for block {block_number}" + )) + } + pub fn block_hash_missing(block_number: u64) -> Self { + Self::BlockHashMissing(block_number) + } +} diff --git a/crates/pathfinder/src/extractor.rs b/crates/pathfinder/src/extractor.rs new file mode 100644 index 00000000..5705d113 --- /dev/null +++ b/crates/pathfinder/src/extractor.rs @@ -0,0 +1,68 @@ +use crate::{connect, EventFetcher, PFResult}; +use anyhow::Result as AnyResult; +use async_trait::async_trait; +use rusqlite::Connection; +use starknet::core::types::EmittedEvent; +use std::collections::HashMap; +use std::path::Path; +use std::sync::{Arc, Mutex}; +use torii::etl::{BlockContext, EngineDb, ExtractionBatch, Extractor}; + +#[derive(Debug)] +pub struct PathfinderExtractor { + pub conn: Mutex, + pub batch: u64, + pub current: u64, + pub end: u64, +} + +impl PathfinderExtractor { + pub fn new(path: &Path, batch: u64, start: u64, end: u64) -> PFResult { + Ok(Self { + conn: connect(path)?.into(), + batch, + current: start, + end, + }) + } + + pub fn next_batch(&mut self) -> PFResult<(Vec, Vec)> { + let next = (self.current + self.batch).min(self.end); + let result = self + .conn + .lock()? + .get_emitted_events_with_context(self.current, next - 1); + self.current = next; + result + } +} + +#[async_trait] +impl Extractor for PathfinderExtractor { + async fn extract( + &mut self, + _cursor: Option, + _engine_db: &EngineDb, + ) -> AnyResult { + let (blocks, events) = self.next_batch()?; + let blocks = blocks + .into_iter() + .map(|b| (b.number, Arc::new(b))) + .collect(); + Ok(ExtractionBatch { + events, + blocks, + transactions: HashMap::new(), + declared_classes: Vec::new(), + deployed_contracts: Vec::new(), + cursor: None, + chain_head: None, + }) + } + fn is_finished(&self) -> bool { + self.current >= self.end + } + fn as_any(&self) -> &dyn std::any::Any { + self + } +} diff --git a/crates/pathfinder/src/fetcher.rs b/crates/pathfinder/src/fetcher.rs new file mode 100644 index 00000000..0f7c3353 --- /dev/null +++ b/crates/pathfinder/src/fetcher.rs @@ -0,0 +1,178 @@ +use std::collections::HashMap; + +use crate::sqlite::{BlockContextRow, SqliteExt}; +use crate::PFError; +use crate::{decoding::BlockEvents, PFResult}; +use itertools::Itertools; +use rusqlite::Connection; +use starknet::core::types::{EmittedEvent, Felt}; + +pub struct BlockTxHashes { + pub block_number: u64, + pub hashes: Vec, +} + +#[cfg(not(feature = "etl"))] +/// Block context information Copied from core to avoid importing the entire crate +#[derive(Debug, Clone, Default)] +pub struct BlockContext { + pub number: u64, + pub hash: Felt, + pub parent_hash: Felt, + pub timestamp: u64, +} + +#[cfg(feature = "etl")] +pub use torii::etl::extractor::BlockContext; + +impl From for BlockContext { + fn from(value: BlockContextRow) -> Self { + Self { + number: value.number, + hash: Felt::from_bytes_be_slice(&value.hash), + parent_hash: Felt::from_bytes_be_slice(&value.parent_hash), + timestamp: value.timestamp, + } + } +} + +impl BlockTxHashes { + pub fn new(block_number: u64) -> Self { + Self { + block_number, + hashes: Vec::new(), + } + } +} + +pub trait EmittedEventExt { + fn with_block_hash(self, block_hashes: &HashMap) -> PFResult; +} + +impl EmittedEventExt for EmittedEvent { + fn with_block_hash(mut self, block_hashes: &HashMap) -> PFResult { + let block_number = self.block_number.unwrap(); + match block_hashes.get(&block_number) { + Some(block_hash) => self.block_hash = Some(*block_hash), + None => return Err(PFError::block_hash_missing(block_number)), + } + Ok(self) + } +} + +pub trait EmittedEventsExt { + fn with_block_hashes(self, block_hashes: &HashMap) -> PFResult>; +} + +impl EmittedEventsExt for Vec { + fn with_block_hashes(self, block_hashes: &HashMap) -> PFResult> { + self.into_iter() + .map(|event| event.with_block_hash(block_hashes)) + .collect() + } +} + +pub trait EventFetcher { + fn get_events_for_blocks(&self, from_block: u64, to_block: u64) -> PFResult>; + fn get_tx_hashes_for_blocks( + &self, + from_block: u64, + to_block: u64, + ) -> PFResult>; + fn get_emitted_events_wo_block_hashes( + &self, + from_block: u64, + to_block: u64, + ) -> PFResult>; + fn get_emitted_events(&self, from_block: u64, to_block: u64) -> PFResult>; + fn get_emitted_events_with_context( + &self, + from_block: u64, + to_block: u64, + ) -> PFResult<(Vec, Vec)>; +} + +impl EventFetcher for Connection { + fn get_events_for_blocks(&self, from_block: u64, to_block: u64) -> PFResult> { + self.get_block_events_rows(from_block, to_block)? + .into_iter() + .map(BlockEvents::try_from) + .collect() + } + fn get_tx_hashes_for_blocks( + &self, + from_block: u64, + to_block: u64, + ) -> PFResult> { + let mut values = Vec::with_capacity((to_block - from_block + 1) as usize); + let mut current = BlockTxHashes::new(from_block); + for row in self.get_block_tx_hash_rows(from_block, to_block)? { + let (block_number, hash) = row.into(); + if block_number != current.block_number { + values.push(current); + current = BlockTxHashes::new(block_number); + } + current.hashes.push(hash); + } + values.push(current); + Ok(values) + } + fn get_emitted_events_wo_block_hashes( + &self, + from_block: u64, + to_block: u64, + ) -> PFResult> { + let block_events = self.get_events_for_blocks(from_block, to_block)?; + let mut tx_hashes = self + .get_block_tx_hash_rows(from_block, to_block)? + .into_iter() + .map_into(); + let mut emitted_events = Vec::new(); + for block in block_events { + for transaction in block.transactions { + let transaction_hash = tx_hashes + .next() + .filter(|(tx_bn, _)| tx_bn == &block.block_number) + .ok_or_else(|| PFError::tx_hash_mismatch(block.block_number))? + .1; + for event in transaction { + emitted_events.push(EmittedEvent { + block_hash: None, + block_number: Some(block.block_number), + data: event.data, + from_address: event.from_address, + keys: event.keys, + transaction_hash, + }); + } + } + } + Ok(emitted_events) + } + fn get_emitted_events(&self, from_block: u64, to_block: u64) -> PFResult> { + let block_hashes: HashMap = self + .get_block_hash_rows(from_block, to_block)? + .into_iter() + .map_into() + .collect(); + self.get_emitted_events_wo_block_hashes(from_block, to_block)? + .with_block_hashes(&block_hashes) + } + fn get_emitted_events_with_context( + &self, + from_block: u64, + to_block: u64, + ) -> PFResult<(Vec, Vec)> { + let events = self.get_emitted_events_wo_block_hashes(from_block, to_block)?; + let block_contexts: Vec = self + .get_block_context_rows(from_block, to_block)? + .into_iter() + .map_into() + .collect_vec(); + let block_hashes: HashMap = block_contexts + .iter() + .map(|ctx| (ctx.number, ctx.hash)) + .collect(); + Ok((block_contexts, events.with_block_hashes(&block_hashes)?)) + } +} diff --git a/crates/pathfinder/src/lib.rs b/crates/pathfinder/src/lib.rs new file mode 100644 index 00000000..6cbd3456 --- /dev/null +++ b/crates/pathfinder/src/lib.rs @@ -0,0 +1,15 @@ +pub mod decoding; +pub mod error; +pub mod fetcher; +pub mod sqlite; +pub mod utils; + +#[cfg(feature = "etl")] +pub mod extractor; + +#[cfg(test)] +mod test; + +pub use error::{PFError, PFResult}; +pub use fetcher::{BlockContext, BlockTxHashes, EventFetcher}; +pub use utils::connect; diff --git a/crates/pathfinder/src/sqlite.rs b/crates/pathfinder/src/sqlite.rs new file mode 100644 index 00000000..d1e345e5 --- /dev/null +++ b/crates/pathfinder/src/sqlite.rs @@ -0,0 +1,123 @@ +use rusqlite::params; +use rusqlite::Connection; +use rusqlite::Error as SqliteError; +use rusqlite::Row; +use starknet::core::types::Felt; + +pub type SqliteResult = Result; + +#[derive(Debug)] +pub struct BlockEventsRow { + pub block_number: u64, + pub events: Option>, +} + +#[derive(Debug)] +pub struct NumberAndHashRow(pub u64, pub Vec); + +#[derive(Debug)] +pub struct BlockContextRow { + pub number: u64, + pub hash: Vec, + pub parent_hash: Vec, + pub timestamp: u64, +} + +impl From for (u64, Felt) { + fn from(value: NumberAndHashRow) -> Self { + (value.0 as u64, Felt::from_bytes_be_slice(&value.1)) + } +} + +impl BlockEventsRow { + pub fn from_row(row: &Row<'_>) -> SqliteResult { + Ok(Self { + block_number: row.get::<_, i64>(0)? as u64, + events: row.get(1)?, + }) + } +} + +impl NumberAndHashRow { + pub fn from_row(row: &Row<'_>) -> SqliteResult { + Ok(Self(row.get::<_, i64>(0)? as u64, row.get(1)?)) + } +} + +impl BlockContextRow { + pub fn from_row(row: &Row<'_>) -> SqliteResult { + Ok(Self { + number: row.get::<_, i64>(0)? as u64, + hash: row.get(1)?, + parent_hash: row.get(2)?, + timestamp: row.get::<_, i64>(3)? as u64, + }) + } +} + +pub trait SqliteExt { + fn get_block_events_rows( + &self, + from_block: u64, + to_block: u64, + ) -> SqliteResult>; + fn get_block_tx_hash_rows( + &self, + from_block: u64, + to_block: u64, + ) -> SqliteResult>; + fn get_block_hash_rows( + &self, + from_block: u64, + to_block: u64, + ) -> SqliteResult>; + fn get_block_context_rows( + &self, + from_block: u64, + to_block: u64, + ) -> SqliteResult>; +} + +impl SqliteExt for Connection { + fn get_block_events_rows( + &self, + from_block: u64, + to_block: u64, + ) -> SqliteResult> { + let mut stmt = self.prepare_cached("SELECT block_number, events FROM transactions WHERE block_number BETWEEN ? AND ? ORDER BY block_number")?; + let rows = stmt.query_map(params![from_block, to_block], BlockEventsRow::from_row)?; + rows.collect::, _>>() + } + + fn get_block_tx_hash_rows( + &self, + from_block: u64, + to_block: u64, + ) -> SqliteResult> { + let mut stmt = self.prepare_cached("SELECT block_number, hash FROM transaction_hashes WHERE block_number BETWEEN ? AND ? ORDER BY block_number, idx")?; + let rows = stmt.query_map(params![from_block, to_block], NumberAndHashRow::from_row)?; + rows.collect::, _>>() + } + fn get_block_hash_rows( + &self, + from_block: u64, + to_block: u64, + ) -> SqliteResult> { + let mut stmt = self.prepare_cached( + "SELECT number, hash FROM block_headers WHERE number BETWEEN ? AND ? ORDER BY number", + )?; + let rows = stmt.query_map(params![from_block, to_block], NumberAndHashRow::from_row)?; + rows.collect::, _>>() + } + fn get_block_context_rows( + &self, + from_block: u64, + to_block: u64, + ) -> SqliteResult> { + let mut stmt = self.prepare_cached( + "SELECT number, hash, parent_hash, timestamp FROM block_headers WHERE number BETWEEN ? AND ? ORDER BY number", + )?; + let rows = stmt.query_map(params![from_block, to_block], BlockContextRow::from_row)?; + rows.collect::, _>>() + } +} diff --git a/crates/pathfinder/src/test.rs b/crates/pathfinder/src/test.rs new file mode 100644 index 00000000..bf2b3549 --- /dev/null +++ b/crates/pathfinder/src/test.rs @@ -0,0 +1,20 @@ +use rusqlite::Connection; + +use crate::fetcher::EventFetcher; + +const DB_PATH: &str = "/mnt/store/mainnet.sqlite"; + +pub fn make_connection() -> Connection { + Connection::open(DB_PATH).expect("failed to open database") +} + +#[test] +fn test_emitted_events() { + let conn = make_connection(); + let events = conn + .get_emitted_events(1000000, 1000010) + .expect("failed to fetch events"); + for event in events { + println!("{event:#?}"); + } +} diff --git a/crates/pathfinder/src/utils.rs b/crates/pathfinder/src/utils.rs new file mode 100644 index 00000000..c0a75fb5 --- /dev/null +++ b/crates/pathfinder/src/utils.rs @@ -0,0 +1,10 @@ +use std::path::Path; + +use rusqlite::{Connection, OpenFlags, Result as RusqliteResult}; + +pub fn connect>(path: P) -> RusqliteResult { + Connection::open_with_flags( + path, + OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX, + ) +} From 595aaaf41bbddf6c7176a3fecd6337f28743a695 Mon Sep 17 00:00:00 2001 From: Bengineer Date: Thu, 2 Apr 2026 14:56:35 +0100 Subject: [PATCH 2/7] fix: update bincode version to 2.0.0 and set default feature --- crates/pathfinder/Cargo.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/pathfinder/Cargo.toml b/crates/pathfinder/Cargo.toml index 14c759dd..f08f8cb6 100644 --- a/crates/pathfinder/Cargo.toml +++ b/crates/pathfinder/Cargo.toml @@ -10,7 +10,7 @@ rusqlite = { version = "0.32.1", features = ["bundled"] } zstd = { version = "0.13.2", features = ["experimental"] } serde.workspace = true itertools.workspace = true -bincode = { version = "2.0.0-rc.3", features = ["serde"] } +bincode = { version = "2.0.0", features = ["serde"] } thiserror.workspace = true rand = "0.8.5" @@ -23,5 +23,4 @@ anyhow = { workspace = true, optional = true } workspace = true [features] -default = ["etl"] etl = ["torii", "async-trait", "anyhow"] From aeb1c307fd8f6daeb0c39840b1a09dcc25909e53 Mon Sep 17 00:00:00 2001 From: Bengineer Date: Thu, 2 Apr 2026 15:38:19 +0100 Subject: [PATCH 3/7] feat: add default feature for ETL and improve PathfinderExtractor initialization --- crates/pathfinder/Cargo.toml | 1 + crates/pathfinder/src/extractor.rs | 2 +- crates/pathfinder/src/test.rs | 20 +++++++++++++------- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/crates/pathfinder/Cargo.toml b/crates/pathfinder/Cargo.toml index f08f8cb6..9eb30275 100644 --- a/crates/pathfinder/Cargo.toml +++ b/crates/pathfinder/Cargo.toml @@ -23,4 +23,5 @@ anyhow = { workspace = true, optional = true } workspace = true [features] +default = ["etl"] etl = ["torii", "async-trait", "anyhow"] diff --git a/crates/pathfinder/src/extractor.rs b/crates/pathfinder/src/extractor.rs index 5705d113..d2dc511c 100644 --- a/crates/pathfinder/src/extractor.rs +++ b/crates/pathfinder/src/extractor.rs @@ -17,7 +17,7 @@ pub struct PathfinderExtractor { } impl PathfinderExtractor { - pub fn new(path: &Path, batch: u64, start: u64, end: u64) -> PFResult { + pub fn new>(path: P, batch: u64, start: u64, end: u64) -> PFResult { Ok(Self { conn: connect(path)?.into(), batch, diff --git a/crates/pathfinder/src/test.rs b/crates/pathfinder/src/test.rs index bf2b3549..34ef1270 100644 --- a/crates/pathfinder/src/test.rs +++ b/crates/pathfinder/src/test.rs @@ -1,6 +1,6 @@ use rusqlite::Connection; -use crate::fetcher::EventFetcher; +use crate::{extractor::PathfinderExtractor, fetcher::EventFetcher}; const DB_PATH: &str = "/mnt/store/mainnet.sqlite"; @@ -10,11 +10,17 @@ pub fn make_connection() -> Connection { #[test] fn test_emitted_events() { - let conn = make_connection(); - let events = conn - .get_emitted_events(1000000, 1000010) - .expect("failed to fetch events"); - for event in events { - println!("{event:#?}"); + let mut extractor = + PathfinderExtractor::new(DB_PATH, 2000, 0, 4000000).expect("failed to create extractor"); + for n in 0..10000 { + let (blocks, events) = extractor.next_batch().expect("failed to fetch batch"); + println!( + "Fetched {} blocks and {} events", + blocks.len(), + events.len() + ); + if blocks.is_empty() || n >= 20 { + break; + } } } From bac1237936886387b9b4323b8593d776b968f7a4 Mon Sep 17 00:00:00 2001 From: Bengineer Date: Thu, 2 Apr 2026 20:45:58 +0100 Subject: [PATCH 4/7] feat(starknet): add Felt type and serialization support - Introduced a new `Felt` type to represent StarkNet field elements. - Implemented serialization and deserialization for `Felt` using Serde. - Added conversion implementations between `Felt` and StarkNet's native types. - Created `EmittedEvent` and `Event` structs for handling StarkNet events. - Added tests for serialization and deserialization of `Felt` to ensure correctness. - Updated the `test.rs` file to include tests for fetching emitted events with context. --- Cargo.lock | 104 ++++- Cargo.toml | 3 + crates/pathfinder/Cargo.toml | 6 +- crates/pathfinder/src/decoding.rs | 86 ++-- crates/pathfinder/src/error.rs | 5 + crates/pathfinder/src/extractor.rs | 6 +- crates/pathfinder/src/fetcher.rs | 172 ++++--- crates/pathfinder/src/lib.rs | 2 +- crates/pathfinder/src/sqlite.rs | 202 ++++++-- crates/pathfinder/src/test.rs | 30 +- crates/starknet/Cargo.toml | 23 + crates/starknet/src/event.rs | 23 + crates/starknet/src/felt.rs | 725 +++++++++++++++++++++++++++++ crates/starknet/src/lib.rs | 10 + crates/starknet/src/serde.rs | 139 ++++++ crates/starknet/src/starknet.rs | 73 +++ 16 files changed, 1419 insertions(+), 190 deletions(-) create mode 100644 crates/starknet/Cargo.toml create mode 100644 crates/starknet/src/event.rs create mode 100644 crates/starknet/src/felt.rs create mode 100644 crates/starknet/src/lib.rs create mode 100644 crates/starknet/src/serde.rs create mode 100644 crates/starknet/src/starknet.rs diff --git a/Cargo.lock b/Cargo.lock index 1ec75777..9e256bdd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -145,6 +145,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "assert_matches" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" + [[package]] name = "async-stream" version = "0.3.6" @@ -913,6 +919,18 @@ dependencies = [ "syn", ] +[[package]] +name = "darrentsung_debug_parser" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf488eca7807ce3c8e64bee95c3fbf8f1935c905b3b73835e75db16fc458fdc4" +dependencies = [ + "anyhow", + "html-escape", + "nom", + "ordered-float", +] + [[package]] name = "data-encoding" version = "2.10.0" @@ -949,6 +967,12 @@ dependencies = [ "serde_core", ] +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "digest" version = "0.10.7" @@ -1577,6 +1601,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "html-escape" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476" +dependencies = [ + "utf8-width", +] + [[package]] name = "http" version = "1.4.0" @@ -2341,6 +2374,12 @@ dependencies = [ "unicase", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -2396,6 +2435,16 @@ dependencies = [ "libc", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -2566,6 +2615,15 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + [[package]] name = "parity-scale-codec" version = "3.7.5" @@ -2863,6 +2921,26 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + +[[package]] +name = "pretty_assertions_sorted" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa95d32882f2adbdfd30312733271b83c527ee8007bf78dc21afe510463ac6a0" +dependencies = [ + "darrentsung_debug_parser", + "pretty_assertions", +] + [[package]] name = "prettyplease" version = "0.2.37" @@ -5251,13 +5329,13 @@ dependencies = [ "anyhow", "async-trait", "bincode", - "itertools 0.14.0", "rand 0.8.5", "rusqlite", "serde", "starknet", "thiserror 2.0.18", "torii", + "torii-starknet", "zstd", ] @@ -5327,6 +5405,18 @@ dependencies = [ "torii-common", ] +[[package]] +name = "torii-starknet" +version = "0.1.0" +dependencies = [ + "assert_matches", + "bincode", + "pretty_assertions_sorted", + "serde", + "serde_json", + "starknet", +] + [[package]] name = "torii-test-utils" version = "0.1.0" @@ -5676,6 +5766,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf8-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -6390,6 +6486,12 @@ version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + [[package]] name = "yoke" version = "0.8.2" diff --git a/Cargo.toml b/Cargo.toml index c9edc27f..7043f6e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ members = [ "crates/postgres", "crates/sqlite", "crates/pathfinder", + "crates/starknet", "bins/torii-erc20", "bins/torii-erc20-synth", "bins/torii-introspect-bin", @@ -123,6 +124,7 @@ dojo-introspect = { git = "https://github.com/dojoengine/dojo-introspect", rev = # Utils resolve-path = "0.1.0" itertools = "0.14.0" +bincode = { version = "2.0.0" } # Torii core torii.path = "." @@ -148,6 +150,7 @@ torii-test-utils.path = "crates/testing" torii-postgres.path = "crates/postgres" torii-sqlite.path = "crates/sqlite" torii-pathfinder.path = "crates/pathfinder" +torii-starknet.path = "crates/starknet" [lib] diff --git a/crates/pathfinder/Cargo.toml b/crates/pathfinder/Cargo.toml index 9eb30275..92bad707 100644 --- a/crates/pathfinder/Cargo.toml +++ b/crates/pathfinder/Cargo.toml @@ -9,10 +9,10 @@ starknet.workspace = true rusqlite = { version = "0.32.1", features = ["bundled"] } zstd = { version = "0.13.2", features = ["experimental"] } serde.workspace = true -itertools.workspace = true -bincode = { version = "2.0.0", features = ["serde"] } +bincode = { workspace = true, features = ["serde"] } thiserror.workspace = true rand = "0.8.5" +torii-starknet.workspace = true # Etl dependencies torii = { workspace = true, optional = true } @@ -24,4 +24,4 @@ workspace = true [features] default = ["etl"] -etl = ["torii", "async-trait", "anyhow"] +etl = ["torii", "async-trait", "anyhow", "torii-starknet/starknet"] diff --git a/crates/pathfinder/src/decoding.rs b/crates/pathfinder/src/decoding.rs index fefb5cd7..cb525918 100644 --- a/crates/pathfinder/src/decoding.rs +++ b/crates/pathfinder/src/decoding.rs @@ -1,14 +1,31 @@ -use itertools::Itertools; -use serde::{Deserialize, Serialize}; -use starknet::core::types::Event; -use std::fmt::{Formatter, Result as FmtResult}; - use crate::sqlite::BlockEventsRow; use crate::{PFError, PFResult}; +use serde::{Deserialize, Serialize}; +use std::cell::RefCell; +use std::fmt::{Formatter, Result as FmtResult}; +use std::io::Result as IoResult; +use std::sync::LazyLock; +use torii_starknet::event::Event; +use torii_starknet::Felt; +use zstd::bulk::Decompressor; /// Mostly taken from pathfinder-common but with some optimizations +const MAX_EVENTS_UNCOMPRESSED_SIZE: usize = 128usize * 1024 * 1024; + +static ZSTD_EVENTS_DECODER_DICTIONARY: LazyLock> = + LazyLock::new(|| zstd::dict::DecoderDictionary::new(include_bytes!("assets/events.zdict"))); + +thread_local! { + static EVENT_DECOMPRESSOR: RefCell> = RefCell::new( + Decompressor::with_prepared_dictionary( + &ZSTD_EVENTS_DECODER_DICTIONARY + ).expect("failed to create decompressor") + ); +} + /// Minimally encoded Felt value. +#[repr(transparent)] #[derive(Clone, Debug, PartialEq, Eq, Default)] pub struct MinimalFelt([u8; 32]); @@ -57,9 +74,20 @@ impl<'de> serde::Deserialize<'de> for MinimalFelt { } } -impl From for starknet::core::types::Felt { +impl MinimalFelt { + #[allow(unsafe_code)] + fn into_felt_vec(v: Vec) -> Vec { + // Safe: both repr(transparent) over [u8; 32] + unsafe { + let mut v = std::mem::ManuallyDrop::new(v); + Vec::from_raw_parts(v.as_mut_ptr() as *mut Felt, v.len(), v.capacity()) + } + } +} + +impl From for Felt { fn from(value: MinimalFelt) -> Self { - starknet::core::types::Felt::from_bytes_be(&value.0) + value.0.into() } } @@ -70,18 +98,21 @@ pub enum EventsForBlock { } impl EventsForBlock { - pub fn encoded_events(self) -> Vec> { + pub fn events(self) -> Vec> { match self { - EventsForBlock::V0 { events } => events, + EventsForBlock::V0 { events } => events + .into_iter() + .map(|evs| evs.into_iter().map(Event::from).collect()) + .collect(), } } +} - pub fn events(self) -> Vec> { - self.encoded_events() - .into_iter() - .map(|evs| evs.into_iter().map_into().collect()) - .collect() - } +pub(crate) fn decompress_events(input: &[u8]) -> IoResult> { + EVENT_DECOMPRESSOR.with(|d| { + d.borrow_mut() + .decompress(input, MAX_EVENTS_UNCOMPRESSED_SIZE) + }) } #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] @@ -95,36 +126,19 @@ pub struct EncodedEvent { impl From for Event { fn from(value: EncodedEvent) -> Self { Self { - data: value.data.into_iter().map_into().collect(), + data: MinimalFelt::into_felt_vec(value.data), from_address: value.from_address.into(), - keys: value.keys.into_iter().map_into().collect(), + keys: MinimalFelt::into_felt_vec(value.keys), } } } -pub(crate) mod compression { - use std::sync::LazyLock; - - const MAX_EVENTS_UNCOMPRESSED_SIZE: usize = 128usize * 1024 * 1024; - - static ZSTD_EVENTS_DECODER_DICTIONARY: LazyLock> = - LazyLock::new(|| zstd::dict::DecoderDictionary::new(include_bytes!("assets/events.zdict"))); - - fn new_events_decompressor() -> std::io::Result> { - zstd::bulk::Decompressor::with_prepared_dictionary(&ZSTD_EVENTS_DECODER_DICTIONARY) - } - pub(crate) fn decompress_events(input: &[u8]) -> std::io::Result> { - let mut decompressor = new_events_decompressor()?; - decompressor.decompress(input, MAX_EVENTS_UNCOMPRESSED_SIZE) - } -} - impl BlockEventsRow { pub fn decompress_events(&self) -> PFResult>> { match self.events { - None => Ok(vec![]), + None => Ok(Vec::new()), Some(ref events) => { - let events = compression::decompress_events(events)?; + let events = decompress_events(events)?; let events: EventsForBlock = bincode::serde::decode_from_slice(&events, bincode::config::standard())?.0; Ok(events.events()) diff --git a/crates/pathfinder/src/error.rs b/crates/pathfinder/src/error.rs index feb5e039..45dbab12 100644 --- a/crates/pathfinder/src/error.rs +++ b/crates/pathfinder/src/error.rs @@ -14,6 +14,8 @@ pub enum PFError { DataMismatch(String), #[error("Block Hash missing for block {0}")] BlockHashMissing(u64), + #[error("Block context missing for block {0}")] + BlockContextMissing(u64), #[error("Lock poisoned: {0}")] LockPoison(String), } @@ -35,4 +37,7 @@ impl PFError { pub fn block_hash_missing(block_number: u64) -> Self { Self::BlockHashMissing(block_number) } + pub fn block_context_missing(block_number: u64) -> Self { + Self::BlockContextMissing(block_number) + } } diff --git a/crates/pathfinder/src/extractor.rs b/crates/pathfinder/src/extractor.rs index d2dc511c..ef096894 100644 --- a/crates/pathfinder/src/extractor.rs +++ b/crates/pathfinder/src/extractor.rs @@ -28,12 +28,12 @@ impl PathfinderExtractor { pub fn next_batch(&mut self) -> PFResult<(Vec, Vec)> { let next = (self.current + self.batch).min(self.end); - let result = self + let (blocks, events) = self .conn .lock()? - .get_emitted_events_with_context(self.current, next - 1); + .get_emitted_events_with_context(self.current, next - 1)?; self.current = next; - result + Ok((blocks, events.into_iter().map(Into::into).collect())) } } diff --git a/crates/pathfinder/src/fetcher.rs b/crates/pathfinder/src/fetcher.rs index 0f7c3353..330a684a 100644 --- a/crates/pathfinder/src/fetcher.rs +++ b/crates/pathfinder/src/fetcher.rs @@ -3,14 +3,7 @@ use std::collections::HashMap; use crate::sqlite::{BlockContextRow, SqliteExt}; use crate::PFError; use crate::{decoding::BlockEvents, PFResult}; -use itertools::Itertools; use rusqlite::Connection; -use starknet::core::types::{EmittedEvent, Felt}; - -pub struct BlockTxHashes { - pub block_number: u64, - pub hashes: Vec, -} #[cfg(not(feature = "etl"))] /// Block context information Copied from core to avoid importing the entire crate @@ -24,27 +17,20 @@ pub struct BlockContext { #[cfg(feature = "etl")] pub use torii::etl::extractor::BlockContext; +use torii_starknet::event::EmittedEvent; +use torii_starknet::Felt; impl From for BlockContext { fn from(value: BlockContextRow) -> Self { - Self { + BlockContext { number: value.number, - hash: Felt::from_bytes_be_slice(&value.hash), - parent_hash: Felt::from_bytes_be_slice(&value.parent_hash), + hash: value.hash.into(), + parent_hash: value.parent_hash.into(), timestamp: value.timestamp, } } } -impl BlockTxHashes { - pub fn new(block_number: u64) -> Self { - Self { - block_number, - hashes: Vec::new(), - } - } -} - pub trait EmittedEventExt { fn with_block_hash(self, block_hashes: &HashMap) -> PFResult; } @@ -60,25 +46,7 @@ impl EmittedEventExt for EmittedEvent { } } -pub trait EmittedEventsExt { - fn with_block_hashes(self, block_hashes: &HashMap) -> PFResult>; -} - -impl EmittedEventsExt for Vec { - fn with_block_hashes(self, block_hashes: &HashMap) -> PFResult> { - self.into_iter() - .map(|event| event.with_block_hash(block_hashes)) - .collect() - } -} - pub trait EventFetcher { - fn get_events_for_blocks(&self, from_block: u64, to_block: u64) -> PFResult>; - fn get_tx_hashes_for_blocks( - &self, - from_block: u64, - to_block: u64, - ) -> PFResult>; fn get_emitted_events_wo_block_hashes( &self, from_block: u64, @@ -91,50 +59,25 @@ pub trait EventFetcher { to_block: u64, ) -> PFResult<(Vec, Vec)>; } - impl EventFetcher for Connection { - fn get_events_for_blocks(&self, from_block: u64, to_block: u64) -> PFResult> { - self.get_block_events_rows(from_block, to_block)? - .into_iter() - .map(BlockEvents::try_from) - .collect() - } - fn get_tx_hashes_for_blocks( - &self, - from_block: u64, - to_block: u64, - ) -> PFResult> { - let mut values = Vec::with_capacity((to_block - from_block + 1) as usize); - let mut current = BlockTxHashes::new(from_block); - for row in self.get_block_tx_hash_rows(from_block, to_block)? { - let (block_number, hash) = row.into(); - if block_number != current.block_number { - values.push(current); - current = BlockTxHashes::new(block_number); - } - current.hashes.push(hash); - } - values.push(current); - Ok(values) - } fn get_emitted_events_wo_block_hashes( &self, from_block: u64, to_block: u64, ) -> PFResult> { - let block_events = self.get_events_for_blocks(from_block, to_block)?; + let total_events = self.get_number_of_events_for_blocks(from_block, to_block)?; let mut tx_hashes = self .get_block_tx_hash_rows(from_block, to_block)? - .into_iter() - .map_into(); - let mut emitted_events = Vec::new(); - for block in block_events { + .into_iter(); + let mut emitted_events = Vec::with_capacity(total_events as usize); + + for row in self.get_block_events_rows(from_block, to_block)? { + let block: BlockEvents = row.try_into()?; for transaction in block.transactions { let transaction_hash = tx_hashes .next() - .filter(|(tx_bn, _)| tx_bn == &block.block_number) - .ok_or_else(|| PFError::tx_hash_mismatch(block.block_number))? - .1; + .map(|r| r.1) + .ok_or_else(|| PFError::tx_hash_mismatch(block.block_number))?; for event in transaction { emitted_events.push(EmittedEvent { block_hash: None, @@ -149,30 +92,83 @@ impl EventFetcher for Connection { } Ok(emitted_events) } + fn get_emitted_events(&self, from_block: u64, to_block: u64) -> PFResult> { - let block_hashes: HashMap = self - .get_block_hash_rows(from_block, to_block)? - .into_iter() - .map_into() - .collect(); - self.get_emitted_events_wo_block_hashes(from_block, to_block)? - .with_block_hashes(&block_hashes) + let contexts = self.get_block_hash_rows_with_count(from_block, to_block)?; + let total_events: u64 = contexts.iter().map(|r| r.event_count).sum(); + let mut tx_hashes = self + .get_block_tx_hash_rows(from_block, to_block)? + .into_iter(); + let mut hash_rows = contexts.into_iter(); + let mut emitted_events = Vec::with_capacity(total_events as usize); + + for row in self.get_block_events_rows(from_block, to_block)? { + let block: BlockEvents = row.try_into()?; + let block_hash = hash_rows + .next() + .map(|r| r.hash) + .ok_or_else(|| PFError::block_hash_missing(block.block_number))?; + for transaction in block.transactions { + let transaction_hash = tx_hashes + .next() + .map(|r| r.1.into()) + .ok_or_else(|| PFError::tx_hash_mismatch(block.block_number))?; + for event in transaction { + emitted_events.push(EmittedEvent { + block_hash: Some(block_hash), + block_number: Some(block.block_number), + data: event.data, + from_address: event.from_address, + keys: event.keys, + transaction_hash, + }); + } + } + } + Ok(emitted_events) } + fn get_emitted_events_with_context( &self, from_block: u64, to_block: u64, ) -> PFResult<(Vec, Vec)> { - let events = self.get_emitted_events_wo_block_hashes(from_block, to_block)?; - let block_contexts: Vec = self - .get_block_context_rows(from_block, to_block)? - .into_iter() - .map_into() - .collect_vec(); - let block_hashes: HashMap = block_contexts - .iter() - .map(|ctx| (ctx.number, ctx.hash)) - .collect(); - Ok((block_contexts, events.with_block_hashes(&block_hashes)?)) + let context_rows = self.get_block_context_rows(from_block, to_block)?; + let total_events: u64 = context_rows.iter().map(|r| r.event_count).sum(); + let mut tx_hashes = self + .get_block_tx_hash_rows(from_block, to_block)? + .into_iter(); + let mut emitted_events = Vec::with_capacity(total_events as usize); + let mut contexts = Vec::with_capacity(context_rows.len()); + let mut ctx_iter = context_rows.into_iter(); + + for row in self.get_block_events_rows(from_block, to_block)? { + let block = BlockEvents::try_from(row)?; + let ctx = ctx_iter + .next() + .ok_or_else(|| PFError::block_context_missing(block.block_number))?; + if ctx.number != block.block_number { + return Err(PFError::block_context_missing(block.block_number)); + } + for transaction in block.transactions { + let transaction_hash = tx_hashes + .next() + .map(|r| r.1.into()) + .ok_or_else(|| PFError::tx_hash_mismatch(block.block_number))?; + for event in transaction { + emitted_events.push(EmittedEvent { + block_hash: Some(ctx.hash), + block_number: Some(block.block_number), + data: event.data, + from_address: event.from_address, + keys: event.keys, + transaction_hash, + }); + } + } + contexts.push(ctx.into()); + } + + Ok((contexts, emitted_events)) } } diff --git a/crates/pathfinder/src/lib.rs b/crates/pathfinder/src/lib.rs index 6cbd3456..2286741f 100644 --- a/crates/pathfinder/src/lib.rs +++ b/crates/pathfinder/src/lib.rs @@ -11,5 +11,5 @@ pub mod extractor; mod test; pub use error::{PFError, PFResult}; -pub use fetcher::{BlockContext, BlockTxHashes, EventFetcher}; +pub use fetcher::{BlockContext, EventFetcher}; pub use utils::connect; diff --git a/crates/pathfinder/src/sqlite.rs b/crates/pathfinder/src/sqlite.rs index d1e345e5..fb71976c 100644 --- a/crates/pathfinder/src/sqlite.rs +++ b/crates/pathfinder/src/sqlite.rs @@ -1,8 +1,9 @@ use rusqlite::params; +use rusqlite::types::ValueRef; use rusqlite::Connection; use rusqlite::Error as SqliteError; use rusqlite::Row; -use starknet::core::types::Felt; +use torii_starknet::Felt; pub type SqliteResult = Result; @@ -13,54 +14,128 @@ pub struct BlockEventsRow { } #[derive(Debug)] -pub struct NumberAndHashRow(pub u64, pub Vec); +pub struct NumberAndHashRow(pub u64, pub Felt); #[derive(Debug)] pub struct BlockContextRow { pub number: u64, - pub hash: Vec, - pub parent_hash: Vec, + pub hash: Felt, + pub parent_hash: Felt, pub timestamp: u64, + pub event_count: u64, +} + +#[derive(Debug)] +pub struct BlockHashAndEventCountRow { + pub number: u64, + pub hash: Felt, + pub event_count: u64, } impl From for (u64, Felt) { fn from(value: NumberAndHashRow) -> Self { - (value.0 as u64, Felt::from_bytes_be_slice(&value.1)) + (value.0, value.1) } } -impl BlockEventsRow { - pub fn from_row(row: &Row<'_>) -> SqliteResult { +impl TryFrom<&Row<'_>> for BlockEventsRow { + type Error = SqliteError; + + fn try_from(value: &Row<'_>) -> Result { Ok(Self { - block_number: row.get::<_, i64>(0)? as u64, - events: row.get(1)?, + block_number: value.get::<_, i64>(0)? as u64, + events: value.get(1)?, }) } } -impl NumberAndHashRow { - pub fn from_row(row: &Row<'_>) -> SqliteResult { - Ok(Self(row.get::<_, i64>(0)? as u64, row.get(1)?)) +impl TryFrom<&Row<'_>> for NumberAndHashRow { + type Error = SqliteError; + + fn try_from(value: &Row<'_>) -> Result { + Ok(Self(value.get::<_, i64>(0)? as u64, value.get_felt(1)?)) } } -impl BlockContextRow { - pub fn from_row(row: &Row<'_>) -> SqliteResult { +impl TryFrom<&Row<'_>> for BlockContextRow { + type Error = SqliteError; + + fn try_from(value: &Row<'_>) -> Result { + Ok(Self { + number: value.get::<_, i64>(0)? as u64, + hash: value.get_felt(1)?, + parent_hash: value.get_felt(2)?, + timestamp: value.get::<_, i64>(3)? as u64, + event_count: value.get::<_, i64>(4)? as u64, + }) + } +} + +impl TryFrom<&Row<'_>> for BlockHashAndEventCountRow { + type Error = SqliteError; + + fn try_from(value: &Row<'_>) -> Result { Ok(Self { - number: row.get::<_, i64>(0)? as u64, - hash: row.get(1)?, - parent_hash: row.get(2)?, - timestamp: row.get::<_, i64>(3)? as u64, + number: value.get::<_, i64>(0)? as u64, + hash: value.get_felt(1)?, + event_count: value.get::<_, i64>(2)? as u64, }) } } +pub trait RowExt { + fn get_felt(&self, idx: usize) -> SqliteResult; + fn get_felt_opt(&self, idx: usize) -> SqliteResult>; +} + +impl RowExt for Row<'_> { + fn get_felt(&self, idx: usize) -> SqliteResult { + match self.get_ref(idx)? { + ValueRef::Blob(b) => Ok(Felt::from_bytes_be_slice(b)), + _ => Err(SqliteError::InvalidColumnType( + idx, + "felt".into(), + rusqlite::types::Type::Blob, + )), + } + } + + fn get_felt_opt(&self, idx: usize) -> SqliteResult> { + match self.get_ref(idx)? { + ValueRef::Blob(b) => Ok(Some(Felt::from_bytes_be_slice(b))), + ValueRef::Null => Ok(None), + _ => Err(SqliteError::InvalidColumnType( + idx, + "felt".into(), + rusqlite::types::Type::Blob, + )), + } + } +} + pub trait SqliteExt { + fn query_range( + &self, + sql: &str, + from_block: u64, + to_block: u64, + count: usize, + ) -> SqliteResult> + where + T: for<'a> TryFrom<&'a Row<'a>, Error = SqliteError>; + fn get_block_events_rows( &self, from_block: u64, to_block: u64, - ) -> SqliteResult>; + ) -> SqliteResult> { + self.query_range( + "SELECT block_number, events FROM transactions WHERE block_number BETWEEN ? AND ? ORDER BY block_number", + from_block, + to_block, + (to_block.saturating_sub(from_block) + 1) as usize, + ) + } fn get_block_tx_hash_rows( &self, from_block: u64, @@ -70,54 +145,79 @@ pub trait SqliteExt { &self, from_block: u64, to_block: u64, - ) -> SqliteResult>; - fn get_block_context_rows( + ) -> SqliteResult> { + self.query_range( + "SELECT number, hash FROM block_headers WHERE number BETWEEN ? AND ? ORDER BY number", + from_block, + to_block, + (to_block.saturating_sub(from_block) + 1) as usize, + ) + } + fn get_block_hash_rows_with_count( &self, from_block: u64, to_block: u64, - ) -> SqliteResult>; -} - -impl SqliteExt for Connection { - fn get_block_events_rows( + ) -> SqliteResult> { + self.query_range( + "SELECT number, hash, event_count FROM block_headers WHERE number BETWEEN ? AND ? ORDER BY number", + from_block, + to_block, + (to_block.saturating_sub(from_block) + 1) as usize, + ) + } + fn get_block_context_rows( &self, from_block: u64, to_block: u64, - ) -> SqliteResult> { - let mut stmt = self.prepare_cached("SELECT block_number, events FROM transactions WHERE block_number BETWEEN ? AND ? ORDER BY block_number")?; - let rows = stmt.query_map(params![from_block, to_block], BlockEventsRow::from_row)?; - rows.collect::, _>>() + ) -> SqliteResult> { + self.query_range("SELECT number, hash, parent_hash, timestamp, event_count FROM block_headers WHERE number BETWEEN ? AND ? ORDER BY number", from_block, to_block, (to_block.saturating_sub(from_block) + 1) as usize) } + fn get_number_of_events_for_blocks(&self, from_block: u64, to_block: u64) -> SqliteResult; +} - fn get_block_tx_hash_rows( +impl SqliteExt for Connection { + fn query_range( &self, + sql: &str, from_block: u64, to_block: u64, - ) -> SqliteResult> { - let mut stmt = self.prepare_cached("SELECT block_number, hash FROM transaction_hashes WHERE block_number BETWEEN ? AND ? ORDER BY block_number, idx")?; - let rows = stmt.query_map(params![from_block, to_block], NumberAndHashRow::from_row)?; - rows.collect::, _>>() + count: usize, + ) -> SqliteResult> + where + T: for<'a> TryFrom<&'a Row<'a>, Error = SqliteError>, + { + let mut stmt = self.prepare_cached(sql)?; + let mut rows = stmt.query(params![from_block, to_block])?; + let mut result = Vec::with_capacity(count); + while let Some(row) = rows.next()? { + result.push(T::try_from(row)?); + } + Ok(result) } - fn get_block_hash_rows( + fn get_block_tx_hash_rows( &self, from_block: u64, to_block: u64, ) -> SqliteResult> { - let mut stmt = self.prepare_cached( - "SELECT number, hash FROM block_headers WHERE number BETWEEN ? AND ? ORDER BY number", - )?; - let rows = stmt.query_map(params![from_block, to_block], NumberAndHashRow::from_row)?; - rows.collect::, _>>() + let count = self + .prepare_cached( + "SELECT COUNT(*) FROM transaction_hashes WHERE block_number BETWEEN ? AND ?", + )? + .query_row(params![from_block, to_block], |r| r.get::<_, i64>(0)) + .map(|v| v as usize)?; + self.query_range( + "SELECT block_number, hash FROM transaction_hashes WHERE block_number BETWEEN ? AND ? ORDER BY block_number", + from_block, + to_block, + count, + ) } - fn get_block_context_rows( - &self, - from_block: u64, - to_block: u64, - ) -> SqliteResult> { - let mut stmt = self.prepare_cached( - "SELECT number, hash, parent_hash, timestamp FROM block_headers WHERE number BETWEEN ? AND ? ORDER BY number", - )?; - let rows = stmt.query_map(params![from_block, to_block], BlockContextRow::from_row)?; - rows.collect::, _>>() + + fn get_number_of_events_for_blocks(&self, from_block: u64, to_block: u64) -> SqliteResult { + self.prepare_cached( + "SELECT SUM(event_count) FROM block_headers WHERE number BETWEEN ? AND ?", + )? + .query_row(params![from_block, to_block], |r| r.get::<_, i64>(0)) + .map(|v| v as u64) } } diff --git a/crates/pathfinder/src/test.rs b/crates/pathfinder/src/test.rs index 34ef1270..4e58a028 100644 --- a/crates/pathfinder/src/test.rs +++ b/crates/pathfinder/src/test.rs @@ -1,13 +1,7 @@ -use rusqlite::Connection; - -use crate::{extractor::PathfinderExtractor, fetcher::EventFetcher}; +use crate::{connect, extractor::PathfinderExtractor, fetcher::EventFetcher}; const DB_PATH: &str = "/mnt/store/mainnet.sqlite"; -pub fn make_connection() -> Connection { - Connection::open(DB_PATH).expect("failed to open database") -} - #[test] fn test_emitted_events() { let mut extractor = @@ -24,3 +18,25 @@ fn test_emitted_events() { } } } + +#[test] +fn test_get_emitted_events_with_context() { + let conn = connect(DB_PATH).unwrap(); + let (blocks, events) = conn + .get_emitted_events_with_context(3000000, 3000010) + .expect("failed to fetch events with context"); + println!( + "Fetched {} blocks and {} events", + blocks.len(), + events.len() + ); + for block in blocks { + println!( + "Block {}: hash {} timestamp {}", + block.number, block.hash, block.timestamp + ); + } + for event in events { + println!("{event:?}"); + } +} diff --git a/crates/starknet/Cargo.toml b/crates/starknet/Cargo.toml new file mode 100644 index 00000000..a4c63536 --- /dev/null +++ b/crates/starknet/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "torii-starknet" +version = "0.1.0" +edition = "2021" +description = "StarkNet utilities for Torii" + +[dependencies] +starknet = { workspace = true, optional = true } +serde = { workspace = true, optional = true } + +[dev-dependencies] +pretty_assertions_sorted = "1.2.3" +assert_matches = "1.5.0" +serde_json.workspace = true +bincode = { workspace = true, features = ["serde"] } + +[features] +default = ["serde", "starknet"] +starknet = ["dep:starknet"] +serde = ["dep:serde"] + +[lints] +workspace = true diff --git a/crates/starknet/src/event.rs b/crates/starknet/src/event.rs new file mode 100644 index 00000000..d04e86d6 --- /dev/null +++ b/crates/starknet/src/event.rs @@ -0,0 +1,23 @@ +use crate::Felt; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct EmittedEvent { + pub from_address: Felt, + pub keys: Vec, + pub data: Vec, + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] + pub block_hash: Option, + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] + pub block_number: Option, + pub transaction_hash: Felt, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct Event { + pub from_address: Felt, + pub keys: Vec, + pub data: Vec, +} diff --git a/crates/starknet/src/felt.rs b/crates/starknet/src/felt.rs new file mode 100644 index 00000000..b18d77c8 --- /dev/null +++ b/crates/starknet/src/felt.rs @@ -0,0 +1,725 @@ +use std::borrow::Cow; +use std::error::Error; + +/// Starknet Field Element. +/// +/// A field element is a number 0..p-1 with p=2^{251}+17*2^{192}+1, and it forms +/// the basic building block of most Starknet interactions. +#[repr(transparent)] +#[derive(Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord)] +pub struct Felt(pub(crate) [u8; 32]); + +const MODULUS_U64: [u64; 4] = [576460752303423505u64, 0, 0, 1]; + +impl std::fmt::Debug for Felt { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "0x{self:x}") + } +} + +impl std::fmt::Display for Felt { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{self:x}") + } +} + +impl std::fmt::LowerHex for Felt { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let bytes = &self.0; + let first = bytes.iter().position(|&b| b != 0).unwrap_or(31); + write!(f, "{:x}", bytes[first])?; + bytes[first + 1..] + .iter() + .try_for_each(|&b| write!(f, "{b:02x}")) + } +} + +impl std::fmt::UpperHex for Felt { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let bytes = &self.0; + let first = bytes.iter().position(|&b| b != 0).unwrap_or(31); + write!(f, "{:X}", bytes[first])?; + bytes[first + 1..] + .iter() + .try_for_each(|&b| write!(f, "{b:02X}")) + } +} +impl Default for Felt { + fn default() -> Self { + Felt::ZERO + } +} + +/// Error returned by [Felt::from_be_bytes] indicating the maximum field value +/// was exceeded. +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub struct OverflowError; + +impl Error for OverflowError {} + +const OVERFLOW_MSG: &str = "The maximum field value was exceeded."; + +impl std::fmt::Display for OverflowError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(OVERFLOW_MSG) + } +} + +impl Felt { + pub const ZERO: Felt = Felt([0u8; 32]); + pub const ONE: Felt = Self::from_u64(1); + + /// Return true if the element is zero. + pub fn is_zero(&self) -> bool { + self == &Felt::ZERO + } + + /// Returns the big-endian representation of this [Felt]. + pub const fn to_be_bytes(self) -> [u8; 32] { + self.0 + } + + /// Returns the little-endian representation of this [Felt]. + pub fn to_le_bytes(self) -> [u8; 32] { + let mut tmp = self.0; + tmp.reverse(); + tmp + } + + /// Big-endian representation of this [Felt]. + pub const fn as_be_bytes(&self) -> &[u8; 32] { + &self.0 + } + + /// Big-endian mutable representation of this [Felt]. + pub fn as_mut_be_bytes(&mut self) -> &mut [u8; 32] { + &mut self.0 + } + + /// Convenience function which extends [Felt::from_be_bytes] to work with + /// slices. + pub const fn from_be_slice(bytes: &[u8]) -> Result { + if bytes.len() > 32 { + return Err(OverflowError); + } + + let mut buf = [0u8; 32]; + let mut index = 0; + loop { + if index == bytes.len() { + break; + } + buf[32 - bytes.len() + index] = bytes[index]; + index += 1; + } + + Felt::from_be_bytes(buf) + } + + /// Creates a [Felt] from big-endian bytes. + /// + /// Returns [OverflowError] if not less than the field modulus. + pub const fn from_be_bytes(bytes: [u8; 32]) -> Result { + #[rustfmt::skip] + let limbs = [ + u64::from_be_bytes([ + bytes[0], bytes[1], bytes[2], bytes[3], + bytes[4], bytes[5], bytes[6], bytes[7], + ]), + u64::from_be_bytes([ + bytes[8], bytes[9], bytes[10], bytes[11], + bytes[12], bytes[13], bytes[14], bytes[15], + ]), + u64::from_be_bytes([ + bytes[16], bytes[17], bytes[18], bytes[19], + bytes[20], bytes[21], bytes[22], bytes[23], + ]), + u64::from_be_bytes([ + bytes[24], bytes[25], bytes[26], bytes[27], + bytes[28], bytes[29], bytes[30], bytes[31], + ]), + ]; + + // Loop over each word, if all previous are equal and current is less, we are + // good. + let mut maybe_overflow = true; + let mut i = 0; + while i < 4 && maybe_overflow { + if limbs[i] < MODULUS_U64[i] { + maybe_overflow = false; + } else if limbs[i] > MODULUS_U64[i] { + return Err(OverflowError); + } + i += 1; + } + if maybe_overflow { + Err(OverflowError) + } else { + Ok(Felt(bytes)) + } + } + + /// Creates a Felt from a big-endian byte slice of up to 32 bytes. + /// Panics if slice is longer than 32 bytes. + pub fn from_bytes_be_slice(bytes: &[u8]) -> Self { + assert!(bytes.len() <= 32, "slice too long"); + let mut buf = [0u8; 32]; + buf[32 - bytes.len()..].copy_from_slice(bytes); + Self(buf) + } + + /// Creates a Felt from a little-endian byte slice of up to 32 bytes. + pub fn from_bytes_le_slice(bytes: &[u8]) -> Self { + assert!(bytes.len() <= 32, "slice too long"); + let mut buf = [0u8; 32]; + for (i, &b) in bytes.iter().enumerate() { + buf[31 - i] = b; + } + Self(buf) + } + + /// Returns `true` if the value of [`Felt`] is larger than `2^251 - 1`. + /// + /// Every [`Felt`] that is used to traverse a Merkle-Patricia Tree + /// must not exceed 251 bits, since 251 is the height of the tree. + pub const fn has_more_than_251_bits(&self) -> bool { + self.0[0] & 0b1111_1000 > 0 + } + + pub const fn from_u64(u: u64) -> Self { + const_expect!( + Self::from_be_slice(&u.to_be_bytes()), + "64 bits is less than 251 bits" + ) + } + + pub const fn from_u128(u: u128) -> Self { + const_expect!( + Self::from_be_slice(&u.to_be_bytes()), + "128 bits is less than 251 bits" + ) + } +} + +macro_rules! const_expect { + ($e:expr, $why:expr) => {{ + match $e { + Ok(x) => x, + Err(_) => panic!(concat!("Expectation failed: ", $why)), + } + }}; +} + +use const_expect; + +impl From for Felt { + fn from(value: u64) -> Self { + Self::from_u64(value) + } +} + +impl From for Felt { + fn from(value: usize) -> Self { + Self::from_u64(value.try_into().expect("ptr size is 64 bits")) + } +} + +impl From for Felt { + fn from(value: u128) -> Self { + Self::from_u128(value) + } +} + +impl From<[u8; 32]> for Felt { + fn from(bytes: [u8; 32]) -> Self { + Self(bytes) + } +} + +impl From<[u8; 31]> for Felt { + fn from(bytes: [u8; 31]) -> Self { + let mut buf = [0u8; 32]; + buf[1..].copy_from_slice(&bytes); + Self(buf) + } +} + +impl TryInto for Felt { + type Error = OverflowError; + + fn try_into(self) -> Result { + let initial_zeroes = self.0.iter().take_while(|b| **b == 0).count(); + const EXPECTED_ZEROES: usize = (32 - u128::BITS / u8::BITS) as usize; + + if initial_zeroes < EXPECTED_ZEROES { + return Err(OverflowError); + } + + let bytes = self.0[EXPECTED_ZEROES..] + .try_into() + .expect("Should match u128 size"); + Ok(u128::from_be_bytes(bytes)) + } +} + +impl TryInto for Felt { + type Error = OverflowError; + + fn try_into(self) -> Result { + let initial_zeroes = self.0.iter().take_while(|b| **b == 0).count(); + const EXPECTED_ZEROES: usize = (32 - u64::BITS / u8::BITS) as usize; + + if initial_zeroes < EXPECTED_ZEROES { + return Err(OverflowError); + } + let bytes = self.0[EXPECTED_ZEROES..] + .try_into() + .expect("Should match u64 size"); + Ok(u64::from_be_bytes(bytes)) + } +} + +impl Felt { + /// A convenience function which parses a hex string into a [Felt]. + /// + /// Supports both upper and lower case hex strings, as well as an optional + /// "0x" prefix. + pub const fn from_hex_str(hex_str: &str) -> Result { + const fn parse_hex_digit(digit: u8) -> Result { + match digit { + b'0'..=b'9' => Ok(digit - b'0'), + b'A'..=b'F' => Ok(digit - b'A' + 10), + b'a'..=b'f' => Ok(digit - b'a' + 10), + other => Err(HexParseError::InvalidNibble(other)), + } + } + + let bytes = hex_str.as_bytes(); + let start = if bytes.len() >= 2 && bytes[0] == b'0' && bytes[1] == b'x' { + 2 + } else { + 0 + }; + let len = bytes.len() - start; + + if len > 64 { + return Err(HexParseError::InvalidLength { + max: 64, + actual: bytes.len(), + }); + } + + let mut buf = [0u8; 32]; + + // We want the result in big-endian so reverse iterate over each pair of + // nibbles. let chunks = hex_str.as_bytes().rchunks_exact(2); + + // Handle a possible odd nibble remaining nibble. + if len % 2 == 1 { + let idx = len / 2; + buf[31 - idx] = match parse_hex_digit(bytes[start]) { + Ok(b) => b, + Err(e) => return Err(e), + }; + } + + let chunks = len / 2; + let mut chunk = 0; + + while chunk < chunks { + let lower = match parse_hex_digit(bytes[bytes.len() - chunk * 2 - 1]) { + Ok(b) => b, + Err(e) => return Err(e), + }; + let upper = match parse_hex_digit(bytes[bytes.len() - chunk * 2 - 2]) { + Ok(b) => b, + Err(e) => return Err(e), + }; + buf[31 - chunk] = (upper << 4) | lower; + chunk += 1; + } + + let felt = match Felt::from_be_bytes(buf) { + Ok(felt) => felt, + Err(OverflowError) => return Err(HexParseError::Overflow), + }; + Ok(felt) + } + + /// The first stage of conversion - skip leading zeros + fn skip_zeros(&self) -> (impl Iterator, usize, usize) { + // Skip all leading zero bytes + let it = self.0.iter().skip_while(|&&b| b == 0); + let num_bytes = it.clone().count(); + let skipped = self.0.len() - num_bytes; + // The first high nibble can be 0 + let start = if self.0[skipped] < 0x10 { 1 } else { 2 }; + // Number of characters to display + let len = start + num_bytes * 2; + (it, start, len) + } + + /// The second stage of conversion - map bytes to hex str + fn it_to_hex_str<'a>( + it: impl Iterator, + start: usize, + len: usize, + buf: &'a mut [u8], + ) -> &'a [u8] { + const LUT: [u8; 16] = *b"0123456789abcdef"; + buf[0] = b'0'; + // Same small lookup table is ~25% faster than hex::encode_from_slice 🤷 + it.enumerate().for_each(|(i, &b)| { + let idx = b as usize; + let pos = start + i * 2; + let x = [LUT[(idx & 0xf0) >> 4], LUT[idx & 0x0f]]; + buf[pos..pos + 2].copy_from_slice(&x); + }); + buf[1] = b'x'; + &buf[..len] + } + + /// A convenience function which produces a "0x" prefixed hex str slice in a + /// given buffer `buf` from a [Felt]. + /// Panics if `self.0.len() * 2 + 2 > buf.len()` + pub fn as_hex_str<'a>(&'a self, buf: &'a mut [u8]) -> &'a str { + let expected_buf_len = self.0.len() * 2 + 2; + assert!( + buf.len() >= expected_buf_len, + "buffer size is {}, expected at least {}", + buf.len(), + expected_buf_len + ); + + if !self.0.iter().any(|b| *b != 0) { + return "0x0"; + } + + let (it, start, len) = self.skip_zeros(); + let res = Self::it_to_hex_str(it, start, len, buf); + // Unwrap is safe because `buf` holds valid UTF8 characters. + std::str::from_utf8(res).unwrap() + } + + /// A convenience function which produces a "0x" prefixed hex string from a + /// [Felt]. + pub fn to_hex_str(&self) -> Cow<'static, str> { + if !self.0.iter().any(|b| *b != 0) { + return Cow::from("0x0"); + } + let (it, start, len) = self.skip_zeros(); + let mut buf = vec![0u8; len]; + Self::it_to_hex_str(it, start, len, &mut buf); + // Unwrap is safe as the buffer contains valid utf8 + String::from_utf8(buf).unwrap().into() + } +} + +/// Error returned by [Felt::from_hex_str] indicating an invalid hex string. +#[derive(Debug, PartialEq, Eq)] +pub enum HexParseError { + InvalidNibble(u8), + InvalidLength { max: usize, actual: usize }, + Overflow, +} + +impl Error for HexParseError {} + +impl From for HexParseError { + fn from(_: OverflowError) -> Self { + Self::Overflow + } +} + +impl std::fmt::Display for HexParseError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::InvalidNibble(n) => f.write_fmt(format_args!("Invalid nibble found: 0x{:x}", *n)), + Self::InvalidLength { max, actual } => { + f.write_fmt(format_args!("More than {} digits found: {}", *max, *actual)) + } + Self::Overflow => f.write_str(OVERFLOW_MSG), + } + } +} + +#[cfg(test)] +mod tests { + use pretty_assertions_sorted::assert_eq; + + use super::*; + + const MODULUS_U8: [u8; 32] = [ + 8, 0, 0, 0, 0, 0, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, + ]; + + #[test] + fn bytes_round_trip() { + let original = [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, + 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, + 0x1C, 0x1D, 0x1E, 0x1F, + ]; + let hash = Felt::from_be_bytes(original).unwrap(); + let bytes = hash.to_be_bytes(); + assert_eq!(bytes, original); + } + + #[test] + fn from_bytes_overflow() { + // Field modulus + assert_eq!(Felt::from_be_bytes(MODULUS_U8), Err(OverflowError)); + // Field modulus - 1 + let mut max_val = MODULUS_U8; + max_val[31] -= 1; + Felt::from_be_bytes(max_val).unwrap(); + } + + mod from_be_slice { + use pretty_assertions_sorted::assert_eq; + + use super::*; + + #[test] + fn round_trip() { + let original = Felt::from_hex_str("abcdef0123456789").unwrap(); + let bytes = original.to_be_bytes(); + let result = Felt::from_be_slice(&bytes[..]).unwrap(); + + assert_eq!(result, original); + } + + #[test] + fn too_long() { + let original = Felt::from_hex_str("abcdef0123456789").unwrap(); + let mut bytes = original.to_be_bytes().to_vec(); + bytes.push(0); + Felt::from_be_slice(&bytes[..]).unwrap_err(); + } + + #[test] + fn short_slice() { + let original = Felt::from_hex_str("abcdef0123456789").unwrap(); + let bytes = original.to_be_bytes(); + let result = Felt::from_be_slice(&bytes[24..]); + + assert_eq!(result, Ok(original)); + } + + #[test] + fn max() { + let mut max_val = MODULUS_U8; + max_val[31] -= 1; + Felt::from_be_slice(&max_val[..]).unwrap(); + } + + #[test] + fn overflow() { + assert_eq!(Felt::from_be_slice(&MODULUS_U8[..]), Err(OverflowError)); + } + } + + mod fmt { + use pretty_assertions_sorted::assert_eq; + + use super::Felt; + + #[test] + fn debug() { + let hex_str = "1234567890abcdef000edcba0987654321"; + let felt = Felt::from_hex_str(hex_str).unwrap(); + let result = format!("{felt:?}"); + + let mut expected = "0".repeat(64 - hex_str.len()); + expected.push_str(hex_str); + let expected = format!("Felt({felt})"); + + assert_eq!(result, expected); + } + + #[test] + fn fmt() { + let hex_str = "1234567890abcdef000edcba0987654321"; + let starkhash = Felt::from_hex_str(hex_str).unwrap(); + let result = format!("{starkhash:x}"); + + let mut expected = "0".repeat(64 - hex_str.len()); + expected.push_str(hex_str); + + // We don't really care which casing is used by fmt. + assert_eq!(result.to_lowercase(), expected.to_lowercase()); + } + + #[test] + fn lower_hex() { + let hex_str = "1234567890abcdef000edcba0987654321"; + let starkhash = Felt::from_hex_str(hex_str).unwrap(); + let result = format!("{starkhash:x}"); + + let mut expected = "0".repeat(64 - hex_str.len()); + expected.push_str(hex_str); + + assert_eq!(result, expected.to_lowercase()); + } + + #[test] + fn upper_hex() { + let hex_str = "1234567890abcdef000edcba0987654321"; + let starkhash = Felt::from_hex_str(hex_str).unwrap(); + let result = format!("{starkhash:X}"); + + let mut expected = "0".repeat(64 - hex_str.len()); + expected.push_str(hex_str); + + assert_eq!(result, expected.to_uppercase()); + } + } + + mod from_hex_str { + use assert_matches::assert_matches; + use pretty_assertions_sorted::assert_eq; + + use super::*; + + /// Test hex string with its expected [Felt]. + fn test_data() -> (&'static str, Felt) { + let mut expected = [0; 32]; + expected[31] = 0xEF; + expected[30] = 0xCD; + expected[29] = 0xAB; + expected[28] = 0xef; + expected[27] = 0xcd; + expected[26] = 0xab; + expected[25] = 0x89; + expected[24] = 0x67; + expected[23] = 0x45; + expected[22] = 0x23; + expected[21] = 0x01; + let expected = Felt::from_be_bytes(expected).unwrap(); + + ("0123456789abcdefABCDEF", expected) + } + + #[test] + fn simple() { + let (test_str, expected) = test_data(); + let uut = Felt::from_hex_str(test_str).unwrap(); + assert_eq!(uut, expected); + } + + #[test] + fn prefix() { + let (test_str, expected) = test_data(); + let uut = Felt::from_hex_str(&format!("0x{test_str}")).unwrap(); + assert_eq!(uut, expected); + } + + #[test] + fn leading_zeros() { + let (test_str, expected) = test_data(); + let uut = Felt::from_hex_str(&format!("000000000{test_str}")).unwrap(); + assert_eq!(uut, expected); + } + + #[test] + fn prefix_and_leading_zeros() { + let (test_str, expected) = test_data(); + let uut = Felt::from_hex_str(&format!("0x000000000{test_str}")).unwrap(); + assert_eq!(uut, expected); + } + + #[test] + fn invalid_nibble() { + assert_matches!(Felt::from_hex_str("0x123z").unwrap_err(), HexParseError::InvalidNibble(n) => assert_eq!(n, b'z')) + } + + #[test] + fn invalid_len() { + assert_matches!(Felt::from_hex_str(&"1".repeat(65)).unwrap_err(), HexParseError::InvalidLength{max: 64, actual: n} => assert_eq!(n, 65)) + } + + #[test] + fn overflow() { + // Field modulus + let mut modulus = + "0x800000000000011000000000000000000000000000000000000000000000001".to_string(); + assert_eq!( + Felt::from_hex_str(&modulus).unwrap_err(), + HexParseError::Overflow + ); + // Field modulus - 1 + modulus.pop(); + modulus.push('0'); + Felt::from_hex_str(&modulus).unwrap(); + } + } + + mod to_hex_str { + use pretty_assertions_sorted::assert_eq; + + use super::*; + + const ODD: &str = "0x1234567890abcde"; + const EVEN: &str = "0x1234567890abcdef"; + const MAX: &str = "0x800000000000011000000000000000000000000000000000000000000000000"; + + #[test] + fn zero() { + assert_eq!(Felt::ZERO.to_hex_str(), "0x0"); + let mut buf = [0u8; 66]; + assert_eq!(Felt::ZERO.as_hex_str(&mut buf), "0x0"); + } + + #[test] + fn odd() { + let hash = Felt::from_hex_str(ODD).unwrap(); + assert_eq!(hash.to_hex_str(), ODD); + let mut buf = [0u8; 66]; + assert_eq!(hash.as_hex_str(&mut buf), ODD); + } + + #[test] + fn even() { + let hash = Felt::from_hex_str(EVEN).unwrap(); + assert_eq!(hash.to_hex_str(), EVEN); + let mut buf = [0u8; 66]; + assert_eq!(hash.as_hex_str(&mut buf), EVEN); + } + + #[test] + fn max() { + let hash = Felt::from_hex_str(MAX).unwrap(); + assert_eq!(hash.to_hex_str(), MAX); + let mut buf = [0u8; 66]; + assert_eq!(hash.as_hex_str(&mut buf), MAX); + } + + #[test] + #[should_panic] + fn buffer_too_small() { + let mut buf = [0u8; 65]; + Felt::ZERO.as_hex_str(&mut buf); + } + } + + mod has_more_than_251_bits { + use super::*; + + #[test] + fn has_251_bits() { + let mut bytes = [0xFFu8; 32]; + bytes[0] = 0x07; + let h = Felt::from_be_bytes(bytes).unwrap(); + assert!(!h.has_more_than_251_bits()); + } + + #[test] + fn has_252_bits() { + let mut bytes = [0u8; 32]; + bytes[0] = 0x08; + let h = Felt::from_be_bytes(bytes).unwrap(); + assert!(h.has_more_than_251_bits()); + } + } +} diff --git a/crates/starknet/src/lib.rs b/crates/starknet/src/lib.rs new file mode 100644 index 00000000..54ae3603 --- /dev/null +++ b/crates/starknet/src/lib.rs @@ -0,0 +1,10 @@ +pub mod event; +pub mod felt; + +#[cfg(feature = "serde")] +pub mod serde; + +#[cfg(feature = "starknet")] +pub mod starknet; + +pub use felt::Felt; diff --git a/crates/starknet/src/serde.rs b/crates/starknet/src/serde.rs new file mode 100644 index 00000000..ab41058d --- /dev/null +++ b/crates/starknet/src/serde.rs @@ -0,0 +1,139 @@ +use serde::{Deserialize, Serialize}; + +use crate::Felt; + +impl Serialize for Felt { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + if serializer.is_human_readable() { + serializer.serialize_str(&self.to_hex_str()) + } else { + let bytes = self.as_be_bytes(); + let first = bytes.iter().position(|&b| b != 0).unwrap_or(31); + serializer.serialize_bytes(&bytes[first..]) + } + } +} + +impl<'de> Deserialize<'de> for Felt { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + if deserializer.is_human_readable() { + deserializer.deserialize_str(FeltVisitor) + } else { + deserializer.deserialize_bytes(FeltVisitor) + } + } +} + +struct FeltVisitor; + +impl serde::de::Visitor<'_> for FeltVisitor { + type Value = Felt; + + fn expecting(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "a hex string prefixed with 0x or up to 32 bytes") + } + + fn visit_str(self, value: &str) -> Result { + Felt::from_hex_str(value).map_err(|e| E::custom(e.to_string())) + } + + fn visit_bytes(self, value: &[u8]) -> Result { + if value.len() > 32 { + return Err(E::invalid_length(value.len(), &self)); + } + let mut buf = [0u8; 32]; + buf[32 - value.len()..].copy_from_slice(value); + Ok(Felt(buf)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json; + + #[test] + fn serialize_zero_human_readable() { + let felt = Felt::ZERO; + assert_eq!(serde_json::to_string(&felt).unwrap(), "\"0x0\""); + } + + #[test] + fn serialize_one_human_readable() { + let felt = Felt::ONE; + assert_eq!(serde_json::to_string(&felt).unwrap(), "\"0x1\""); + } + + #[test] + fn serialize_human_readable_round_trip() { + let original = Felt::from_hex_str("0xdeadbeef").unwrap(); + let serialized = serde_json::to_string(&original).unwrap(); + let deserialized: Felt = serde_json::from_str(&serialized).unwrap(); + assert_eq!(original, deserialized); + } + + #[test] + fn serialize_max_human_readable() { + let max = + Felt::from_hex_str("0x800000000000011000000000000000000000000000000000000000000000000") + .unwrap(); + let serialized = serde_json::to_string(&max).unwrap(); + let deserialized: Felt = serde_json::from_str(&serialized).unwrap(); + assert_eq!(max, deserialized); + } + + #[test] + fn deserialize_without_prefix_fails() { + assert!(serde_json::from_str::("\"deadbeef\"").is_err()); + } + + #[test] + fn deserialize_overflow_fails() { + // Field modulus - should fail + assert!(serde_json::from_str::( + "\"0x800000000000011000000000000000000000000000000000000000000000001\"" + ) + .is_err()); + } + + #[test] + fn serialize_binary_round_trip() { + let original = Felt::from_hex_str("0xdeadbeef").unwrap(); + let serialized = + bincode::serde::encode_to_vec(&original, bincode::config::standard()).unwrap(); + let deserialized: Felt = + bincode::serde::decode_from_slice(&serialized, bincode::config::standard()) + .unwrap() + .0; + assert_eq!(original, deserialized); + } + + #[test] + fn serialize_binary_zero() { + let felt = Felt::ZERO; + let serialized = bincode::serde::encode_to_vec(&felt, bincode::config::standard()).unwrap(); + let deserialized: Felt = + bincode::serde::decode_from_slice(&serialized, bincode::config::standard()) + .unwrap() + .0; + assert_eq!(felt, deserialized); + } + + #[test] + fn serialize_binary_compact() { + let felt = Felt::from_hex_str("0xbabe").unwrap(); + let serialized = bincode::serde::encode_to_vec(&felt, bincode::config::standard()).unwrap(); + assert!(serialized.len() < 32); + let deserialized: Felt = + bincode::serde::decode_from_slice(&serialized, bincode::config::standard()) + .unwrap() + .0; + assert_eq!(felt, deserialized); + } +} diff --git a/crates/starknet/src/starknet.rs b/crates/starknet/src/starknet.rs new file mode 100644 index 00000000..d3ac96c7 --- /dev/null +++ b/crates/starknet/src/starknet.rs @@ -0,0 +1,73 @@ +use crate::event::{EmittedEvent, Event}; +use crate::Felt; +use starknet::core::types::Felt as SnFelt; +use starknet::core::types::{EmittedEvent as SnEmittedEvent, Event as SnEvent}; + +impl From for Felt { + fn from(value: SnFelt) -> Self { + Self(value.to_bytes_be()) + } +} + +impl From<&SnFelt> for Felt { + fn from(value: &SnFelt) -> Self { + Self(value.to_bytes_be()) + } +} + +impl From for SnFelt { + fn from(value: Felt) -> Self { + Self::from_bytes_be(&value.0) + } +} +impl From<&Felt> for SnFelt { + fn from(value: &Felt) -> Self { + Self::from_bytes_be(&value.0) + } +} + +impl From for Event { + fn from(value: SnEvent) -> Self { + Self { + from_address: value.from_address.into(), + keys: value.keys.into_iter().map(Into::into).collect(), + data: value.data.into_iter().map(Into::into).collect(), + } + } +} + +impl From for EmittedEvent { + fn from(value: SnEmittedEvent) -> Self { + Self { + from_address: value.from_address.into(), + keys: value.keys.into_iter().map(Into::into).collect(), + data: value.data.into_iter().map(Into::into).collect(), + block_hash: value.block_hash.map(Into::into), + block_number: value.block_number, + transaction_hash: value.transaction_hash.into(), + } + } +} + +impl From for SnEvent { + fn from(value: Event) -> Self { + Self { + from_address: value.from_address.into(), + keys: value.keys.into_iter().map(Into::into).collect(), + data: value.data.into_iter().map(Into::into).collect(), + } + } +} + +impl From for SnEmittedEvent { + fn from(value: EmittedEvent) -> Self { + Self { + from_address: value.from_address.into(), + keys: value.keys.into_iter().map(Into::into).collect(), + data: value.data.into_iter().map(Into::into).collect(), + block_hash: value.block_hash.map(Into::into), + block_number: value.block_number, + transaction_hash: value.transaction_hash.into(), + } + } +} From 88e1a308ecdc536d43862b89270f050c924454fb Mon Sep 17 00:00:00 2001 From: Bengineer Date: Thu, 2 Apr 2026 21:44:52 +0100 Subject: [PATCH 5/7] feat(pathfinder): integrate torii-pathfinder and enhance extractor functionality --- Cargo.lock | 1 + bins/torii-arcade/Cargo.toml | 7 ++- bins/torii-arcade/src/config.rs | 7 +++ bins/torii-arcade/src/main.rs | 37 +++++++++++--- crates/pathfinder/src/extractor.rs | 71 ++++++++++++++++++++++++-- src/etl/extractor/block_range.rs | 4 +- src/etl/extractor/composite.rs | 5 ++ src/etl/extractor/event.rs | 12 +++-- src/etl/extractor/global_event.rs | 4 ++ src/etl/extractor/mod.rs | 4 ++ src/etl/extractor/sample.rs | 3 ++ src/etl/extractor/synthetic_adapter.rs | 1 + src/etl/extractor/synthetic_erc20.rs | 3 ++ 13 files changed, 143 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9e256bdd..60af43bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4907,6 +4907,7 @@ dependencies = [ "torii-erc721", "torii-introspect-postgres-sink", "torii-introspect-sqlite-sink", + "torii-pathfinder", "torii-runtime-common", "torii-sqlite", "tracing", diff --git a/bins/torii-arcade/Cargo.toml b/bins/torii-arcade/Cargo.toml index d3a56e75..0ec65787 100644 --- a/bins/torii-arcade/Cargo.toml +++ b/bins/torii-arcade/Cargo.toml @@ -23,6 +23,7 @@ torii-introspect-postgres-sink = { path = "../../crates/introspect-postgres-sink torii-introspect-sqlite-sink = { path = "../../crates/introspect-sqlite-sink" } torii-runtime-common.workspace = true torii-sqlite.workspace = true +torii-pathfinder = { workspace = true, features = ["etl"] } tokio = { version = "1", features = ["full"] } tracing = "0.1" @@ -31,7 +32,11 @@ starknet = "0.17" url = "2.5" clap = { version = "4.5", features = ["derive", "env"] } anyhow = "1.0" -sqlx = { version = "0.8", features = ["postgres", "sqlite", "runtime-tokio-rustls"] } +sqlx = { version = "0.8", features = [ + "postgres", + "sqlite", + "runtime-tokio-rustls", +] } tonic.workspace = true tonic-reflection.workspace = true tonic-web.workspace = true diff --git a/bins/torii-arcade/src/config.rs b/bins/torii-arcade/src/config.rs index 55052d51..661d5b1b 100644 --- a/bins/torii-arcade/src/config.rs +++ b/bins/torii-arcade/src/config.rs @@ -204,6 +204,9 @@ pub struct Config { #[arg(long, value_delimiter = ',')] pub historical: Vec, + + #[arg(long)] + pub pathfinder_path: Option, } impl Config { @@ -358,6 +361,10 @@ impl Config { _ => bail!("--tls-cert and --tls-key must be provided together"), } } + + pub fn pathfinder_path(&self) -> Option { + self.pathfinder_path.as_ref().map(PathBuf::from) + } } fn is_postgres_url(url: &str) -> bool { diff --git a/bins/torii-arcade/src/main.rs b/bins/torii-arcade/src/main.rs index 6ab09c01..5d91e07c 100644 --- a/bins/torii-arcade/src/main.rs +++ b/bins/torii-arcade/src/main.rs @@ -1,6 +1,6 @@ mod config; -use anyhow::Result; +use anyhow::{Error, Result}; use clap::Parser; use config::{Config, MetadataMode}; use sqlx::postgres::PgPoolOptions; @@ -52,6 +52,7 @@ use torii_erc721::{ }; use torii_introspect_postgres_sink::processor::IntrospectPgDb; use torii_introspect_sqlite_sink::processor::IntrospectSqliteDb; +use torii_pathfinder::extractor::PathfinderCombinedExtractor; use torii_runtime_common::database::{ validate_uniform_backends, DatabaseBackend, DEFAULT_SQLITE_MAX_CONNECTIONS, }; @@ -392,7 +393,7 @@ async fn run_indexer(config: Config) -> Result<()> { &erc721_addresses, &erc1155_addresses, &config, - ); + )?; let (dojo_decoder, introspect_sink): ( Arc, @@ -738,7 +739,7 @@ fn build_extractor( erc721_addresses: &[Felt], erc1155_addresses: &[Felt], config: &Config, -) -> Box { +) -> Result> { let to_block = config.to_block.unwrap_or(u64::MAX); let mut contracts = Vec::new(); let mut seen = HashSet::new(); @@ -771,18 +772,38 @@ fn build_extractor( config.from_block, to_block, ); - - Box::new(EventExtractor::new( - provider, + let event_extractor = EventExtractor::new( + provider.clone(), EventExtractorConfig { - contracts, + contracts: contracts.clone(), chunk_size: config.event_chunk_size, block_batch_size: config.event_block_batch_size, retry_policy: RetryPolicy::default(), ignore_saved_state: config.ignore_saved_state, rpc_parallelism: config.rpc_parallelism, }, - )) + ); + match config.pathfinder_path() { + Some(path) => { + tracing::info!( + "Using Pathfinder for event extraction with endpoint: {}", + path.display() + ); + PathfinderCombinedExtractor::new( + path, + config.event_block_batch_size, + config.from_block, + config.to_block.unwrap_or(u64::MAX), + event_extractor, + ) + .map(|e| Box::new(e) as Box) + .map_err(Error::new) + } + None => { + tracing::info!("Using direct RPC for event extraction"); + Ok(Box::new(event_extractor)) + } + } } fn append_unique_contract_configs( diff --git a/crates/pathfinder/src/extractor.rs b/crates/pathfinder/src/extractor.rs index ef096894..db1f59d5 100644 --- a/crates/pathfinder/src/extractor.rs +++ b/crates/pathfinder/src/extractor.rs @@ -14,6 +14,14 @@ pub struct PathfinderExtractor { pub batch: u64, pub current: u64, pub end: u64, + pub finished: bool, +} + +#[derive(Debug)] +pub struct PathfinderCombinedExtractor { + pub pathfinder: PathfinderExtractor, + pub head: T, + pub on_head: bool, } impl PathfinderExtractor { @@ -23,22 +31,50 @@ impl PathfinderExtractor { batch, current: start, end, + finished: false, }) } pub fn next_batch(&mut self) -> PFResult<(Vec, Vec)> { - let next = (self.current + self.batch).min(self.end); + let mut last = self.current + self.batch; + if last >= self.end { + last = self.end; + self.finished = true; + } let (blocks, events) = self .conn .lock()? - .get_emitted_events_with_context(self.current, next - 1)?; - self.current = next; + .get_emitted_events_with_context(self.current, last)?; + let last_block = blocks.last().map(|b| b.number).unwrap_or(self.current); + if last_block < last { + self.finished = true; + } + self.current = last_block + 1; Ok((blocks, events.into_iter().map(Into::into).collect())) } } +impl PathfinderCombinedExtractor { + pub fn new>( + path: P, + batch: u64, + start: u64, + end: u64, + head: T, + ) -> PFResult { + Ok(Self { + pathfinder: PathfinderExtractor::new(path, batch, start, end)?, + head, + on_head: false, + }) + } +} + #[async_trait] impl Extractor for PathfinderExtractor { + fn set_start_block(&mut self, start_block: u64) { + self.current = start_block.max(self.current); + } async fn extract( &mut self, _cursor: Option, @@ -66,3 +102,32 @@ impl Extractor for PathfinderExtractor { self } } + +#[async_trait] +impl Extractor for PathfinderCombinedExtractor { + fn set_start_block(&mut self, start_block: u64) { + self.pathfinder.set_start_block(start_block); + self.head.set_start_block(start_block); + } + async fn extract( + &mut self, + cursor: Option, + engine_db: &EngineDb, + ) -> AnyResult { + if self.on_head { + self.head.extract(cursor, engine_db).await + } else if self.pathfinder.is_finished() { + self.on_head = true; + self.head.set_start_block(self.pathfinder.current); + self.head.extract(cursor, engine_db).await + } else { + self.pathfinder.extract(cursor, engine_db).await + } + } + fn is_finished(&self) -> bool { + self.pathfinder.is_finished() && self.head.is_finished() + } + fn as_any(&self) -> &dyn std::any::Any { + self + } +} diff --git a/src/etl/extractor/block_range.rs b/src/etl/extractor/block_range.rs index a31f5945..060c279d 100644 --- a/src/etl/extractor/block_range.rs +++ b/src/etl/extractor/block_range.rs @@ -400,7 +400,9 @@ impl Extractor for BlockRangeExtractor { fn is_finished(&self) -> bool { self.reached_end } - + fn set_start_block(&mut self, start_block: u64) { + self.current_block = start_block.max(self.current_block); + } async fn commit_cursor(&mut self, cursor: &str, engine_db: &EngineDb) -> Result<()> { if let Some(block_str) = cursor.strip_prefix("block:") { let block_num: u64 = block_str.parse().context("Invalid cursor format")?; diff --git a/src/etl/extractor/composite.rs b/src/etl/extractor/composite.rs index e0cc18cc..9ad93d84 100644 --- a/src/etl/extractor/composite.rs +++ b/src/etl/extractor/composite.rs @@ -96,6 +96,11 @@ impl CompositeExtractor { #[async_trait] impl Extractor for CompositeExtractor { + fn set_start_block(&mut self, start_block: u64) { + for extractor in &mut self.extractors { + extractor.set_start_block(start_block); + } + } async fn extract( &mut self, _cursor: Option, diff --git a/src/etl/extractor/event.rs b/src/etl/extractor/event.rs index 0b6b4856..5f8d8511 100644 --- a/src/etl/extractor/event.rs +++ b/src/etl/extractor/event.rs @@ -269,6 +269,8 @@ pub struct EventExtractor { /// Per-contract extraction state. contract_states: HashMap, + start_block: u64, + /// Whether the extractor has been initialized. initialized: bool, @@ -294,6 +296,7 @@ impl EventExtractor { initialized: false, chain_head: None, has_following_contracts, + start_block: 0, } } @@ -448,7 +451,7 @@ impl EventExtractor { tracing::trace!( target: "torii::etl::event", contract = %state.state_key(), - from_block = state.current_block, + from_block = state.current_block.max(self.start_block), to_block = range_end, following_head = state.is_following_head(), "Queueing event range request" @@ -457,7 +460,7 @@ impl EventExtractor { tracing::debug!( target: "torii::etl::event", contract = %state.state_key(), - from_block = state.current_block, + from_block = state.current_block.max(self.start_block), to_block = range_end, continuation_token = %state.continuation_token.as_deref().unwrap_or_default(), "Queueing paginated event request" @@ -466,7 +469,7 @@ impl EventExtractor { let request = ProviderRequestData::GetEvents(GetEventsRequest { filter: EventFilterWithPage { event_filter: EventFilter { - from_block: Some(BlockId::Number(state.current_block)), + from_block: Some(BlockId::Number(state.current_block.max(self.start_block))), to_block: Some(BlockId::Number(range_end)), address: Some(state.address), keys: None, @@ -533,6 +536,9 @@ impl EventExtractor { #[async_trait] impl Extractor for EventExtractor { + fn set_start_block(&mut self, start_block: u64) { + self.start_block = start_block; + } async fn extract( &mut self, _cursor: Option, diff --git a/src/etl/extractor/global_event.rs b/src/etl/extractor/global_event.rs index f1b3e89a..d9b115bd 100644 --- a/src/etl/extractor/global_event.rs +++ b/src/etl/extractor/global_event.rs @@ -261,6 +261,10 @@ impl GlobalEventExtractor { #[async_trait] impl Extractor for GlobalEventExtractor { + fn set_start_block(&mut self, start_block: u64) { + self.state.current_block = start_block.max(self.state.current_block); + self.config.from_block = start_block.max(self.config.from_block); + } async fn extract( &mut self, _cursor: Option, diff --git a/src/etl/extractor/mod.rs b/src/etl/extractor/mod.rs index a408461f..541fab4a 100644 --- a/src/etl/extractor/mod.rs +++ b/src/etl/extractor/mod.rs @@ -324,6 +324,10 @@ impl ExtractionBatch { /// Extractor trait for fetching enriched event batches #[async_trait] pub trait Extractor: Send + Sync { + /// Set the starting block for extraction + /// Must be called before the first call to `extract()`. Extractors that support block-based extraction + fn set_start_block(&mut self, start_block: u64); + /// Extract events with enriched context (blocks, transactions) /// /// The cursor parameter is an opaque string that allows resuming from a previous extraction. diff --git a/src/etl/extractor/sample.rs b/src/etl/extractor/sample.rs index b6e42909..54797cb5 100644 --- a/src/etl/extractor/sample.rs +++ b/src/etl/extractor/sample.rs @@ -75,6 +75,9 @@ impl SampleExtractor { #[async_trait] impl Extractor for SampleExtractor { + fn set_start_block(&mut self, start_block: u64) { + self.current_block = start_block.max(self.current_block); + } fn is_finished(&self) -> bool { false // Sample extractor cycles infinitely, never finishes } diff --git a/src/etl/extractor/synthetic_adapter.rs b/src/etl/extractor/synthetic_adapter.rs index c545520b..5d8dca21 100644 --- a/src/etl/extractor/synthetic_adapter.rs +++ b/src/etl/extractor/synthetic_adapter.rs @@ -40,6 +40,7 @@ impl Extractor for SyntheticExtractorAdapter where T: SyntheticExtractor + Send + Sync + 'static, { + fn set_start_block(&mut self, _start_block: u64) {} async fn extract( &mut self, cursor: Option, diff --git a/src/etl/extractor/synthetic_erc20.rs b/src/etl/extractor/synthetic_erc20.rs index 2a655c22..b418ff7d 100644 --- a/src/etl/extractor/synthetic_erc20.rs +++ b/src/etl/extractor/synthetic_erc20.rs @@ -257,6 +257,9 @@ impl SyntheticErc20Extractor { #[async_trait] impl Extractor for SyntheticErc20Extractor { + fn set_start_block(&mut self, start_block: u64) { + self.current_block = start_block.max(self.current_block); + } async fn extract( &mut self, cursor: Option, From b1566fb4b822f6cf8fd4fc4908fb7f47cf0a8dfa Mon Sep 17 00:00:00 2001 From: Bengineer Date: Thu, 2 Apr 2026 22:00:21 +0100 Subject: [PATCH 6/7] feat: optimize extractor logic and improve serialization handling --- bins/torii-arcade/src/main.rs | 3 ++- bins/torii-introspect-synth/src/main.rs | 4 ++++ crates/pathfinder/src/decoding.rs | 4 ++-- crates/pathfinder/src/extractor.rs | 2 +- crates/pathfinder/src/fetcher.rs | 4 ++-- crates/starknet/Cargo.toml | 2 +- crates/starknet/src/felt.rs | 25 +++++++------------------ crates/starknet/src/serde.rs | 11 +++-------- 8 files changed, 22 insertions(+), 33 deletions(-) diff --git a/bins/torii-arcade/src/main.rs b/bins/torii-arcade/src/main.rs index 5d91e07c..f2896b91 100644 --- a/bins/torii-arcade/src/main.rs +++ b/bins/torii-arcade/src/main.rs @@ -773,7 +773,7 @@ fn build_extractor( to_block, ); let event_extractor = EventExtractor::new( - provider.clone(), + provider, EventExtractorConfig { contracts: contracts.clone(), chunk_size: config.event_chunk_size, @@ -783,6 +783,7 @@ fn build_extractor( rpc_parallelism: config.rpc_parallelism, }, ); + #[allow(clippy::single_match_else)] match config.pathfinder_path() { Some(path) => { tracing::info!( diff --git a/bins/torii-introspect-synth/src/main.rs b/bins/torii-introspect-synth/src/main.rs index 9078bd37..9f71e3e6 100644 --- a/bins/torii-introspect-synth/src/main.rs +++ b/bins/torii-introspect-synth/src/main.rs @@ -354,6 +354,10 @@ impl SyntheticIntrospectExtractor { #[async_trait] impl Extractor for SyntheticIntrospectExtractor { + fn set_start_block(&mut self, start_block: u64) { + self.current_block = start_block.max(self.current_block); + } + async fn extract( &mut self, cursor: Option, diff --git a/crates/pathfinder/src/decoding.rs b/crates/pathfinder/src/decoding.rs index cb525918..eb7cd0d1 100644 --- a/crates/pathfinder/src/decoding.rs +++ b/crates/pathfinder/src/decoding.rs @@ -9,7 +9,7 @@ use torii_starknet::event::Event; use torii_starknet::Felt; use zstd::bulk::Decompressor; -/// Mostly taken from pathfinder-common but with some optimizations +// Taken from pathfinder-common but with some optimizations const MAX_EVENTS_UNCOMPRESSED_SIZE: usize = 128usize * 1024 * 1024; @@ -80,7 +80,7 @@ impl MinimalFelt { // Safe: both repr(transparent) over [u8; 32] unsafe { let mut v = std::mem::ManuallyDrop::new(v); - Vec::from_raw_parts(v.as_mut_ptr() as *mut Felt, v.len(), v.capacity()) + Vec::from_raw_parts(v.as_mut_ptr().cast::(), v.len(), v.capacity()) } } } diff --git a/crates/pathfinder/src/extractor.rs b/crates/pathfinder/src/extractor.rs index db1f59d5..46b86456 100644 --- a/crates/pathfinder/src/extractor.rs +++ b/crates/pathfinder/src/extractor.rs @@ -45,7 +45,7 @@ impl PathfinderExtractor { .conn .lock()? .get_emitted_events_with_context(self.current, last)?; - let last_block = blocks.last().map(|b| b.number).unwrap_or(self.current); + let last_block = blocks.last().map_or(self.current, |b| b.number); if last_block < last { self.finished = true; } diff --git a/crates/pathfinder/src/fetcher.rs b/crates/pathfinder/src/fetcher.rs index 330a684a..8f64f919 100644 --- a/crates/pathfinder/src/fetcher.rs +++ b/crates/pathfinder/src/fetcher.rs @@ -111,7 +111,7 @@ impl EventFetcher for Connection { for transaction in block.transactions { let transaction_hash = tx_hashes .next() - .map(|r| r.1.into()) + .map(|r| r.1) .ok_or_else(|| PFError::tx_hash_mismatch(block.block_number))?; for event in transaction { emitted_events.push(EmittedEvent { @@ -153,7 +153,7 @@ impl EventFetcher for Connection { for transaction in block.transactions { let transaction_hash = tx_hashes .next() - .map(|r| r.1.into()) + .map(|r| r.1) .ok_or_else(|| PFError::tx_hash_mismatch(block.block_number))?; for event in transaction { emitted_events.push(EmittedEvent { diff --git a/crates/starknet/Cargo.toml b/crates/starknet/Cargo.toml index a4c63536..9517b699 100644 --- a/crates/starknet/Cargo.toml +++ b/crates/starknet/Cargo.toml @@ -15,7 +15,7 @@ serde_json.workspace = true bincode = { workspace = true, features = ["serde"] } [features] -default = ["serde", "starknet"] +default = ["serde"] starknet = ["dep:starknet"] serde = ["dep:serde"] diff --git a/crates/starknet/src/felt.rs b/crates/starknet/src/felt.rs index b18d77c8..90dbb411 100644 --- a/crates/starknet/src/felt.rs +++ b/crates/starknet/src/felt.rs @@ -531,9 +531,7 @@ mod tests { let felt = Felt::from_hex_str(hex_str).unwrap(); let result = format!("{felt:?}"); - let mut expected = "0".repeat(64 - hex_str.len()); - expected.push_str(hex_str); - let expected = format!("Felt({felt})"); + let expected = format!("0x{felt}"); assert_eq!(result, expected); } @@ -544,11 +542,8 @@ mod tests { let starkhash = Felt::from_hex_str(hex_str).unwrap(); let result = format!("{starkhash:x}"); - let mut expected = "0".repeat(64 - hex_str.len()); - expected.push_str(hex_str); - // We don't really care which casing is used by fmt. - assert_eq!(result.to_lowercase(), expected.to_lowercase()); + assert_eq!(result.to_lowercase(), hex_str.to_lowercase()); } #[test] @@ -557,10 +552,7 @@ mod tests { let starkhash = Felt::from_hex_str(hex_str).unwrap(); let result = format!("{starkhash:x}"); - let mut expected = "0".repeat(64 - hex_str.len()); - expected.push_str(hex_str); - - assert_eq!(result, expected.to_lowercase()); + assert_eq!(result, hex_str.to_lowercase()); } #[test] @@ -569,10 +561,7 @@ mod tests { let starkhash = Felt::from_hex_str(hex_str).unwrap(); let result = format!("{starkhash:X}"); - let mut expected = "0".repeat(64 - hex_str.len()); - expected.push_str(hex_str); - - assert_eq!(result, expected.to_uppercase()); + assert_eq!(result, hex_str.to_uppercase()); } } @@ -631,12 +620,12 @@ mod tests { #[test] fn invalid_nibble() { - assert_matches!(Felt::from_hex_str("0x123z").unwrap_err(), HexParseError::InvalidNibble(n) => assert_eq!(n, b'z')) + assert_matches!(Felt::from_hex_str("0x123z").unwrap_err(), HexParseError::InvalidNibble(n) => assert_eq!(n, b'z')); } #[test] fn invalid_len() { - assert_matches!(Felt::from_hex_str(&"1".repeat(65)).unwrap_err(), HexParseError::InvalidLength{max: 64, actual: n} => assert_eq!(n, 65)) + assert_matches!(Felt::from_hex_str(&"1".repeat(65)).unwrap_err(), HexParseError::InvalidLength{max: 64, actual: n} => assert_eq!(n, 65)); } #[test] @@ -696,7 +685,7 @@ mod tests { } #[test] - #[should_panic] + #[should_panic(expected = "buffer size is 65, expected at least 66")] fn buffer_too_small() { let mut buf = [0u8; 65]; Felt::ZERO.as_hex_str(&mut buf); diff --git a/crates/starknet/src/serde.rs b/crates/starknet/src/serde.rs index ab41058d..1aece7f2 100644 --- a/crates/starknet/src/serde.rs +++ b/crates/starknet/src/serde.rs @@ -88,11 +88,6 @@ mod tests { assert_eq!(max, deserialized); } - #[test] - fn deserialize_without_prefix_fails() { - assert!(serde_json::from_str::("\"deadbeef\"").is_err()); - } - #[test] fn deserialize_overflow_fails() { // Field modulus - should fail @@ -106,7 +101,7 @@ mod tests { fn serialize_binary_round_trip() { let original = Felt::from_hex_str("0xdeadbeef").unwrap(); let serialized = - bincode::serde::encode_to_vec(&original, bincode::config::standard()).unwrap(); + bincode::serde::encode_to_vec(original, bincode::config::standard()).unwrap(); let deserialized: Felt = bincode::serde::decode_from_slice(&serialized, bincode::config::standard()) .unwrap() @@ -117,7 +112,7 @@ mod tests { #[test] fn serialize_binary_zero() { let felt = Felt::ZERO; - let serialized = bincode::serde::encode_to_vec(&felt, bincode::config::standard()).unwrap(); + let serialized = bincode::serde::encode_to_vec(felt, bincode::config::standard()).unwrap(); let deserialized: Felt = bincode::serde::decode_from_slice(&serialized, bincode::config::standard()) .unwrap() @@ -128,7 +123,7 @@ mod tests { #[test] fn serialize_binary_compact() { let felt = Felt::from_hex_str("0xbabe").unwrap(); - let serialized = bincode::serde::encode_to_vec(&felt, bincode::config::standard()).unwrap(); + let serialized = bincode::serde::encode_to_vec(felt, bincode::config::standard()).unwrap(); assert!(serialized.len() < 32); let deserialized: Felt = bincode::serde::decode_from_slice(&serialized, bincode::config::standard()) From 72d1b6c65704ba164d8a48437648605c6eb84ba4 Mon Sep 17 00:00:00 2001 From: MartianGreed Date: Fri, 3 Apr 2026 09:21:46 +0200 Subject: [PATCH 7/7] fix(pathfinder): integration tests now requires file --- crates/pathfinder/src/test.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/pathfinder/src/test.rs b/crates/pathfinder/src/test.rs index 4e58a028..33324343 100644 --- a/crates/pathfinder/src/test.rs +++ b/crates/pathfinder/src/test.rs @@ -3,6 +3,7 @@ use crate::{connect, extractor::PathfinderExtractor, fetcher::EventFetcher}; const DB_PATH: &str = "/mnt/store/mainnet.sqlite"; #[test] +#[ignore = "requires /mnt/store/mainnet.sqlite snapshot"] fn test_emitted_events() { let mut extractor = PathfinderExtractor::new(DB_PATH, 2000, 0, 4000000).expect("failed to create extractor"); @@ -20,6 +21,7 @@ fn test_emitted_events() { } #[test] +#[ignore = "requires /mnt/store/mainnet.sqlite snapshot"] fn test_get_emitted_events_with_context() { let conn = connect(DB_PATH).unwrap(); let (blocks, events) = conn