diff --git a/.env.example b/.env.example index 3424d3323..a06c6e62b 100644 --- a/.env.example +++ b/.env.example @@ -3,6 +3,10 @@ # Rust Environment RUST_LOG=debug +# Mongo +MONGO_CONNECTION_STRING=mongodb+srv:// +MONGO_DATABASE_NAME=Kakarot-Testnet-0 + # Starknet Environment STARKNET_NETWORK= ## Katana specific configurations @@ -15,7 +19,9 @@ MADARA_PRIVATE_KEY=0x00c1cf1490de1352865301bb8705143f3ef938f97fdf892f1090dcb5ac7 # Kakarot Environment KAKAROT_RPC_URL=127.0.0.1:3030 RPC_MAX_CONNECTIONS=100 -## check `./deployments/katana/deployments.json` after running `make devnet` + +# Kakarot Core EVM contract addresses and class hashes, +# respectively deployed and declared on the underlying StarknetOS chain KAKAROT_ADDRESS= PROXY_ACCOUNT_CLASS_HASH= EXTERNALLY_OWNED_ACCOUNT_CLASS_HASH= diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index 72c821d12..03ad5e02a 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -15,6 +15,7 @@ runtimes: - python@3.10.8 lint: disabled: + - yamllint - checkov enabled: - actionlint@1.6.26 @@ -22,16 +23,16 @@ lint: - git-diff-check - hadolint@2.12.0 - markdownlint@0.38.0 - - osv-scanner@1.5.0 + - osv-scanner@1.6.1 - oxipng@9.0.0 - - prettier@3.2.2 + - prettier@3.2.4 - rustfmt@1.65.0 - shellcheck@0.9.0 - shfmt@3.6.0 - taplo@0.8.1 - terrascan@1.18.11 - trivy@0.48.3 - - trufflehog@3.63.9 + - trufflehog@3.64.0 - yamllint@1.33.0 ignore: - linters: [ALL] diff --git a/Cargo.lock b/Cargo.lock index e9abc3126..c0c87a5ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -791,6 +791,27 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "bson" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88c18b51216e1f74b9d769cead6ace2f82b965b807e3d73330aabe9faec31c84" +dependencies = [ + "ahash 0.8.6", + "base64 0.13.1", + "bitvec", + "hex", + "indexmap 1.9.3", + "js-sys", + "once_cell", + "rand", + "serde", + "serde_bytes", + "serde_json", + "time", + "uuid 1.6.1", +] + [[package]] name = "bstr" version = "1.8.0" @@ -1498,15 +1519,15 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.31" +version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +checksum = "41daef31d7a747c5c847246f36de49ced6f7403b4cdabc807a97b5cc184cda7a" dependencies = [ "android-tzdata", "iana-time-zone", "num-traits 0.2.17", "serde", - "windows-targets 0.48.5", + "windows-targets 0.52.0", ] [[package]] @@ -1922,6 +1943,16 @@ dependencies = [ "cipher", ] +[[package]] +name = "darling" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" +dependencies = [ + "darling_core 0.13.4", + "darling_macro 0.13.4", +] + [[package]] name = "darling" version = "0.14.4" @@ -1942,6 +1973,20 @@ dependencies = [ "darling_macro 0.20.3", ] +[[package]] +name = "darling_core" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 1.0.109", +] + [[package]] name = "darling_core" version = "0.14.4" @@ -1970,6 +2015,17 @@ dependencies = [ "syn 2.0.39", ] +[[package]] +name = "darling_macro" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" +dependencies = [ + "darling_core 0.13.4", + "quote", + "syn 1.0.109", +] + [[package]] name = "darling_macro" version = "0.14.4" @@ -2311,6 +2367,12 @@ version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" +[[package]] +name = "downcast" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" + [[package]] name = "dunce" version = "1.0.4" @@ -2404,6 +2466,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "enum-as-inner" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21cdad81446a7f7dc43f6a77409efeb9733d2fa65553efef6018ef257c959b73" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "enumn" version = "0.1.12" @@ -2826,6 +2900,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "finl_unicode" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" + [[package]] name = "fixed-hash" version = "0.8.0" @@ -2954,6 +3034,12 @@ dependencies = [ "walkdir", ] +[[package]] +name = "fragile" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" + [[package]] name = "fs2" version = "0.4.3" @@ -4128,6 +4214,17 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "hostname" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +dependencies = [ + "libc", + "match_cfg", + "winapi", +] + [[package]] name = "http" version = "0.2.11" @@ -4255,6 +4352,17 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "idna" version = "0.4.0" @@ -4442,6 +4550,18 @@ dependencies = [ "winapi", ] +[[package]] +name = "ipconfig" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" +dependencies = [ + "socket2 0.5.5", + "widestring", + "windows-sys 0.48.0", + "winreg", +] + [[package]] name = "ipnet" version = "2.9.0" @@ -4799,6 +4919,7 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", + "auto_impl", "bytes", "ctor", "dojo-test-utils", @@ -4814,6 +4935,8 @@ dependencies = [ "katana-core", "lazy_static", "log", + "mockall", + "mongodb", "reqwest", "reth-primitives", "reth-rlp", @@ -4993,6 +5116,12 @@ dependencies = [ "redox_syscall 0.4.1", ] +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "linux-raw-sys" version = "0.4.11" @@ -5024,6 +5153,21 @@ dependencies = [ "hashbrown 0.12.3", ] +[[package]] +name = "lru-cache" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "match_cfg" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" + [[package]] name = "matchers" version = "0.1.0" @@ -5033,6 +5177,12 @@ dependencies = [ "regex-automata 0.1.10", ] +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + [[package]] name = "matrixmultiply" version = "0.2.4" @@ -5146,6 +5296,33 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "mockall" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43766c2b5203b10de348ffe19f7e54564b64f3d6018ff7648d1e2d6d3a0f0a48" +dependencies = [ + "cfg-if", + "downcast", + "fragile", + "lazy_static", + "mockall_derive", + "predicates", + "predicates-tree", +] + +[[package]] +name = "mockall_derive" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af7cbce79ec385a1d4f54baa90a76401eb15d9cab93685f62e7e9f942aa00ae2" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn 2.0.39", +] + [[package]] name = "modular-bitfield" version = "0.11.2" @@ -5173,6 +5350,53 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4519a88847ba2d5ead3dc53f1060ec6a571de93f325d9c5c4968147382b1cbc3" +[[package]] +name = "mongodb" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c30763a5c6c52079602be44fa360ca3bfacee55fca73f4734aecd23706a7f2" +dependencies = [ + "async-trait", + "base64 0.13.1", + "bitflags 1.3.2", + "bson", + "chrono", + "derivative", + "derive_more", + "futures-core", + "futures-executor", + "futures-io", + "futures-util", + "hex", + "hmac", + "lazy_static", + "md-5", + "pbkdf2 0.11.0", + "percent-encoding", + "rand", + "rustc_version_runtime", + "rustls", + "rustls-pemfile", + "serde", + "serde_bytes", + "serde_with 1.14.0", + "sha-1 0.10.1", + "sha2", + "socket2 0.4.10", + "stringprep", + "strsim", + "take_mut", + "thiserror", + "tokio", + "tokio-rustls", + "tokio-util", + "trust-dns-proto", + "trust-dns-resolver", + "typed-builder 0.10.0", + "uuid 1.6.1", + "webpki-roots 0.25.2", +] + [[package]] name = "ndarray" version = "0.13.1" @@ -6151,6 +6375,16 @@ dependencies = [ "winreg", ] +[[package]] +name = "resolv-conf" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00" +dependencies = [ + "hostname", + "quick-error", +] + [[package]] name = "reth-codecs" version = "0.1.0-alpha.10" @@ -6425,6 +6659,15 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver 0.9.0", +] + [[package]] name = "rustc_version" version = "0.3.3" @@ -6443,6 +6686,16 @@ dependencies = [ "semver 1.0.20", ] +[[package]] +name = "rustc_version_runtime" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d31b7153270ebf48bf91c65ae5b0c00e749c4cfad505f66530ac74950249582f" +dependencies = [ + "rustc_version 0.2.3", + "semver 0.9.0", +] + [[package]] name = "rustix" version = "0.38.25" @@ -6661,7 +6914,7 @@ dependencies = [ "tracing", "tracing-log 0.1.4", "tracing-subscriber", - "typed-builder", + "typed-builder 0.16.2", "url", "walkdir", "which", @@ -6825,13 +7078,22 @@ dependencies = [ "libc", ] +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser 0.7.0", +] + [[package]] name = "semver" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" dependencies = [ - "semver-parser", + "semver-parser 0.10.2", ] [[package]] @@ -6843,6 +7105,12 @@ dependencies = [ "serde", ] +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + [[package]] name = "semver-parser" version = "0.10.2" @@ -6893,6 +7161,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_bytes" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b8497c313fd43ab992087548117643f6fcd935cbf36f176ffda0aacf9591734" +dependencies = [ + "serde", +] + [[package]] name = "serde_derive" version = "1.0.192" @@ -6921,6 +7198,7 @@ version = "1.0.108" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" dependencies = [ + "indexmap 2.1.0", "itoa", "ryu", "serde", @@ -6979,6 +7257,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678b5a069e50bf00ecd22d0cd8ddf7c236f68581b03db652061ed5eb13a312ff" +dependencies = [ + "serde", + "serde_with_macros 1.5.2", +] + [[package]] name = "serde_with" version = "2.3.3" @@ -7012,6 +7300,18 @@ dependencies = [ "time", ] +[[package]] +name = "serde_with_macros" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082" +dependencies = [ + "darling 0.13.4", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "serde_with_macros" version = "2.3.3" @@ -7059,6 +7359,17 @@ dependencies = [ "opaque-debug", ] +[[package]] +name = "sha-1" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + [[package]] name = "sha1" version = "0.10.6" @@ -7216,7 +7527,7 @@ dependencies = [ "httparse", "log", "rand", - "sha-1", + "sha-1 0.9.8", ] [[package]] @@ -7518,6 +7829,17 @@ dependencies = [ "precomputed-hash", ] +[[package]] +name = "stringprep" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb41d74e231a107a1b4ee36bd1214b11285b77768d2e3824aedafa988fd36ee6" +dependencies = [ + "finl_unicode", + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "strsim" version = "0.10.0" @@ -7656,6 +7978,12 @@ dependencies = [ "libc", ] +[[package]] +name = "take_mut" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60" + [[package]] name = "tap" version = "1.0.1" @@ -8111,12 +8439,68 @@ dependencies = [ "tracing-log 0.2.0", ] +[[package]] +name = "trust-dns-proto" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c31f240f59877c3d4bb3b3ea0ec5a6a0cff07323580ff8c7a605cd7d08b255d" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "idna 0.2.3", + "ipnet", + "lazy_static", + "log", + "rand", + "smallvec", + "thiserror", + "tinyvec", + "tokio", + "url", +] + +[[package]] +name = "trust-dns-resolver" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4ba72c2ea84515690c9fcef4c6c660bb9df3036ed1051686de84605b74fd558" +dependencies = [ + "cfg-if", + "futures-util", + "ipconfig", + "lazy_static", + "log", + "lru-cache", + "parking_lot 0.12.1", + "resolv-conf", + "smallvec", + "thiserror", + "tokio", + "trust-dns-proto", +] + [[package]] name = "try-lock" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +[[package]] +name = "typed-builder" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89851716b67b937e393b3daa8423e67ddfc4bbbf1654bcf05488e95e0828db0c" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "typed-builder" version = "0.16.2" @@ -8267,7 +8651,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" dependencies = [ "form_urlencoded", - "idna", + "idna 0.4.0", "percent-encoding", "serde", ] @@ -8295,6 +8679,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560" dependencies = [ "getrandom", + "serde", ] [[package]] @@ -8446,6 +8831,12 @@ dependencies = [ "rustix", ] +[[package]] +name = "widestring" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "653f141f39ec16bba3c5abe400a0c60da7468261cc2cbf36805022876bc721a8" + [[package]] name = "winapi" version = "0.3.9" @@ -8543,6 +8934,21 @@ dependencies = [ "windows_x86_64_msvc 0.48.5", ] +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -8555,6 +8961,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -8567,6 +8979,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -8579,6 +8997,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -8591,6 +9015,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -8603,6 +9033,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -8615,6 +9051,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -8627,6 +9069,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + [[package]] name = "winnow" version = "0.5.19" diff --git a/Cargo.toml b/Cargo.toml index dde767e77..474ea0b2a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,7 @@ serde_with = { version = "2.3.1", default-features = false } # Others anyhow = { version = "1.0.68", default-features = false } async-trait = { version = "0.1.58", default-features = false } +auto_impl = { version = "1.1.0", default-features = false } bytes = { version = "1", default-features = false } ctor = { version = "0.2.4", default-features = false } dotenv = { version = "0.15.0", default-features = false } @@ -53,6 +54,10 @@ futures = { version = "0.3.26", default-features = false } hex = { version = "0.4", default-features = false } lazy_static = { version = "1.4.0", default-features = false } log = { version = "0.4.17", default-features = false } +mockall = { version = "0.12.1", default-features = false } +mongodb = { version = "2.8.0", default-features = false, features = [ + "tokio-runtime", +] } reqwest = { version = "0.11.13", default-features = false } ruint = { version = "1.9.0", default-features = false } rstest = { version = "0.18.1", default-features = false } @@ -83,5 +88,5 @@ cairo-vm = { git = "https://github.com/dojoengine/cairo-rs.git", rev = "262b7eb4 [dev-dependencies] dojo-test-utils = { git = 'https://github.com/dojoengine/dojo', rev = "be16762" } -toml = { version = "0.7.5", default-features = false } rstest = { version = "0.18.1", default-features = false } +toml = { version = "0.7.5", default-features = false } diff --git a/Makefile b/Makefile index fccb7f8de..52410134d 100644 --- a/Makefile +++ b/Makefile @@ -21,21 +21,21 @@ run-dev: # Run Katana, Deploy Kakarot, Run Kakarot RPC katana-rpc-up: - docker compose -f docker-compose.katana.yaml up -d --force-recreate + docker compose up -d --force-recreate # Run Madara, Deploy Kakarot, Run Kakarot RPC madara-rpc-up: - docker compose up -d --force-recreate + docker compose -f docker-compose.madara.yaml up -d --force-recreate docker-down: - docker compose down --remove-orphans + docker compose down --remove-orphans && docker compose rm install-katana: cargo install --git https://github.com/dojoengine/dojo --locked --rev be16762 katana run-katana: install-katana rm -fr .katana/ && mkdir .katana - katana --disable-fee --dump-state .katana/dump.bin & echo $$! > .katana/pid + katana --disable-fee --chain-id=KKRT --dump-state .katana/dump.bin & echo $$! > .katana/pid kill-katana: kill -2 `cat .katana/pid` && rm -fr .katana/pid @@ -56,4 +56,4 @@ benchmark-katana: cd benchmarks && bun i && bun run benchmark-katana -.PHONY: devnet test +.PHONY: test diff --git a/README.md b/README.md index b64ee75ed..48f0efa5f 100644 --- a/README.md +++ b/README.md @@ -71,10 +71,17 @@ This adapter layer is based on: ## Architecture +### High level + Here is a high level overview of the architecture of Kakarot RPC. ![Kakarot RPC Adapter flow](<./docs/images/Kakarot%20RPC%20(lower%20level).png>) +### Low level + +Below is a lower level detailed overview of the internal architecture. +![Kakarot RPC Adapter flow](https://github.com/kkrt-labs/ef-tests/assets/82421016/4b34cbbb-df50-4ce3-9aaa-ed42b80ecd3b) + ## Getting Started TL;DR: diff --git a/docker-compose.katana.yaml b/docker-compose.katana.yaml deleted file mode 100644 index 5d708fc29..000000000 --- a/docker-compose.katana.yaml +++ /dev/null @@ -1,49 +0,0 @@ -# trunk-ignore-all(yamllint/empty-values) -version: "3.2" - -services: - starknet: - image: greged93/katana:v0.4.4 - command: - - "katana" - - "--block-time" - - "6000" - - "--disable-fee" - - "--validate-max-steps" - - "16777216" - - "--invoke-max-steps" - - "16777216" - - "--gas-price" - - "1" - ports: - - "5050:5050" - networks: - - internal - - kakarot-deployer: - extends: - file: docker-compose.yaml - service: kakarot-deployer - environment: - - ACCOUNT_ADDRESS=0x517ececd29116499f4a1b64b094da79ba08dfd54a3edaa316134c41f8160973 - - PRIVATE_KEY=0x1800000000300000180000000000030000000000003006001800006600 - # Custom RPC URL for docker - - RPC_URL=http://starknet:5050 - - deployments-parser: - extends: - file: docker-compose.yaml - service: deployments-parser - - kakarot-rpc: - extends: - file: docker-compose.yaml - service: kakarot-rpc - environment: - - STARKNET_NETWORK=http://starknet:5050 - -networks: - internal: - -volumes: - deployments: diff --git a/docker-compose.madara.yaml b/docker-compose.madara.yaml new file mode 100644 index 000000000..9c1d72bf1 --- /dev/null +++ b/docker-compose.madara.yaml @@ -0,0 +1,65 @@ +version: "3.2" + +services: + starknet: + image: greged93/madara:v0.6.0 + ports: + - 9615:9615 + - 30333:30333 + - 5050:5050 + entrypoint: [/bin/bash, -c] + command: > + "/madara-bin setup --from-remote --chain=dev && /madara-bin --rpc-external --rpc-methods=unsafe --rpc-cors=all --rpc-port 5050 --chain=dev --base-path=/home/madara --alice --force-authoring" + volumes: + - madara:/home/madara + networks: + - internal + restart: on-failure + + kakarot-deployer: + extends: + file: docker-compose.yaml + service: kakarot-deployer + environment: + - ACCOUNT_ADDRESS=0x3 + - PRIVATE_KEY=0x00c1cf1490de1352865301bb8705143f3ef938f97fdf892f1090dcb5ac7bcd1d + + deployments-parser: + extends: + file: docker-compose.yaml + service: deployments-parser + + kakarot-rpc: + extends: + file: docker-compose.yaml + service: kakarot-rpc + + apibara-dna: + extends: + file: docker-compose.yaml + service: apibara-dna + + mongo: + extends: + file: docker-compose.yaml + service: mongo + + clone-repo: + extends: + file: docker-compose.yaml + service: clone-repo + + indexer: + extends: + file: docker-compose.yaml + service: indexer + +networks: + internal: + +volumes: + deployments: + apibara: + mongo_data: + indexer_code: + madara: diff --git a/docker-compose.yaml b/docker-compose.yaml index cbd184f0e..073d8d997 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -2,27 +2,33 @@ version: "3.2" services: starknet: - image: greged93/madara:v0.6.0 + image: greged93/katana:v0.4.4 + command: + - katana + - --block-time + - "6000" + - --disable-fee + - --validate-max-steps + - "16777216" + - --invoke-max-steps + - "16777216" + - --gas-price + - "1" + - --chain-id + - "KKRT" ports: - - "9615:9615" - - "9944:9944" - - "30333:30333" - entrypoint: [/bin/bash, -c] - command: > - "/madara-bin setup --from-remote --base-path=/home/madara --chain=dev && /madara-bin --rpc-external --rpc-methods=unsafe --rpc-cors=all --chain=dev --base-path=/home/madara --alice --force-authoring" - volumes: - - madara:/home/madara + - "5050:5050" networks: - internal + restart: on-failure kakarot-deployer: image: ghcr.io/kkrt-labs/kakarot/deployer:latest environment: - - ACCOUNT_ADDRESS=0x3 - - PRIVATE_KEY=0x00c1cf1490de1352865301bb8705143f3ef938f97fdf892f1090dcb5ac7bcd1d - # Custom RPC URL for docker - - RPC_URL=http://starknet:9944 + - ACCOUNT_ADDRESS=0x517ececd29116499f4a1b64b094da79ba08dfd54a3edaa316134c41f8160973 + - PRIVATE_KEY=0x1800000000300000180000000000030000000000003006001800006600 - EVM_PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 + - RPC_URL=http://starknet:5050 volumes: # Since STARKNET_NETWORK is not set, deployments will be saved without network folder - deployments:/app/kakarot/deployments @@ -55,13 +61,17 @@ services: kakarot-rpc: image: ghcr.io/kkrt-labs/kakarot-rpc/node:latest + # Always pull the latest image, until we use release tags + # TODO: use release tags + pull_policy: always ports: - 3030:3030 environment: - KAKAROT_RPC_URL=0.0.0.0:3030 - - STARKNET_NETWORK=http://starknet:9944 + - STARKNET_NETWORK=http://starknet:5050 - RUST_LOG=kakarot_rpc=info - - DEPLOYER_ACCOUNT_PRIVATE_KEY=0x0288a51c164874bb6a1ca7bd1cb71823c234a86d0f7b150d70fa8f06de645396 + - MONGO_CONNECTION_STRING=mongodb://mongo:mongo@mongo:27017 + - MONGO_DATABASE_NAME=kakarot-local volumes: # Mount the volume on workdir and use .env stored in root of the volume - deployments:/usr/src/app @@ -72,9 +82,84 @@ services: networks: - internal + apibara-dna: + image: quay.io/apibara/starknet:1.2.0 + command: + - start + - --rpc=http://starknet:5050 + - --wait-for-rpc + - --data=/data + ports: + - 7171:7171 + volumes: + - apibara:/data + networks: + - internal + restart: on-failure + depends_on: + kakarot-deployer: + condition: service_completed_successfully + starknet: + condition: service_healthy + + ### MongoDB with Mongo Express + mongo: + image: mongo:6.0.8 + restart: always + ports: + - 27017:27017 + volumes: + - mongo_data:/data/db + networks: + - internal + environment: + MONGO_INITDB_ROOT_USERNAME: mongo + MONGO_INITDB_ROOT_PASSWORD: mongo + + clone-repo: + image: docker.io/alpine/git:latest + entrypoint: "/bin/sh" + command: + - "-c" + # clone the repository in `/code`, removing any old copy. + - "cd /code && rm -rf kakarot-indexer && git clone -v https://github.com/kkrt-labs/kakarot-indexer.git" + volumes: + - "indexer_code:/code" + restart: on-failure + + indexer: + image: quay.io/apibara/sink-mongo + command: + - run + - /code/kakarot-indexer/src/main.ts + environment: + # Whitelist environment variables + - ALLOW_ENV_FROM_ENV=DEBUG,APIBARA_AUTH_TOKEN,STARTING_BLOCK,STREAM_URL,SINK_TYPE,MONGO_CONNECTION_STRING,MONGO_DATABASE_NAME + - DEBUG="" + - APIBARA_AUTH_TOKEN="" + - MONGO_CONNECTION_STRING=mongodb://mongo:mongo@mongo:27017 + - MONGO_DATABASE_NAME=kakarot-local + - STARTING_BLOCK=0 + - STREAM_URL=http://apibara-dna:7171 + - SINK_TYPE=mongo + restart: on-failure + volumes: + - "indexer_code:/code" + networks: + - internal + depends_on: + clone-repo: + condition: service_completed_successfully + starknet: + condition: service_healthy + kakarot-deployer: + condition: service_completed_successfully + networks: internal: volumes: deployments: - madara: + apibara: + mongo_data: + indexer_code: diff --git a/src/eth_provider/database/mod.rs b/src/eth_provider/database/mod.rs new file mode 100644 index 000000000..a34d48e5c --- /dev/null +++ b/src/eth_provider/database/mod.rs @@ -0,0 +1,53 @@ +pub mod types; + +use futures::TryStreamExt; +use mongodb::{ + bson::Document, + options::{FindOneOptions, FindOptions}, + Database as MongoDatabase, +}; +use serde::de::DeserializeOwned; + +use super::provider::EthProviderResult; + +/// Wrapper around a MongoDB database +pub struct Database(MongoDatabase); + +impl Database { + pub fn new(database: MongoDatabase) -> Self { + Self(database) + } + + /// Get a list of documents from a collection + pub async fn get( + &self, + collection: &str, + filter: impl Into>, + project: impl Into>, + ) -> EthProviderResult> { + let find_options = FindOptions::builder().projection(project).build(); + let collection = self.0.collection::(collection); + let result = collection.find(filter, find_options).await?.try_collect().await?; + Ok(result) + } + + /// Get a single document from a collection + pub async fn get_one( + &self, + collection: &str, + filter: impl Into>, + sort: impl Into>, + ) -> EthProviderResult> { + let find_one_option = FindOneOptions::builder().sort(sort).build(); + let collection = self.0.collection::(collection); + let result = collection.find_one(filter, find_one_option).await?; + Ok(result) + } + + /// Count the number of documents in a collection matching the filter + pub async fn count(&self, collection: &str, filter: impl Into>) -> EthProviderResult { + let collection = self.0.collection::(collection); + let count = collection.count_documents(filter, None).await?; + Ok(count) + } +} diff --git a/src/eth_provider/database/types/header.rs b/src/eth_provider/database/types/header.rs new file mode 100644 index 000000000..50553ce5f --- /dev/null +++ b/src/eth_provider/database/types/header.rs @@ -0,0 +1,9 @@ +use reth_rpc_types::Header; +use serde::Deserialize; + +/// A header as stored in the database +#[derive(Debug, Deserialize)] +pub struct StoredHeader { + #[serde(deserialize_with = "crate::eth_provider::database::types::serde::deserialize_intermediate")] + pub header: Header, +} diff --git a/src/eth_provider/database/types/log.rs b/src/eth_provider/database/types/log.rs new file mode 100644 index 000000000..5ed977db5 --- /dev/null +++ b/src/eth_provider/database/types/log.rs @@ -0,0 +1,15 @@ +use reth_rpc_types::Log; +use serde::Deserialize; + +/// A transaction receipt as stored in the database +#[derive(Debug, Deserialize)] +pub struct StoredLog { + #[serde(deserialize_with = "crate::eth_provider::database::types::serde::deserialize_intermediate")] + pub log: Log, +} + +impl From for Log { + fn from(log: StoredLog) -> Self { + log.log + } +} diff --git a/src/eth_provider/database/types/mod.rs b/src/eth_provider/database/types/mod.rs new file mode 100644 index 000000000..ef99dce01 --- /dev/null +++ b/src/eth_provider/database/types/mod.rs @@ -0,0 +1,5 @@ +pub mod header; +pub mod log; +pub mod receipt; +pub mod serde; +pub mod transaction; diff --git a/src/eth_provider/database/types/receipt.rs b/src/eth_provider/database/types/receipt.rs new file mode 100644 index 000000000..cf985d945 --- /dev/null +++ b/src/eth_provider/database/types/receipt.rs @@ -0,0 +1,15 @@ +use reth_rpc_types::TransactionReceipt; +use serde::Deserialize; + +/// A transaction receipt as stored in the database +#[derive(Debug, Deserialize)] +pub struct StoredTransactionReceipt { + #[serde(deserialize_with = "crate::eth_provider::database::types::serde::deserialize_intermediate")] + receipt: TransactionReceipt, +} + +impl From for TransactionReceipt { + fn from(receipt: StoredTransactionReceipt) -> Self { + receipt.receipt + } +} diff --git a/src/eth_provider/database/types/serde/mod.rs b/src/eth_provider/database/types/serde/mod.rs new file mode 100644 index 000000000..128db4ec7 --- /dev/null +++ b/src/eth_provider/database/types/serde/mod.rs @@ -0,0 +1,27 @@ +use std::collections::HashMap; + +use serde::{de::value::MapDeserializer, Deserialize, Deserializer}; +use serde_json::Value; + +/// Used in order to perform a custom deserialization of the stored +/// Ethereum data from the database. All the primitive types are stored +/// as strings in the database. This caused problems when deserializing. +/// +/// # Example +/// +/// The database stores {"hash": "0x1234"}. This gets serialized to +/// "{\"hash\":\"0x1234\"}". It's not possible to deserialize \"0x1234\" +/// into a U64 or H256 type from reth_primitives (since \"0x1234\" is the +/// serialized representation of the string "0x1234"). This function provides +/// a custom deserialization that first deserializes the data into a +/// HashMap, which can them be used to deserialize into the +/// desired types. +pub fn deserialize_intermediate<'de, D, T>(deserializer: D) -> Result +where + D: Deserializer<'de>, + T: Deserialize<'de>, +{ + let s: HashMap = HashMap::deserialize(deserializer)?; + let deserializer = MapDeserializer::new(s.into_iter()); + T::deserialize(deserializer).map_err(|err: serde_json::Error| serde::de::Error::custom(err.to_string())) +} diff --git a/src/eth_provider/database/types/transaction.rs b/src/eth_provider/database/types/transaction.rs new file mode 100644 index 000000000..eb3f2e0de --- /dev/null +++ b/src/eth_provider/database/types/transaction.rs @@ -0,0 +1,39 @@ +use reth_primitives::H256; +use reth_rpc_types::Transaction; +use serde::Deserialize; + +/// A full transaction as stored in the database +#[derive(Debug, Deserialize)] +pub struct StoredTransaction { + #[serde(deserialize_with = "crate::eth_provider::database::types::serde::deserialize_intermediate")] + pub tx: Transaction, +} + +impl From for Transaction { + fn from(tx: StoredTransaction) -> Self { + tx.tx + } +} + +/// A transaction hash as stored in the database +/// This wrapper is used to deserialize a transaction +/// from the database, on which a projection was +/// performed in order to only return the transaction +/// hash (e.g. {tx: {hash: "0x1234"}}) +#[derive(Debug, Deserialize)] +pub struct StoredTransactionHash { + #[serde(rename = "tx")] + pub tx_hash: Hash, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Hash { + pub hash: H256, +} + +impl From for H256 { + fn from(hash: StoredTransactionHash) -> Self { + hash.tx_hash.hash + } +} diff --git a/src/eth_provider/error.rs b/src/eth_provider/error.rs new file mode 100644 index 000000000..dcd59bbc4 --- /dev/null +++ b/src/eth_provider/error.rs @@ -0,0 +1,40 @@ +use jsonrpsee::types::ErrorObject; +use starknet::providers::ProviderError as StarknetProviderError; +use thiserror::Error; + +use crate::{ + models::errors::ConversionError, + starknet_client::errors::{rpc_err, EthRpcErrorCode}, +}; + +/// Error that can occur when interacting with the database. +#[derive(Debug, Error)] +pub enum EthProviderError { + /// MongoDB error. + #[error(transparent)] + MongoDbError(#[from] mongodb::error::Error), + /// Starknet Provider error. + #[error(transparent)] + StarknetProviderError(#[from] StarknetProviderError), + /// Contract call error. + #[error(transparent)] + ContractCallError(#[from] starknet_abigen_parser::cairo_types::Error), + /// Conversion error. + #[error(transparent)] + ConversionError(#[from] ConversionError), + /// Value not found in the database. + #[error("Did not find value in the database.")] + ValueNotFound, +} + +impl From for jsonrpsee::core::Error { + fn from(err: EthProviderError) -> Self { + Self::Call(err.into()) + } +} + +impl From for ErrorObject<'static> { + fn from(value: EthProviderError) -> Self { + rpc_err(EthRpcErrorCode::InternalError, value.to_string()) + } +} diff --git a/src/eth_provider/mod.rs b/src/eth_provider/mod.rs new file mode 100644 index 000000000..9b9809230 --- /dev/null +++ b/src/eth_provider/mod.rs @@ -0,0 +1,5 @@ +pub mod database; +pub mod error; +pub mod provider; +pub mod starknet; +pub mod utils; diff --git a/src/eth_provider/provider.rs b/src/eth_provider/provider.rs new file mode 100644 index 000000000..b3b65f67e --- /dev/null +++ b/src/eth_provider/provider.rs @@ -0,0 +1,415 @@ +use async_trait::async_trait; +use auto_impl::auto_impl; +use eyre::Result; +use mockall::automock; +use mongodb::bson::doc; +use mongodb::bson::Document; +use reth_primitives::Address; +use reth_primitives::BlockId; +use reth_primitives::Bytes; +use reth_primitives::{BlockNumberOrTag, H256, U256, U64}; +use reth_rpc_types::Filter; +use reth_rpc_types::FilterChanges; +use reth_rpc_types::Index; +use reth_rpc_types::Transaction; +use reth_rpc_types::TransactionReceipt; +use reth_rpc_types::ValueOrArray; +use reth_rpc_types::{Block, BlockTransactions, RichBlock}; +use reth_rpc_types::{SyncInfo, SyncStatus}; +use starknet::core::types::BlockId as StarknetBlockId; +use starknet::core::types::SyncStatusType; +use starknet::core::utils::get_contract_address; +use starknet::core::utils::get_storage_var_address; +use starknet::providers::Provider as StarknetProvider; +use starknet_crypto::FieldElement; + +use super::database::types::log::StoredLog; +use super::database::types::{ + header::StoredHeader, receipt::StoredTransactionReceipt, transaction::StoredTransaction, + transaction::StoredTransactionHash, +}; +use super::database::Database; +use super::starknet::kakarot_core::{ + ContractAccountReader, ProxyReader, CONTRACT_ACCOUNT_CLASS_HASH, EXTERNALLY_OWNED_ACCOUNT_CLASS_HASH, + KAKAROT_ADDRESS, PROXY_ACCOUNT_CLASS_HASH, +}; +use super::starknet::ERC20Reader; +use super::starknet::STARKNET_NATIVE_TOKEN; +use super::utils::iter_into; +use super::utils::split_u256; +use super::utils::try_from_u8_iterator; +use super::{error::EthProviderError, utils::into_filter}; +use crate::eth_provider::utils::format_hex; +use crate::into_via_wrapper; +use crate::models::block::EthBlockId; +use crate::models::errors::ConversionError; +use crate::models::felt::Felt252Wrapper; + +pub type EthProviderResult = Result; + +/// Ethereum provider trait. Used to abstract away the database and the network. +#[async_trait] +#[auto_impl(Arc)] +#[automock] +pub trait EthereumProvider { + /// Returns the latest block number. + async fn block_number(&self) -> EthProviderResult; + /// Returns the syncing status. + async fn syncing(&self) -> EthProviderResult; + /// Returns the chain id. + async fn chain_id(&self) -> EthProviderResult>; + /// Returns a block by hash. Block can be full or just the hashes of the transactions. + async fn block_by_hash(&self, hash: H256, full: bool) -> EthProviderResult>; + /// Returns a block by number. Block can be full or just the hashes of the transactions. + async fn block_by_number( + &self, + number_or_tag: BlockNumberOrTag, + full: bool, + ) -> EthProviderResult>; + /// Returns the transaction count for a block by hash. + async fn block_transaction_count_by_hash(&self, hash: H256) -> EthProviderResult; + /// Returns the transaction count for a block by number. + async fn block_transaction_count_by_number(&self, number_or_tag: BlockNumberOrTag) -> EthProviderResult; + /// Returns the transaction by hash. + async fn transaction_by_hash(&self, hash: H256) -> EthProviderResult>; + /// Returns the transaction by block hash and index. + async fn transaction_by_block_hash_and_index( + &self, + hash: H256, + index: Index, + ) -> EthProviderResult>; + /// Returns the transaction by block number and index. + async fn transaction_by_block_number_and_index( + &self, + number_or_tag: BlockNumberOrTag, + index: Index, + ) -> EthProviderResult>; + /// Returns the transaction receipt by hash of the transaction. + async fn transaction_receipt(&self, hash: H256) -> EthProviderResult>; + /// Returns the balance of an address. + async fn balance(&self, address: Address, block_id: Option) -> EthProviderResult; + /// Returns the storage of an address at a certain index. + async fn storage_at(&self, address: Address, index: U256, block_id: Option) -> EthProviderResult; + /// Returns the nonce for the address at the given block. + async fn transaction_count(&self, address: Address, block_id: Option) -> EthProviderResult; + /// Returns the code for the address at the given block. + async fn get_code(&self, address: Address, block_id: Option) -> EthProviderResult; + /// Returns the logs for the given filter + async fn get_logs(&self, filter: Filter) -> EthProviderResult; +} + +/// Structure that implements the EthereumProvider trait. +/// Uses an access to a database to certain data, while +/// the rest is fetched from the Starknet Provider. +pub struct EthDataProvider +where + SP: StarknetProvider + Send + Sync, +{ + database: Database, + starknet_provider: SP, +} + +#[async_trait] +impl EthereumProvider for EthDataProvider +where + SP: StarknetProvider + Send + Sync, +{ + async fn block_number(&self) -> EthProviderResult { + let filter = doc! {}; + let sort = doc! { "header.number": -1 }; + let header: Option = self.database.get_one("headers", filter, sort).await?; + let block_number = match header { + None => self.starknet_provider.block_number().await?.into(), // in case the database is empty, use the starknet provider + Some(header) => { + let number = header.header.number.ok_or(EthProviderError::ValueNotFound)?; + let n = number.as_le_bytes_trimmed(); + // Block number is U64 + if n.len() > 8 { + return Err(ConversionError::ValueOutOfRange("Block number too large".to_string()).into()); + } + U64::from_little_endian(n.as_ref()) + } + }; + Ok(block_number) + } + + async fn syncing(&self) -> EthProviderResult { + let syncing_status = self.starknet_provider.syncing().await?; + + match syncing_status { + SyncStatusType::NotSyncing => Ok(SyncStatus::None), + + SyncStatusType::Syncing(data) => { + let starting_block: U256 = U256::from(data.starting_block_num); + let current_block: U256 = U256::from(data.current_block_num); + let highest_block: U256 = U256::from(data.highest_block_num); + + let status_info = SyncInfo { + starting_block, + current_block, + highest_block, + warp_chunks_amount: None, + warp_chunks_processed: None, + }; + + Ok(SyncStatus::Info(status_info)) + } + } + } + + async fn chain_id(&self) -> EthProviderResult> { + let chain_id = self.starknet_provider.chain_id().await?; + let chain_id: Option = chain_id.try_into().ok(); + Ok(chain_id.map(Into::into)) + } + + async fn block_by_hash(&self, hash: H256, full: bool) -> EthProviderResult> { + let header_filter = into_filter("header.hash", hash, 64); + let tx_filter = into_filter("tx.blockHash", hash, 64); + let block = self.block(header_filter, tx_filter, full).await?; + + Ok(block) + } + + async fn block_by_number( + &self, + number_or_tag: BlockNumberOrTag, + full: bool, + ) -> EthProviderResult> { + let block_number = self.tag_into_block_number(number_or_tag).await?; + + let header_filter = into_filter("header.number", block_number, 64); + let tx_filter = into_filter("tx.blockNumber", block_number, 64); + let block = self.block(header_filter, tx_filter, full).await?; + + Ok(block) + } + + async fn block_transaction_count_by_hash(&self, hash: H256) -> EthProviderResult { + let filter = into_filter("tx.blockHash", hash, 64); + let count = self.database.count("transactions", filter).await?; + Ok(count.into()) + } + + async fn block_transaction_count_by_number(&self, number_or_tag: BlockNumberOrTag) -> EthProviderResult { + let block_number = self.tag_into_block_number(number_or_tag).await?; + + let filter = into_filter("tx.blockNumber", block_number, 64); + let count = self.database.count("transactions", filter).await?; + Ok(count.into()) + } + + async fn transaction_by_hash(&self, hash: H256) -> EthProviderResult> { + let filter = into_filter("tx.hash", hash, 64); + let tx: Option = self.database.get_one("transactions", filter, None).await?; + Ok(tx.map(Into::into)) + } + + async fn transaction_by_block_hash_and_index( + &self, + hash: H256, + index: Index, + ) -> EthProviderResult> { + let mut filter = into_filter("tx.blockHash", hash, 64); + let index: usize = index.into(); + filter.insert("tx.transactionIndex", index as i32); + let tx: Option = self.database.get_one("transactions", filter, None).await?; + Ok(tx.map(Into::into)) + } + + async fn transaction_by_block_number_and_index( + &self, + number_or_tag: BlockNumberOrTag, + index: Index, + ) -> EthProviderResult> { + let block_number = self.tag_into_block_number(number_or_tag).await?; + let mut filter = into_filter("tx.blockNumber", block_number, 64); + let index: usize = index.into(); + filter.insert("tx.transactionIndex", index as i32); + let tx: Option = self.database.get_one("transactions", filter, None).await?; + Ok(tx.map(Into::into)) + } + + async fn transaction_receipt(&self, hash: H256) -> EthProviderResult> { + let filter = into_filter("receipt.transactionHash", hash, 64); + let tx: Option = self.database.get_one("receipts", filter, None).await?; + Ok(tx.map(Into::into)) + } + + async fn balance(&self, address: Address, block_id: Option) -> EthProviderResult { + let eth_block_id = EthBlockId::new(block_id.unwrap_or(BlockId::Number(BlockNumberOrTag::Latest))); + let starknet_block_id: StarknetBlockId = eth_block_id.try_into()?; + + let eth_contract = ERC20Reader::new(*STARKNET_NATIVE_TOKEN, &self.starknet_provider); + + let address = self.starknet_address(address); + let balance = eth_contract.balanceOf(&address).block_id(starknet_block_id).call().await?; + + let low: U256 = into_via_wrapper!(balance.low); + let high: U256 = into_via_wrapper!(balance.high); + Ok(low + (high << 128)) + } + + async fn storage_at(&self, address: Address, index: U256, block_id: Option) -> EthProviderResult { + let eth_block_id = EthBlockId::new(block_id.unwrap_or(BlockId::Number(BlockNumberOrTag::Latest))); + let starknet_block_id: StarknetBlockId = eth_block_id.try_into()?; + + let address = self.starknet_address(address); + let contract = ContractAccountReader::new(address, &self.starknet_provider); + + let keys = split_u256::(index); + let storage_address = get_storage_var_address("storage_", &keys).expect("Storage var name is not ASCII"); + + let storage = contract.storage(&storage_address).block_id(starknet_block_id).call().await?; + + let low: U256 = into_via_wrapper!(storage.low); + let high: U256 = into_via_wrapper!(storage.high); + Ok(low + (high << 128)) + } + + async fn transaction_count(&self, address: Address, block_id: Option) -> EthProviderResult { + let eth_block_id = EthBlockId::new(block_id.unwrap_or(BlockId::Number(BlockNumberOrTag::Latest))); + let starknet_block_id: StarknetBlockId = eth_block_id.try_into()?; + + let address = self.starknet_address(address); + let proxy = ProxyReader::new(address, &self.starknet_provider); + let address_class_hash = proxy.get_implementation().block_id(starknet_block_id).call().await?; + + let nonce = if address_class_hash == *EXTERNALLY_OWNED_ACCOUNT_CLASS_HASH { + self.starknet_provider.get_nonce(starknet_block_id, address).await? + } else if address_class_hash == *CONTRACT_ACCOUNT_CLASS_HASH { + let contract = ContractAccountReader::new(address, &self.starknet_provider); + contract.get_nonce().block_id(starknet_block_id).call().await? + } else { + FieldElement::ZERO + }; + Ok(into_via_wrapper!(nonce)) + } + + async fn get_code(&self, address: Address, block_id: Option) -> EthProviderResult { + let eth_block_id = EthBlockId::new(block_id.unwrap_or(BlockId::Number(BlockNumberOrTag::Latest))); + let starknet_block_id: StarknetBlockId = eth_block_id.try_into()?; + + let address = self.starknet_address(address); + let contract = ContractAccountReader::new(address, &self.starknet_provider); + let (_, bytecode) = contract.bytecode().block_id(starknet_block_id).call().await?; + + Ok(Bytes::from(try_from_u8_iterator::<_, Vec>(bytecode.0.into_iter()))) + } + + async fn get_logs(&self, filter: Filter) -> EthProviderResult { + let current_block = self.block_number().await?.low_u64(); + let from = filter.get_from_block().unwrap_or_default(); + let to = filter.get_to_block().unwrap_or(current_block); + + let (from, to) = match (from, to) { + (from, _) if from > current_block => return Ok(FilterChanges::Empty), + (from, to) if to > current_block => (from, current_block), + (from, to) if to < from => return Ok(FilterChanges::Empty), + _ => (from, to), + }; + + // Convert the topics to a vector of H256 + let topics = filter + .topics + .into_iter() + .filter_map(|t| t.to_value_or_array()) + .flat_map(|t| match t { + ValueOrArray::Value(topic) => vec![topic], + ValueOrArray::Array(topics) => topics, + }) + .collect::>(); + + // Create the database filter. We filter by block number using $gte and $lte, + // and by topics using $expr and $eq. The topics query will: + // 1. Slice the topics array to the same length as the filter topics + // 2. Match on values for which the sliced topics equal the filter topics + let mut database_filter = doc! { + "log.blockNumber": {"$gte": format_hex(from, 64), "$lte": format_hex(to, 64)}, + "$expr": { + "$eq": [ + { "$slice": ["$log.topics", topics.len() as i32] }, + topics.into_iter().map(|t| format_hex(t, 64)).collect::>() + ] + } + }; + + // Add the address filter if any + let addresses = filter.address.to_value_or_array().map(|a| match a { + ValueOrArray::Value(address) => vec![address], + ValueOrArray::Array(addresses) => addresses, + }); + addresses.map(|adds| { + database_filter + .insert("log.address", doc! {"$in": adds.into_iter().map(|a| format_hex(a, 40)).collect::>()}) + }); + + let logs: Vec = self.database.get("logs", database_filter, None).await?; + Ok(FilterChanges::Logs(logs.into_iter().map(Into::into).collect())) + } +} + +impl EthDataProvider +where + SP: StarknetProvider + Send + Sync, +{ + pub fn new(database: Database, starknet_provider: SP) -> Self { + Self { database, starknet_provider } + } + + /// Get a block from the database based on the header and transaction filters + /// If full is true, the block will contain the full transactions, otherwise just the hashes + async fn block( + &self, + header_filter: impl Into>, + transactions_filter: impl Into>, + full: bool, + ) -> EthProviderResult> { + let header = self.database.get_one::("headers", header_filter, None).await?; + let header = match header { + Some(header) => header, + None => return Ok(None), + }; + let total_difficulty = Some(header.header.difficulty); + + let transactions = if full { + BlockTransactions::Full(iter_into( + self.database.get::("transactions", transactions_filter, None).await?, + )) + } else { + BlockTransactions::Hashes(iter_into( + self.database + .get::("transactions", transactions_filter, doc! {"tx.hash": 1}) + .await?, + )) + }; + + let block = Block { + header: header.header, + transactions, + total_difficulty, + uncles: Vec::new(), + size: None, + withdrawals: None, + }; + + Ok(Some(block.into())) + } + + /// Convert the given BlockNumberOrTag into a block number + async fn tag_into_block_number(&self, tag: BlockNumberOrTag) -> EthProviderResult { + match tag { + BlockNumberOrTag::Earliest => Ok(U64::zero()), + BlockNumberOrTag::Number(number) => Ok(number.into()), + BlockNumberOrTag::Latest | BlockNumberOrTag::Finalized | BlockNumberOrTag::Safe => { + self.block_number().await + } + BlockNumberOrTag::Pending => todo!("pending block number not implemented"), + } + } + + /// Compute the starknet address given a eth address + fn starknet_address(&self, address: Address) -> FieldElement { + get_contract_address(into_via_wrapper!(address), *PROXY_ACCOUNT_CLASS_HASH, &[], *KAKAROT_ADDRESS) + } +} diff --git a/src/eth_provider/starknet/kakarot_core.rs b/src/eth_provider/starknet/kakarot_core.rs new file mode 100644 index 000000000..eb0dba186 --- /dev/null +++ b/src/eth_provider/starknet/kakarot_core.rs @@ -0,0 +1,26 @@ +use dotenv::dotenv; +use lazy_static::lazy_static; +use starknet_abigen_macros::abigen_legacy; +use starknet_abigen_parser; +use starknet_crypto::FieldElement; + +fn env_var_to_field_element(var_name: &str) -> FieldElement { + dotenv().ok(); + let env_var = std::env::var(var_name).unwrap_or_else(|_| panic!("Missing environment variable {var_name}")); + + FieldElement::from_hex_be(&env_var).unwrap_or_else(|_| panic!("Invalid hex string for {var_name}")) +} + +lazy_static! { + // Contract addresses + pub static ref KAKAROT_ADDRESS: FieldElement = env_var_to_field_element("KAKAROT_ADDRESS"); + + // Contract class hashes + pub static ref PROXY_ACCOUNT_CLASS_HASH: FieldElement = env_var_to_field_element("PROXY_ACCOUNT_CLASS_HASH"); + pub static ref EXTERNALLY_OWNED_ACCOUNT_CLASS_HASH: FieldElement = + env_var_to_field_element("EXTERNALLY_OWNED_ACCOUNT_CLASS_HASH"); + pub static ref CONTRACT_ACCOUNT_CLASS_HASH: FieldElement = env_var_to_field_element("CONTRACT_ACCOUNT_CLASS_HASH"); +} + +abigen_legacy!(Proxy, "./artifacts/proxy.json"); +abigen_legacy!(ContractAccount, "./artifacts/contract_account.json"); diff --git a/src/eth_provider/starknet/mod.rs b/src/eth_provider/starknet/mod.rs new file mode 100644 index 000000000..97c92a680 --- /dev/null +++ b/src/eth_provider/starknet/mod.rs @@ -0,0 +1,14 @@ +#![allow(non_snake_case)] +pub mod kakarot_core; + +use lazy_static::lazy_static; +use starknet_abigen_macros::abigen_legacy; +use starknet_abigen_parser; +use starknet_crypto::FieldElement; + +abigen_legacy!(ERC20, "./artifacts/fixtures/ERC20.json"); + +lazy_static! { + pub static ref STARKNET_NATIVE_TOKEN: FieldElement = + FieldElement::from_hex_be("0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7").unwrap(); +} diff --git a/src/eth_provider/utils.rs b/src/eth_provider/utils.rs new file mode 100644 index 000000000..37f734c51 --- /dev/null +++ b/src/eth_provider/utils.rs @@ -0,0 +1,35 @@ +use std::fmt::LowerHex; + +use mongodb::bson::{doc, Document}; +use reth_primitives::{U128, U256}; + +/// Converts an iterator of `Into` into a `Vec`. +pub(crate) fn iter_into>(iter: impl IntoIterator) -> Vec { + iter.into_iter().map(Into::into).collect::>() +} + +/// Converts an iterator of `TryInto` into a `FromIterator`. +pub(crate) fn try_from_u8_iterator, T: FromIterator>(it: impl Iterator) -> T { + it.filter_map(|x| TryInto::::try_into(x).ok()).collect() +} + +pub(crate) fn format_hex(value: impl LowerHex, width: usize) -> String { + format!("0x{:0width$x}", value, width = width) +} + +/// Converts a key and value into a MongoDB filter. +pub(crate) fn into_filter(key: &str, value: T, width: usize) -> Document +where + T: LowerHex, +{ + doc! {key: format_hex(value, width)} +} + +/// Helper function to split a U256 value into two generic values +/// implementing the From trait +pub fn split_u256>(value: U256) -> [T; 2] { + let low: u128 = (value & U256::from(U128::MAX)).try_into().unwrap(); // safe to unwrap + let high: U256 = value >> 128; + let high: u128 = high.try_into().unwrap(); // safe to unwrap + [T::from(low), T::from(high)] +} diff --git a/src/eth_rpc/rpc.rs b/src/eth_rpc/rpc.rs index e85f85c64..eb223dc5a 100644 --- a/src/eth_rpc/rpc.rs +++ b/src/eth_rpc/rpc.rs @@ -7,6 +7,7 @@ use jsonrpsee::core::Error; use jsonrpsee::{Methods, RpcModule}; use starknet::providers::Provider; +use crate::eth_provider::provider::EthereumProvider; use crate::eth_rpc::api::alchemy_api::AlchemyApiServer; use crate::eth_rpc::api::eth_api::EthApiServer; use crate::eth_rpc::api::net_api::NetApiServer; @@ -25,14 +26,22 @@ pub enum KakarotRpcModule { Net, } -pub struct KakarotRpcModuleBuilder { +pub struct KakarotRpcModuleBuilder +where + P: Provider + Send + Sync, + DB: EthereumProvider + Send + Sync, +{ modules: HashMap, - _phantom: PhantomData

, + _phantom: PhantomData<(P, DB)>, } -impl KakarotRpcModuleBuilder

{ - pub fn new(kakarot_client: Arc>) -> Self { - let eth_rpc_module = KakarotEthRpc::new(kakarot_client.clone()).into_rpc(); +impl KakarotRpcModuleBuilder +where + P: Provider + Send + Sync + 'static, + DB: EthereumProvider + Send + Sync + 'static, +{ + pub fn new(kakarot_client: Arc>, eth_provider: DB) -> Self { + let eth_rpc_module = KakarotEthRpc::new(eth_provider, kakarot_client.clone()).into_rpc(); let alchemy_rpc_module = AlchemyRpc::new(kakarot_client.clone()).into_rpc(); let web3_rpc_module = Web3Rpc::default().into_rpc(); let net_rpc_module = NetRpc::new(kakarot_client.clone()).into_rpc(); diff --git a/src/eth_rpc/servers/eth_rpc.rs b/src/eth_rpc/servers/eth_rpc.rs index 17d91c93e..96ddd87fc 100644 --- a/src/eth_rpc/servers/eth_rpc.rs +++ b/src/eth_rpc/servers/eth_rpc.rs @@ -1,293 +1,167 @@ use std::sync::Arc; -use crate::models::block::EthBlockId; -use crate::models::event::StarknetEvent; -use crate::models::event_filter::EthEventFilter; -use crate::models::felt::Felt252Wrapper; -use crate::models::transaction::transaction::StarknetTransaction; -use crate::models::transaction_receipt::StarknetTransactionReceipt as TransactionReceiptWrapper; -use crate::starknet_client::constants::{CHAIN_ID, CHUNK_SIZE_LIMIT}; use crate::starknet_client::errors::EthApiError; -use crate::starknet_client::helpers::try_from_u8_iterator; -use crate::starknet_client::{ContractAccountReader, KakarotClient}; -use crate::{into_via_try_wrapper, into_via_wrapper}; +use crate::{eth_provider::provider::EthereumProvider, starknet_client::KakarotClient}; use jsonrpsee::core::{async_trait, RpcResult as Result}; use reth_primitives::{AccessListWithGasUsed, Address, BlockId, BlockNumberOrTag, Bytes, H256, H64, U128, U256, U64}; use reth_rpc_types::{ - CallRequest, EIP1186AccountProofResponse, FeeHistory, Filter, FilterChanges, Index, RichBlock, SyncInfo, - SyncStatus, Transaction as EtherTransaction, TransactionReceipt, TransactionRequest, Work, + CallRequest, EIP1186AccountProofResponse, FeeHistory, Filter, FilterChanges, Index, RichBlock, SyncStatus, + Transaction as EtherTransaction, TransactionReceipt, TransactionRequest, Work, }; use serde_json::Value; -use starknet::core::types::{ - BlockId as StarknetBlockId, Event, EventFilterWithPage, FieldElement, MaybePendingTransactionReceipt, - ResultPageRequest, SyncStatusType, TransactionReceipt as StarknetTransactionReceipt, -}; use starknet::providers::Provider; use crate::eth_rpc::api::eth_api::EthApiServer; /// The RPC module for the Ethereum protocol required by Kakarot. -pub struct KakarotEthRpc { - pub kakarot_client: Arc>, +pub struct KakarotEthRpc +where + P: EthereumProvider, + SP: Provider + Send + Sync, +{ + pub eth_provider: P, + // TODO remove kakaort_client from here + pub kakarot_client: Arc>, } -impl KakarotEthRpc

{ - pub fn new(kakarot_client: Arc>) -> Self { - Self { kakarot_client } +impl KakarotEthRpc +where + P: EthereumProvider, + SP: Provider + Send + Sync, +{ + pub fn new(eth_provider: P, kakarot_client: Arc>) -> Self { + Self { eth_provider, kakarot_client } } } #[async_trait] -impl EthApiServer for KakarotEthRpc

{ - #[tracing::instrument(skip_all, ret)] +impl EthApiServer for KakarotEthRpc +where + P: EthereumProvider + Send + Sync + 'static, + SP: Provider + Send + Sync + 'static, +{ + #[tracing::instrument(skip_all, ret, err)] async fn block_number(&self) -> Result { - let block_number = self.kakarot_client.starknet_provider().block_number().await.map_err(EthApiError::from)?; - Ok(block_number.into()) + Ok(self.eth_provider.block_number().await?) } - #[tracing::instrument(skip_all, ret)] + #[tracing::instrument(skip_all, ret, err)] async fn syncing(&self) -> Result { - let status = self.kakarot_client.starknet_provider().syncing().await.map_err(EthApiError::from)?; - - match status { - SyncStatusType::NotSyncing => Ok(SyncStatus::None), - - SyncStatusType::Syncing(data) => { - let starting_block: U256 = U256::from(data.starting_block_num); - let current_block: U256 = U256::from(data.current_block_num); - let highest_block: U256 = U256::from(data.highest_block_num); - - let status_info = SyncInfo { - starting_block, - current_block, - highest_block, - warp_chunks_amount: None, - warp_chunks_processed: None, - }; - - Ok(SyncStatus::Info(status_info)) - } - } + Ok(self.syncing().await?) } async fn coinbase(&self) -> Result

{ Err(EthApiError::MethodNotSupported("eth_coinbase".to_string()).into()) } - #[tracing::instrument(skip_all, ret)] + #[tracing::instrument(skip_all, ret, err)] async fn accounts(&self) -> Result> { Ok(Vec::new()) } - #[tracing::instrument(skip_all, ret)] + #[tracing::instrument(skip_all, ret, err)] async fn chain_id(&self) -> Result> { - Ok(Some(CHAIN_ID.into())) + Ok(self.eth_provider.chain_id().await?) } - #[tracing::instrument(skip_all, ret, fields(hash = %hash))] + #[tracing::instrument(skip_all, ret, err, fields(hash = %hash))] async fn block_by_hash(&self, hash: H256, full: bool) -> Result> { - let block_id = EthBlockId::new(BlockId::Hash(hash.into())); - let starknet_block_id: StarknetBlockId = block_id.try_into().map_err(EthApiError::from)?; - let block = self.kakarot_client.get_eth_block_from_starknet_block(starknet_block_id, full).await?; - Ok(Some(block)) + Ok(self.eth_provider.block_by_hash(hash, full).await?) } - #[tracing::instrument(skip_all, ret, fields(number = %number, full = full))] + #[tracing::instrument(skip_all, ret, err, fields(number = %number, full = full))] async fn block_by_number(&self, number: BlockNumberOrTag, full: bool) -> Result> { - let block_id = EthBlockId::new(BlockId::Number(number)); - let starknet_block_id: StarknetBlockId = block_id.try_into().map_err(EthApiError::from)?; - let block = self.kakarot_client.get_eth_block_from_starknet_block(starknet_block_id, full).await?; - Ok(Some(block)) + Ok(self.eth_provider.block_by_number(number, full).await?) } - #[tracing::instrument(skip_all, ret, fields(hash = %hash))] + #[tracing::instrument(skip_all, ret, err, fields(hash = %hash))] async fn block_transaction_count_by_hash(&self, hash: H256) -> Result { - let block_id = BlockId::Hash(hash.into()); - let count = self.kakarot_client.get_transaction_count_by_block(block_id).await.map_err(EthApiError::from)?; - Ok(count) + Ok(self.eth_provider.block_transaction_count_by_hash(hash).await?) } - #[tracing::instrument(skip_all, ret, fields(number = %number))] + #[tracing::instrument(skip_all, ret, err, fields(number = %number))] async fn block_transaction_count_by_number(&self, number: BlockNumberOrTag) -> Result { - let block_id = BlockId::Number(number); - let count = self.kakarot_client.get_transaction_count_by_block(block_id).await.map_err(EthApiError::from)?; - Ok(count) + Ok(self.eth_provider.block_transaction_count_by_number(number).await?) } + #[tracing::instrument(skip_all, ret, err, fields(hash = %_hash))] async fn block_uncles_count_by_block_hash(&self, _hash: H256) -> Result { - Err(EthApiError::MethodNotSupported("eth_getUncleCountByBlockHash".to_string()).into()) + tracing::warn!("Kakarot chain does not produce uncles"); + Ok(U256::ZERO) } + #[tracing::instrument(skip_all, ret, err, fields(number = %_number))] async fn block_uncles_count_by_block_number(&self, _number: BlockNumberOrTag) -> Result { - Err(EthApiError::MethodNotSupported("eth_getUncleCountByBlockNumber".to_string()).into()) + tracing::warn!("Kakarot chain does not produce uncles"); + Ok(U256::ZERO) } + #[tracing::instrument(skip_all, ret, err, fields(hash = %_hash, index = ?_index))] async fn uncle_by_block_hash_and_index(&self, _hash: H256, _index: Index) -> Result> { - Err(EthApiError::MethodNotSupported("eth_getUncleByBlockHashAndIndex".to_string()).into()) + tracing::warn!("Kakarot chain does not produce uncles"); + Ok(None) } + #[tracing::instrument(skip_all, ret, err, fields(hash = %_number, index = ?_index))] async fn uncle_by_block_number_and_index( &self, _number: BlockNumberOrTag, _index: Index, ) -> Result> { - Err(EthApiError::MethodNotSupported("eth_getUncleByBlockNumberAndIndex".to_string()).into()) + tracing::warn!("Kakarot chain does not produce uncles"); + Ok(None) } - #[tracing::instrument(skip_all, ret, fields(hash = %hash))] + #[tracing::instrument(skip_all, ret, err, fields(hash = %hash))] async fn transaction_by_hash(&self, hash: H256) -> Result> { - let hash: Felt252Wrapper = hash.try_into().map_err(EthApiError::from)?; - let hash: FieldElement = hash.into(); - - let transaction: StarknetTransaction = - match self.kakarot_client.starknet_provider().get_transaction_by_hash(hash).await { - Err(_) => return Ok(None), - Ok(transaction) => transaction.into(), - }; - - let tx_receipt = match self.kakarot_client.starknet_provider().get_transaction_receipt(hash).await { - Err(_) => return Ok(None), - Ok(receipt) => receipt, - }; - - let (block_hash, block_num) = match tx_receipt { - MaybePendingTransactionReceipt::Receipt(StarknetTransactionReceipt::Invoke(tr)) => { - (Some(into_via_wrapper!(tr.block_hash)), Some(U256::from(tr.block_number))) - } - _ => (None, None), // skip all transactions other than Invoke, covers the pending case - }; - let eth_transaction = transaction.to_eth_transaction(&self.kakarot_client, block_hash, block_num, None).await?; - Ok(Some(eth_transaction)) - } - - #[tracing::instrument(skip_all, ret, fields(hash = %hash, index = ?index))] + Ok(self.eth_provider.transaction_by_hash(hash).await?) + } + + #[tracing::instrument(skip_all, ret, err, fields(hash = %hash, index = ?index))] async fn transaction_by_block_hash_and_index(&self, hash: H256, index: Index) -> Result> { - let block_id = BlockId::Hash(hash.into()); - let tx = self.kakarot_client.transaction_by_block_id_and_index(block_id, index).await?; - Ok(Some(tx)) + Ok(self.eth_provider.transaction_by_block_hash_and_index(hash, index).await?) } - #[tracing::instrument(skip_all, ret, fields(number = %number, index = ?index))] + #[tracing::instrument(skip_all, ret, err, fields(number = %number, index = ?index))] async fn transaction_by_block_number_and_index( &self, number: BlockNumberOrTag, index: Index, ) -> Result> { - let block_id = BlockId::Number(number); - let tx = self.kakarot_client.transaction_by_block_id_and_index(block_id, index).await?; - Ok(Some(tx)) + Ok(self.eth_provider.transaction_by_block_number_and_index(number, index).await?) } - #[tracing::instrument(skip_all, ret, fields(hash = %hash))] + #[tracing::instrument(skip_all, ret, err, fields(hash = %hash))] async fn transaction_receipt(&self, hash: H256) -> Result> { - // TODO: Error when trying to transform 32 bytes hash to FieldElement - let transaction_hash = into_via_try_wrapper!(hash); - let starknet_tx_receipt: TransactionReceiptWrapper = match self - .kakarot_client - .starknet_provider() - .get_transaction_receipt::(transaction_hash) - .await - { - Err(_) => return Ok(None), - Ok(receipt) => receipt, - } - .into(); - - let res_receipt = starknet_tx_receipt.to_eth_transaction_receipt(&self.kakarot_client).await?; - Ok(res_receipt) - } - - #[tracing::instrument(skip_all, ret, fields(address = %address, block_id = ?block_id))] + Ok(self.eth_provider.transaction_receipt(hash).await?) + } + + #[tracing::instrument(skip_all, ret, err, fields(address = %address, block_id = ?block_id))] async fn balance(&self, address: Address, block_id: Option) -> Result { - let block_id = block_id.unwrap_or(BlockId::Number(BlockNumberOrTag::Latest)); - let balance = self.kakarot_client.balance(address, block_id).await?; - Ok(balance) + Ok(self.eth_provider.balance(address, block_id).await?) } - #[tracing::instrument(skip_all, ret, fields(address = %address, index = ?index, block_id = ?block_id))] + #[tracing::instrument(skip_all, ret, err, fields(address = %address, index = ?index, block_id = ?block_id))] async fn storage_at(&self, address: Address, index: U256, block_id: Option) -> Result { - let block_id = block_id.unwrap_or(BlockId::Number(BlockNumberOrTag::Latest)); - let value = self.kakarot_client.storage_at(address, index, block_id).await?; - Ok(value) + Ok(self.eth_provider.storage_at(address, index, block_id).await?) } - #[tracing::instrument(skip_all, ret, fields(address = %address, block_id = ?block_id))] + #[tracing::instrument(skip_all, ret, err, fields(address = %address, block_id = ?block_id))] async fn transaction_count(&self, address: Address, block_id: Option) -> Result { - let block_id = block_id.unwrap_or(BlockId::Number(BlockNumberOrTag::Latest)); - - let transaction_count = self.kakarot_client.nonce(address, block_id).await?; - - Ok(transaction_count) + Ok(self.eth_provider.transaction_count(address, block_id).await?) } - #[tracing::instrument(skip_all, ret, fields(address = %address, block_id = ?block_id))] + #[tracing::instrument(skip_all, ret, err, fields(address = %address, block_id = ?block_id))] async fn get_code(&self, address: Address, block_id: Option) -> Result { - let block_id = block_id.unwrap_or(BlockId::Number(BlockNumberOrTag::Latest)); - let starknet_block_id: StarknetBlockId = EthBlockId::new(block_id).try_into().map_err(EthApiError::from)?; - - let starknet_contract_address = self.kakarot_client.compute_starknet_address(&address).await?; - - let provider = self.kakarot_client.starknet_provider(); - - // Get the nonce of the contract account -> a storage variable - let contract_account = ContractAccountReader::new(starknet_contract_address, provider); - let (_, bytecode) = - contract_account.bytecode().block_id(starknet_block_id).call().await.map_err(EthApiError::from)?; - Ok(Bytes::from(try_from_u8_iterator::<_, Vec>(bytecode.0.into_iter()))) + Ok(self.eth_provider.get_code(address, block_id).await?) } - #[tracing::instrument(skip_all, ret, fields(filter = ?filter))] + #[tracing::instrument(skip_all, ret, err, fields(filter = ?filter))] async fn get_logs(&self, filter: Filter) -> Result { - // Check the block range - let current_block: u64 = - self.kakarot_client.starknet_provider().block_number().await.map_err(EthApiError::from)?; - let from_block = filter.get_from_block(); - let to_block = filter.get_to_block(); - - let filter = match (from_block, to_block) { - (Some(from), _) if from > current_block => return Ok(FilterChanges::Empty), - (_, Some(to)) if to > current_block => filter.to_block(current_block), - (Some(from), Some(to)) if to < from => return Ok(FilterChanges::Empty), - _ => filter, - }; - - // Convert the eth log filter to a starknet event filter - let filter: EthEventFilter = filter.into(); - let event_filter = filter.to_starknet_event_filter(&self.kakarot_client.clone())?; - - // Filter events - let events = self - .kakarot_client - .filter_events(EventFilterWithPage { - event_filter, - result_page_request: ResultPageRequest { continuation_token: None, chunk_size: CHUNK_SIZE_LIMIT }, - }) - .await?; - - // Convert events to eth logs - let logs = events - .into_iter() - .filter_map(|emitted| { - let event: StarknetEvent = - Event { from_address: emitted.from_address, keys: emitted.keys, data: emitted.data }.into(); - let block_hash = into_via_wrapper!(emitted.block_hash); - let transaction_hash = into_via_wrapper!(emitted.transaction_hash); - event - .to_eth_log( - &self.kakarot_client.clone(), - Some(block_hash), - Some(U256::from(emitted.block_number)), - Some(transaction_hash), - None, - None, - ) - .ok() - }) - .collect::>(); - Ok(FilterChanges::Logs(logs)) + Ok(self.eth_provider.get_logs(filter).await?) } - #[tracing::instrument(skip_all, ret, fields(request = ?request, block_id = ?block_id))] + #[tracing::instrument(skip_all, ret, err, fields(request = ?request, block_id = ?block_id))] async fn call(&self, request: CallRequest, block_id: Option) -> Result { let block_id = block_id.unwrap_or(BlockId::Number(BlockNumberOrTag::Latest)); let result = self.kakarot_client.call(request, block_id).await?; @@ -310,13 +184,13 @@ impl EthApiServer for KakarotEthRpc

{ Ok(self.kakarot_client.estimate_gas(request, block_id).await?) } - #[tracing::instrument(skip_all, ret)] + #[tracing::instrument(skip_all, ret, err)] async fn gas_price(&self) -> Result { let gas_price = self.kakarot_client.base_fee_per_gas(); Ok(gas_price) } - #[tracing::instrument(skip_all, ret, fields(block_count = %block_count, newest_block = %newest_block, reward_percentiles = ?reward_percentiles))] + #[tracing::instrument(skip_all, ret, err, fields(block_count = %block_count, newest_block = %newest_block, reward_percentiles = ?reward_percentiles))] async fn fee_history( &self, block_count: U256, @@ -328,7 +202,7 @@ impl EthApiServer for KakarotEthRpc

{ Ok(fee_history) } - #[tracing::instrument(skip_all, ret)] + #[tracing::instrument(skip_all, ret, err)] async fn max_priority_fee_per_gas(&self) -> Result { let max_priority_fee = self.kakarot_client.max_priority_fee_per_gas(); Ok(max_priority_fee) @@ -358,7 +232,7 @@ impl EthApiServer for KakarotEthRpc

{ Err(EthApiError::MethodNotSupported("eth_sendTransaction".to_string()).into()) } - #[tracing::instrument(skip_all, ret, fields(bytes = %bytes))] + #[tracing::instrument(skip_all, ret, err, fields(bytes = %bytes))] async fn send_raw_transaction(&self, bytes: Bytes) -> Result { let transaction_hash = self.kakarot_client.send_transaction(bytes).await?; Ok(transaction_hash) diff --git a/src/lib.rs b/src/lib.rs index d96871c6a..5b2e727ec 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ pub mod contracts; +pub mod eth_provider; pub mod eth_rpc; pub mod models; pub mod starknet_client; diff --git a/src/main.rs b/src/main.rs index e4ea14a4a..bcf745e5c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,13 +2,16 @@ use std::sync::Arc; use dotenv::dotenv; use eyre::Result; +use kakarot_rpc::eth_provider::database::Database; +use kakarot_rpc::eth_provider::provider::EthDataProvider; use kakarot_rpc::eth_rpc::config::RPCConfig; use kakarot_rpc::eth_rpc::rpc::KakarotRpcModuleBuilder; use kakarot_rpc::eth_rpc::run_server; use kakarot_rpc::starknet_client::config::{ - JsonRpcClientBuilder, KakarotRpcConfig, Network, SequencerGatewayProviderBuilder, + env_var, JsonRpcClientBuilder, KakarotRpcConfig, Network, SequencerGatewayProviderBuilder, }; use kakarot_rpc::starknet_client::KakarotClient; +use mongodb::options::{DatabaseOptions, ReadConcern, WriteConcern}; use starknet::providers::jsonrpc::HttpTransport; use starknet::providers::{JsonRpcClient, SequencerGatewayProvider}; use tracing_subscriber::util::SubscriberInitExt; @@ -22,7 +25,6 @@ enum StarknetProvider { async fn main() -> Result<()> { dotenv().ok(); // Environment variables are safe to use after this - let filter = tracing_subscriber::EnvFilter::try_from_default_env()?; tracing_subscriber::FmtSubscriber::builder().with_env_filter(filter).finish().try_init()?; @@ -30,7 +32,7 @@ async fn main() -> Result<()> { let rpc_config = RPCConfig::from_env()?; - let starknet_provider: StarknetProvider = match &starknet_config.network { + let starknet_provider = match &starknet_config.network { Network::Madara | Network::Katana | Network::Sharingan => { StarknetProvider::JsonRpcClient(JsonRpcClientBuilder::with_http(&starknet_config).unwrap().build()) } @@ -42,16 +44,27 @@ async fn main() -> Result<()> { ), }; + let db_client = mongodb::Client::with_uri_str( + env_var("MONGO_CONNECTION_STRING").expect("Missing MONGO_CONNECTION_STRING .env"), + ) + .await?; + let db = Database::new(db_client.database_with_options( + &env_var("MONGO_DATABASE_NAME").expect("Missing MONGO_DATABASE_NAME from .env"), + DatabaseOptions::builder().read_concern(ReadConcern::MAJORITY).write_concern(WriteConcern::MAJORITY).build(), + )); + let kakarot_rpc_module = match starknet_provider { StarknetProvider::JsonRpcClient(starknet_provider) => { let starknet_provider = Arc::new(starknet_provider); - let kakarot_client = Arc::new(KakarotClient::new(starknet_config, starknet_provider)); - KakarotRpcModuleBuilder::new(kakarot_client).rpc_module() + let kakarot_client = Arc::new(KakarotClient::new(starknet_config, starknet_provider.clone())); + let eth_provider = EthDataProvider::new(db, starknet_provider); + KakarotRpcModuleBuilder::new(kakarot_client, eth_provider).rpc_module() } StarknetProvider::SequencerGatewayProvider(starknet_provider) => { let starknet_provider = Arc::new(starknet_provider); - let kakarot_client = Arc::new(KakarotClient::new(starknet_config, starknet_provider)); - KakarotRpcModuleBuilder::new(kakarot_client).rpc_module() + let kakarot_client = Arc::new(KakarotClient::new(starknet_config, starknet_provider.clone())); + let eth_provider = EthDataProvider::new(db, starknet_provider); + KakarotRpcModuleBuilder::new(kakarot_client, eth_provider).rpc_module() } }?; diff --git a/src/starknet_client/config.rs b/src/starknet_client/config.rs index d3d4d0cc9..ed1dc24ab 100644 --- a/src/starknet_client/config.rs +++ b/src/starknet_client/config.rs @@ -7,7 +7,7 @@ use url::Url; use super::constants::{KATANA_RPC_URL, MADARA_RPC_URL}; use super::errors::ConfigError; -fn env_var(name: &str) -> Result { +pub fn env_var(name: &str) -> Result { std::env::var(name).map_err(|_| ConfigError::EnvironmentVariableMissing(name.into())) } diff --git a/src/starknet_client/errors.rs b/src/starknet_client/errors.rs index 9118e86d0..87ba06170 100644 --- a/src/starknet_client/errors.rs +++ b/src/starknet_client/errors.rs @@ -30,7 +30,7 @@ pub enum EthRpcErrorCode { JsonRpcVersionUnsupported = -32006, } -// Error that can accure when preparing configuration. +/// Error that can occur when preparing configuration. #[derive(Debug, Error)] pub enum ConfigError { /// Missing mandatory environment variable error. diff --git a/tests/ethereum_integration.rs b/tests/ethereum_integration.rs index 71c867fa0..b76ef4497 100644 --- a/tests/ethereum_integration.rs +++ b/tests/ethereum_integration.rs @@ -27,6 +27,8 @@ abigen!(ERC20, "tests/ERC20/IERC20.json"); // { code: 98, kind: AddrInUse, message: "Address already in use" }'` #[rstest] #[awt] +// We ignore this test for now, because we need `send_transaction` to be implemented on the EthereumProvider +#[ignore] #[tokio::test(flavor = "multi_thread")] async fn test_erc20(#[future] katana: Katana) { let (server_addr, server_handle) = @@ -67,6 +69,7 @@ async fn test_erc20(#[future] katana: Katana) { ); let contract = factory.deploy(()).unwrap().send().await.unwrap(); + let _: U64 = client.get_block_number().await.unwrap(); let token = ERC20::new(contract.address(), client.clone()); // Assert initial balance is 0 diff --git a/tests/test_utils/eth_provider/mod.rs b/tests/test_utils/eth_provider/mod.rs new file mode 100644 index 000000000..e13e6e4ac --- /dev/null +++ b/tests/test_utils/eth_provider/mod.rs @@ -0,0 +1,31 @@ +use std::sync::Mutex; + +use kakarot_rpc::{eth_provider::provider::MockEthereumProvider, starknet_client::constants::CHAIN_ID}; +use lazy_static::lazy_static; +use reth_primitives::U64; + +lazy_static! { + /// A Mutex-wrapped U64 that is used to mock the block number. + pub static ref MOCK_BLOCK_NUMBER: Mutex = Mutex::new(U64::from(0)); +} + +/// Returns a MockEthereumProvider that can be used to mock the EthereumProvider. +pub fn mock_ethereum_provider() -> MockEthereumProvider { + let mut eth_db = MockEthereumProvider::new(); + + eth_db.expect_chain_id().returning(|| Box::pin(async { Ok(Some(U64::from(CHAIN_ID))) })); + + // In order to increment the block number, we use a Mutex to lock the block number and increment + // it by 1 each time the function is called. + eth_db.expect_block_number().returning(|| { + Box::pin(async { + let mut lock = MOCK_BLOCK_NUMBER.lock().unwrap(); + let block_number = *lock; + *lock += U64::from(1); + drop(lock); + Ok(block_number) + }) + }); + + eth_db +} diff --git a/tests/test_utils/mod.rs b/tests/test_utils/mod.rs index 1f57f22da..15c6d33ee 100644 --- a/tests/test_utils/mod.rs +++ b/tests/test_utils/mod.rs @@ -1,5 +1,6 @@ pub mod constants; pub mod eoa; +pub mod eth_provider; pub mod evm_contract; pub mod fixtures; pub mod macros; diff --git a/tests/test_utils/rpc/mod.rs b/tests/test_utils/rpc/mod.rs index ba6dc8d71..8ad722ec8 100644 --- a/tests/test_utils/rpc/mod.rs +++ b/tests/test_utils/rpc/mod.rs @@ -10,6 +10,7 @@ use kakarot_rpc::starknet_client::KakarotClient; use starknet::providers::jsonrpc::HttpTransport; use starknet::providers::JsonRpcClient; +use super::eth_provider::mock_ethereum_provider; use super::sequencer::Katana; /// Sets up the environment for Kakarot RPC integration tests by deploying the Kakarot contracts @@ -76,9 +77,10 @@ pub async fn start_kakarot_rpc_server(katana: &Katana) -> Result<(SocketAddr, Se ); let kakarot_client = Arc::new(KakarotClient::new(starknet_config, provider)); + let eth_db = mock_ethereum_provider(); // Create and run Kakarot RPC module. - let kakarot_rpc_module = KakarotRpcModuleBuilder::new(kakarot_client).rpc_module()?; + let kakarot_rpc_module = KakarotRpcModuleBuilder::new(kakarot_client, eth_db).rpc_module()?; let rpc_config = RPCConfig::from_env()?; let (server_addr, server_handle) = run_server(kakarot_rpc_module, rpc_config).await?;