diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index e6b7f8766e..debc3e48b2 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -2,7 +2,7 @@ // https://github.com/microsoft/vscode-dev-containers/tree/v0.245.2/containers/rust { "name": "Rust", - "image": "ghcr.io/dojoengine/dojo-dev:v1.0.1", + "image": "ghcr.io/dojoengine/dojo-dev:v1.0.10", "runArgs": [ "--cap-add=SYS_PTRACE", "--security-opt", diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index 2d95437861..7d06ae9423 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -14,7 +14,7 @@ jobs: bench-katana: runs-on: ubuntu-latest container: - image: ghcr.io/dojoengine/dojo-dev:v1.0.1 + image: ghcr.io/dojoengine/dojo-dev:v1.0.10 steps: - uses: actions/checkout@v3 - run: git config --global --add safe.directory "$GITHUB_WORKSPACE" @@ -36,7 +36,7 @@ jobs: bench-sozo: runs-on: ubuntu-latest container: - image: ghcr.io/dojoengine/dojo-dev:v1.0.1 + image: ghcr.io/dojoengine/dojo-dev:v1.0.10 steps: - uses: actions/checkout@v3 - run: git config --global --add safe.directory "$GITHUB_WORKSPACE" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6355be8857..38c074564e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,11 +8,12 @@ on: env: CARGO_TERM_COLOR: always - RUST_VERSION: 1.79.0 + RUST_VERSION: 1.80.0 jobs: build: runs-on: ubuntu-latest-4-cores + needs: [fmt, cairofmt] steps: - uses: actions/checkout@v3 - uses: Swatinem/rust-cache@v2 @@ -31,7 +32,7 @@ jobs: needs: ensure-docker runs-on: ubuntu-latest-32-cores container: - image: ghcr.io/dojoengine/dojo-dev:v1.0.1 + image: ghcr.io/dojoengine/dojo-dev:v1.0.10 steps: - uses: actions/checkout@v3 - uses: Swatinem/rust-cache@v2 @@ -55,8 +56,9 @@ jobs: ensure-wasm: runs-on: ubuntu-latest + needs: [fmt, cairofmt] container: - image: ghcr.io/dojoengine/dojo-dev:v1.0.1 + image: ghcr.io/dojoengine/dojo-dev:v1.0.10 steps: - uses: actions/checkout@v3 - uses: Swatinem/rust-cache@v2 @@ -123,7 +125,7 @@ jobs: needs: build runs-on: ubuntu-latest container: - image: ghcr.io/dojoengine/dojo-dev:v1.0.1 + image: ghcr.io/dojoengine/dojo-dev:v1.0.10 steps: - uses: actions/download-artifact@v4 with: @@ -139,7 +141,7 @@ jobs: needs: build runs-on: ubuntu-latest container: - image: ghcr.io/dojoengine/dojo-dev:v1.0.1 + image: ghcr.io/dojoengine/dojo-dev:v1.0.10 steps: - uses: actions/download-artifact@v4 with: @@ -153,8 +155,9 @@ jobs: clippy: runs-on: ubuntu-latest-4-cores + needs: [fmt, cairofmt] container: - image: ghcr.io/dojoengine/dojo-dev:v1.0.1 + image: ghcr.io/dojoengine/dojo-dev:v1.0.10 steps: - uses: actions/checkout@v3 - uses: Swatinem/rust-cache@v2 @@ -163,7 +166,7 @@ jobs: fmt: runs-on: ubuntu-latest container: - image: ghcr.io/dojoengine/dojo-dev:v1.0.1 + image: ghcr.io/dojoengine/dojo-dev:v1.0.10 steps: - uses: actions/checkout@v3 - uses: Swatinem/rust-cache@v2 @@ -171,8 +174,9 @@ jobs: docs: runs-on: ubuntu-latest + needs: [fmt, cairofmt] container: - image: ghcr.io/dojoengine/dojo-dev:v1.0.1 + image: ghcr.io/dojoengine/dojo-dev:v1.0.10 steps: - uses: actions/checkout@v3 - uses: Swatinem/rust-cache@v2 diff --git a/.github/workflows/devcontainer.yml b/.github/workflows/devcontainer.yml index 08672eb7f1..ce4ccc1ec7 100644 --- a/.github/workflows/devcontainer.yml +++ b/.github/workflows/devcontainer.yml @@ -100,7 +100,7 @@ jobs: with: # We have to use a PAT in order to trigger ci token: ${{ secrets.CREATE_PR_TOKEN }} - title: "Update devcontainer image: ${{ needs.build-and-push.outputs.tag_name }}" + title: "chore(devcontainer): update image: ${{ needs.build-and-push.outputs.tag_name }}" commit-message: "Update devcontainer image: ${{ needs.build-and-push.outputs.tag_name }}" branch: bump-devcontainer base: main diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index fc1d296883..9719ce4e03 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -81,6 +81,6 @@ jobs: name: Move cache run: | rm -rf /tmp/.buildx-cache/prebuild - mv /tmp/.buildx-cache-new/prebuild /tmp/.buildx-cache/prebuild + mv /tmp/.buildx-cache-new/prebuild /tmp/.buildx-cache/prebuild || true rm -rf /tmp/.buildx-cache/build - mv /tmp/.buildx-cache-new/build /tmp/.buildx-cache/build + mv /tmp/.buildx-cache-new/build /tmp/.buildx-cache/build || true diff --git a/.github/workflows/release-dispatch.yml b/.github/workflows/release-dispatch.yml index 822b6ebc70..62bf2ef517 100644 --- a/.github/workflows/release-dispatch.yml +++ b/.github/workflows/release-dispatch.yml @@ -14,7 +14,7 @@ jobs: contents: write runs-on: ubuntu-latest container: - image: ghcr.io/dojoengine/dojo-dev:v1.0.1 + image: ghcr.io/dojoengine/dojo-dev:v1.0.10 env: VERSION: "" steps: diff --git a/.gitmodules b/.gitmodules index 53caedc389..72284c0578 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "crates/katana/contracts/messaging/solidity/lib/forge-std"] path = crates/katana/contracts/messaging/solidity/lib/forge-std url = https://github.com/foundry-rs/forge-std +[submodule "crates/katana/contracts/piltover"] + path = crates/katana/contracts/piltover + url = https://github.com/cartridge-gg/piltover.git diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d5da9be0c1..d72f5783f1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -98,4 +98,4 @@ The [Dojo Book repository](https://github.com/dojoengine/book) is where you shou ## Final Notes -Again, thank you for considering to contribute to Dojo. Your contribution is invaluable to us. We hope this guide makes the contribution process clear and answers any questions you might have. If not, feel free to ask on the [Discord](https://discord.gg/PwDa2mKhR4) or on [GitHub](https://github.com/dojoengine/dojo/issues). +Again, thank you for considering to contribute to Dojo. Your contribution is invaluable to us. We hope this guide makes the contribution process clear and answers any questions you might have. If not, feel free to ask on the [Discord](https://discord.com/invite/dojoengine) or on [GitHub](https://github.com/dojoengine/dojo/issues). diff --git a/Cargo.lock b/Cargo.lock index a9989e963b..86d581540d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -40,12 +40,12 @@ dependencies = [ "serde-wasm-bindgen", "serde_cbor_2", "serde_json", - "serde_with 3.9.0", + "serde_with 3.11.0", "sha2 0.10.8", "starknet 0.12.0", "starknet-crypto 0.7.2", "starknet-types-core", - "thiserror", + "thiserror 1.0.63", "tokio", "toml 0.8.19", "tsify-next", @@ -251,7 +251,7 @@ dependencies = [ "alloy-transport 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "futures", "futures-util", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -270,7 +270,7 @@ dependencies = [ "alloy-transport 0.3.6 (git+https://github.com/alloy-rs/alloy)", "futures", "futures-util", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -391,7 +391,7 @@ dependencies = [ "alloy-sol-types", "serde", "serde_json", - "thiserror", + "thiserror 1.0.63", "tracing", ] @@ -404,7 +404,7 @@ dependencies = [ "alloy-sol-types", "serde", "serde_json", - "thiserror", + "thiserror 1.0.63", "tracing", ] @@ -426,7 +426,7 @@ dependencies = [ "async-trait", "auto_impl", "futures-utils-wasm", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -446,7 +446,7 @@ dependencies = [ "async-trait", "auto_impl", "futures-utils-wasm", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -483,7 +483,7 @@ dependencies = [ "rand 0.8.5", "serde_json", "tempfile", - "thiserror", + "thiserror 1.0.63", "tracing", "url", ] @@ -541,7 +541,7 @@ dependencies = [ "reqwest 0.12.7", "serde", "serde_json", - "thiserror", + "thiserror 1.0.63", "tokio", "tracing", "url", @@ -577,7 +577,7 @@ dependencies = [ "reqwest 0.12.7", "serde", "serde_json", - "thiserror", + "thiserror 1.0.63", "tokio", "tracing", "url", @@ -602,7 +602,7 @@ checksum = "4d0f2d905ebd295e7effec65e5f6868d153936130ae718352771de3e7d03c75c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -729,7 +729,7 @@ dependencies = [ "auto_impl", "elliptic-curve", "k256", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -742,7 +742,7 @@ dependencies = [ "auto_impl", "elliptic-curve", "k256", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -757,7 +757,7 @@ dependencies = [ "async-trait", "k256", "rand 0.8.5", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -771,7 +771,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -788,7 +788,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", "syn-solidity", "tiny-keccak", ] @@ -806,7 +806,7 @@ dependencies = [ "proc-macro2", "quote", "serde_json", - "syn 2.0.77", + "syn 2.0.90", "syn-solidity", ] @@ -845,7 +845,7 @@ dependencies = [ "futures-utils-wasm", "serde", "serde_json", - "thiserror", + "thiserror 1.0.63", "tokio", "tower 0.5.1", "tracing", @@ -863,7 +863,7 @@ dependencies = [ "futures-utils-wasm", "serde", "serde_json", - "thiserror", + "thiserror 1.0.63", "tokio", "tower 0.5.1", "tracing", @@ -1017,7 +1017,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -1259,7 +1259,7 @@ dependencies = [ "nom", "num-traits 0.2.19", "rusticata-macros", - "thiserror", + "thiserror 1.0.63", "time", ] @@ -1275,7 +1275,7 @@ dependencies = [ "nom", "num-traits 0.2.19", "rusticata-macros", - "thiserror", + "thiserror 1.0.63", "time", ] @@ -1299,7 +1299,7 @@ checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", "synstructure 0.13.1", ] @@ -1322,7 +1322,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -1445,7 +1445,7 @@ dependencies = [ "serde_urlencoded", "static_assertions_next", "tempfile", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -1461,8 +1461,8 @@ dependencies = [ "proc-macro2", "quote", "strum 0.26.3", - "syn 2.0.77", - "thiserror", + "syn 2.0.90", + "thiserror 1.0.63", ] [[package]] @@ -1568,7 +1568,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -1616,7 +1616,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -1627,13 +1627,13 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.82" +version = "0.1.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -1683,7 +1683,7 @@ checksum = "3c87f3f15e7794432337fc718554eaa4dc8f04c9677a950ffe366f20a162ae42" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -1842,6 +1842,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "backon" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5289ec98f68f28dd809fd601059e6aa908bb8f6108620930828283d4ee23d7" +dependencies = [ + "fastrand 2.1.1", + "gloo-timers 0.3.0", + "tokio", +] + [[package]] name = "backtrace" version = "0.3.74" @@ -1978,7 +1989,7 @@ dependencies = [ "regex", "rustc-hash 1.1.0", "shlex", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -1990,7 +2001,7 @@ dependencies = [ "bitflags 2.6.0", "cexpr", "clang-sys", - "itertools 0.12.1", + "itertools 0.11.0", "lazy_static", "lazycell", "log", @@ -2000,7 +2011,7 @@ dependencies = [ "regex", "rustc-hash 1.1.0", "shlex", - "syn 2.0.77", + "syn 2.0.90", "which 4.4.2", ] @@ -2010,7 +2021,16 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" dependencies = [ - "bit-vec", + "bit-vec 0.6.3", +] + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec 0.8.0", ] [[package]] @@ -2019,6 +2039,12 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bit_field" version = "0.10.2" @@ -2132,7 +2158,7 @@ dependencies = [ "starknet_api", "strum 0.25.0", "strum_macros 0.25.3", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -2157,7 +2183,7 @@ dependencies = [ "cid", "dashmap 6.1.0", "multihash 0.19.1", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -2175,17 +2201,20 @@ dependencies = [ [[package]] name = "bonsai-trie" version = "0.1.0" -source = "git+https://github.com/madara-alliance/bonsai-trie/?rev=56d7d62#56d7d62232fd72419f1d50de8bc747b70a9db68f" +source = "git+https://github.com/dojoengine/bonsai-trie/?branch=kariy/indexmap#d2741d49d14210f675195a9a607910bf76bbed04" dependencies = [ "bitvec", "derive_more 0.99.18", "hashbrown 0.14.5", + "indexmap 2.5.0", "log", "parity-scale-codec", "rayon", "serde", + "slotmap", "smallvec", "starknet-types-core", + "thiserror 2.0.7", ] [[package]] @@ -2208,7 +2237,7 @@ dependencies = [ "proc-macro-crate 3.2.0", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", "syn_derive", ] @@ -2392,7 +2421,7 @@ dependencies = [ "hashbrown 0.13.2", "instant", "once_cell", - "thiserror", + "thiserror 1.0.63", "tokio", ] @@ -2434,7 +2463,7 @@ dependencies = [ "serde_json", "starknet 0.12.0", "starknet-types-core", - "thiserror", + "thiserror 1.0.63", "tracing", "tracing-subscriber", "url", @@ -2442,16 +2471,16 @@ dependencies = [ [[package]] name = "cainome" -version = "0.4.6" -source = "git+https://github.com/cartridge-gg/cainome?rev=5c2616c273faca7700d2ba565503fcefb5b9d720#5c2616c273faca7700d2ba565503fcefb5b9d720" +version = "0.4.11" +source = "git+https://github.com/cartridge-gg/cainome?tag=v0.4.11#355b88b7b808656d729e9dfd16f81d80c5c30fbf" dependencies = [ "anyhow", "async-trait", - "cainome-cairo-serde 0.1.0 (git+https://github.com/cartridge-gg/cainome?rev=5c2616c273faca7700d2ba565503fcefb5b9d720)", + "cainome-cairo-serde 0.1.0 (git+https://github.com/cartridge-gg/cainome?tag=v0.4.11)", "cainome-cairo-serde-derive", - "cainome-parser 0.1.0 (git+https://github.com/cartridge-gg/cainome?rev=5c2616c273faca7700d2ba565503fcefb5b9d720)", - "cainome-rs 0.1.0 (git+https://github.com/cartridge-gg/cainome?rev=5c2616c273faca7700d2ba565503fcefb5b9d720)", - "cainome-rs-macro 0.1.0 (git+https://github.com/cartridge-gg/cainome?rev=5c2616c273faca7700d2ba565503fcefb5b9d720)", + "cainome-parser 0.1.0 (git+https://github.com/cartridge-gg/cainome?tag=v0.4.11)", + "cainome-rs 0.1.0 (git+https://github.com/cartridge-gg/cainome?tag=v0.4.11)", + "cainome-rs-macro 0.1.0 (git+https://github.com/cartridge-gg/cainome?tag=v0.4.11)", "camino", "clap", "clap_complete", @@ -2460,7 +2489,7 @@ dependencies = [ "serde_json", "starknet 0.12.0", "starknet-types-core", - "thiserror", + "thiserror 1.0.63", "tracing", "tracing-subscriber", "url", @@ -2469,130 +2498,132 @@ dependencies = [ [[package]] name = "cainome-cairo-serde" version = "0.1.0" -source = "git+https://github.com/cartridge-gg/cainome?tag=v0.4.2#4e3924fb82b7299d56d3619aa5d7b9863f581e0a" +source = "git+https://github.com/cartridge-gg/cainome?tag=v0.4.11#355b88b7b808656d729e9dfd16f81d80c5c30fbf" dependencies = [ + "num-bigint", "serde", + "serde_with 3.11.0", "starknet 0.12.0", - "thiserror", + "thiserror 1.0.63", ] [[package]] name = "cainome-cairo-serde" version = "0.1.0" -source = "git+https://github.com/cartridge-gg/cainome?rev=5c2616c273faca7700d2ba565503fcefb5b9d720#5c2616c273faca7700d2ba565503fcefb5b9d720" +source = "git+https://github.com/cartridge-gg/cainome?tag=v0.4.2#4e3924fb82b7299d56d3619aa5d7b9863f581e0a" dependencies = [ "serde", "starknet 0.12.0", - "thiserror", + "thiserror 1.0.63", ] [[package]] name = "cainome-cairo-serde-derive" version = "0.1.0" -source = "git+https://github.com/cartridge-gg/cainome?rev=5c2616c273faca7700d2ba565503fcefb5b9d720#5c2616c273faca7700d2ba565503fcefb5b9d720" +source = "git+https://github.com/cartridge-gg/cainome?tag=v0.4.11#355b88b7b808656d729e9dfd16f81d80c5c30fbf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", "unzip-n", ] [[package]] name = "cainome-parser" version = "0.1.0" -source = "git+https://github.com/cartridge-gg/cainome?tag=v0.4.2#4e3924fb82b7299d56d3619aa5d7b9863f581e0a" +source = "git+https://github.com/cartridge-gg/cainome?tag=v0.4.11#355b88b7b808656d729e9dfd16f81d80c5c30fbf" dependencies = [ "convert_case 0.6.0", "quote", "serde_json", "starknet 0.12.0", - "syn 2.0.77", - "thiserror", + "syn 2.0.90", + "thiserror 1.0.63", ] [[package]] name = "cainome-parser" version = "0.1.0" -source = "git+https://github.com/cartridge-gg/cainome?rev=5c2616c273faca7700d2ba565503fcefb5b9d720#5c2616c273faca7700d2ba565503fcefb5b9d720" +source = "git+https://github.com/cartridge-gg/cainome?tag=v0.4.2#4e3924fb82b7299d56d3619aa5d7b9863f581e0a" dependencies = [ "convert_case 0.6.0", "quote", "serde_json", "starknet 0.12.0", - "syn 2.0.77", - "thiserror", + "syn 2.0.90", + "thiserror 1.0.63", ] [[package]] name = "cainome-rs" version = "0.1.0" -source = "git+https://github.com/cartridge-gg/cainome?tag=v0.4.2#4e3924fb82b7299d56d3619aa5d7b9863f581e0a" +source = "git+https://github.com/cartridge-gg/cainome?tag=v0.4.11#355b88b7b808656d729e9dfd16f81d80c5c30fbf" dependencies = [ "anyhow", - "cainome-cairo-serde 0.1.0 (git+https://github.com/cartridge-gg/cainome?tag=v0.4.2)", - "cainome-parser 0.1.0 (git+https://github.com/cartridge-gg/cainome?tag=v0.4.2)", + "cainome-cairo-serde 0.1.0 (git+https://github.com/cartridge-gg/cainome?tag=v0.4.11)", + "cainome-parser 0.1.0 (git+https://github.com/cartridge-gg/cainome?tag=v0.4.11)", "camino", "prettyplease", "proc-macro2", "quote", "serde_json", "starknet 0.12.0", - "syn 2.0.77", - "thiserror", + "syn 2.0.90", + "thiserror 1.0.63", ] [[package]] name = "cainome-rs" version = "0.1.0" -source = "git+https://github.com/cartridge-gg/cainome?rev=5c2616c273faca7700d2ba565503fcefb5b9d720#5c2616c273faca7700d2ba565503fcefb5b9d720" +source = "git+https://github.com/cartridge-gg/cainome?tag=v0.4.2#4e3924fb82b7299d56d3619aa5d7b9863f581e0a" dependencies = [ "anyhow", - "cainome-cairo-serde 0.1.0 (git+https://github.com/cartridge-gg/cainome?rev=5c2616c273faca7700d2ba565503fcefb5b9d720)", - "cainome-parser 0.1.0 (git+https://github.com/cartridge-gg/cainome?rev=5c2616c273faca7700d2ba565503fcefb5b9d720)", + "cainome-cairo-serde 0.1.0 (git+https://github.com/cartridge-gg/cainome?tag=v0.4.2)", + "cainome-parser 0.1.0 (git+https://github.com/cartridge-gg/cainome?tag=v0.4.2)", "camino", "prettyplease", "proc-macro2", "quote", "serde_json", "starknet 0.12.0", - "syn 2.0.77", - "thiserror", + "syn 2.0.90", + "thiserror 1.0.63", ] [[package]] name = "cainome-rs-macro" version = "0.1.0" -source = "git+https://github.com/cartridge-gg/cainome?tag=v0.4.2#4e3924fb82b7299d56d3619aa5d7b9863f581e0a" +source = "git+https://github.com/cartridge-gg/cainome?tag=v0.4.11#355b88b7b808656d729e9dfd16f81d80c5c30fbf" dependencies = [ "anyhow", - "cainome-cairo-serde 0.1.0 (git+https://github.com/cartridge-gg/cainome?tag=v0.4.2)", - "cainome-parser 0.1.0 (git+https://github.com/cartridge-gg/cainome?tag=v0.4.2)", - "cainome-rs 0.1.0 (git+https://github.com/cartridge-gg/cainome?tag=v0.4.2)", + "cainome-cairo-serde 0.1.0 (git+https://github.com/cartridge-gg/cainome?tag=v0.4.11)", + "cainome-parser 0.1.0 (git+https://github.com/cartridge-gg/cainome?tag=v0.4.11)", + "cainome-rs 0.1.0 (git+https://github.com/cartridge-gg/cainome?tag=v0.4.11)", "proc-macro-error", "proc-macro2", "quote", "serde_json", "starknet 0.12.0", - "syn 2.0.77", - "thiserror", + "syn 2.0.90", + "thiserror 1.0.63", ] [[package]] name = "cainome-rs-macro" version = "0.1.0" -source = "git+https://github.com/cartridge-gg/cainome?rev=5c2616c273faca7700d2ba565503fcefb5b9d720#5c2616c273faca7700d2ba565503fcefb5b9d720" +source = "git+https://github.com/cartridge-gg/cainome?tag=v0.4.2#4e3924fb82b7299d56d3619aa5d7b9863f581e0a" dependencies = [ "anyhow", - "cainome-cairo-serde 0.1.0 (git+https://github.com/cartridge-gg/cainome?rev=5c2616c273faca7700d2ba565503fcefb5b9d720)", - "cainome-parser 0.1.0 (git+https://github.com/cartridge-gg/cainome?rev=5c2616c273faca7700d2ba565503fcefb5b9d720)", - "cainome-rs 0.1.0 (git+https://github.com/cartridge-gg/cainome?rev=5c2616c273faca7700d2ba565503fcefb5b9d720)", + "cainome-cairo-serde 0.1.0 (git+https://github.com/cartridge-gg/cainome?tag=v0.4.2)", + "cainome-parser 0.1.0 (git+https://github.com/cartridge-gg/cainome?tag=v0.4.2)", + "cainome-rs 0.1.0 (git+https://github.com/cartridge-gg/cainome?tag=v0.4.2)", "proc-macro-error", "proc-macro2", "quote", "serde_json", "starknet 0.12.0", - "syn 2.0.77", - "thiserror", + "syn 2.0.90", + "thiserror 1.0.63", ] [[package]] @@ -2602,7 +2633,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd4d6659539ace9649c8e8a7434e51b0c50a7a700111d0a2b967dde220ddff49" dependencies = [ "cairo-lang-utils", - "indoc 2.0.5", + "indoc", "num-bigint", "num-traits 0.2.19", "parity-scale-codec", @@ -2627,12 +2658,12 @@ dependencies = [ "cairo-lang-sierra-generator", "cairo-lang-syntax", "cairo-lang-utils", - "indoc 2.0.5", + "indoc", "rayon", "rust-analyzer-salsa", "semver 1.0.23", "smol_str", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -2731,7 +2762,7 @@ dependencies = [ "rust-analyzer-salsa", "serde", "smol_str", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -2756,7 +2787,7 @@ dependencies = [ "cairo-lang-test-plugin", "cairo-lang-utils", "indent", - "indoc 2.0.5", + "indoc", "itertools 0.12.1", "rust-analyzer-salsa", "scarb-metadata 1.12.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2798,7 +2829,6 @@ dependencies = [ [[package]] name = "cairo-lang-macro" version = "0.1.0" -source = "git+https://github.com/dojoengine/scarb?rev=7eac49b3e61236ce466e712225d9c989f9db1ef3#7eac49b3e61236ce466e712225d9c989f9db1ef3" dependencies = [ "cairo-lang-macro-attributes", "cairo-lang-macro-stable", @@ -2813,7 +2843,7 @@ checksum = "e32e958decd95ae122ee64daa26721da2f76e83231047f947fd9cdc5d3c90cc6" dependencies = [ "quote", "scarb-stable-hash 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -2855,7 +2885,7 @@ dependencies = [ "cairo-lang-syntax", "cairo-lang-utils", "indent", - "indoc 2.0.5", + "indoc", "itertools 0.12.1", "rust-analyzer-salsa", "smol_str", @@ -2869,7 +2899,7 @@ checksum = "d72f17373740f242d6995e896b9195c2cedff7e8b14e496afdd16b405039d1fb" dependencies = [ "cairo-lang-debug", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -2882,7 +2912,7 @@ dependencies = [ "cairo-lang-utils", "serde", "smol_str", - "thiserror", + "thiserror 1.0.63", "toml 0.8.19", ] @@ -2914,7 +2944,7 @@ dependencies = [ "sha2 0.10.8", "smol_str", "starknet-types-core", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -2934,7 +2964,7 @@ dependencies = [ "cairo-lang-test-utils", "cairo-lang-utils", "id-arena", - "indoc 2.0.5", + "indoc", "itertools 0.12.1", "num-bigint", "num-traits 0.2.19", @@ -2967,7 +2997,7 @@ dependencies = [ "sha3", "smol_str", "starknet-types-core", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -2983,7 +3013,7 @@ dependencies = [ "itertools 0.12.1", "num-bigint", "num-traits 0.2.19", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -2999,7 +3029,7 @@ dependencies = [ "itertools 0.12.1", "num-bigint", "num-traits 0.2.19", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -3039,12 +3069,12 @@ dependencies = [ "cairo-lang-sierra-gas", "cairo-lang-sierra-type-size", "cairo-lang-utils", - "indoc 2.0.5", + "indoc", "itertools 0.12.1", "num-bigint", "num-traits 0.2.19", "starknet-types-core", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -3078,13 +3108,13 @@ dependencies = [ "cairo-lang-utils", "const_format", "indent", - "indoc 2.0.5", + "indoc", "itertools 0.12.1", "serde", "serde_json", "smol_str", "starknet-types-core", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -3107,7 +3137,7 @@ dependencies = [ "sha3", "smol_str", "starknet-types-core", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -3155,7 +3185,7 @@ dependencies = [ "cairo-lang-starknet-classes", "cairo-lang-syntax", "cairo-lang-utils", - "indoc 2.0.5", + "indoc", "itertools 0.12.1", "num-bigint", "num-traits 0.2.19", @@ -3325,7 +3355,7 @@ dependencies = [ "serde", "serde_json", "starknet-types-core", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -3357,7 +3387,7 @@ dependencies = [ "semver 1.0.23", "serde", "serde_json", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -3423,7 +3453,7 @@ dependencies = [ "http 1.1.0", "jsonrpsee 0.24.6", "serde", - "thiserror", + "thiserror 1.0.63", "tracing", ] @@ -3499,7 +3529,7 @@ dependencies = [ "serde", "serde_repr", "sha2 0.10.8", - "thiserror", + "thiserror 1.0.63", "time", ] @@ -3670,7 +3700,6 @@ dependencies = [ "anstyle", "clap_lex", "strsim 0.11.1", - "terminal_size", ] [[package]] @@ -3682,20 +3711,6 @@ dependencies = [ "clap", ] -[[package]] -name = "clap_config" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46efb9cbf691f5505d0b7b2c8055aec0c9a770eaac8a06834b6d84b5be93279a" -dependencies = [ - "clap", - "heck 0.5.0", - "proc-macro2", - "quote", - "serde", - "syn 2.0.77", -] - [[package]] name = "clap_derive" version = "4.5.18" @@ -3705,7 +3720,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -3798,7 +3813,7 @@ version = "7.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b34115915337defe99b2aff5c2ce6771e5fbc4079f4b506301f5cf394c8452f7" dependencies = [ - "crossterm", + "crossterm 0.27.0", "strum 0.26.3", "strum_macros 0.26.4", "unicode-width", @@ -3812,7 +3827,7 @@ dependencies = [ "ed25519-dalek", "serde", "serde_json", - "serde_with 3.9.0", + "serde_with 3.11.0", "starknet-types-core", ] @@ -3829,7 +3844,7 @@ dependencies = [ "mime", "mime_guess", "rand 0.8.5", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -4076,7 +4091,6 @@ dependencies = [ [[package]] name = "create-output-dir" version = "1.0.0" -source = "git+https://github.com/dojoengine/scarb?rev=7eac49b3e61236ce466e712225d9c989f9db1ef3#7eac49b3e61236ce466e712225d9c989f9db1ef3" dependencies = [ "anyhow", "core-foundation 0.10.0", @@ -4163,6 +4177,22 @@ version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +[[package]] +name = "crossterm" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" +dependencies = [ + "bitflags 1.3.2", + "crossterm_winapi", + "libc", + "mio 0.8.11", + "parking_lot 0.12.3", + "signal-hook", + "signal-hook-mio", + "winapi", +] + [[package]] name = "crossterm" version = "0.27.0" @@ -4258,7 +4288,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -4319,7 +4349,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -4341,7 +4371,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core 0.20.10", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -4424,7 +4454,7 @@ dependencies = [ "monch", "os_pipe", "path-dedot", - "thiserror", + "thiserror 1.0.63", "tokio", "tokio-util", ] @@ -4497,7 +4527,7 @@ checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -4518,7 +4548,7 @@ dependencies = [ "darling 0.20.10", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -4528,7 +4558,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4abae7035bf79b9877b779505d8cf3749285b80c43941eda66604841889451dc" dependencies = [ "derive_builder_core", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -4541,7 +4571,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version 0.4.1", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -4561,7 +4591,7 @@ checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", "unicode-xid", ] @@ -4574,7 +4604,7 @@ dependencies = [ "console", "shell-words", "tempfile", - "thiserror", + "thiserror 1.0.63", "zeroize", ] @@ -4699,7 +4729,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -4710,12 +4740,12 @@ checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" [[package]] name = "dojo-bindgen" -version = "1.0.1" +version = "1.0.11" dependencies = [ "anyhow", "assert_matches", "async-trait", - "cainome 0.4.6", + "cainome 0.4.11", "camino", "chrono", "convert_case 0.6.0", @@ -4727,59 +4757,21 @@ dependencies = [ "serde_json", "sozo-scarbext", "starknet 0.12.0", - "thiserror", + "thiserror 1.0.63", "tokio", ] [[package]] name = "dojo-contracts" -version = "1.0.1" +version = "1.0.11" [[package]] name = "dojo-examples-spawn-and-move" -version = "1.0.1" - -[[package]] -name = "dojo-lang" -version = "1.0.0-rc.2" -source = "git+https://github.com/dojoengine/dojo?rev=6725a8f20af56213fa7382aa1e158817f3ee623c#6725a8f20af56213fa7382aa1e158817f3ee623c" -dependencies = [ - "anyhow", - "cairo-lang-compiler", - "cairo-lang-defs", - "cairo-lang-diagnostics", - "cairo-lang-filesystem", - "cairo-lang-plugins", - "cairo-lang-project", - "cairo-lang-semantic", - "cairo-lang-sierra-generator", - "cairo-lang-starknet", - "cairo-lang-starknet-classes", - "cairo-lang-syntax", - "cairo-lang-test-plugin", - "cairo-lang-utils", - "camino", - "convert_case 0.6.0", - "dojo-types 1.0.0-rc.2", - "indoc 1.0.9", - "itertools 0.12.1", - "regex", - "semver 1.0.23", - "serde", - "serde_json", - "serde_with 3.9.0", - "smol_str", - "starknet 0.12.0", - "starknet-crypto 0.7.2", - "tempfile", - "toml 0.8.19", - "tracing", - "url", -] +version = "1.0.11" [[package]] name = "dojo-lang" -version = "1.0.1" +version = "1.0.11" dependencies = [ "anyhow", "cairo-lang-defs", @@ -4789,7 +4781,7 @@ dependencies = [ "cairo-lang-semantic", "cairo-lang-syntax", "cairo-lang-utils", - "dojo-types 1.0.1", + "dojo-types", "itertools 0.12.1", "serde", "smol_str", @@ -4800,16 +4792,16 @@ dependencies = [ [[package]] name = "dojo-language-server" -version = "1.0.1" +version = "1.0.11" dependencies = [ "cairo-lang-language-server", "clap", - "dojo-lang 1.0.1", + "dojo-lang", ] [[package]] name = "dojo-metrics" -version = "1.0.1" +version = "1.0.11" dependencies = [ "hyper 0.14.30", "jemalloc-ctl", @@ -4819,62 +4811,42 @@ dependencies = [ "metrics-exporter-prometheus", "metrics-process", "metrics-util", - "thiserror", + "thiserror 1.0.63", "tracing", ] [[package]] name = "dojo-test-utils" -version = "1.0.1" +version = "1.0.11" dependencies = [ "anyhow", "assert_fs", "async-trait", "camino", - "jsonrpsee 0.16.3", "katana-core", "katana-executor", "katana-node", "katana-primitives", + "katana-rpc", "scarb", "scarb-ui", "serde", "serde_json", "starknet 0.12.0", - "thiserror", + "thiserror 1.0.63", "toml 0.8.19", "url", ] [[package]] name = "dojo-types" -version = "1.0.0-rc.2" -source = "git+https://github.com/dojoengine/dojo?rev=6725a8f20af56213fa7382aa1e158817f3ee623c#6725a8f20af56213fa7382aa1e158817f3ee623c" -dependencies = [ - "anyhow", - "cainome 0.4.6", - "crypto-bigint", - "hex", - "itertools 0.12.1", - "num-traits 0.2.19", - "regex", - "serde", - "serde_json", - "starknet 0.12.0", - "starknet-crypto 0.7.2", - "strum 0.25.0", - "strum_macros 0.25.3", - "thiserror", -] - -[[package]] -name = "dojo-types" -version = "1.0.1" +version = "1.0.11" dependencies = [ "anyhow", - "cainome 0.4.6", + "cainome 0.4.11", "crypto-bigint", "hex", + "indexmap 2.5.0", "itertools 0.12.1", "num-traits 0.2.19", "regex", @@ -4884,12 +4856,12 @@ dependencies = [ "starknet-crypto 0.7.2", "strum 0.25.0", "strum_macros 0.25.3", - "thiserror", + "thiserror 1.0.63", ] [[package]] name = "dojo-utils" -version = "1.0.1" +version = "1.0.11" dependencies = [ "anyhow", "assert_matches", @@ -4900,20 +4872,21 @@ dependencies = [ "rpassword", "serde_json", "starknet 0.12.0", - "thiserror", + "thiserror 1.0.63", "tokio", "tracing", ] [[package]] name = "dojo-world" -version = "1.0.1" +version = "1.0.11" dependencies = [ "anyhow", "async-trait", - "cainome 0.4.6", + "cainome 0.4.11", "cairo-lang-starknet-classes", - "dojo-types 1.0.1", + "dojo-types", + "futures", "hex", "hex-literal", "ipfs-api-backend-hyper", @@ -4921,10 +4894,10 @@ dependencies = [ "regex", "serde", "serde_json", - "serde_with 3.9.0", + "serde_with 3.11.0", "starknet 0.12.0", "starknet-crypto 0.7.2", - "thiserror", + "thiserror 1.0.63", "tokio", "toml 0.8.19", "tracing", @@ -4933,10 +4906,10 @@ dependencies = [ [[package]] name = "dojo-world-abigen" -version = "1.0.1" +version = "1.0.11" dependencies = [ "anyhow", - "cainome 0.4.6", + "cainome 0.4.11", "cairo-lang-starknet", "cairo-lang-starknet-classes", "camino", @@ -5104,7 +5077,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -5116,7 +5089,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -5197,7 +5170,7 @@ dependencies = [ "serde_json", "sha2 0.10.8", "sha3", - "thiserror", + "thiserror 1.0.63", "uuid 0.8.2", ] @@ -5595,7 +5568,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -5666,6 +5639,24 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42012b0f064e01aa58b545fe3727f90f7dd4020f4a3ea735b50344965f5a57e9" +[[package]] +name = "fuzzy-matcher" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" +dependencies = [ + "thread_local", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + [[package]] name = "genco" version = "0.17.9" @@ -5685,7 +5676,7 @@ checksum = "553630feadf7b76442b0849fd25fdf89b860d933623aec9693fed19af0400c78" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -5793,7 +5784,7 @@ dependencies = [ "regex", "signal-hook", "smallvec", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -5806,7 +5797,7 @@ dependencies = [ "gix-date", "gix-utils", "itoa", - "thiserror", + "thiserror 1.0.63", "winnow", ] @@ -5821,7 +5812,7 @@ dependencies = [ "gix-object", "gix-worktree-stream", "jiff", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -5837,7 +5828,7 @@ dependencies = [ "gix-trace", "kstring", "smallvec", - "thiserror", + "thiserror 1.0.63", "unicode-bom", ] @@ -5847,7 +5838,7 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a371db66cbd4e13f0ed9dc4c0fea712d7276805fccc877f77e96374d317e87ae" dependencies = [ - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -5856,7 +5847,7 @@ version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45c8751169961ba7640b513c3b24af61aa962c967aaf04116734975cd5af0c52" dependencies = [ - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -5882,7 +5873,7 @@ dependencies = [ "gix-features", "gix-hash", "memmap2", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -5901,7 +5892,7 @@ dependencies = [ "memchr", "once_cell", "smallvec", - "thiserror", + "thiserror 1.0.63", "unicode-bom", "winnow", ] @@ -5916,7 +5907,7 @@ dependencies = [ "bstr", "gix-path", "libc", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -5933,7 +5924,7 @@ dependencies = [ "gix-sec", "gix-trace", "gix-url", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -5945,7 +5936,7 @@ dependencies = [ "bstr", "itoa", "jiff", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -5965,7 +5956,7 @@ dependencies = [ "gix-trace", "gix-worktree", "imara-diff", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -5985,7 +5976,7 @@ dependencies = [ "gix-trace", "gix-utils", "gix-worktree", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -6001,7 +5992,7 @@ dependencies = [ "gix-path", "gix-ref", "gix-sec", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -6023,7 +6014,7 @@ dependencies = [ "parking_lot 0.12.3", "prodash", "sha1_smol", - "thiserror", + "thiserror 1.0.63", "walkdir", ] @@ -6045,7 +6036,7 @@ dependencies = [ "gix-trace", "gix-utils", "smallvec", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -6078,7 +6069,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f93d7df7366121b5018f947a04d37f034717e113dcf9ccd85c34b58e57a74d5e" dependencies = [ "faster-hex", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -6130,7 +6121,7 @@ dependencies = [ "memmap2", "rustix 0.38.37", "smallvec", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -6141,7 +6132,7 @@ checksum = "e3bc7fe297f1f4614774989c00ec8b1add59571dc9b024b4c00acb7dedd4e19d" dependencies = [ "gix-tempfile", "gix-utils", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -6153,7 +6144,7 @@ dependencies = [ "bstr", "gix-actor", "gix-date", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -6169,7 +6160,7 @@ dependencies = [ "gix-object", "gix-revwalk", "smallvec", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -6187,7 +6178,7 @@ dependencies = [ "gix-validate", "itoa", "smallvec", - "thiserror", + "thiserror 1.0.63", "winnow", ] @@ -6208,7 +6199,7 @@ dependencies = [ "gix-quote", "parking_lot 0.12.3", "tempfile", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -6226,7 +6217,7 @@ dependencies = [ "gix-path", "memmap2", "smallvec", - "thiserror", + "thiserror 1.0.63", "uluru", ] @@ -6239,7 +6230,7 @@ dependencies = [ "bstr", "faster-hex", "gix-trace", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -6252,7 +6243,7 @@ dependencies = [ "gix-trace", "home", "once_cell", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -6267,7 +6258,7 @@ dependencies = [ "gix-config-value", "gix-glob", "gix-path", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -6280,7 +6271,7 @@ dependencies = [ "gix-config-value", "parking_lot 0.12.3", "rustix 0.38.37", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -6291,7 +6282,7 @@ checksum = "cbff4f9b9ea3fa7a25a70ee62f545143abef624ac6aa5884344e70c8b0a1d9ff" dependencies = [ "bstr", "gix-utils", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -6311,7 +6302,7 @@ dependencies = [ "gix-utils", "gix-validate", "memmap2", - "thiserror", + "thiserror 1.0.63", "winnow", ] @@ -6326,7 +6317,7 @@ dependencies = [ "gix-revision", "gix-validate", "smallvec", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -6342,7 +6333,7 @@ dependencies = [ "gix-object", "gix-revwalk", "gix-trace", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -6357,7 +6348,7 @@ dependencies = [ "gix-hashtable", "gix-object", "smallvec", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -6392,7 +6383,7 @@ dependencies = [ "gix-pathspec", "gix-worktree", "portable-atomic", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -6407,7 +6398,7 @@ dependencies = [ "gix-pathspec", "gix-refspec", "gix-url", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -6446,7 +6437,7 @@ dependencies = [ "gix-object", "gix-revwalk", "smallvec", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -6459,7 +6450,7 @@ dependencies = [ "gix-features", "gix-path", "home", - "thiserror", + "thiserror 1.0.63", "url", ] @@ -6481,7 +6472,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81f2badbb64e57b404593ee26b752c26991910fd0d81fe6f9a71c1a8309b6c86" dependencies = [ "bstr", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -6520,7 +6511,7 @@ dependencies = [ "gix-path", "gix-worktree", "io-close", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -6538,7 +6529,7 @@ dependencies = [ "gix-path", "gix-traverse", "parking_lot 0.12.3", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -6585,7 +6576,7 @@ dependencies = [ "pin-project", "serde", "serde_json", - "thiserror", + "thiserror 1.0.63", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -6606,7 +6597,7 @@ dependencies = [ "pin-project", "serde", "serde_json", - "thiserror", + "thiserror 1.0.63", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -6688,7 +6679,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2ebc8013b4426d5b81a4364c419a95ed0b404af2b82e2457de52d9348f0e474" dependencies = [ "combine 3.8.1", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -6806,7 +6797,7 @@ dependencies = [ "pest_derive", "serde", "serde_json", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -6918,7 +6909,7 @@ dependencies = [ "serde", "serde_json", "starknet-types-core", - "thiserror", + "thiserror 1.0.63", "tokio", "tracing", "tracing-log 0.1.4", @@ -6964,7 +6955,7 @@ dependencies = [ "once_cell", "rand 0.8.5", "socket2 0.5.7", - "thiserror", + "thiserror 1.0.63", "tinyvec", "tokio", "tracing", @@ -6987,7 +6978,7 @@ dependencies = [ "rand 0.8.5", "resolv-conf", "smallvec", - "thiserror", + "thiserror 1.0.63", "tokio", "tracing", ] @@ -7278,6 +7269,19 @@ dependencies = [ "tower-service", ] +[[package]] +name = "hyper-tungstenite" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cc7dcb1ab67cd336f468a12491765672e61a3b6b148634dbfe2fe8acd3fe7d9" +dependencies = [ + "hyper 0.14.30", + "pin-project-lite", + "tokio", + "tokio-tungstenite 0.20.1", + "tungstenite 0.20.1", +] + [[package]] name = "hyper-util" version = "0.1.8" @@ -7580,12 +7584,6 @@ dependencies = [ "unicode-width", ] -[[package]] -name = "indoc" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa799dd5ed20a7e349f3b4639aa80d74549c81716d9ec4f994c9b5815598306" - [[package]] name = "indoc" version = "2.0.5" @@ -7640,6 +7638,23 @@ dependencies = [ "generic-array", ] +[[package]] +name = "inquire" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fddf93031af70e75410a2511ec04d49e758ed2f26dad3404a934e0fb45cc12a" +dependencies = [ + "bitflags 2.6.0", + "crossterm 0.25.0", + "dyn-clone", + "fuzzy-matcher", + "fxhash", + "newline-converter", + "once_cell", + "unicode-segmentation", + "unicode-width", +] + [[package]] name = "instant" version = "0.1.13" @@ -7661,7 +7676,7 @@ dependencies = [ "rand 0.8.5", "rtcp", "rtp 0.9.0", - "thiserror", + "thiserror 1.0.63", "tokio", "waitgroup", "webrtc-srtp", @@ -7676,7 +7691,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -7726,7 +7741,7 @@ dependencies = [ "hyper-multipart-rfc7578", "hyper-rustls 0.23.2", "ipfs-api-prelude", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -7746,7 +7761,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "thiserror", + "thiserror 1.0.63", "tokio", "tokio-util", "tracing", @@ -7894,7 +7909,7 @@ dependencies = [ "combine 4.6.7", "jni-sys", "log", - "thiserror", + "thiserror 1.0.63", "walkdir", ] @@ -7909,7 +7924,7 @@ dependencies = [ "combine 4.6.7", "jni-sys", "log", - "thiserror", + "thiserror 1.0.63", "walkdir", "windows-sys 0.45.0", ] @@ -7992,7 +8007,7 @@ dependencies = [ "pin-project", "rustls-native-certs 0.6.3", "soketto 0.7.1", - "thiserror", + "thiserror 1.0.63", "tokio", "tokio-rustls 0.24.1", "tokio-util", @@ -8015,7 +8030,7 @@ dependencies = [ "rustls-pki-types", "rustls-platform-verifier", "soketto 0.8.0", - "thiserror", + "thiserror 1.0.63", "tokio", "tokio-rustls 0.26.0", "tokio-util", @@ -8046,7 +8061,7 @@ dependencies = [ "serde", "serde_json", "soketto 0.7.1", - "thiserror", + "thiserror 1.0.63", "tokio", "tracing", "wasm-bindgen-futures", @@ -8070,7 +8085,7 @@ dependencies = [ "rustc-hash 2.0.0", "serde", "serde_json", - "thiserror", + "thiserror 1.0.63", "tokio", "tokio-stream", "tracing", @@ -8090,7 +8105,7 @@ dependencies = [ "rustc-hash 1.1.0", "serde", "serde_json", - "thiserror", + "thiserror 1.0.63", "tokio", "tracing", ] @@ -8113,7 +8128,7 @@ dependencies = [ "rustls-platform-verifier", "serde", "serde_json", - "thiserror", + "thiserror 1.0.63", "tokio", "tower 0.4.13", "tracing", @@ -8143,7 +8158,7 @@ dependencies = [ "proc-macro-crate 3.2.0", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -8178,7 +8193,7 @@ dependencies = [ "beef", "serde", "serde_json", - "thiserror", + "thiserror 1.0.63", "tracing", ] @@ -8191,7 +8206,7 @@ dependencies = [ "http 1.1.0", "serde", "serde_json", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -8245,24 +8260,35 @@ dependencies = [ [[package]] name = "katana" -version = "1.0.1" +version = "1.0.11" dependencies = [ "anyhow", "assert_matches", "byte-unit", + "cainome 0.4.11", "clap", "clap_complete", "comfy-table", + "dirs 5.0.1", + "dojo-utils", + "inquire", + "katana-cairo", "katana-cli", "katana-db", "katana-node", + "katana-primitives", + "serde", + "serde_json", "shellexpand", + "spinoff", "starknet 0.12.0", + "tokio", + "toml 0.8.19", ] [[package]] name = "katana-cairo" -version = "1.0.1" +version = "1.0.11" dependencies = [ "cairo-lang-casm", "cairo-lang-runner", @@ -8277,18 +8303,19 @@ dependencies = [ [[package]] name = "katana-cli" -version = "1.0.1" +version = "1.0.11" dependencies = [ "alloy-primitives", "anyhow", "assert_matches", - "cainome-cairo-serde 0.1.0 (git+https://github.com/cartridge-gg/cainome?rev=5c2616c273faca7700d2ba565503fcefb5b9d720)", + "cainome-cairo-serde 0.1.0 (git+https://github.com/cartridge-gg/cainome?tag=v0.4.11)", "clap", "console", "dojo-utils", "katana-core", "katana-node", "katana-primitives", + "katana-rpc", "katana-slot-controller", "serde", "serde_json", @@ -8304,7 +8331,7 @@ dependencies = [ [[package]] name = "katana-codecs" -version = "1.0.1" +version = "1.0.11" dependencies = [ "bytes", "katana-primitives", @@ -8312,17 +8339,17 @@ dependencies = [ [[package]] name = "katana-codecs-derive" -version = "1.0.1" +version = "1.0.11" dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] name = "katana-core" -version = "1.0.1" +version = "1.0.11" dependencies = [ "alloy-contract 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "alloy-network 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", @@ -8353,9 +8380,10 @@ dependencies = [ "serde", "serde_json", "starknet 0.12.0", + "starknet-crypto 0.7.2", "starknet-types-core", "tempfile", - "thiserror", + "thiserror 1.0.63", "tokio", "tracing", "url", @@ -8363,11 +8391,10 @@ dependencies = [ [[package]] name = "katana-db" -version = "1.0.1" +version = "1.0.11" dependencies = [ "anyhow", "arbitrary", - "bitvec", "criterion", "dojo-metrics", "katana-primitives", @@ -8376,21 +8403,22 @@ dependencies = [ "page_size", "parking_lot 0.12.3", "postcard", + "proptest", "reth-libmdbx", "roaring", + "rstest 0.18.2", "serde", "serde_json", "smallvec", "starknet 0.12.0", - "starknet-types-core", "tempfile", - "thiserror", + "thiserror 1.0.63", "tracing", ] [[package]] name = "katana-executor" -version = "1.0.1" +version = "1.0.11" dependencies = [ "alloy-primitives", "anyhow", @@ -8400,6 +8428,7 @@ dependencies = [ "katana-primitives", "katana-provider", "katana-rpc-types", + "katana-trie", "num-traits 0.2.19", "oneshot", "parking_lot 0.12.3", @@ -8410,14 +8439,29 @@ dependencies = [ "serde_json", "similar-asserts", "starknet 0.12.0", - "thiserror", + "thiserror 1.0.63", + "tokio", + "tracing", +] + +[[package]] +name = "katana-feeder-gateway" +version = "1.0.11" +dependencies = [ + "katana-primitives", + "katana-rpc-types", + "reqwest 0.11.27", + "serde", + "starknet 0.12.0", + "thiserror 1.0.63", "tokio", "tracing", + "url", ] [[package]] name = "katana-grpc" -version = "1.0.1" +version = "1.0.11" dependencies = [ "tonic 0.11.0", "tonic-build 0.11.0", @@ -8426,44 +8470,55 @@ dependencies = [ [[package]] name = "katana-node" -version = "1.0.1" +version = "1.0.11" dependencies = [ "anyhow", + "clap", "const_format", "dojo-metrics", + "dojo-utils", "futures", "hyper 0.14.30", "jsonrpsee 0.16.3", "katana-core", "katana-db", "katana-executor", + "katana-feeder-gateway", "katana-pipeline", "katana-pool", "katana-primitives", + "katana-provider", "katana-rpc", "katana-rpc-api", + "katana-stage", "katana-tasks", + "serde", "serde_json", "starknet 0.12.0", "strum 0.25.0", "strum_macros 0.25.3", + "thiserror 1.0.63", + "tokio", "tower 0.4.13", "tower-http 0.4.4", "tracing", + "tracing-log 0.1.4", + "tracing-subscriber", + "url", "vergen", "vergen-gitcl", ] [[package]] name = "katana-node-bindings" -version = "1.0.1" +version = "1.0.11" dependencies = [ "regex", "serde", "serde_json", "starknet 0.12.0", "tempfile", - "thiserror", + "thiserror 1.0.63", "tokio", "tracing", "url", @@ -8471,23 +8526,21 @@ dependencies = [ [[package]] name = "katana-pipeline" -version = "1.0.1" +version = "1.0.11" dependencies = [ - "anyhow", "async-trait", "futures", - "katana-core", - "katana-executor", - "katana-pool", - "katana-tasks", - "thiserror", + "katana-primitives", + "katana-provider", + "katana-stage", + "thiserror 1.0.63", "tokio", "tracing", ] [[package]] name = "katana-pool" -version = "1.0.1" +version = "1.0.11" dependencies = [ "futures", "futures-util", @@ -8496,14 +8549,14 @@ dependencies = [ "katana-provider", "parking_lot 0.12.3", "rand 0.8.5", - "thiserror", + "thiserror 1.0.63", "tokio", "tracing", ] [[package]] name = "katana-primitives" -version = "1.0.1" +version = "1.0.11" dependencies = [ "alloy-primitives", "anyhow", @@ -8521,17 +8574,20 @@ dependencies = [ "rstest 0.18.2", "serde", "serde_json", - "serde_with 3.9.0", + "serde_json_pythonic", + "serde_with 3.11.0", "similar-asserts", "starknet 0.12.0", "starknet-crypto 0.7.2", "starknet-types-core", - "thiserror", + "strum 0.25.0", + "strum_macros 0.25.3", + "thiserror 1.0.63", ] [[package]] name = "katana-provider" -version = "1.0.1" +version = "1.0.11" dependencies = [ "alloy-primitives", "anyhow", @@ -8551,7 +8607,7 @@ dependencies = [ "starknet 0.12.0", "starknet-types-core", "tempfile", - "thiserror", + "thiserror 1.0.63", "tokio", "tracing", "url", @@ -8559,17 +8615,18 @@ dependencies = [ [[package]] name = "katana-rpc" -version = "1.0.1" +version = "1.0.11" dependencies = [ "alloy", "alloy-primitives", "anyhow", "assert_matches", - "cainome 0.4.6", + "cainome 0.4.11", "dojo-metrics", "dojo-test-utils", "dojo-utils", "futures", + "http 0.2.12", "indexmap 2.5.0", "jsonrpsee 0.16.3", "katana-cairo", @@ -8583,6 +8640,7 @@ dependencies = [ "katana-rpc-types", "katana-rpc-types-builder", "katana-tasks", + "katana-trie", "metrics", "num-traits 0.2.19", "rand 0.8.5", @@ -8592,15 +8650,17 @@ dependencies = [ "similar-asserts", "starknet 0.12.0", "tempfile", - "thiserror", + "thiserror 1.0.63", "tokio", + "tower 0.4.13", + "tower-http 0.4.4", "tracing", "url", ] [[package]] name = "katana-rpc-api" -version = "1.0.1" +version = "1.0.11" dependencies = [ "jsonrpsee 0.16.3", "katana-core", @@ -8611,11 +8671,12 @@ dependencies = [ [[package]] name = "katana-rpc-types" -version = "1.0.1" +version = "1.0.11" dependencies = [ "alloy-primitives", "anyhow", "derive_more 0.99.18", + "flate2", "futures", "jsonrpsee 0.16.3", "katana-cairo", @@ -8624,18 +8685,21 @@ dependencies = [ "katana-pool", "katana-primitives", "katana-provider", + "katana-trie", "num-traits 0.2.19", "rstest 0.18.2", "serde", "serde_json", - "serde_with 3.9.0", + "serde_json_pythonic", + "serde_with 3.11.0", + "similar-asserts", "starknet 0.12.0", - "thiserror", + "thiserror 1.0.63", ] [[package]] name = "katana-rpc-types-builder" -version = "1.0.1" +version = "1.0.11" dependencies = [ "anyhow", "katana-executor", @@ -8647,7 +8711,7 @@ dependencies = [ [[package]] name = "katana-runner" -version = "1.0.1" +version = "1.0.11" dependencies = [ "anyhow", "assert_fs", @@ -8660,16 +8724,16 @@ dependencies = [ [[package]] name = "katana-runner-macro" -version = "1.0.1" +version = "1.0.11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] name = "katana-slot-controller" -version = "1.0.1" +version = "1.0.11" dependencies = [ "alloy-primitives", "anyhow", @@ -8685,13 +8749,36 @@ dependencies = [ "webauthn-rs-proto", ] +[[package]] +name = "katana-stage" +version = "1.0.11" +dependencies = [ + "anyhow", + "async-trait", + "backon", + "futures", + "katana-core", + "katana-executor", + "katana-feeder-gateway", + "katana-pool", + "katana-primitives", + "katana-provider", + "katana-rpc-types", + "katana-tasks", + "num-traits 0.2.19", + "starknet 0.12.0", + "thiserror 1.0.63", + "tokio", + "tracing", +] + [[package]] name = "katana-tasks" -version = "1.0.1" +version = "1.0.11" dependencies = [ "futures", "rayon", - "thiserror", + "thiserror 1.0.63", "tokio", "tokio-metrics", "tokio-util", @@ -8700,7 +8787,7 @@ dependencies = [ [[package]] name = "katana-trie" -version = "1.0.1" +version = "1.0.11" dependencies = [ "anyhow", "bitvec", @@ -8710,7 +8797,7 @@ dependencies = [ "slab", "starknet 0.12.0", "starknet-types-core", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -8777,7 +8864,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55cb077ad656299f160924eb2912aa147d7339ea7d69e1b5517326fdcec3c1ca" dependencies = [ "ascii-canvas", - "bit-set", + "bit-set 0.5.3", "ena", "itertools 0.11.0", "lalrpop-util", @@ -8852,7 +8939,7 @@ checksum = "ee58dbc414bd23885d7da915e0457618b36d1fc950a6169ef2cb29829d1b1a1d" dependencies = [ "bytes", "lazy_static", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -8919,7 +9006,7 @@ dependencies = [ "multiaddr 0.18.1", "pin-project", "rw-stream-sink", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -8964,7 +9051,7 @@ dependencies = [ "rand 0.8.5", "rw-stream-sink", "smallvec", - "thiserror", + "thiserror 1.0.63", "tracing", "unsigned-varint 0.8.0", "void", @@ -9033,7 +9120,7 @@ dependencies = [ "quick-protobuf", "quick-protobuf-codec", "smallvec", - "thiserror", + "thiserror 1.0.63", "tracing", "void", ] @@ -9051,7 +9138,7 @@ dependencies = [ "quick-protobuf", "rand 0.8.5", "sha2 0.10.8", - "thiserror", + "thiserror 1.0.63", "tracing", "zeroize", ] @@ -9113,7 +9200,7 @@ dependencies = [ "sha2 0.10.8", "snow", "static_assertions", - "thiserror", + "thiserror 1.0.63", "tracing", "x25519-dalek", "zeroize", @@ -9154,7 +9241,7 @@ dependencies = [ "ring 0.17.8", "rustls 0.23.13", "socket2 0.5.7", - "thiserror", + "thiserror 1.0.63", "tokio", "tracing", ] @@ -9177,7 +9264,7 @@ dependencies = [ "quick-protobuf-codec", "rand 0.8.5", "static_assertions", - "thiserror", + "thiserror 1.0.63", "tracing", "void", "web-time", @@ -9216,7 +9303,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -9248,7 +9335,7 @@ dependencies = [ "ring 0.17.8", "rustls 0.23.13", "rustls-webpki 0.101.7", - "thiserror", + "thiserror 1.0.63", "x509-parser 0.16.0", "yasna", ] @@ -9288,7 +9375,7 @@ dependencies = [ "rcgen", "serde", "stun 0.6.0", - "thiserror", + "thiserror 1.0.63", "tinytemplate", "tokio", "tokio-util", @@ -9313,7 +9400,7 @@ dependencies = [ "rand 0.8.5", "serde", "sha2 0.10.8", - "thiserror", + "thiserror 1.0.63", "tinytemplate", "tracing", ] @@ -9332,7 +9419,7 @@ dependencies = [ "libp2p-identity", "libp2p-webrtc-utils", "send_wrapper 0.6.0", - "thiserror", + "thiserror 1.0.63", "tracing", "wasm-bindgen", "wasm-bindgen-futures", @@ -9353,7 +9440,7 @@ dependencies = [ "pin-project-lite", "rw-stream-sink", "soketto 0.8.0", - "thiserror", + "thiserror 1.0.63", "tracing", "url", "webpki-roots 0.25.4", @@ -9370,7 +9457,7 @@ dependencies = [ "libp2p-core", "parking_lot 0.12.3", "send_wrapper 0.6.0", - "thiserror", + "thiserror 1.0.63", "tracing", "wasm-bindgen", "web-sys", @@ -9384,7 +9471,7 @@ dependencies = [ "either", "futures", "libp2p-core", - "thiserror", + "thiserror 1.0.63", "tracing", "yamux 0.12.1", "yamux 0.13.3", @@ -9446,7 +9533,7 @@ checksum = "cb26336e6dc7cc76e7927d2c9e7e3bb376d7af65a6f56a0b16c47d18a9b1abc5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -9630,7 +9717,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -9649,7 +9736,7 @@ dependencies = [ "metrics", "metrics-util", "quanta", - "thiserror", + "thiserror 1.0.63", "tokio", "tracing", ] @@ -9749,6 +9836,18 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + [[package]] name = "mio" version = "1.0.2" @@ -9983,7 +10082,7 @@ dependencies = [ "anyhow", "byteorder", "paste", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -9997,7 +10096,7 @@ dependencies = [ "log", "netlink-packet-core", "netlink-sys", - "thiserror", + "thiserror 1.0.63", "tokio", ] @@ -10020,6 +10119,15 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "newline-converter" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b6b097ecb1cbfed438542d16e84fd7ad9b0c76c8a65b7f9039212a3d14dc7f" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "nibble_vec" version = "0.1.0" @@ -10112,7 +10220,7 @@ dependencies = [ "kqueue", "libc", "log", - "mio", + "mio 1.0.2", "notify-types", "walkdir", "windows-sys 0.52.0", @@ -10210,7 +10318,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -10328,7 +10436,7 @@ checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -10429,7 +10537,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -10708,7 +10816,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdbef9d1d47087a895abd220ed25eb4ad973a5e26f6a4367b038c25e28dfc2d9" dependencies = [ "memchr", - "thiserror", + "thiserror 1.0.63", "ucd-trie", ] @@ -10732,7 +10840,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -10786,7 +10894,7 @@ dependencies = [ "phf_shared 0.11.2", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -10830,7 +10938,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -11021,7 +11129,7 @@ dependencies = [ "smallvec", "symbolic-demangle", "tempfile", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -11092,7 +11200,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba" dependencies = [ "proc-macro2", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -11123,7 +11231,7 @@ version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e17d47ce914bf4de440332250b0edd23ce48c005f59fab39d3335866b114f11a" dependencies = [ - "thiserror", + "thiserror 1.0.63", "toml 0.5.11", ] @@ -11179,14 +11287,14 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] name = "proc-macro2" -version = "1.0.86" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] @@ -11240,7 +11348,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a65f2e60fbf1063868558d69c6beacf412dc755f9fc020f514b7955fc914fe30" dependencies = [ "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -11263,17 +11371,17 @@ checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] name = "proptest" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4c2511913b88df1637da85cc8d96ec8e43a3f8bb8ccb71ee1ac240d6f3df58d" +checksum = "14cae93065090804185d3b75f0bf93b8eeda30c7a9b4a33d3bdb3988d6229e50" dependencies = [ - "bit-set", - "bit-vec", + "bit-set 0.8.0", + "bit-vec 0.8.0", "bitflags 2.6.0", "lazy_static", "num-traits 0.2.19", @@ -11294,7 +11402,7 @@ checksum = "6ff7ff745a347b87471d859a377a9a404361e7efc2a971d73424a6d183c0fc77" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -11324,8 +11432,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4" dependencies = [ "bytes", - "heck 0.5.0", - "itertools 0.12.1", + "heck 0.4.1", + "itertools 0.11.0", "log", "multimap", "once_cell", @@ -11334,7 +11442,7 @@ dependencies = [ "prost 0.12.6", "prost-types 0.12.6", "regex", - "syn 2.0.77", + "syn 2.0.90", "tempfile", ] @@ -11345,7 +11453,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c1318b19085f08681016926435853bbf7858f9c082d0999b80550ff5d9abe15" dependencies = [ "bytes", - "heck 0.5.0", + "heck 0.4.1", "itertools 0.13.0", "log", "multimap", @@ -11355,7 +11463,7 @@ dependencies = [ "prost 0.13.3", "prost-types 0.13.3", "regex", - "syn 2.0.77", + "syn 2.0.90", "tempfile", ] @@ -11366,10 +11474,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" dependencies = [ "anyhow", - "itertools 0.12.1", + "itertools 0.11.0", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -11382,7 +11490,7 @@ dependencies = [ "itertools 0.13.0", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -11419,7 +11527,7 @@ dependencies = [ "serde", "serde_json", "starknet-types-core", - "thiserror", + "thiserror 1.0.63", "tokio", "tracing", "url", @@ -11514,7 +11622,7 @@ dependencies = [ "asynchronous-codec", "bytes", "quick-protobuf", - "thiserror", + "thiserror 1.0.63", "unsigned-varint 0.8.0", ] @@ -11541,7 +11649,7 @@ dependencies = [ "rustc-hash 2.0.0", "rustls 0.23.13", "socket2 0.5.7", - "thiserror", + "thiserror 1.0.63", "tokio", "tracing", ] @@ -11558,7 +11666,7 @@ dependencies = [ "rustc-hash 2.0.0", "rustls 0.23.13", "slab", - "thiserror", + "thiserror 1.0.63", "tinyvec", "tracing", ] @@ -11708,7 +11816,7 @@ dependencies = [ "rand_chacha", "simd_helpers", "system-deps", - "thiserror", + "thiserror 1.0.63", "v_frame", "wasm-bindgen", ] @@ -11825,7 +11933,7 @@ checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ "getrandom", "libredox", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -12029,7 +12137,7 @@ dependencies = [ "libc", "parking_lot 0.12.3", "reth-mdbx-sys", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -12232,7 +12340,7 @@ dependencies = [ "regex", "relative-path", "rustc_version 0.4.1", - "syn 2.0.77", + "syn 2.0.90", "unicode-ident", ] @@ -12245,7 +12353,7 @@ dependencies = [ "quote", "rand 0.8.5", "rustc_version 0.4.1", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -12255,7 +12363,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33648a781874466a62d89e265fee9f17e32bc7d05a256e6cca41bf97eadcd8aa" dependencies = [ "bytes", - "thiserror", + "thiserror 1.0.63", "webrtc-util 0.8.1", ] @@ -12270,7 +12378,7 @@ dependencies = [ "netlink-packet-route", "netlink-proto", "nix 0.24.3", - "thiserror", + "thiserror 1.0.63", "tokio", ] @@ -12293,7 +12401,7 @@ dependencies = [ "bytes", "rand 0.8.5", "serde", - "thiserror", + "thiserror 1.0.63", "webrtc-util 0.8.1", ] @@ -12306,7 +12414,7 @@ dependencies = [ "bytes", "rand 0.8.5", "serde", - "thiserror", + "thiserror 1.0.63", "webrtc-util 0.8.1", ] @@ -12367,7 +12475,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -12684,7 +12792,7 @@ dependencies = [ [[package]] name = "saya" -version = "1.0.1" +version = "1.0.11" dependencies = [ "anyhow", "clap", @@ -12702,7 +12810,7 @@ dependencies = [ [[package]] name = "saya-core" -version = "1.0.1" +version = "1.0.11" dependencies = [ "anyhow", "async-trait", @@ -12727,7 +12835,7 @@ dependencies = [ "starknet 0.12.0", "starknet-crypto 0.7.2", "tempdir", - "thiserror", + "thiserror 1.0.63", "tokio", "tracing", "url", @@ -12735,7 +12843,7 @@ dependencies = [ [[package]] name = "saya-provider" -version = "1.0.1" +version = "1.0.11" dependencies = [ "alloy-primitives", "anyhow", @@ -12756,9 +12864,9 @@ dependencies = [ "num-traits 0.2.19", "serde", "serde_json", - "serde_with 3.9.0", + "serde_with 3.11.0", "starknet 0.12.0", - "thiserror", + "thiserror 1.0.63", "tokio", "tracing", "url", @@ -12767,7 +12875,6 @@ dependencies = [ [[package]] name = "scarb" version = "2.8.4" -source = "git+https://github.com/dojoengine/scarb?rev=7eac49b3e61236ce466e712225d9c989f9db1ef3#7eac49b3e61236ce466e712225d9c989f9db1ef3" dependencies = [ "anyhow", "async-trait", @@ -12797,7 +12904,7 @@ dependencies = [ "derive_builder", "dialoguer", "directories", - "dojo-lang 1.0.0-rc.2", + "dojo-lang", "dunce", "fs4", "fs_extra", @@ -12807,7 +12914,7 @@ dependencies = [ "glob", "ignore", "include_dir", - "indoc 2.0.5", + "indoc", "itertools 0.12.1", "libloading", "once_cell", @@ -12817,8 +12924,8 @@ dependencies = [ "redb", "reqwest 0.11.27", "scarb-build-metadata", - "scarb-metadata 1.12.0 (git+https://github.com/dojoengine/scarb?rev=7eac49b3e61236ce466e712225d9c989f9db1ef3)", - "scarb-stable-hash 1.0.0 (git+https://github.com/dojoengine/scarb?rev=7eac49b3e61236ce466e712225d9c989f9db1ef3)", + "scarb-metadata 1.12.0", + "scarb-stable-hash 1.0.0", "scarb-ui", "semver 1.0.23", "serde", @@ -12830,7 +12937,7 @@ dependencies = [ "smallvec", "smol_str", "tar", - "thiserror", + "thiserror 1.0.63", "tokio", "toml 0.8.19", "toml_edit", @@ -12848,7 +12955,6 @@ dependencies = [ [[package]] name = "scarb-build-metadata" version = "2.8.4" -source = "git+https://github.com/dojoengine/scarb?rev=7eac49b3e61236ce466e712225d9c989f9db1ef3#7eac49b3e61236ce466e712225d9c989f9db1ef3" dependencies = [ "cargo_metadata", ] @@ -12856,34 +12962,31 @@ dependencies = [ [[package]] name = "scarb-metadata" version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "170ebce1774a85568646ba4096827f898306665187eebd9282fee313e316518d" dependencies = [ "camino", + "derive_builder", "semver 1.0.23", "serde", "serde_json", - "thiserror", + "thiserror 1.0.63", ] [[package]] name = "scarb-metadata" version = "1.12.0" -source = "git+https://github.com/dojoengine/scarb?rev=7eac49b3e61236ce466e712225d9c989f9db1ef3#7eac49b3e61236ce466e712225d9c989f9db1ef3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170ebce1774a85568646ba4096827f898306665187eebd9282fee313e316518d" dependencies = [ "camino", - "derive_builder", "semver 1.0.23", "serde", "serde_json", - "thiserror", + "thiserror 1.0.63", ] [[package]] name = "scarb-stable-hash" version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1902536b23a05dd165d3992865870aaf1b0650317767cbf171ed2ca5903732a9" dependencies = [ "data-encoding", "xxhash-rust", @@ -12892,7 +12995,8 @@ dependencies = [ [[package]] name = "scarb-stable-hash" version = "1.0.0" -source = "git+https://github.com/dojoengine/scarb?rev=7eac49b3e61236ce466e712225d9c989f9db1ef3#7eac49b3e61236ce466e712225d9c989f9db1ef3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1902536b23a05dd165d3992865870aaf1b0650317767cbf171ed2ca5903732a9" dependencies = [ "data-encoding", "xxhash-rust", @@ -12901,14 +13005,13 @@ dependencies = [ [[package]] name = "scarb-ui" version = "0.1.5" -source = "git+https://github.com/dojoengine/scarb?rev=7eac49b3e61236ce466e712225d9c989f9db1ef3#7eac49b3e61236ce466e712225d9c989f9db1ef3" dependencies = [ "anyhow", "camino", "clap", "console", "indicatif", - "scarb-metadata 1.12.0 (git+https://github.com/dojoengine/scarb?rev=7eac49b3e61236ce466e712225d9c989f9db1ef3)", + "scarb-metadata 1.12.0", "serde", "serde_json", "tracing-core", @@ -12925,7 +13028,7 @@ dependencies = [ [[package]] name = "scheduler" -version = "1.0.1" +version = "1.0.11" dependencies = [ "anyhow", "clap", @@ -12960,7 +13063,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -13005,7 +13108,7 @@ checksum = "13254db766b17451aced321e7397ebf0a446ef0c8d2942b6e67a95815421093f" dependencies = [ "rand 0.8.5", "substring", - "thiserror", + "thiserror 1.0.63", "url", ] @@ -13173,7 +13276,7 @@ checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -13184,14 +13287,14 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] name = "serde_json" -version = "1.0.132" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" +checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" dependencies = [ "itoa", "memchr", @@ -13228,7 +13331,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -13270,9 +13373,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.9.0" +version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cecfa94848272156ea67b2b1a53f20fc7bc638c4a46d2f8abde08f05f4b857" +checksum = "8e28bdad6db2b8340e449f7108f020b3b092e8583a9e3fb82713e1d4e71fe817" dependencies = [ "base64 0.22.1", "chrono", @@ -13282,7 +13385,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "serde_with_macros 3.9.0", + "serde_with_macros 3.11.0", "time", ] @@ -13295,19 +13398,19 @@ dependencies = [ "darling 0.20.10", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] name = "serde_with_macros" -version = "3.9.0" +version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8fee4991ef4f274617a51ad4af30519438dacb2f56ac773b08a1922ff743350" +checksum = "9d846214a9854ef724f3da161b426242d8de7c1fc7de2f89bb1efcb154dca79d" dependencies = [ "darling 0.20.10", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -13342,7 +13445,7 @@ checksum = "91d129178576168c589c9ec973feedf7d3126c01ac2bf08795109aa35b69fb8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -13459,6 +13562,17 @@ dependencies = [ "signal-hook-registry", ] +[[package]] +name = "signal-hook-mio" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +dependencies = [ + "libc", + "mio 0.8.11", + "signal-hook", +] + [[package]] name = "signal-hook-registry" version = "1.4.2" @@ -13557,7 +13671,7 @@ dependencies = [ "serde_json", "starknet 0.12.0", "tempfile", - "thiserror", + "thiserror 1.0.63", "tokio", "tower-http 0.5.2", "tracing", @@ -13566,6 +13680,15 @@ dependencies = [ "webbrowser", ] +[[package]] +name = "slotmap" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" +dependencies = [ + "version_check", +] + [[package]] name = "smallvec" version = "1.13.2" @@ -13654,11 +13777,11 @@ dependencies = [ [[package]] name = "sozo" -version = "1.0.1" +version = "1.0.11" dependencies = [ "anyhow", "async-trait", - "cainome 0.4.6", + "cainome 0.4.11", "cairo-lang-compiler", "cairo-lang-filesystem", "cairo-lang-project", @@ -13671,9 +13794,9 @@ dependencies = [ "clap-verbosity-flag", "colored", "dojo-bindgen", - "dojo-lang 1.0.1", + "dojo-lang", "dojo-test-utils", - "dojo-types 1.0.1", + "dojo-types", "dojo-utils", "dojo-world", "itertools 0.12.1", @@ -13694,7 +13817,7 @@ dependencies = [ "starknet 0.12.0", "starknet-crypto 0.7.2", "tabled", - "thiserror", + "thiserror 1.0.63", "tokio", "toml 0.8.19", "tracing", @@ -13705,32 +13828,31 @@ dependencies = [ [[package]] name = "sozo-ops" -version = "1.0.1" +version = "1.0.11" dependencies = [ "anyhow", "assert_fs", "async-trait", - "cainome 0.4.6", + "cainome 0.4.11", "colored", "colored_json", "dojo-test-utils", - "dojo-types 1.0.1", + "dojo-types", "dojo-utils", "dojo-world", "futures", - "ipfs-api-backend-hyper", "katana-runner", "num-traits 0.2.19", "scarb", "serde", "serde_json", - "serde_with 3.9.0", + "serde_with 3.11.0", "sozo-scarbext", "sozo-walnut", "spinoff", "starknet 0.12.0", "starknet-crypto 0.7.2", - "thiserror", + "thiserror 1.0.63", "tokio", "toml 0.8.19", "tracing", @@ -13738,7 +13860,7 @@ dependencies = [ [[package]] name = "sozo-scarbext" -version = "1.0.1" +version = "1.0.11" dependencies = [ "anyhow", "camino", @@ -13751,7 +13873,7 @@ dependencies = [ [[package]] name = "sozo-signers" -version = "1.0.1" +version = "1.0.11" dependencies = [ "anyhow", "starknet 0.12.0", @@ -13759,19 +13881,19 @@ dependencies = [ [[package]] name = "sozo-walnut" -version = "1.0.1" +version = "1.0.11" dependencies = [ "anyhow", + "clap", "console", - "dojo-world", + "dojo-utils", "reqwest 0.11.27", "scarb", "scarb-ui", "serde", "serde_json", - "sozo-scarbext", "starknet 0.12.0", - "thiserror", + "thiserror 1.0.63", "url", "urlencoding", "walkdir", @@ -13882,7 +14004,7 @@ dependencies = [ "sha2 0.10.8", "smallvec", "sqlformat", - "thiserror", + "thiserror 1.0.63", "tokio", "tokio-stream", "tracing", @@ -13900,7 +14022,7 @@ dependencies = [ "quote", "sqlx-core", "sqlx-macros-core", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -13924,7 +14046,7 @@ dependencies = [ "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn 2.0.77", + "syn 2.0.90", "tempfile", "tokio", "url", @@ -13968,7 +14090,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror", + "thiserror 1.0.63", "tracing", "uuid 1.10.0", "whoami", @@ -14008,7 +14130,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror", + "thiserror 1.0.63", "tracing", "uuid 1.10.0", "whoami", @@ -14088,7 +14210,7 @@ dependencies = [ "starknet-crypto 0.7.2", "starknet-providers 0.11.0", "starknet-signers 0.9.0", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -14103,7 +14225,7 @@ dependencies = [ "starknet-crypto 0.7.2", "starknet-providers 0.12.0", "starknet-signers 0.10.0", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -14118,7 +14240,7 @@ dependencies = [ "starknet-accounts 0.10.0", "starknet-core 0.11.1", "starknet-providers 0.11.0", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -14129,11 +14251,11 @@ checksum = "bd6ee5762d24c4f06ab7e9406550925df406712e73719bd2de905c879c674a87" dependencies = [ "serde", "serde_json", - "serde_with 3.9.0", + "serde_with 3.11.0", "starknet-accounts 0.11.0", "starknet-core 0.12.0", "starknet-providers 0.12.0", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -14168,7 +14290,7 @@ dependencies = [ "serde", "serde_json", "serde_json_pythonic", - "serde_with 3.9.0", + "serde_with 3.11.0", "sha3", "starknet-crypto 0.7.2", "starknet-types-core", @@ -14241,7 +14363,7 @@ checksum = "bbc159a1934c7be9761c237333a57febe060ace2bc9e3b337a59a37af206d19f" dependencies = [ "starknet-curve 0.4.2", "starknet-ff", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -14292,7 +14414,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8986a940af916fc0a034f4e42c6ba76d94f1e97216d75447693dfd7aefaf3ef2" dependencies = [ "starknet-core 0.12.0", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -14312,7 +14434,7 @@ dependencies = [ "serde_json", "serde_with 2.3.3", "starknet-core 0.11.1", - "thiserror", + "thiserror 1.0.63", "url", ] @@ -14331,9 +14453,9 @@ dependencies = [ "reqwest 0.11.27", "serde", "serde_json", - "serde_with 3.9.0", + "serde_with 3.11.0", "starknet-core 0.12.0", - "thiserror", + "thiserror 1.0.63", "url", ] @@ -14351,7 +14473,7 @@ dependencies = [ "rand 0.8.5", "starknet-core 0.11.1", "starknet-crypto 0.7.2", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -14368,7 +14490,7 @@ dependencies = [ "rand 0.8.5", "starknet-core 0.12.0", "starknet-crypto 0.7.2", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -14408,7 +14530,7 @@ dependencies = [ "starknet-types-core", "strum 0.24.1", "strum_macros 0.24.3", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -14509,7 +14631,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -14522,7 +14644,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -14538,7 +14660,7 @@ dependencies = [ "rand 0.8.5", "ring 0.17.8", "subtle", - "thiserror", + "thiserror 1.0.63", "tokio", "url", "webrtc-util 0.8.1", @@ -14557,7 +14679,7 @@ dependencies = [ "rand 0.8.5", "ring 0.17.8", "subtle", - "thiserror", + "thiserror 1.0.63", "tokio", "url", "webrtc-util 0.9.0", @@ -14629,9 +14751,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.77" +version = "2.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" dependencies = [ "proc-macro2", "quote", @@ -14647,7 +14769,7 @@ dependencies = [ "paste", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -14659,7 +14781,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -14697,7 +14819,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -14830,16 +14952,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "terminal_size" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" -dependencies = [ - "rustix 0.38.37", - "windows-sys 0.48.0", -] - [[package]] name = "termtree" version = "0.4.1" @@ -14852,7 +14964,16 @@ version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.63", +] + +[[package]] +name = "thiserror" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93605438cbd668185516ab499d589afb7ee1859ea3d5fc8f6b0755e1c7443767" +dependencies = [ + "thiserror-impl 2.0.7", ] [[package]] @@ -14863,7 +14984,18 @@ checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d8749b4531af2117677a5fcd12b1348a3fe2b81e36e61ffeac5c4aa3273e36" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", ] [[package]] @@ -14992,7 +15124,7 @@ dependencies = [ "backtrace", "bytes", "libc", - "mio", + "mio 1.0.2", "parking_lot 0.12.3", "pin-project-lite", "signal-hook-registry", @@ -15019,7 +15151,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -15099,6 +15231,18 @@ dependencies = [ "tokio-util", ] +[[package]] +name = "tokio-tungstenite" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite 0.20.1", +] + [[package]] name = "tokio-tungstenite" version = "0.21.0" @@ -15108,7 +15252,7 @@ dependencies = [ "futures-util", "log", "tokio", - "tungstenite", + "tungstenite 0.21.0", ] [[package]] @@ -15200,6 +15344,7 @@ dependencies = [ "tower-layer", "tower-service", "tracing", + "webpki-roots 0.26.6", ] [[package]] @@ -15234,7 +15379,7 @@ dependencies = [ "proc-macro2", "prost-build 0.12.6", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -15248,7 +15393,7 @@ dependencies = [ "prost-build 0.13.3", "prost-types 0.13.3", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -15300,7 +15445,7 @@ dependencies = [ "httparse", "js-sys", "pin-project", - "thiserror", + "thiserror 1.0.63", "tonic 0.12.3", "tower-service", "wasm-bindgen", @@ -15311,57 +15456,18 @@ dependencies = [ [[package]] name = "torii" -version = "1.0.1" +version = "1.0.11" dependencies = [ "anyhow", - "assert_matches", - "async-trait", - "base64 0.21.7", - "camino", - "chrono", "clap", - "clap_complete", - "clap_config", - "ctrlc", - "dojo-metrics", - "dojo-types 1.0.1", - "dojo-utils", - "dojo-world", - "either", - "futures", - "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.30", - "hyper-reverse-proxy", - "indexmap 2.5.0", - "lazy_static", - "serde", - "serde_json", - "sqlx", - "starknet 0.12.0", - "starknet-crypto 0.7.2", - "tempfile", "tokio", - "tokio-stream", - "tokio-util", - "toml 0.8.19", "torii-cli", - "torii-core", - "torii-graphql", - "torii-grpc", - "torii-relay", - "torii-server", - "tower 0.4.13", - "tower-http 0.4.4", - "tracing", - "tracing-subscriber", - "url", - "webbrowser", + "torii-runner", ] [[package]] name = "torii-cli" -version = "1.0.1" +version = "1.0.11" dependencies = [ "anyhow", "assert_matches", @@ -15371,18 +15477,18 @@ dependencies = [ "serde", "starknet 0.12.0", "toml 0.8.19", - "torii-core", + "torii-sqlite", "url", ] [[package]] name = "torii-client" -version = "1.0.1" +version = "1.0.11" dependencies = [ "async-trait", "camino", "crypto-bigint", - "dojo-types 1.0.1", + "dojo-types", "dojo-world", "futures", "futures-util", @@ -15394,7 +15500,7 @@ dependencies = [ "serde_json", "starknet 0.12.0", "starknet-crypto 0.7.2", - "thiserror", + "thiserror 1.0.63", "tokio", "tonic 0.11.0", "tonic 0.12.3", @@ -15403,48 +15509,9 @@ dependencies = [ "url", ] -[[package]] -name = "torii-core" -version = "1.0.1" -dependencies = [ - "anyhow", - "async-trait", - "base64 0.21.7", - "bitflags 2.6.0", - "cainome 0.4.6", - "chrono", - "crypto-bigint", - "data-url", - "dojo-test-utils", - "dojo-types 1.0.1", - "dojo-utils", - "dojo-world", - "futures-channel", - "futures-util", - "hashlink", - "ipfs-api-backend-hyper", - "katana-runner", - "num-traits 0.2.19", - "once_cell", - "reqwest 0.11.27", - "scarb", - "serde", - "serde_json", - "slab", - "sozo-scarbext", - "sqlx", - "starknet 0.12.0", - "starknet-crypto 0.7.2", - "tempfile", - "thiserror", - "tokio", - "tokio-util", - "tracing", -] - [[package]] name = "torii-graphql" -version = "1.0.1" +version = "1.0.11" dependencies = [ "anyhow", "async-graphql", @@ -15455,7 +15522,7 @@ dependencies = [ "chrono", "convert_case 0.6.0", "dojo-test-utils", - "dojo-types 1.0.1", + "dojo-types", "dojo-utils", "dojo-world", "katana-runner", @@ -15472,11 +15539,12 @@ dependencies = [ "strum 0.25.0", "strum_macros 0.25.3", "tempfile", - "thiserror", + "thiserror 1.0.63", "tokio", "tokio-stream", "toml 0.8.19", - "torii-core", + "torii-indexer", + "torii-sqlite", "tracing", "url", "warp", @@ -15484,13 +15552,13 @@ dependencies = [ [[package]] name = "torii-grpc" -version = "1.0.1" +version = "1.0.11" dependencies = [ - "cainome 0.4.6", + "cainome 0.4.11", "camino", "crypto-bigint", "dojo-test-utils", - "dojo-types 1.0.1", + "dojo-types", "dojo-utils", "dojo-world", "futures", @@ -15513,7 +15581,7 @@ dependencies = [ "strum 0.25.0", "strum_macros 0.25.3", "tempfile", - "thiserror", + "thiserror 1.0.63", "tokio", "tokio-stream", "tonic 0.11.0", @@ -15523,21 +15591,60 @@ dependencies = [ "tonic-reflection", "tonic-web", "tonic-web-wasm-client", - "torii-core", + "torii-indexer", + "torii-sqlite", "tower 0.4.13", "tower-http 0.4.4", "tracing", ] [[package]] -name = "torii-relay" -version = "1.0.1" +name = "torii-indexer" +version = "1.0.11" dependencies = [ "anyhow", - "cainome 0.4.6", + "async-trait", + "base64 0.21.7", + "bitflags 2.6.0", + "cainome 0.4.11", "chrono", "crypto-bigint", - "dojo-types 1.0.1", + "data-url", + "dojo-test-utils", + "dojo-types", + "dojo-utils", + "dojo-world", + "futures-channel", + "futures-util", + "hashlink", + "ipfs-api-backend-hyper", + "katana-runner", + "num-traits 0.2.19", + "once_cell", + "reqwest 0.11.27", + "scarb", + "serde", + "serde_json", + "slab", + "sozo-scarbext", + "sqlx", + "starknet 0.12.0", + "starknet-crypto 0.7.2", + "tempfile", + "thiserror 1.0.63", + "tokio", + "tokio-util", + "torii-sqlite", + "tracing", +] + +[[package]] +name = "torii-relay" +version = "1.0.11" +dependencies = [ + "anyhow", + "chrono", + "dojo-types", "dojo-world", "futures", "indexmap 2.5.0", @@ -15553,9 +15660,10 @@ dependencies = [ "starknet 0.12.0", "starknet-crypto 0.7.2", "tempfile", - "thiserror", + "thiserror 1.0.63", "tokio", - "torii-core", + "torii-sqlite", + "torii-typed-data", "tracing", "tracing-subscriber", "tracing-wasm", @@ -15564,19 +15672,70 @@ dependencies = [ "wasm-timer", ] +[[package]] +name = "torii-runner" +version = "1.0.11" +dependencies = [ + "anyhow", + "assert_matches", + "async-trait", + "base64 0.21.7", + "camino", + "chrono", + "ctrlc", + "dojo-metrics", + "dojo-types", + "dojo-utils", + "dojo-world", + "either", + "futures", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.30", + "hyper-reverse-proxy", + "indexmap 2.5.0", + "lazy_static", + "serde", + "serde_json", + "sqlx", + "starknet 0.12.0", + "starknet-crypto 0.7.2", + "tempfile", + "tokio", + "tokio-stream", + "tokio-util", + "toml 0.8.19", + "torii-cli", + "torii-graphql", + "torii-grpc", + "torii-indexer", + "torii-relay", + "torii-server", + "torii-sqlite", + "tower 0.4.13", + "tower-http 0.4.4", + "tracing", + "tracing-subscriber", + "url", + "webbrowser", +] + [[package]] name = "torii-server" -version = "1.0.1" +version = "1.0.11" dependencies = [ "anyhow", + "async-trait", "base64 0.21.7", "camino", "data-url", "form_urlencoded", + "futures-util", "http 0.2.12", "http-body 0.4.6", "hyper 0.14.30", "hyper-reverse-proxy", + "hyper-tungstenite", "image", "indexmap 2.5.0", "lazy_static", @@ -15586,14 +15745,69 @@ dependencies = [ "serde_json", "sqlx", "tokio", + "tokio-tungstenite 0.20.1", "tokio-util", - "torii-core", + "torii-sqlite", "tower 0.4.13", "tower-http 0.4.4", "tracing", "warp", ] +[[package]] +name = "torii-sqlite" +version = "1.0.11" +dependencies = [ + "anyhow", + "async-trait", + "base64 0.21.7", + "bitflags 2.6.0", + "cainome 0.4.11", + "chrono", + "crypto-bigint", + "data-url", + "dojo-test-utils", + "dojo-types", + "dojo-utils", + "dojo-world", + "futures-channel", + "futures-util", + "hashlink", + "ipfs-api-backend-hyper", + "katana-runner", + "once_cell", + "reqwest 0.11.27", + "scarb", + "serde", + "serde_json", + "slab", + "sozo-scarbext", + "sqlx", + "starknet 0.12.0", + "starknet-crypto 0.7.2", + "tempfile", + "thiserror 1.0.63", + "tokio", + "tokio-util", + "torii-indexer", + "tracing", +] + +[[package]] +name = "torii-typed-data" +version = "1.0.11" +dependencies = [ + "cainome 0.4.11", + "crypto-bigint", + "dojo-types", + "indexmap 2.5.0", + "serde", + "serde_json", + "starknet 0.12.0", + "starknet-crypto 0.7.2", + "thiserror 1.0.63", +] + [[package]] name = "tower" version = "0.4.13" @@ -15715,7 +15929,7 @@ checksum = "84fd902d4e0b9a4b27f2f440108dc034e1758628a9b702f8ec61ad66355422fa" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -15744,7 +15958,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -15870,7 +16084,26 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.77", + "syn 2.0.90", +] + +[[package]] +name = "tungstenite" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http 0.2.12", + "httparse", + "log", + "rand 0.8.5", + "sha1", + "thiserror 1.0.63", + "url", + "utf-8", ] [[package]] @@ -15887,7 +16120,7 @@ dependencies = [ "log", "rand 0.8.5", "sha1", - "thiserror", + "thiserror 1.0.63", "url", "utf-8", ] @@ -15906,7 +16139,7 @@ dependencies = [ "rand 0.8.5", "ring 0.17.8", "stun 0.5.1", - "thiserror", + "thiserror 1.0.63", "tokio", "tokio-util", "webrtc-util 0.8.1", @@ -15929,7 +16162,7 @@ checksum = "560b82d656506509d43abe30e0ba64c56b1953ab3d4fe7ba5902747a7a3cedd5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -15946,7 +16179,7 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "types-test" -version = "1.0.1" +version = "1.0.11" [[package]] name = "u256-literal" @@ -15997,7 +16230,7 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c878a167baa8afd137494101a688ef8c67125089ff2249284bd2b5f9bfedb815" dependencies = [ - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -16248,7 +16481,7 @@ dependencies = [ [[package]] name = "verify_db_balances" -version = "1.0.1" +version = "1.0.11" dependencies = [ "clap", "num-traits 0.2.19", @@ -16364,7 +16597,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "tokio", - "tokio-tungstenite", + "tokio-tungstenite 0.21.0", "tokio-util", "tower-service", "tracing", @@ -16404,7 +16637,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", "wasm-bindgen-shared", ] @@ -16438,7 +16671,7 @@ checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -16472,7 +16705,7 @@ checksum = "4b8220be1fa9e4c889b30fd207d4906657e7e90b12e0e6b0c8b8d8709f5de021" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -16605,7 +16838,7 @@ dependencies = [ "sha2 0.10.8", "smol_str", "stun 0.5.1", - "thiserror", + "thiserror 1.0.63", "time", "tokio", "turn", @@ -16629,7 +16862,7 @@ checksum = "e8c08e648e10572b9edbe741074e0f4d3cb221aa7cdf9a814ee71606de312f33" dependencies = [ "bytes", "log", - "thiserror", + "thiserror 1.0.63", "tokio", "webrtc-sctp", "webrtc-util 0.8.1", @@ -16665,7 +16898,7 @@ dependencies = [ "sha1", "sha2 0.10.8", "subtle", - "thiserror", + "thiserror 1.0.63", "tokio", "webrtc-util 0.8.1", "x25519-dalek", @@ -16686,7 +16919,7 @@ dependencies = [ "serde", "serde_json", "stun 0.5.1", - "thiserror", + "thiserror 1.0.63", "tokio", "turn", "url", @@ -16704,7 +16937,7 @@ checksum = "ce981f93104a8debb3563bb0cedfe4aa2f351fdf6b53f346ab50009424125c08" dependencies = [ "log", "socket2 0.5.7", - "thiserror", + "thiserror 1.0.63", "tokio", "webrtc-util 0.8.1", ] @@ -16719,7 +16952,7 @@ dependencies = [ "bytes", "rand 0.8.5", "rtp 0.10.0", - "thiserror", + "thiserror 1.0.63", ] [[package]] @@ -16734,7 +16967,7 @@ dependencies = [ "crc", "log", "rand 0.8.5", - "thiserror", + "thiserror 1.0.63", "tokio", "webrtc-util 0.8.1", ] @@ -16757,7 +16990,7 @@ dependencies = [ "rtp 0.9.0", "sha1", "subtle", - "thiserror", + "thiserror 1.0.63", "tokio", "webrtc-util 0.8.1", ] @@ -16777,7 +17010,7 @@ dependencies = [ "log", "nix 0.26.4", "rand 0.8.5", - "thiserror", + "thiserror 1.0.63", "tokio", "winapi", ] @@ -16798,7 +17031,7 @@ dependencies = [ "nix 0.26.4", "portable-atomic", "rand 0.8.5", - "thiserror", + "thiserror 1.0.63", "tokio", "winapi", ] @@ -16939,7 +17172,7 @@ checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -16950,7 +17183,7 @@ checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -17260,7 +17493,7 @@ dependencies = [ "oid-registry 0.6.1", "ring 0.16.20", "rusticata-macros", - "thiserror", + "thiserror 1.0.63", "time", ] @@ -17277,7 +17510,7 @@ dependencies = [ "nom", "oid-registry 0.7.1", "rusticata-macros", - "thiserror", + "thiserror 1.0.63", "time", ] @@ -17324,7 +17557,7 @@ checksum = "9d422e8e38ec76e2f06ee439ccc765e9c6a9638b9e7c9f2e8255e4d41e8bd852" [[package]] name = "xtask-generate-test-db" -version = "1.0.1" +version = "1.0.11" dependencies = [ "anyhow", "dojo-test-utils", @@ -17415,7 +17648,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] @@ -17435,7 +17668,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.90", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 43968de1b8..4d9b9c3030 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,13 +16,14 @@ members = [ "crates/dojo/utils", "crates/dojo/world", "crates/dojo/world/abigen", + "crates/katana/cli", "crates/katana/controller", "crates/katana/core", "crates/katana/executor", + "crates/katana/feeder-gateway", "crates/katana/grpc", "crates/katana/node", "crates/katana/node-bindings", - "crates/katana/pipeline", "crates/katana/pool", "crates/katana/primitives", "crates/katana/rpc/rpc", @@ -35,19 +36,20 @@ members = [ "crates/katana/storage/codecs/derive", "crates/katana/storage/db", "crates/katana/storage/provider", + "crates/katana/sync/pipeline", + "crates/katana/sync/stage", "crates/katana/tasks", "crates/katana/trie", - "crates/katana/cli", "crates/metrics", "crates/saya/core", "crates/saya/provider", "crates/sozo/scarbext", "crates/sozo/signers", "crates/sozo/walnut", + "crates/torii/cli", "crates/torii/client", "crates/torii/server", "crates/torii/types-test", - "crates/torii/cli", "examples/spawn-and-move", "scripts/verify_db_balances", "xtask/generate-test-db", @@ -58,7 +60,7 @@ edition = "2021" license = "Apache-2.0" license-file = "LICENSE" repository = "https://github.com/dojoengine/dojo/" -version = "1.0.1" +version = "1.0.11" [profile.performance] codegen-units = 1 @@ -66,9 +68,13 @@ incremental = false inherits = "release" lto = "fat" +[profile.profiling] +debug = true +inherits = "release" + [workspace.dependencies] -cainome = { git = "https://github.com/cartridge-gg/cainome", rev = "5c2616c273faca7700d2ba565503fcefb5b9d720", features = [ "abigen-rs" ] } -cainome-cairo-serde = { git = "https://github.com/cartridge-gg/cainome", rev = "5c2616c273faca7700d2ba565503fcefb5b9d720" } +cainome = { git = "https://github.com/cartridge-gg/cainome", tag = "v0.4.11", features = [ "abigen-rs" ] } +cainome-cairo-serde = { git = "https://github.com/cartridge-gg/cainome", tag = "v0.4.11" } dojo-utils = { path = "crates/dojo/utils" } # metrics @@ -87,14 +93,16 @@ topological-sort = "0.2" # katana katana-cairo = { path = "crates/katana/cairo" } +katana-cli = { path = "crates/katana/cli" } katana-codecs = { path = "crates/katana/storage/codecs" } katana-codecs-derive = { path = "crates/katana/storage/codecs/derive" } katana-core = { path = "crates/katana/core", default-features = false } katana-db = { path = "crates/katana/storage/db" } katana-executor = { path = "crates/katana/executor" } +katana-feeder-gateway = { path = "crates/katana/feeder-gateway" } katana-node = { path = "crates/katana/node", default-features = false } katana-node-bindings = { path = "crates/katana/node-bindings" } -katana-pipeline = { path = "crates/katana/pipeline" } +katana-pipeline = { path = "crates/katana/sync/pipeline" } katana-pool = { path = "crates/katana/pool" } katana-primitives = { path = "crates/katana/primitives" } katana-provider = { path = "crates/katana/storage/provider" } @@ -104,18 +112,21 @@ katana-rpc-types = { path = "crates/katana/rpc/rpc-types" } katana-rpc-types-builder = { path = "crates/katana/rpc/rpc-types-builder" } katana-runner = { path = "crates/katana/runner" } katana-slot-controller = { path = "crates/katana/controller" } +katana-stage = { path = "crates/katana/sync/stage" } katana-tasks = { path = "crates/katana/tasks" } katana-trie = { path = "crates/katana/trie" } -katana-cli = { path = "crates/katana/cli" } # torii +torii-cli = { path = "crates/torii/cli" } torii-client = { path = "crates/torii/client" } -torii-core = { path = "crates/torii/core" } +torii-indexer = { path = "crates/torii/indexer" } +torii-sqlite = { path = "crates/torii/sqlite" } torii-graphql = { path = "crates/torii/graphql" } torii-grpc = { path = "crates/torii/grpc" } torii-relay = { path = "crates/torii/libp2p" } torii-server = { path = "crates/torii/server" } -torii-cli = { path = "crates/torii/cli" } +torii-runner = { path = "crates/torii/runner" } +torii-typed-data = { path = "crates/torii/typed-data" } # saya saya-core = { path = "crates/saya/core" } @@ -136,28 +147,28 @@ auto_impl = "1.2.0" base64 = "0.21.2" bigdecimal = "0.4.1" bytes = "1.6" -cairo-lang-compiler = "2.8.4" -cairo-lang-debug = "2.8.4" -cairo-lang-defs = "2.8.4" +cairo-lang-compiler = "=2.8.4" +cairo-lang-debug = "=2.8.4" +cairo-lang-defs = "=2.8.4" cairo-lang-diagnostics = "2.7.0" -cairo-lang-filesystem = "2.8.4" -cairo-lang-formatter = "2.8.4" -cairo-lang-language-server = "2.8.4" -cairo-lang-lowering = "2.8.4" -cairo-lang-parser = "2.8.4" -cairo-lang-plugins = { version = "2.8.4", features = [ "testing" ] } -cairo-lang-project = "2.8.4" -cairo-lang-semantic = "2.8.4" -cairo-lang-sierra = "2.8.4" -cairo-lang-sierra-generator = "2.8.4" -cairo-lang-sierra-to-casm = "2.8.4" -cairo-lang-starknet = "2.8.4" -cairo-lang-starknet-classes = "2.8.4" -cairo-lang-syntax = "2.8.4" -cairo-lang-test-plugin = "2.8.4" -cairo-lang-test-runner = "2.8.4" -cairo-lang-test-utils = "2.8.4" -cairo-lang-utils = "2.8.4" +cairo-lang-filesystem = "=2.8.4" +cairo-lang-formatter = "=2.8.4" +cairo-lang-language-server = "=2.8.4" +cairo-lang-lowering = "=2.8.4" +cairo-lang-parser = "=2.8.4" +cairo-lang-plugins = { version = "=2.8.4", features = [ "testing" ] } +cairo-lang-project = "=2.8.4" +cairo-lang-semantic = "=2.8.4" +cairo-lang-sierra = "=2.8.4" +cairo-lang-sierra-generator = "=2.8.4" +cairo-lang-sierra-to-casm = "=2.8.4" +cairo-lang-starknet = "=2.8.4" +cairo-lang-starknet-classes = "=2.8.4" +cairo-lang-syntax = "=2.8.4" +cairo-lang-test-plugin = "=2.8.4" +cairo-lang-test-runner = "=2.8.4" +cairo-lang-test-utils = "=2.8.4" +cairo-lang-utils = "=2.8.4" cairo-vm = "1.0.0-rc4" camino = { version = "1.1.2", features = [ "serde1" ] } chrono = { version = "0.4.24", features = [ "serde" ] } @@ -201,12 +212,14 @@ rpassword = "7.2.0" rstest = "0.18.2" rstest_reuse = "0.6.0" salsa = "0.16.1" -scarb = { git = "https://github.com/dojoengine/scarb", rev = "7eac49b3e61236ce466e712225d9c989f9db1ef3" } -scarb-ui = { git = "https://github.com/dojoengine/scarb", rev = "7eac49b3e61236ce466e712225d9c989f9db1ef3" } +scarb = { path = "/Users/glihm/swm/scarb/scarb" } +scarb-ui = { path = "/Users/glihm/swm/scarb/utils/scarb-ui" } +#scarb = { git = "https://github.com/dojoengine/scarb", rev = "7eac49b3e61236ce466e712225d9c989f9db1ef3" } +#scarb-ui = { git = "https://github.com/dojoengine/scarb", rev = "7eac49b3e61236ce466e712225d9c989f9db1ef3" } semver = "1.0.5" serde = { version = "1.0", features = [ "derive" ] } serde_json = { version = "1.0", features = [ "arbitrary_precision" ] } -serde_with = "3.9.0" +serde_with = "3.11.0" similar-asserts = "1.5.0" smol_str = { version = "0.2.0", features = [ "serde" ] } spinoff = "0.8.0" @@ -237,7 +250,7 @@ warp = "0.3" # gRPC prost = "0.12" -tonic = { version = "0.11", features = [ "gzip", "tls", "tls-roots" ] } +tonic = { version = "0.11", features = [ "gzip", "tls", "tls-roots", "tls-webpki-roots" ] } tonic-build = "0.11" tonic-reflection = "0.11" tonic-web = "0.11" @@ -259,7 +272,7 @@ slot = { git = "https://github.com/cartridge-gg/slot", rev = "1298a30" } alloy-contract = { version = "0.3", default-features = false } alloy-json-rpc = { version = "0.3", default-features = false } alloy-network = { version = "0.3", default-features = false } -alloy-provider = { version = "0.3", default-features = false, features = [ "reqwest" ] } +alloy-provider = { version = "0.3", default-features = false } alloy-rpc-types-eth = { version = "0.3", default-features = false } alloy-signer = { version = "0.3", default-features = false } alloy-transport = { version = "0.3", default-features = false } @@ -269,3 +282,8 @@ starknet-crypto = "0.7.1" starknet-types-core = { version = "0.1.7", features = [ "arbitrary", "hash" ] } bitvec = "1.0.1" + +# macro +proc-macro2 = "1.0" +quote = "1.0" +syn = { version = "2.0", default-features = false } diff --git a/README.md b/README.md index c9acbb3200..89c4e5d5f6 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,9 @@ ![Dojo Feature Matrix](.github/feature_matrix.png) -# Dojo: Provable Games and Applications [![discord](https://img.shields.io/badge/join-dojo-green?logo=discord&logoColor=white)](https://discord.com/invite/dojoengine) [![Telegram Chat][tg-badge]][tg-url] ![Github Actions][gha-badge] +# Dojo: Provable Games and Applications [![discord](https://img.shields.io/badge/join-dojo-green?logo=discord&logoColor=white)](https://discord.com/invite/dojoengine) [![Telegram Chat][tg-badge]][tg-url] [![Github Actions][gha-badge]][gha-url] [gha-badge]: https://img.shields.io/github/actions/workflow/status/dojoengine/dojo/ci.yml?branch=main +[gha-url]: https://github.com/dojoengine/dojo/actions/workflows/ci.yml?query=branch%3Amain [tg-badge]: https://img.shields.io/endpoint?color=neon&logo=telegram&label=chat&style=flat-square&url=https%3A%2F%2Ftg.sumanjay.workers.dev%2Fdojoengine [tg-url]: https://t.me/dojoengine @@ -31,4 +32,5 @@ If you encounter issues or have questions, you can [submit an issue on GitHub](h Dojo core smart contracts have been audited: -- Feb-24: [Nethermind Security](https://github.com/NethermindEth/PublicAuditReports/blob/main/NM0159-FINAL_DOJO.pdf) +- Feb-24: [Nethermind Security](https://github.com/NethermindEth/PublicAuditReports/blob/main/NM0159-FINAL_DOJO.pdf). +- Nov-24: [OpenZeppelin](https://blog.openzeppelin.com/dojo-security-review) and it's [diff-audit](https://blog.openzeppelin.com/dojo-namespace-diff-audit). diff --git a/bin/katana/Cargo.toml b/bin/katana/Cargo.toml index 1845e968f5..04f8c76f57 100644 --- a/bin/katana/Cargo.toml +++ b/bin/katana/Cargo.toml @@ -7,16 +7,28 @@ repository.workspace = true version.workspace = true [dependencies] +katana-cairo.workspace = true katana-cli.workspace = true katana-db.workspace = true katana-node.workspace = true +katana-primitives.workspace = true anyhow.workspace = true byte-unit = "5.1.4" +cainome.workspace = true clap.workspace = true clap_complete.workspace = true comfy-table = "7.1.1" +dirs = "5.0.1" +dojo-utils.workspace = true +inquire = "0.7.5" +serde.workspace = true +serde_json.workspace = true shellexpand = "3.1.0" +spinoff.workspace = true +starknet.workspace = true +tokio.workspace = true +toml.workspace = true [dev-dependencies] assert_matches.workspace = true diff --git a/bin/katana/src/cli/init/mod.rs b/bin/katana/src/cli/init/mod.rs new file mode 100644 index 0000000000..51f27e5016 --- /dev/null +++ b/bin/katana/src/cli/init/mod.rs @@ -0,0 +1,335 @@ +use std::fmt::Display; +use std::fs; +use std::path::PathBuf; +use std::str::FromStr; +use std::sync::Arc; + +use anyhow::{anyhow, Context, Result}; +use cainome::rs::abigen; +use clap::Args; +use dojo_utils::TransactionWaiter; +use inquire::{Confirm, CustomType, Text}; +use katana_cairo::lang::starknet_classes::casm_contract_class::CasmContractClass; +use katana_cairo::lang::starknet_classes::contract_class::ContractClass; +use katana_primitives::{felt, ContractAddress, Felt}; +use serde::{Deserialize, Serialize}; +use starknet::accounts::{Account, ConnectedAccount, ExecutionEncoding, SingleOwnerAccount}; +use starknet::contract::ContractFactory; +use starknet::core::types::contract::{CompiledClass, SierraClass}; +use starknet::core::types::{BlockId, BlockTag, FlattenedSierraClass, StarknetError}; +use starknet::core::utils::{cairo_short_string_to_felt, parse_cairo_short_string}; +use starknet::providers::jsonrpc::HttpTransport; +use starknet::providers::{JsonRpcClient, Provider, ProviderError, Url}; +use starknet::signers::{LocalWallet, SigningKey}; +use tokio::runtime::Runtime; + +#[derive(Debug)] +struct InitInput { + /// the account address that is used to send the transactions for contract + /// deployment/initialization. + account: ContractAddress, + + // the id of the new chain to be initialized. + id: String, + + // the chain id of the settlement layer. + settlement_id: String, + + // the rpc url for the settlement layer. + rpc_url: Url, + + fee_token: ContractAddress, + + settlement_contract: ContractAddress, + + // path at which the config file will be written at. + output_path: PathBuf, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct SettlementLayer { + // the account address that was used to initialized the l1 deployments + pub account: ContractAddress, + + // The id of the settlement chain. + pub id: String, + + pub rpc_url: Url, + + // - The token that will be used to pay for tx fee in the appchain. + // - For now, this must be the native token that is used to pay for tx fee in the settlement + // chain. + pub fee_token: ContractAddress, + + // - The bridge contract for bridging the fee token from L1 to the appchain + // - This will be part of the initialization process. + pub bridge_contract: ContractAddress, + + // - The core appchain contract used to settlement + // - This is deployed on the L1 + pub settlement_contract: ContractAddress, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct InitConfiguration { + // the initialized chain id + pub id: String, + + // the fee token contract + // + // this corresponds to the l1 token contract + pub fee_token: ContractAddress, + + pub settlement: SettlementLayer, +} + +#[derive(Debug, Args)] +pub struct InitArgs { + #[arg(value_name = "PATH")] + pub output_path: Option, +} + +impl InitArgs { + // TODO: + // - deploy bridge contract + // - generate the genesis + pub(crate) fn execute(self) -> Result<()> { + let rt = tokio::runtime::Builder::new_multi_thread().enable_all().build()?; + let input = self.prompt(&rt)?; + + let output = InitConfiguration { + id: input.id, + fee_token: ContractAddress::default(), + settlement: SettlementLayer { + account: input.account, + id: input.settlement_id, + rpc_url: input.rpc_url, + fee_token: input.fee_token, + bridge_contract: ContractAddress::default(), + settlement_contract: input.settlement_contract, + }, + }; + + let content = toml::to_string_pretty(&output)?; + fs::write(input.output_path, content)?; + + Ok(()) + } + + fn prompt(&self, rt: &Runtime) -> Result { + let chain_id = Text::new("Id").prompt()?; + + let url = CustomType::::new("Settlement RPC URL") + .with_default(Url::parse("http://localhost:5050")?) + .with_error_message("Please enter a valid URL") + .prompt()?; + + let l1_provider = Arc::new(JsonRpcClient::new(HttpTransport::new(url.clone()))); + + let contract_exist_parser = &|input: &str| { + let block_id = BlockId::Tag(BlockTag::Pending); + let address = Felt::from_str(input).map_err(|_| ())?; + let result = rt.block_on(l1_provider.clone().get_class_hash_at(block_id, address)); + + match result { + Ok(..) => Ok(ContractAddress::from(address)), + Err(..) => Err(()), + } + }; + + let account_address = CustomType::::new("Account") + .with_error_message("Please enter a valid account address") + .with_parser(contract_exist_parser) + .prompt()?; + + let private_key = CustomType::::new("Private key") + .with_formatter(&|input: Felt| format!("{input:#x}")) + .prompt()?; + + let l1_chain_id = rt.block_on(l1_provider.chain_id())?; + let account = SingleOwnerAccount::new( + l1_provider.clone(), + LocalWallet::from_signing_key(SigningKey::from_secret_scalar(private_key)), + account_address.into(), + l1_chain_id, + ExecutionEncoding::New, + ); + + // The L1 fee token. Must be an existing token. + let fee_token = CustomType::::new("Fee token") + .with_parser(contract_exist_parser) + .with_error_message("Please enter a valid fee token (the token must exist on L1)") + .prompt()?; + + // The core settlement contract on L1 + let settlement_contract = + // Prompt the user whether to deploy the settlement contract or not. + if Confirm::new("Deploy settlement contract?").with_default(true).prompt()? { + let result = rt.block_on(init_core_contract(&account)); + result.context("Failed to deploy settlement contract")? + } + // If denied, prompt the user for an already deployed contract. + else { + // TODO: add a check to make sure the contract is indeed a valid settlement contract. + CustomType::::new("Settlement contract") + .with_parser(contract_exist_parser) + .prompt()? + }; + + let output_path = if let Some(path) = self.output_path.clone() { + path + } else { + CustomType::::new("Output path") + .with_default(config_path(&chain_id).map(Path)?) + .prompt()? + .0 + }; + + Ok(InitInput { + account: account_address, + settlement_contract, + settlement_id: parse_cairo_short_string(&l1_chain_id)?, + id: chain_id, + fee_token, + rpc_url: url, + output_path, + }) + } +} + +async fn init_core_contract

( + account: &SingleOwnerAccount, +) -> Result +where + P: Provider + Send + Sync, +{ + use spinoff::{spinners, Color, Spinner}; + + let mut sp = Spinner::new(spinners::Dots, "", Color::Blue); + + let result = async { + let class = include_str!( + "../../../../../crates/katana/contracts/build/appchain_core_contract.json" + ); + + abigen!( + AppchainContract, + "[{\"type\":\"function\",\"name\":\"set_program_info\",\"inputs\":[{\"name\":\"\ + program_hash\",\"type\":\"core::felt252\"},{\"name\":\"config_hash\",\"type\":\"\ + core::felt252\"}],\"outputs\":[],\"state_mutability\":\"external\"}]" + ); + + let (contract, compiled_class_hash) = prepare_contract_declaration_params(class)?; + let class_hash = contract.class_hash(); + + // Check if the class has already been declared, + match account.provider().get_class(BlockId::Tag(BlockTag::Pending), class_hash).await { + Ok(..) => { + // Class has already been declared, no need to do anything... + } + + Err(ProviderError::StarknetError(StarknetError::ClassHashNotFound)) => { + sp.update_text("Declaring contract..."); + let res = account.declare_v2(contract.into(), compiled_class_hash).send().await?; + let _ = TransactionWaiter::new(res.transaction_hash, account.provider()).await?; + } + + Err(err) => return Err(anyhow!(err)), + } + + sp.update_text("Deploying contract..."); + + let factory = ContractFactory::new(class_hash, &account); + // appchain::constructor() https://github.com/cartridge-gg/piltover/blob/d373a844c3428383a48518adf468bf83249dec3a/src/appchain.cairo#L119-L125 + let request = factory.deploy_v1( + vec![ + account.address(), // owner + Felt::ZERO, // state_root + Felt::ZERO, // block_number + Felt::ZERO, // block_hash + ], + Felt::ZERO, + true, + ); + + let res = request.send().await?; + let _ = TransactionWaiter::new(res.transaction_hash, account.provider()).await?; + + sp.update_text("Initializing..."); + + let deployed_contract_address = request.deployed_address(); + let appchain = AppchainContract::new(deployed_contract_address, account); + + const PROGRAM_HASH: Felt = + felt!("0x5ab580b04e3532b6b18f81cfa654a05e29dd8e2352d88df1e765a84072db07"); + const CONFIG_HASH: Felt = + felt!("0x504fa6e5eb930c0d8329d4a77d98391f2730dab8516600aeaf733a6123432"); + + appchain.set_program_info(&PROGRAM_HASH, &CONFIG_HASH).send().await?; + + Ok(deployed_contract_address.into()) + } + .await; + + match result { + Ok(addr) => sp.success(&format!("Deployment successful ({addr})")), + Err(..) => sp.fail("Deployment failed"), + } + result +} + +fn prepare_contract_declaration_params(artifact: &str) -> Result<(FlattenedSierraClass, Felt)> { + let class = get_flattened_class(artifact)?; + let compiled_class_hash = get_compiled_class_hash(artifact)?; + Ok((class, compiled_class_hash)) +} + +fn get_flattened_class(artifact: &str) -> Result { + let contract_artifact: SierraClass = serde_json::from_str(artifact)?; + Ok(contract_artifact.flatten()?) +} + +fn get_compiled_class_hash(artifact: &str) -> Result { + let casm_contract_class: ContractClass = serde_json::from_str(artifact)?; + let casm_contract = + CasmContractClass::from_contract_class(casm_contract_class, true, usize::MAX)?; + let res = serde_json::to_string(&casm_contract)?; + let compiled_class: CompiledClass = serde_json::from_str(&res)?; + Ok(compiled_class.class_hash()?) +} + +// > CONFIG_DIR/$chain_id/config.toml +fn config_path(id: &str) -> Result { + Ok(config_dir(id)?.join("config").with_extension("toml")) +} + +fn config_dir(id: &str) -> Result { + const KATANA_DIR: &str = "katana"; + + let _ = cairo_short_string_to_felt(id).context("Invalid id"); + let path = dirs::config_local_dir().context("unsupported OS")?.join(KATANA_DIR).join(id); + + if !path.exists() { + fs::create_dir_all(&path).expect("failed to create config directory"); + } + + Ok(path) +} + +#[derive(Debug, Clone)] +struct Path(PathBuf); + +impl FromStr for Path { + type Err = ::Err; + fn from_str(s: &str) -> std::result::Result { + PathBuf::from_str(s).map(Self) + } +} + +impl Display for Path { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0.display()) + } +} diff --git a/bin/katana/src/cli/mod.rs b/bin/katana/src/cli/mod.rs index da4cd4e34b..892236a0ec 100644 --- a/bin/katana/src/cli/mod.rs +++ b/bin/katana/src/cli/mod.rs @@ -1,11 +1,12 @@ -mod db; - use anyhow::Result; use clap::{Args, CommandFactory, Parser, Subcommand}; use clap_complete::Shell; use katana_cli::NodeArgs; use katana_node::version::VERSION; +mod db; +mod init; + #[derive(Parser)] #[command(name = "katana", author, version = VERSION, about, long_about = None)] pub struct Cli { @@ -22,6 +23,7 @@ impl Cli { return match cmd { Commands::Completions(args) => args.execute(), Commands::Db(args) => args.execute(), + Commands::Init(args) => args.execute(), }; } @@ -31,11 +33,14 @@ impl Cli { #[derive(Subcommand)] enum Commands { - #[command(about = "Generate shell completion file for specified shell")] - Completions(CompletionsArgs), + #[command(about = "Initialize chain", hide = true)] + Init(init::InitArgs), #[command(about = "Database utilities")] Db(db::DbArgs), + + #[command(about = "Generate shell completion file for specified shell")] + Completions(CompletionsArgs), } #[derive(Debug, Args)] diff --git a/bin/saya/README.md b/bin/saya/README.md index c7c1e51987..050f77c141 100644 --- a/bin/saya/README.md +++ b/bin/saya/README.md @@ -37,7 +37,7 @@ cargo run --bin saya -- \ export DOJO_ACCOUNT_ADDRESS="" export DOJO_PRIVATE_KEY="" ``` - * Setup variables in or use enviroment variables ```bin/saya/scripts/0_account_setup.sh```, and run script + * Setup variables in or use environment variables ```bin/saya/scripts/0_account_setup.sh```, and run script * `sncast` doesn't support environment variables, for now, so you may have to set the options manually. **During this tutorial, we will export environment variables, so you must remain in the same shell session**. @@ -166,7 +166,7 @@ cargo run --bin saya -- \ If not (this includes Apple Silicon), some emulation will take place to run the prover on your machine, and this is very very slow. It's important that the `--start-block` of Saya is the first block produced by Katana as for now Katana is not fetching events from the forked network. To get this value, you can add one to the `SAYA_FORK_BLOCK_NUMBER` value. - **Currently saya supports only persistant mode, ephermal will be implemented in future + **Currently saya supports only persistent mode, ephermal will be implemented in future ```bash cargo run -r --bin saya -- \ diff --git a/bin/saya/saya.sh b/bin/saya/saya.sh index b94da4fd3f..0fba3c7831 100755 --- a/bin/saya/saya.sh +++ b/bin/saya/saya.sh @@ -23,7 +23,7 @@ SAYA_PILTOVER_STARTING_STATE_ROOT=0 SAYA_CONFIG_HASH=42 SAYA_PROGRAM_HASH=0x2aa9e430c145b26d681a8087819ed5bff93f5596105d0e74f00fc7caa46fa18 #need to be reupdated -# Set after runnig the script +# Set after running the script SAYA_WORLD_ADDRESS="" SAYA_WORLD_PREPARED="" # Set to anything after preparing the world successfully for the first time diff --git a/bin/sozo/src/commands/auth.rs b/bin/sozo/src/commands/auth.rs index 121304e46b..10e02841ce 100644 --- a/bin/sozo/src/commands/auth.rs +++ b/bin/sozo/src/commands/auth.rs @@ -7,6 +7,7 @@ use clap::{Args, Subcommand}; use colored::Colorize; use dojo_utils::Invoker; use dojo_world::config::ProfileConfig; +use dojo_world::constants::WORLD; use dojo_world::contracts::{ContractInfo, WorldContract}; use dojo_world::diff::{DiffPermissions, WorldDiff}; use scarb::core::{Config, Workspace}; @@ -305,12 +306,12 @@ async fn clone_permissions( writer_of.extend( external_writer_of .iter() - .map(|r| if r != &Felt::ZERO { format!("{:#066x}", r) } else { "World".to_string() }), + .map(|r| if r != &WORLD { format!("{:#066x}", r) } else { "World".to_string() }), ); owner_of.extend( external_owner_of .iter() - .map(|r| if r != &Felt::ZERO { format!("{:#066x}", r) } else { "World".to_string() }), + .map(|r| if r != &WORLD { format!("{:#066x}", r) } else { "World".to_string() }), ); // Sort the tags to have a deterministic output. @@ -417,13 +418,13 @@ async fn list_permissions( let mut world_writers = world_diff .external_writers - .get(&Felt::ZERO) + .get(&WORLD) .map(|writers| writers.iter().cloned().collect::>()) .unwrap_or_default(); let mut world_owners = world_diff .external_owners - .get(&Felt::ZERO) + .get(&WORLD) .map(|owners| owners.iter().cloned().collect::>()) .unwrap_or_default(); @@ -677,7 +678,7 @@ impl PermissionPair { contracts: &HashMap, ) -> Result<(Felt, Felt)> { let selector = if self.resource_tag == "world" { - Felt::ZERO + WORLD } else if self.resource_tag.starts_with("0x") { Felt::from_str(&self.resource_tag) .map_err(|_| anyhow!("Invalid resource selector: {}", self.resource_tag))? @@ -788,7 +789,7 @@ mod tests { grantee_tag_or_address: "0x123".to_string(), }; let (selector, address) = pair.to_selector_and_address(&contracts).unwrap(); - assert_eq!(selector, Felt::ZERO); + assert_eq!(selector, WORLD); assert_eq!(address, Felt::from_str("0x123").unwrap()); let pair = PermissionPair { diff --git a/bin/sozo/src/commands/dev.rs b/bin/sozo/src/commands/dev.rs index 3949a7a155..f981dfef17 100644 --- a/bin/sozo/src/commands/dev.rs +++ b/bin/sozo/src/commands/dev.rs @@ -16,6 +16,7 @@ use super::options::account::AccountOptions; use super::options::starknet::StarknetOptions; use super::options::transaction::TransactionOptions; use super::options::world::WorldOptions; +use crate::commands::options::ipfs::IpfsOptions; #[derive(Debug, Args)] pub struct DevArgs { @@ -82,11 +83,16 @@ impl DevArgs { build_args.clone().run(config)?; info!("Initial build completed."); + // As this `dev` command is for development purpose only, + // allowing to watch for changes, compile and migrate them, + // there is no need for metadata uploading. That's why, + // `ipfs` is set to its default value meaning it is disabled. let migrate_args = MigrateArgs { world: self.world, starknet: self.starknet, account: self.account, transaction: self.transaction, + ipfs: IpfsOptions::default(), }; let _ = migrate_args.clone().run(config); diff --git a/bin/sozo/src/commands/execute.rs b/bin/sozo/src/commands/execute.rs index c8510d1d16..fe5e37f62c 100644 --- a/bin/sozo/src/commands/execute.rs +++ b/bin/sozo/src/commands/execute.rs @@ -5,6 +5,7 @@ use dojo_world::config::calldata_decoder; use scarb::core::Config; use sozo_ops::resource_descriptor::ResourceDescriptor; use sozo_scarbext::WorkspaceExt; +#[cfg(feature = "walnut")] use sozo_walnut::WalnutDebugger; use starknet::core::types::Call; use starknet::core::utils as snutils; @@ -67,7 +68,7 @@ impl ExecuteArgs { let descriptor = self.tag_or_address.ensure_namespace(&profile_config.namespace.default); #[cfg(feature = "walnut")] - let _walnut_debugger = WalnutDebugger::new_from_flag( + let walnut_debugger = WalnutDebugger::new_from_flag( self.transaction.walnut, self.starknet.url(profile_config.env.as_ref())?, ); @@ -132,9 +133,13 @@ impl ExecuteArgs { .await?; let invoker = Invoker::new(&account, txn_config); - // TODO: add walnut back, perhaps at the invoker level. let tx_result = invoker.invoke(call).await?; + #[cfg(feature = "walnut")] + if let Some(walnut_debugger) = walnut_debugger { + walnut_debugger.debug_transaction(&config.ui(), &tx_result)?; + } + println!("{}", tx_result); Ok(()) }) diff --git a/bin/sozo/src/commands/hash.rs b/bin/sozo/src/commands/hash.rs index acf726350a..dca9619213 100644 --- a/bin/sozo/src/commands/hash.rs +++ b/bin/sozo/src/commands/hash.rs @@ -1,45 +1,83 @@ -use anyhow::Result; -use clap::Args; -use dojo_world::contracts::naming::compute_selector_from_tag; +use std::collections::HashSet; +use std::str::FromStr; + +use anyhow::{bail, Result}; +use clap::{Args, Subcommand}; +use dojo_types::naming::{ + compute_bytearray_hash, compute_selector_from_tag, get_name_from_tag, get_namespace_from_tag, + get_tag, +}; +use scarb::core::Config; +use sozo_scarbext::WorkspaceExt; use starknet::core::types::Felt; use starknet::core::utils::{get_selector_from_name, starknet_keccak}; use starknet_crypto::{poseidon_hash_many, poseidon_hash_single}; -use tracing::trace; +use tracing::{debug, trace}; #[derive(Debug, Args)] pub struct HashArgs { - #[arg(help = "Input to hash. It can be a comma separated list of inputs or a single input. \ - The single input can be a dojo tag or a felt.")] - pub input: String, + #[command(subcommand)] + command: HashCommand, } -impl HashArgs { - pub fn run(self) -> Result> { - trace!(args = ?self); +#[derive(Debug, Subcommand)] +pub enum HashCommand { + #[command(about = "Compute the hash of the provided input.")] + Compute { + #[arg(help = "Input to hash. It can be a comma separated list of inputs or a single \ + input. The single input can be a dojo tag or a felt.")] + input: String, + }, + + #[command(about = "Search the hash among namespaces and resource names/tags hashes. \ + Namespaces and resource names can be provided or read from the project \ + configuration.")] + Find { + #[arg(help = "The hash to search for.")] + hash: String, + + #[arg(short, long)] + #[arg(value_delimiter = ',')] + #[arg(help = "Namespaces to use to compute hashes.")] + namespaces: Option>, + + #[arg(short, long)] + #[arg(value_delimiter = ',')] + #[arg(help = "Resource names to use to compute hashes.")] + resources: Option>, + }, +} - if self.input.is_empty() { +impl HashArgs { + pub fn compute(&self, input: &str) -> Result<()> { + if input.is_empty() { return Err(anyhow::anyhow!("Input is empty")); } - if self.input.contains('-') { - let selector = format!("{:#066x}", compute_selector_from_tag(&self.input)); + if input.contains('-') { + let selector = format!("{:#066x}", compute_selector_from_tag(input)); println!("Dojo selector from tag: {}", selector); - return Ok(vec![selector.to_string()]); + return Ok(()); } // Selector in starknet is used for types, which must starts with a letter. - if self.input.chars().next().map_or(false, |c| c.is_alphabetic()) { - if self.input.len() > 32 { - return Err(anyhow::anyhow!("Input is too long for a starknet selector")); + if input.chars().next().map_or(false, |c| c.is_alphabetic()) { + if input.len() > 32 { + return Err(anyhow::anyhow!( + "Input exceeds the 32-character limit for a Starknet selector" + )); } - let selector = format!("{:#066x}", get_selector_from_name(&self.input)?); + let selector = format!("{:#066x}", get_selector_from_name(input)?); + let ba_hash = format!("{:#066x}", compute_bytearray_hash(input)); + println!("Starknet selector: {}", selector); - return Ok(vec![selector.to_string()]); + println!("ByteArray hash: {}", ba_hash); + return Ok(()); } - if !self.input.contains(',') { - let felt = felt_from_str(&self.input)?; + if !input.contains(',') { + let felt = Felt::from_str(input)?; let poseidon = format!("{:#066x}", poseidon_hash_single(felt)); let poseidon_array = format!("{:#066x}", poseidon_hash_many(&[felt])); let snkeccak = format!("{:#066x}", starknet_keccak(&felt.to_bytes_le())); @@ -48,28 +86,146 @@ impl HashArgs { println!("Poseidon array 1 value: {}", poseidon_array); println!("SnKeccak: {}", snkeccak); - return Ok(vec![poseidon.to_string(), snkeccak.to_string()]); + return Ok(()); } - let inputs: Vec<_> = self - .input + let inputs: Vec<_> = input .split(',') - .map(|s| felt_from_str(s.trim()).expect("Invalid felt value")) + .map(|s| Felt::from_str(s.trim()).expect("Invalid felt value")) .collect(); let poseidon = format!("{:#066x}", poseidon_hash_many(&inputs)); println!("Poseidon many: {}", poseidon); - Ok(vec![poseidon.to_string()]) + Ok(()) } -} -fn felt_from_str(s: &str) -> Result { - if s.starts_with("0x") { - return Ok(Felt::from_hex(s)?); + pub fn find( + &self, + config: &Config, + hash: &String, + namespaces: Option>, + resources: Option>, + ) -> Result<()> { + let hash = Felt::from_str(hash) + .map_err(|_| anyhow::anyhow!("The provided hash is not valid (hash: {hash})"))?; + + let ws = scarb::ops::read_workspace(config.manifest_path(), config)?; + let profile_config = ws.load_profile_config()?; + let manifest = ws.read_manifest_profile()?; + + let namespaces = namespaces.unwrap_or_else(|| { + let mut ns_from_config = HashSet::new(); + + // get namespaces from profile + ns_from_config.insert(profile_config.namespace.default); + + if let Some(mappings) = profile_config.namespace.mappings { + ns_from_config.extend(mappings.into_keys()); + } + + if let Some(models) = &profile_config.models { + ns_from_config.extend(models.iter().map(|m| get_namespace_from_tag(&m.tag))); + } + + if let Some(contracts) = &profile_config.contracts { + ns_from_config.extend(contracts.iter().map(|c| get_namespace_from_tag(&c.tag))); + } + + if let Some(events) = &profile_config.events { + ns_from_config.extend(events.iter().map(|e| get_namespace_from_tag(&e.tag))); + } + + // get namespaces from manifest + if let Some(manifest) = &manifest { + ns_from_config + .extend(manifest.models.iter().map(|m| get_namespace_from_tag(&m.tag))); + + ns_from_config + .extend(manifest.contracts.iter().map(|c| get_namespace_from_tag(&c.tag))); + + ns_from_config + .extend(manifest.events.iter().map(|e| get_namespace_from_tag(&e.tag))); + } + + Vec::from_iter(ns_from_config) + }); + + let resources = resources.unwrap_or_else(|| { + let mut res_from_config = HashSet::new(); + + // get resources from profile + if let Some(models) = &profile_config.models { + res_from_config.extend(models.iter().map(|m| get_name_from_tag(&m.tag))); + } + + if let Some(contracts) = &profile_config.contracts { + res_from_config.extend(contracts.iter().map(|c| get_name_from_tag(&c.tag))); + } + + if let Some(events) = &profile_config.events { + res_from_config.extend(events.iter().map(|e| get_name_from_tag(&e.tag))); + } + + // get resources from manifest + if let Some(manifest) = &manifest { + res_from_config.extend(manifest.models.iter().map(|m| get_name_from_tag(&m.tag))); + + res_from_config + .extend(manifest.contracts.iter().map(|c| get_name_from_tag(&c.tag))); + + res_from_config.extend(manifest.events.iter().map(|e| get_name_from_tag(&e.tag))); + } + + Vec::from_iter(res_from_config) + }); + + debug!(namespaces = ?namespaces, "Namespaces"); + debug!(resources = ?resources, "Resources"); + + // --- find the hash --- + let mut hash_found = false; + + // could be a namespace hash + for ns in &namespaces { + if hash == compute_bytearray_hash(ns) { + println!("Namespace found: {ns}"); + hash_found = true; + } + } + + // could be a resource name hash + for res in &resources { + if hash == compute_bytearray_hash(res) { + println!("Resource name found: {res}"); + hash_found = true; + } + } + + // could be a tag hash (combination of namespace and name) + for ns in &namespaces { + for res in &resources { + let tag = get_tag(ns, res); + if hash == compute_selector_from_tag(&tag) { + println!("Resource tag found: {tag}"); + hash_found = true; + } + } + } + + if hash_found { Ok(()) } else { bail!("No resource matches the provided hash.") } } - Ok(Felt::from_dec_str(s)?) + pub fn run(&self, config: &Config) -> Result<()> { + trace!(args = ?self); + + match &self.command { + HashCommand::Compute { input } => self.compute(input), + HashCommand::Find { hash, namespaces, resources } => { + self.find(config, hash, namespaces.clone(), resources.clone()) + } + } + } } #[cfg(test)] @@ -78,68 +234,58 @@ mod tests { #[test] fn test_hash_dojo_tag() { - let args = HashArgs { input: "dojo_examples-actions".to_string() }; - let result = args.run(); - assert_eq!( - result.unwrap(), - ["0x040b6994c76da51db0c1dee2413641955fb3b15add8a35a2c605b1a050d225ab"] - ); + let input = "dojo_examples-actions".to_string(); + let args = HashArgs { command: HashCommand::Compute { input: input.clone() } }; + let result = args.compute(&input); + assert!(result.is_ok()); } #[test] fn test_hash_single_felt() { - let args = HashArgs { input: "0x1".to_string() }; - let result = args.run(); - assert_eq!( - result.unwrap(), - [ - "0x06d226d4c804cd74567f5ac59c6a4af1fe2a6eced19fb7560a9124579877da25", - "0x00078cfed56339ea54962e72c37c7f588fc4f8e5bc173827ba75cb10a63a96a5" - ] - ); + let input = "0x1".to_string(); + let args = HashArgs { command: HashCommand::Compute { input: input.clone() } }; + let result = args.compute(&input); + assert!(result.is_ok()); } #[test] fn test_hash_starknet_selector() { - let args = HashArgs { input: "dojo".to_string() }; - let result = args.run(); - assert_eq!( - result.unwrap(), - ["0x0120c91ffcb74234971d98abba5372798d16dfa5c6527911956861315c446e35"] - ); + let input = "dojo".to_string(); + let args = HashArgs { command: HashCommand::Compute { input: input.clone() } }; + let result = args.compute(&input); + assert!(result.is_ok()); } #[test] fn test_hash_multiple_felts() { - let args = HashArgs { input: "0x1,0x2,0x3".to_string() }; - let result = args.run(); - assert_eq!( - result.unwrap(), - ["0x02f0d8840bcf3bc629598d8a6cc80cb7c0d9e52d93dab244bbf9cd0dca0ad082"] - ); + let input = "0x1,0x2,0x3".to_string(); + let args = HashArgs { command: HashCommand::Compute { input: input.clone() } }; + let result = args.compute(&input); + assert!(result.is_ok()); } #[test] fn test_hash_empty_input() { - let args = HashArgs { input: "".to_string() }; - let result = args.run(); + let input = "".to_string(); + let args = HashArgs { command: HashCommand::Compute { input: input.clone() } }; + let result = args.compute(&input); assert!(result.is_err()); assert_eq!(result.unwrap_err().to_string(), "Input is empty"); } #[test] fn test_hash_invalid_felt() { - let args = HashArgs { - input: "invalid too long to be a selector supported by starknet".to_string(), - }; - assert!(args.run().is_err()); + let input = "invalid too long to be a selector supported by starknet".to_string(); + let args = HashArgs { command: HashCommand::Compute { input: input.clone() } }; + assert!(args.compute(&input).is_err()); } #[test] #[should_panic] fn test_hash_multiple_invalid_felts() { - let args = HashArgs { input: "0x1,0x2,0x3,fhorihgorh".to_string() }; + let input = "0x1,0x2,0x3,fhorihgorh".to_string(); + let args = HashArgs { command: HashCommand::Compute { input: input.clone() } }; - let _ = args.run(); + let _ = args.compute(&input); } } diff --git a/bin/sozo/src/commands/migrate.rs b/bin/sozo/src/commands/migrate.rs index 95a2d33025..30331257d3 100644 --- a/bin/sozo/src/commands/migrate.rs +++ b/bin/sozo/src/commands/migrate.rs @@ -1,8 +1,11 @@ -use anyhow::{Context, Result}; +use std::sync::Arc; + +use anyhow::{anyhow, Context, Result}; use clap::Args; -use colored::Colorize; -use dojo_utils::{self, TxnConfig}; +use colored::*; +use dojo_utils::{self, provider as provider_utils, TxnConfig}; use dojo_world::contracts::WorldContract; +use dojo_world::services::IpfsService; use scarb::core::{Config, Workspace}; use sozo_ops::migrate::{Migration, MigrationResult}; use sozo_ops::migration_ui::MigrationUi; @@ -11,12 +14,14 @@ use starknet::core::utils::parse_cairo_short_string; use starknet::providers::Provider; use tabled::settings::Style; use tabled::{Table, Tabled}; -use tracing::trace; +use tracing::{error, trace}; use super::options::account::AccountOptions; +use super::options::ipfs::IpfsOptions; use super::options::starknet::StarknetOptions; use super::options::transaction::TransactionOptions; use super::options::world::WorldOptions; +use crate::commands::LOG_TARGET; use crate::utils; #[derive(Debug, Clone, Args)] @@ -32,6 +37,9 @@ pub struct MigrateArgs { #[command(flatten)] pub account: AccountOptions, + + #[command(flatten)] + pub ipfs: IpfsOptions, } impl MigrateArgs { @@ -43,7 +51,7 @@ impl MigrateArgs { ws.profile_check()?; ws.ensure_profile_artifacts()?; - let MigrateArgs { world, starknet, account, .. } = self; + let MigrateArgs { world, starknet, account, ipfs, .. } = self; config.tokio_handle().block_on(async { print_banner(&ws, &starknet).await?; @@ -60,6 +68,7 @@ impl MigrateArgs { .await?; let world_address = world_diff.world_info.address; + let profile_config = ws.load_profile_config()?; let mut txn_config: TxnConfig = self.transaction.try_into()?; txn_config.wait = true; @@ -75,15 +84,45 @@ impl MigrateArgs { let MigrationResult { manifest, has_changes } = migration.migrate(&mut spinner).await.context("Migration failed.")?; + let ipfs_config = + ipfs.config().or(profile_config.env.map(|env| env.ipfs_config).unwrap_or(None)); + + if let Some(config) = ipfs_config { + let mut metadata_service = IpfsService::new(config)?; + + migration + .upload_metadata(&mut spinner, &mut metadata_service) + .await + .context("Metadata upload failed.")?; + } else { + println!(); + println!( + "{}", + "IPFS credentials not found. Metadata upload skipped. To upload metadata, configure IPFS credentials in your profile config or environment variables: https://book.dojoengine.org/framework/world/metadata.".bright_yellow() + ); + }; + spinner.update_text("Writing manifest..."); ws.write_manifest_profile(manifest).context("🪦 Failed to write manifest.")?; let colored_address = format!("{:#066x}", world_address).green(); let (symbol, end_text) = if has_changes { - ("⛩️ ", format!("Migration successful with world at address {}", colored_address)) + ( + "⛩️ ", + format!( + "Migration successful with world at address {}", + colored_address + ), + ) } else { - ("🪨 ", format!("No changes for world at address {:#066x}", world_address)) + ( + "🪨 ", + format!( + "No changes for world at address {:#066x}", + world_address + ), + ) }; spinner.stop_and_persist_boxed(symbol, end_text); @@ -105,6 +144,12 @@ async fn print_banner(ws: &Workspace<'_>, starknet: &StarknetOptions) -> Result< let profile_config = ws.load_profile_config()?; let (provider, rpc_url) = starknet.provider(profile_config.env.as_ref())?; + let provider = Arc::new(provider); + if let Err(e) = provider_utils::health_check_provider(provider.clone()).await { + error!(target: LOG_TARGET,"Provider health check failed during sozo migrate."); + return Err(e); + } + let provider = Arc::try_unwrap(provider).map_err(|_| anyhow!("Failed to unwrap Arc"))?; let chain_id = provider.chain_id().await?; let chain_id = parse_cairo_short_string(&chain_id).with_context(|| "Cannot parse chain_id as string")?; diff --git a/bin/sozo/src/commands/mod.rs b/bin/sozo/src/commands/mod.rs index 6ee218b2da..61efdf83e4 100644 --- a/bin/sozo/src/commands/mod.rs +++ b/bin/sozo/src/commands/mod.rs @@ -5,6 +5,7 @@ use auth::AuthArgs; use clap::Subcommand; use events::EventsArgs; use scarb::core::{Config, Package, Workspace}; +use semver::{Version, VersionReq}; use tracing::info_span; pub(crate) mod auth; @@ -32,8 +33,12 @@ use init::InitArgs; use inspect::InspectArgs; use migrate::MigrateArgs; use model::ModelArgs; +#[cfg(feature = "walnut")] +use sozo_walnut::walnut::WalnutArgs; use test::TestArgs; +pub(crate) const LOG_TARGET: &str = "sozo::cli"; + #[derive(Debug, Subcommand)] pub enum Commands { #[command(about = "Grant or revoke a contract permission to write to a resource")] @@ -63,6 +68,9 @@ pub enum Commands { Model(Box), #[command(about = "Inspect events emitted by the world")] Events(Box), + #[cfg(feature = "walnut")] + #[command(about = "Interact with walnut.dev - transactions debugger and simulator")] + Walnut(Box), } impl fmt::Display for Commands { @@ -81,6 +89,8 @@ impl fmt::Display for Commands { Commands::Init(_) => write!(f, "Init"), Commands::Model(_) => write!(f, "Model"), Commands::Events(_) => write!(f, "Events"), + #[cfg(feature = "walnut")] + Commands::Walnut(_) => write!(f, "WalnutVerify"), } } } @@ -103,10 +113,12 @@ pub fn run(command: Commands, config: &Config) -> Result<()> { Commands::Clean(args) => args.run(config), Commands::Call(args) => args.run(config), Commands::Test(args) => args.run(config), - Commands::Hash(args) => args.run().map(|_| ()), + Commands::Hash(args) => args.run(config).map(|_| ()), Commands::Init(args) => args.run(config), Commands::Model(args) => args.run(config), Commands::Events(args) => args.run(config), + #[cfg(feature = "walnut")] + Commands::Walnut(args) => args.run(config), } } @@ -127,24 +139,43 @@ pub fn check_package_dojo_version(ws: &Workspace<'_>, package: &Package) -> anyh && dojo_dep_str.contains("tag=v") && !dojo_dep_str.contains(dojo_version) { - if let Ok(cp) = ws.current_package() { - let path = - if cp.id == package.id { package.manifest_path() } else { ws.manifest_path() }; - - anyhow::bail!( - "Found dojo-core version mismatch: expected {}. Please verify your dojo \ - dependency in {}", - dojo_version, - path - ) - } else { - // Virtual workspace. - anyhow::bail!( - "Found dojo-core version mismatch: expected {}. Please verify your dojo \ - dependency in {}", - dojo_version, - ws.manifest_path() - ) + // safe to unwrap since we know the string contains "tag=v". + // "dojo * (git+https://github.com/dojoengine/dojo?tag=v1.0.10)" + let dojo_dep_version = dojo_dep_str.split("tag=v") + .nth(1) // Get the part after "tag=v" + .map(|s| s.trim_end_matches(')')) + .expect("Unexpected dojo dependency format"); + + let dojo_dep_version = Version::parse(dojo_dep_version).unwrap(); + + let version_parts: Vec<&str> = dojo_version.split('.').collect(); + let major_minor = format!("{}.{}", version_parts[0], version_parts[1]); + let dojo_req_version = VersionReq::parse(&format!(">={}", major_minor)).unwrap(); + + if !dojo_req_version.matches(&dojo_dep_version) { + if let Ok(cp) = ws.current_package() { + // Selected package. + let path = if cp.id == package.id { + package.manifest_path() + } else { + ws.manifest_path() + }; + + anyhow::bail!( + "Found dojo-core version mismatch: expected {}. Please verify your dojo \ + dependency in {}", + dojo_req_version, + path + ) + } else { + // Virtual workspace. + anyhow::bail!( + "Found dojo-core version mismatch: expected {}. Please verify your dojo \ + dependency in {}", + dojo_req_version, + ws.manifest_path() + ) + } } } } diff --git a/bin/sozo/src/commands/options/ipfs.rs b/bin/sozo/src/commands/options/ipfs.rs new file mode 100644 index 0000000000..146899614e --- /dev/null +++ b/bin/sozo/src/commands/options/ipfs.rs @@ -0,0 +1,116 @@ +use clap::Args; +use dojo_utils::env::{IPFS_PASSWORD_ENV_VAR, IPFS_URL_ENV_VAR, IPFS_USERNAME_ENV_VAR}; +use dojo_world::config::IpfsConfig; +use tracing::trace; +use url::Url; + +#[derive(Debug, Default, Args, Clone)] +#[command(next_help_heading = "IPFS options")] +pub struct IpfsOptions { + #[arg(long, env = IPFS_URL_ENV_VAR)] + #[arg(value_name = "URL")] + #[arg(help = "The IPFS URL.")] + #[arg(global = true)] + pub ipfs_url: Option, + + #[arg(long, env = IPFS_USERNAME_ENV_VAR)] + #[arg(value_name = "USERNAME")] + #[arg(help = "The IPFS username.")] + #[arg(global = true)] + pub ipfs_username: Option, + + #[arg(long, env = IPFS_PASSWORD_ENV_VAR)] + #[arg(value_name = "PASSWORD")] + #[arg(help = "The IPFS password.")] + #[arg(global = true)] + pub ipfs_password: Option, +} + +impl IpfsOptions { + pub fn config(&self) -> Option { + trace!("Retrieving IPFS config for IpfsOptions."); + + let url = self.ipfs_url.as_ref().map(|url| url.to_string()); + let username = self.ipfs_username.clone(); + let password = self.ipfs_password.clone(); + + if let (Some(url), Some(username), Some(password)) = (url, username, password) { + Some(IpfsConfig { url, username, password }) + } else { + None + } + } +} + +#[cfg(test)] +mod tests { + use clap::Parser; + use dojo_utils::env::{IPFS_PASSWORD_ENV_VAR, IPFS_URL_ENV_VAR, IPFS_USERNAME_ENV_VAR}; + + use super::IpfsOptions; + + #[derive(clap::Parser)] + struct Command { + #[clap(flatten)] + options: IpfsOptions, + } + + const ENV_IPFS_URL: &str = "http://ipfs.service/"; + const ENV_IPFS_USERNAME: &str = "johndoe"; + const ENV_IPFS_PASSWORD: &str = "123456"; + + #[test] + fn options_read_from_env_variable() { + std::env::set_var(IPFS_URL_ENV_VAR, ENV_IPFS_URL); + std::env::set_var(IPFS_USERNAME_ENV_VAR, ENV_IPFS_USERNAME); + std::env::set_var(IPFS_PASSWORD_ENV_VAR, ENV_IPFS_PASSWORD); + + let cmd = Command::parse_from([""]); + let config = cmd.options.config().unwrap(); + assert_eq!(config.url, ENV_IPFS_URL.to_string()); + assert_eq!(config.username, ENV_IPFS_USERNAME.to_string()); + assert_eq!(config.password, ENV_IPFS_PASSWORD.to_string()); + } + + #[test] + fn cli_args_override_env_variables() { + std::env::set_var(IPFS_URL_ENV_VAR, ENV_IPFS_URL); + let url = "http://different.url/"; + let username = "bobsmith"; + let password = "654321"; + + let cmd = Command::parse_from([ + "sozo", + "--ipfs-url", + url, + "--ipfs-username", + username, + "--ipfs-password", + password, + ]); + let config = cmd.options.config().unwrap(); + assert_eq!(config.url, url); + assert_eq!(config.username, username); + assert_eq!(config.password, password); + } + + #[test] + fn invalid_url_format() { + let cmd = Command::try_parse_from([ + "sozo", + "--ipfs-url", + "invalid-url", + "--ipfs-username", + "bobsmith", + "--ipfs-password", + "654321", + ]); + assert!(cmd.is_err()); + } + + #[test] + fn options_not_provided_in_env_variable() { + let cmd = Command::parse_from(["sozo"]); + assert!(cmd.options.config().is_none()); + } +} diff --git a/bin/sozo/src/commands/options/mod.rs b/bin/sozo/src/commands/options/mod.rs index a92c824a2d..182bc526fd 100644 --- a/bin/sozo/src/commands/options/mod.rs +++ b/bin/sozo/src/commands/options/mod.rs @@ -1,4 +1,5 @@ pub mod account; +pub mod ipfs; pub mod signer; pub mod starknet; pub mod transaction; diff --git a/bin/sozo/src/utils.rs b/bin/sozo/src/utils.rs index cc108a55cc..818b5ad7ec 100644 --- a/bin/sozo/src/utils.rs +++ b/bin/sozo/src/utils.rs @@ -1,10 +1,12 @@ use std::collections::HashMap; use std::io::{self, Write}; use std::str::FromStr; +use std::sync::Arc; use anyhow::{anyhow, Context, Result}; use camino::Utf8PathBuf; use colored::*; +use dojo_utils::provider as provider_utils; use dojo_world::config::ProfileConfig; use dojo_world::contracts::ContractInfo; use dojo_world::diff::WorldDiff; @@ -19,11 +21,12 @@ use starknet::core::types::Felt; use starknet::core::utils as snutils; use starknet::providers::jsonrpc::HttpTransport; use starknet::providers::{JsonRpcClient, Provider}; -use tracing::trace; +use tracing::{error, trace}; use crate::commands::options::account::{AccountOptions, SozoAccount}; use crate::commands::options::starknet::StarknetOptions; use crate::commands::options::world::WorldOptions; +use crate::commands::LOG_TARGET; /// Computes the world address based on the provided options. pub fn get_world_address( @@ -113,6 +116,12 @@ pub async fn get_world_diff_and_provider( let world_address = get_world_address(&profile_config, &world, &world_local)?; let (provider, rpc_url) = starknet.provider(env)?; + let provider = Arc::new(provider); + if let Err(e) = provider_utils::health_check_provider(provider.clone()).await { + error!(target: LOG_TARGET,"Provider health check failed during sozo inspect."); + return Err(e); + } + let provider = Arc::try_unwrap(provider).map_err(|_| anyhow!("Failed to unwrap Arc"))?; trace!(?provider, "Provider initialized."); let spec_version = provider.spec_version().await?; diff --git a/bin/sozo/tests/test_data/policies.json b/bin/sozo/tests/test_data/policies.json index 2c336026ad..2094e3d30f 100644 --- a/bin/sozo/tests/test_data/policies.json +++ b/bin/sozo/tests/test_data/policies.json @@ -1,130 +1,134 @@ [ { - "target": "0x7e2c18814dd45847ae85d3c8eb40196cc2aa869614efd1ff67edf380c45cb8e", - "method": "upgrade" - }, - { - "target": "0x54d0f13bf3fb5f15a8674c5204aad35e3022af96bcc23bdbd16b7e297ffd399", + "target": "0x52ee4d3cba58d1a0462bbfb6813bf5aa1b35078c3b859cded2b727c1d9469ea", "method": "uuid" }, { - "target": "0x54d0f13bf3fb5f15a8674c5204aad35e3022af96bcc23bdbd16b7e297ffd399", + "target": "0x52ee4d3cba58d1a0462bbfb6813bf5aa1b35078c3b859cded2b727c1d9469ea", "method": "set_metadata" }, { - "target": "0x54d0f13bf3fb5f15a8674c5204aad35e3022af96bcc23bdbd16b7e297ffd399", + "target": "0x52ee4d3cba58d1a0462bbfb6813bf5aa1b35078c3b859cded2b727c1d9469ea", "method": "register_namespace" }, { - "target": "0x54d0f13bf3fb5f15a8674c5204aad35e3022af96bcc23bdbd16b7e297ffd399", + "target": "0x52ee4d3cba58d1a0462bbfb6813bf5aa1b35078c3b859cded2b727c1d9469ea", "method": "register_event" }, { - "target": "0x54d0f13bf3fb5f15a8674c5204aad35e3022af96bcc23bdbd16b7e297ffd399", + "target": "0x52ee4d3cba58d1a0462bbfb6813bf5aa1b35078c3b859cded2b727c1d9469ea", "method": "register_model" }, { - "target": "0x54d0f13bf3fb5f15a8674c5204aad35e3022af96bcc23bdbd16b7e297ffd399", + "target": "0x52ee4d3cba58d1a0462bbfb6813bf5aa1b35078c3b859cded2b727c1d9469ea", "method": "register_contract" }, { - "target": "0x54d0f13bf3fb5f15a8674c5204aad35e3022af96bcc23bdbd16b7e297ffd399", + "target": "0x52ee4d3cba58d1a0462bbfb6813bf5aa1b35078c3b859cded2b727c1d9469ea", "method": "init_contract" }, { - "target": "0x54d0f13bf3fb5f15a8674c5204aad35e3022af96bcc23bdbd16b7e297ffd399", + "target": "0x52ee4d3cba58d1a0462bbfb6813bf5aa1b35078c3b859cded2b727c1d9469ea", "method": "upgrade_event" }, { - "target": "0x54d0f13bf3fb5f15a8674c5204aad35e3022af96bcc23bdbd16b7e297ffd399", + "target": "0x52ee4d3cba58d1a0462bbfb6813bf5aa1b35078c3b859cded2b727c1d9469ea", "method": "upgrade_model" }, { - "target": "0x54d0f13bf3fb5f15a8674c5204aad35e3022af96bcc23bdbd16b7e297ffd399", + "target": "0x52ee4d3cba58d1a0462bbfb6813bf5aa1b35078c3b859cded2b727c1d9469ea", "method": "upgrade_contract" }, { - "target": "0x54d0f13bf3fb5f15a8674c5204aad35e3022af96bcc23bdbd16b7e297ffd399", + "target": "0x52ee4d3cba58d1a0462bbfb6813bf5aa1b35078c3b859cded2b727c1d9469ea", "method": "emit_event" }, { - "target": "0x54d0f13bf3fb5f15a8674c5204aad35e3022af96bcc23bdbd16b7e297ffd399", + "target": "0x52ee4d3cba58d1a0462bbfb6813bf5aa1b35078c3b859cded2b727c1d9469ea", "method": "emit_events" }, { - "target": "0x54d0f13bf3fb5f15a8674c5204aad35e3022af96bcc23bdbd16b7e297ffd399", + "target": "0x52ee4d3cba58d1a0462bbfb6813bf5aa1b35078c3b859cded2b727c1d9469ea", "method": "set_entity" }, { - "target": "0x54d0f13bf3fb5f15a8674c5204aad35e3022af96bcc23bdbd16b7e297ffd399", + "target": "0x52ee4d3cba58d1a0462bbfb6813bf5aa1b35078c3b859cded2b727c1d9469ea", "method": "set_entities" }, { - "target": "0x54d0f13bf3fb5f15a8674c5204aad35e3022af96bcc23bdbd16b7e297ffd399", + "target": "0x52ee4d3cba58d1a0462bbfb6813bf5aa1b35078c3b859cded2b727c1d9469ea", "method": "delete_entity" }, { - "target": "0x54d0f13bf3fb5f15a8674c5204aad35e3022af96bcc23bdbd16b7e297ffd399", + "target": "0x52ee4d3cba58d1a0462bbfb6813bf5aa1b35078c3b859cded2b727c1d9469ea", "method": "delete_entities" }, { - "target": "0x54d0f13bf3fb5f15a8674c5204aad35e3022af96bcc23bdbd16b7e297ffd399", + "target": "0x52ee4d3cba58d1a0462bbfb6813bf5aa1b35078c3b859cded2b727c1d9469ea", "method": "grant_owner" }, { - "target": "0x54d0f13bf3fb5f15a8674c5204aad35e3022af96bcc23bdbd16b7e297ffd399", + "target": "0x52ee4d3cba58d1a0462bbfb6813bf5aa1b35078c3b859cded2b727c1d9469ea", "method": "revoke_owner" }, { - "target": "0x54d0f13bf3fb5f15a8674c5204aad35e3022af96bcc23bdbd16b7e297ffd399", + "target": "0x52ee4d3cba58d1a0462bbfb6813bf5aa1b35078c3b859cded2b727c1d9469ea", "method": "grant_writer" }, { - "target": "0x54d0f13bf3fb5f15a8674c5204aad35e3022af96bcc23bdbd16b7e297ffd399", + "target": "0x52ee4d3cba58d1a0462bbfb6813bf5aa1b35078c3b859cded2b727c1d9469ea", "method": "revoke_writer" }, { - "target": "0x54d0f13bf3fb5f15a8674c5204aad35e3022af96bcc23bdbd16b7e297ffd399", + "target": "0x52ee4d3cba58d1a0462bbfb6813bf5aa1b35078c3b859cded2b727c1d9469ea", + "method": "upgrade" + }, + { + "target": "0x50b1497d463d52cbeb5919a35a82360ea6702db2b9c62c2d69c167995f34c08", + "method": "upgrade" + }, + { + "target": "0x72a9f501c260b2d13f8988ea172680c5c1fdc085c5b44bdcac8477362ed5290", "method": "upgrade" }, { - "target": "0x7d32611d1b96df37b041f0a285bf340227cebc144d46c2f34cedd81e2fd5239", + "target": "0x6bfba78b8f4f42da469860a95291c2fad959c91ea747e2062de763ff4e62c4a", "method": "spawn" }, { - "target": "0x7d32611d1b96df37b041f0a285bf340227cebc144d46c2f34cedd81e2fd5239", + "target": "0x6bfba78b8f4f42da469860a95291c2fad959c91ea747e2062de763ff4e62c4a", "method": "move" }, { - "target": "0x7d32611d1b96df37b041f0a285bf340227cebc144d46c2f34cedd81e2fd5239", + "target": "0x6bfba78b8f4f42da469860a95291c2fad959c91ea747e2062de763ff4e62c4a", "method": "set_player_config" }, { - "target": "0x7d32611d1b96df37b041f0a285bf340227cebc144d46c2f34cedd81e2fd5239", + "target": "0x6bfba78b8f4f42da469860a95291c2fad959c91ea747e2062de763ff4e62c4a", + "method": "update_player_config_name" + }, + { + "target": "0x6bfba78b8f4f42da469860a95291c2fad959c91ea747e2062de763ff4e62c4a", "method": "reset_player_config" }, { - "target": "0x7d32611d1b96df37b041f0a285bf340227cebc144d46c2f34cedd81e2fd5239", + "target": "0x6bfba78b8f4f42da469860a95291c2fad959c91ea747e2062de763ff4e62c4a", "method": "set_player_server_profile" }, { - "target": "0x7d32611d1b96df37b041f0a285bf340227cebc144d46c2f34cedd81e2fd5239", + "target": "0x6bfba78b8f4f42da469860a95291c2fad959c91ea747e2062de763ff4e62c4a", "method": "set_models" }, { - "target": "0x7d32611d1b96df37b041f0a285bf340227cebc144d46c2f34cedd81e2fd5239", + "target": "0x6bfba78b8f4f42da469860a95291c2fad959c91ea747e2062de763ff4e62c4a", "method": "enter_dungeon" }, { - "target": "0x7d32611d1b96df37b041f0a285bf340227cebc144d46c2f34cedd81e2fd5239", - "method": "upgrade" - }, - { - "target": "0x7077c3246c125a91b17e8c0a90f45af790ad6feabe65a3df225277d9eb02310", + "target": "0x6bfba78b8f4f42da469860a95291c2fad959c91ea747e2062de763ff4e62c4a", "method": "upgrade" }, { - "target": "0x74967fafd9ea26b0c14287fdafa5867cf6e2d16d9e6fda3dde4361a7cf75c9d", + "target": "0x4b41a2abaeff170f3a04acb0144790a5a812e25e7a735dfef959247cfeb527", "method": "upgrade" }, { diff --git a/bin/torii/Cargo.toml b/bin/torii/Cargo.toml index efa4339e0a..fb200c89a5 100644 --- a/bin/torii/Cargo.toml +++ b/bin/torii/Cargo.toml @@ -3,59 +3,9 @@ edition.workspace = true name = "torii" version.workspace = true -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] +torii-cli.workspace = true +torii-runner.workspace = true +tokio.workspace = true anyhow.workspace = true -async-trait.workspace = true -base64.workspace = true -camino.workspace = true -chrono.workspace = true clap.workspace = true -clap_complete.workspace = true -ctrlc = { version = "3.4", features = [ "termination" ] } -dojo-metrics.workspace = true -dojo-types.workspace = true -dojo-utils.workspace = true -dojo-world.workspace = true -either = "1.9.0" -futures.workspace = true -http-body = "0.4.5" -http.workspace = true -hyper-reverse-proxy = { git = "https://github.com/tarrencev/hyper-reverse-proxy" } -hyper.workspace = true -indexmap.workspace = true -lazy_static.workspace = true -serde.workspace = true -serde_json.workspace = true -sqlx.workspace = true -starknet-crypto.workspace = true -starknet.workspace = true -tokio-stream = "0.1.11" -tokio-util = "0.7.7" -tokio.workspace = true -toml.workspace = true -torii-cli.workspace = true -torii-core.workspace = true -torii-graphql.workspace = true -torii-grpc = { workspace = true, features = [ "server" ] } -torii-relay.workspace = true -torii-server.workspace = true -tower.workspace = true - -clap_config = "0.1.1" -tempfile.workspace = true -tower-http.workspace = true -tracing-subscriber.workspace = true -tracing.workspace = true -url.workspace = true -webbrowser = "0.8" - -[dev-dependencies] -assert_matches.workspace = true -camino.workspace = true - -[features] -default = [ "jemalloc", "sqlite" ] -jemalloc = [ "dojo-metrics/jemalloc" ] -sqlite = [ "sqlx/sqlite" ] diff --git a/bin/torii/src/main.rs b/bin/torii/src/main.rs index 72c38ead8a..62c4c83d6d 100644 --- a/bin/torii/src/main.rs +++ b/bin/torii/src/main.rs @@ -10,286 +10,16 @@ //! documentation for usage details. This is **not recommended on Windows**. See [here](https://rust-lang.github.io/rfcs/1974-global-allocators.html#jemalloc) //! for more info. -use std::cmp; -use std::net::SocketAddr; -use std::str::FromStr; -use std::sync::Arc; -use std::time::Duration; - -use camino::Utf8PathBuf; use clap::Parser; use cli::Cli; -use dojo_metrics::exporters::prometheus::PrometheusRecorder; -use dojo_world::contracts::world::WorldContractReader; -use sqlx::sqlite::{ - SqliteAutoVacuum, SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions, SqliteSynchronous, -}; -use sqlx::SqlitePool; -use starknet::providers::jsonrpc::HttpTransport; -use starknet::providers::JsonRpcClient; -use tempfile::{NamedTempFile, TempDir}; -use tokio::sync::broadcast; -use tokio::sync::broadcast::Sender; -use tokio_stream::StreamExt; -use torii_core::engine::{Engine, EngineConfig, IndexingFlags, Processors}; -use torii_core::executor::Executor; -use torii_core::processors::store_transaction::StoreTransactionProcessor; -use torii_core::processors::EventProcessorConfig; -use torii_core::simple_broker::SimpleBroker; -use torii_core::sql::cache::ModelCache; -use torii_core::sql::Sql; -use torii_core::types::{Contract, ContractType, Model}; -use torii_server::proxy::Proxy; -use tracing::{error, info}; -use tracing_subscriber::{fmt, EnvFilter}; -use url::{form_urlencoded, Url}; - -pub(crate) const LOG_TARGET: &str = "torii::cli"; +use torii_runner::Runner; mod cli; #[tokio::main] async fn main() -> anyhow::Result<()> { - let mut args = Cli::parse().args.with_config_file()?; - - let world_address = if let Some(world_address) = args.world_address { - world_address - } else { - return Err(anyhow::anyhow!("Please specify a world address.")); - }; - - args.indexing.contracts.push(Contract { address: world_address, r#type: ContractType::WORLD }); - - let filter_layer = EnvFilter::try_from_default_env() - .unwrap_or_else(|_| EnvFilter::new("info,hyper_reverse_proxy=off")); - - let subscriber = fmt::Subscriber::builder().with_env_filter(filter_layer).finish(); - - // Set the global subscriber - tracing::subscriber::set_global_default(subscriber) - .expect("Failed to set the global tracing subscriber"); - - // Setup cancellation for graceful shutdown - let (shutdown_tx, _) = broadcast::channel(1); - - let shutdown_tx_clone = shutdown_tx.clone(); - ctrlc::set_handler(move || { - let _ = shutdown_tx_clone.send(()); - }) - .expect("Error setting Ctrl-C handler"); - - let tempfile = NamedTempFile::new()?; - let database_path = if let Some(db_dir) = args.db_dir { - // Create the directory if it doesn't exist - std::fs::create_dir_all(&db_dir)?; - // Set the database file path inside the directory - db_dir.join("torii.db") - } else { - tempfile.path().to_path_buf() - }; - - let mut options = SqliteConnectOptions::from_str(&database_path.to_string_lossy())? - .create_if_missing(true) - .with_regexp(); - - // Performance settings - options = options.auto_vacuum(SqliteAutoVacuum::None); - options = options.journal_mode(SqliteJournalMode::Wal); - options = options.synchronous(SqliteSynchronous::Normal); - - let pool = SqlitePoolOptions::new().min_connections(1).connect_with(options.clone()).await?; - - let readonly_options = options.read_only(true); - let readonly_pool = SqlitePoolOptions::new() - .min_connections(1) - .max_connections(100) - .connect_with(readonly_options) - .await?; - - // Set the number of threads based on CPU count - let cpu_count = std::thread::available_parallelism().unwrap().get(); - let thread_count = cmp::min(cpu_count, 8); - sqlx::query(&format!("PRAGMA threads = {};", thread_count)).execute(&pool).await?; - - sqlx::migrate!("../../crates/torii/migrations").run(&pool).await?; - - let provider: Arc<_> = JsonRpcClient::new(HttpTransport::new(args.rpc)).into(); - - // Get world address - let world = WorldContractReader::new(world_address, provider.clone()); - - let (mut executor, sender) = Executor::new( - pool.clone(), - shutdown_tx.clone(), - provider.clone(), - args.indexing.max_concurrent_tasks, - ) - .await?; - let executor_handle = tokio::spawn(async move { executor.run().await }); - - let model_cache = Arc::new(ModelCache::new(readonly_pool.clone())); - let db = Sql::new(pool.clone(), sender.clone(), &args.indexing.contracts, model_cache.clone()) - .await?; - - let processors = Processors { - transaction: vec![Box::new(StoreTransactionProcessor)], - ..Processors::default() - }; - - let (block_tx, block_rx) = tokio::sync::mpsc::channel(100); - - let mut flags = IndexingFlags::empty(); - if args.indexing.transactions { - flags.insert(IndexingFlags::TRANSACTIONS); - } - if args.events.raw { - flags.insert(IndexingFlags::RAW_EVENTS); - } - if args.indexing.pending { - flags.insert(IndexingFlags::PENDING_BLOCKS); - } - - let mut engine: Engine>> = Engine::new( - world, - db.clone(), - provider.clone(), - processors, - EngineConfig { - max_concurrent_tasks: args.indexing.max_concurrent_tasks, - blocks_chunk_size: args.indexing.blocks_chunk_size, - events_chunk_size: args.indexing.events_chunk_size, - polling_interval: Duration::from_millis(args.indexing.polling_interval), - flags, - event_processor_config: EventProcessorConfig { - historical_events: args.events.historical.into_iter().collect(), - namespaces: args.indexing.namespaces.into_iter().collect(), - }, - }, - shutdown_tx.clone(), - Some(block_tx), - &args.indexing.contracts, - ); - - let shutdown_rx = shutdown_tx.subscribe(); - let (grpc_addr, grpc_server) = torii_grpc::server::new( - shutdown_rx, - &readonly_pool, - block_rx, - world_address, - Arc::clone(&provider), - model_cache, - ) - .await?; - - let temp_dir = TempDir::new()?; - let artifacts_path = - args.artifacts_path.unwrap_or_else(|| Utf8PathBuf::from(temp_dir.path().to_str().unwrap())); - - tokio::fs::create_dir_all(&artifacts_path).await?; - let absolute_path = artifacts_path.canonicalize_utf8()?; - - let (artifacts_addr, artifacts_server) = torii_server::artifacts::new( - shutdown_tx.subscribe(), - &absolute_path, - readonly_pool.clone(), - ) - .await?; - - let mut libp2p_relay_server = torii_relay::server::Relay::new( - db, - provider.clone(), - args.relay.port, - args.relay.webrtc_port, - args.relay.websocket_port, - args.relay.local_key_path, - args.relay.cert_path, - ) - .expect("Failed to start libp2p relay server"); - - let addr = SocketAddr::new(args.server.http_addr, args.server.http_port); - - let proxy_server = Arc::new(Proxy::new( - addr, - args.server.http_cors_origins.filter(|cors_origins| !cors_origins.is_empty()), - Some(grpc_addr), - None, - Some(artifacts_addr), - Arc::new(readonly_pool.clone()), - )); - - let graphql_server = spawn_rebuilding_graphql_server( - shutdown_tx.clone(), - readonly_pool.into(), - args.external_url, - proxy_server.clone(), - ); - - let gql_endpoint = format!("{addr}/graphql"); - let encoded: String = - form_urlencoded::byte_serialize(gql_endpoint.replace("0.0.0.0", "localhost").as_bytes()) - .collect(); - let explorer_url = format!("https://worlds.dev/torii?url={}", encoded); - info!(target: LOG_TARGET, endpoint = %addr, "Starting torii endpoint."); - info!(target: LOG_TARGET, endpoint = %gql_endpoint, "Serving Graphql playground."); - info!(target: LOG_TARGET, url = %explorer_url, "Serving World Explorer."); - info!(target: LOG_TARGET, path = %artifacts_path, "Serving ERC artifacts at path"); - - if args.explorer { - if let Err(e) = webbrowser::open(&explorer_url) { - error!(target: LOG_TARGET, error = %e, "Opening World Explorer in the browser."); - } - } - - if args.metrics.metrics { - let addr = SocketAddr::new(args.metrics.metrics_addr, args.metrics.metrics_port); - info!(target: LOG_TARGET, %addr, "Starting metrics endpoint."); - let prometheus_handle = PrometheusRecorder::install("torii")?; - let server = dojo_metrics::Server::new(prometheus_handle).with_process_metrics(); - tokio::spawn(server.start(addr)); - } - - let engine_handle = tokio::spawn(async move { engine.start().await }); - let proxy_server_handle = - tokio::spawn(async move { proxy_server.start(shutdown_tx.subscribe()).await }); - let graphql_server_handle = tokio::spawn(graphql_server); - let grpc_server_handle = tokio::spawn(grpc_server); - let libp2p_relay_server_handle = tokio::spawn(async move { libp2p_relay_server.run().await }); - let artifacts_server_handle = tokio::spawn(artifacts_server); - - tokio::select! { - res = engine_handle => res??, - res = executor_handle => res??, - res = proxy_server_handle => res??, - res = graphql_server_handle => res?, - res = grpc_server_handle => res??, - res = libp2p_relay_server_handle => res?, - res = artifacts_server_handle => res?, - _ = dojo_utils::signal::wait_signals() => {}, - }; - + let args = Cli::parse().args.with_config_file()?; + let runner = Runner::new(args); + runner.run().await?; Ok(()) } - -async fn spawn_rebuilding_graphql_server( - shutdown_tx: Sender<()>, - pool: Arc, - external_url: Option, - proxy_server: Arc, -) { - let mut broker = SimpleBroker::::subscribe(); - - loop { - let shutdown_rx = shutdown_tx.subscribe(); - let (new_addr, new_server) = - torii_graphql::server::new(shutdown_rx, &pool, external_url.clone()).await; - - tokio::spawn(new_server); - - proxy_server.set_graphql_addr(new_addr).await; - - // Break the loop if there are no more events - if broker.next().await.is_none() { - break; - } - } -} diff --git a/crates/dojo-world/Cargo.toml b/crates/dojo-world/Cargo.toml deleted file mode 100644 index 39301853ca..0000000000 --- a/crates/dojo-world/Cargo.toml +++ /dev/null @@ -1,55 +0,0 @@ -[package] -description = "Dojo world specification. For example, crates and flags used for compilation." -edition.workspace = true -license-file.workspace = true -name = "dojo-world" -repository.workspace = true -version.workspace = true - -[dependencies] -anyhow.workspace = true -async-trait.workspace = true -cairo-lang-filesystem.workspace = true -cairo-lang-project.workspace = true -cairo-lang-starknet-classes.workspace = true -cairo-lang-starknet.workspace = true -camino.workspace = true -convert_case.workspace = true -dojo-utils = { workspace = true, optional = true } -num-traits = { workspace = true, optional = true } -regex.workspace = true -serde.workspace = true -serde_json.workspace = true -serde_with.workspace = true -smol_str.workspace = true -starknet-crypto.workspace = true -starknet.workspace = true -thiserror.workspace = true -topological-sort.workspace = true -tracing.workspace = true - -cainome.workspace = true -dojo-types = { path = "../dojo-types", optional = true } -http = { workspace = true, optional = true } -ipfs-api-backend-hyper = { workspace = true, optional = true } -scarb = { workspace = true, optional = true } -tokio = { version = "1.32.0", features = [ "time" ], default-features = false, optional = true } -toml.workspace = true -url = { workspace = true, optional = true } -walkdir.workspace = true - -[dev-dependencies] -assert_fs.workspace = true -assert_matches.workspace = true -dojo-lang.workspace = true -dojo-test-utils = { path = "../dojo-test-utils" } -katana-runner.workspace = true -similar-asserts.workspace = true -tempfile.workspace = true -tokio.workspace = true - -[features] -contracts = [ "dep:dojo-types", "dep:http", "dep:num-traits" ] -manifest = [ "contracts", "dep:dojo-types", "dep:scarb", "dep:url" ] -metadata = [ "dep:ipfs-api-backend-hyper", "dep:scarb", "dep:url" ] -migration = [ "dep:dojo-utils", "dep:scarb", "dep:tokio", "manifest" ] diff --git a/crates/dojo/bindgen/src/lib.rs b/crates/dojo/bindgen/src/lib.rs index 5fafebf89e..704745c31e 100644 --- a/crates/dojo/bindgen/src/lib.rs +++ b/crates/dojo/bindgen/src/lib.rs @@ -257,9 +257,10 @@ mod tests { None }; + dbg!(&config.manifest_path()); let data = gather_dojo_data(&config.manifest_path().to_path_buf(), "ns", "dev", skip_migrations) - .unwrap(); + .expect("Failed to gather dojo data"); assert_eq!(data.models.len(), 6); diff --git a/crates/dojo/bindgen/src/plugins/typescript/generator/constants.rs b/crates/dojo/bindgen/src/plugins/typescript/generator/constants.rs new file mode 100644 index 0000000000..08aa90c323 --- /dev/null +++ b/crates/dojo/bindgen/src/plugins/typescript/generator/constants.rs @@ -0,0 +1,31 @@ +pub const CAIRO_FELT252: &str = "felt252"; +pub const CAIRO_CONTRACT_ADDRESS: &str = "ContractAddress"; +pub const CAIRO_BYTE_ARRAY: &str = "ByteArray"; +pub const CAIRO_U8: &str = "u8"; +pub const CAIRO_U16: &str = "u16"; +pub const CAIRO_U32: &str = "u32"; +pub const CAIRO_U64: &str = "u64"; +pub const CAIRO_U128: &str = "u128"; +pub const CAIRO_U256: &str = "u256"; +pub const CAIRO_U256_STRUCT: &str = "U256"; +pub const CAIRO_I128: &str = "i128"; +pub const CAIRO_BOOL: &str = "bool"; +pub const CAIRO_OPTION: &str = "Option"; +pub const CAIRO_UNIT_TYPE: &str = "()"; + +pub(crate) const CAIRO_OPTION_DEFAULT_VALUE: &str = "new CairoOption(CairoOptionVariant.None)"; + +pub const JS_BOOLEAN: &str = "boolean"; +pub const JS_STRING: &str = "string"; +pub const JS_BIGNUMBERISH: &str = "BigNumberish"; + +pub(crate) const BIGNUMNERISH_IMPORT: &str = "import { BigNumberish } from 'starknet';\n"; +pub(crate) const CAIRO_OPTION_IMPORT: &str = "import { CairoOption } from 'starknet';\n"; +pub(crate) const CAIRO_ENUM_IMPORT: &str = "import { CairoCustomEnum } from 'starknet';\n"; +pub(crate) const CAIRO_OPTION_TYPE_PATH: &str = "core::option::Option"; +pub(crate) const SN_IMPORT_SEARCH: &str = "} from 'starknet';"; +pub(crate) const CAIRO_OPTION_TOKEN: &str = "CairoOption, CairoOptionVariant,"; +pub(crate) const CAIRO_ENUM_TOKEN: &str = "CairoCustomEnum,"; + +pub(crate) const WITH_FIELD_ORDER_TYPE_DEF: &str = + "type WithFieldOrder = T & { fieldOrder: string[] };\n"; diff --git a/crates/dojo/bindgen/src/plugins/typescript/generator/enum.rs b/crates/dojo/bindgen/src/plugins/typescript/generator/enum.rs index ebbee65c47..b176d71e4b 100644 --- a/crates/dojo/bindgen/src/plugins/typescript/generator/enum.rs +++ b/crates/dojo/bindgen/src/plugins/typescript/generator/enum.rs @@ -1,28 +1,50 @@ use cainome::parser::tokens::{Composite, CompositeType}; +use super::constants::{CAIRO_ENUM_IMPORT, CAIRO_ENUM_TOKEN, SN_IMPORT_SEARCH}; +use super::token_is_enum; use crate::error::BindgenResult; +use crate::plugins::typescript::generator::JsPrimitiveType; use crate::plugins::{BindgenModelGenerator, Buffer}; pub(crate) struct TsEnumGenerator; +impl TsEnumGenerator { + fn check_import(&self, token: &Composite, buffer: &mut Buffer) { + // type is Enum with type variants, need to import CairoEnum + // if enum has at least one inner that is a composite type + if token_is_enum(token) { + if !buffer.has(SN_IMPORT_SEARCH) { + buffer.push(CAIRO_ENUM_IMPORT.to_owned()); + } else if !buffer.has(CAIRO_ENUM_TOKEN) { + // If 'starknet' import is present, we add CairoEnum to the imported types + buffer.insert_after(format!(" {CAIRO_ENUM_TOKEN}"), SN_IMPORT_SEARCH, "{", 1); + } + } + } +} + impl BindgenModelGenerator for TsEnumGenerator { fn generate(&self, token: &Composite, buffer: &mut Buffer) -> BindgenResult { if token.r#type != CompositeType::Enum || token.inners.is_empty() { return Ok(String::new()); } + self.check_import(token, buffer); let gen = format!( "// Type definition for `{path}` enum -export enum {name} {{ +export type {name} = {{ {variants} }} +export type {name}Enum = CairoCustomEnum; ", path = token.type_path, name = token.type_name(), variants = token .inners .iter() - .map(|inner| format!("\t{},", inner.name)) + .map(|inner| { + format!("\t{}: {};", inner.name, JsPrimitiveType::from(&inner.token)) + }) .collect::>() .join("\n") ); @@ -85,8 +107,9 @@ mod tests { assert_eq!( result, - "// Type definition for `core::test::AvailableTheme` enum\nexport enum AvailableTheme \ - {\n\tLight,\n\tDark,\n\tDojo,\n}\n" + "// Type definition for `core::test::AvailableTheme` enum\nexport type AvailableTheme \ + = {\n\tLight: string;\n\tDark: string;\n\tDojo: string;\n}\nexport type \ + AvailableThemeEnum = CairoCustomEnum;\n" ); } @@ -95,14 +118,16 @@ mod tests { let mut buff = Buffer::new(); let writer = TsEnumGenerator; buff.push( - "// Type definition for `core::test::AvailableTheme` enum\nexport enum AvailableTheme \ - {\n\tLight,\n\tDark,\n\tDojo,\n}\n" + "// Type definition for `core::test::AvailableTheme` enum\nexport type AvailableTheme \ + = {\n\tLight: string;\n\tDark: string;\n\tDojo: string;\n}\nexport type \ + AvailableThemeEnum = CairoCustomEnum;\n" .to_owned(), ); let token_dup = create_available_theme_enum_token(); let result = writer.generate(&token_dup, &mut buff).unwrap(); - assert_eq!(buff.len(), 1); + // Length is 2 because we add import of CairoCustomEnum + assert_eq!(buff.len(), 2); assert!(result.is_empty()) } diff --git a/crates/dojo/bindgen/src/plugins/typescript/generator/erc.rs b/crates/dojo/bindgen/src/plugins/typescript/generator/erc.rs deleted file mode 100644 index b731dcfe3c..0000000000 --- a/crates/dojo/bindgen/src/plugins/typescript/generator/erc.rs +++ /dev/null @@ -1,116 +0,0 @@ -use cainome::parser::tokens::Composite; - -use super::get_namespace_and_path; -use crate::error::BindgenResult; -use crate::plugins::{BindgenModelGenerator, Buffer}; - -const ERC_TORII_TPL: &str = "// Type definition for ERC__Balance struct -export type ERC__Type = 'ERC20' | 'ERC721'; -export interface ERC__Balance { - fieldOrder: string[]; - balance: string; - type: string; - tokenMetadata: ERC__Token; -} -export interface ERC__Token { - fieldOrder: string[]; - name: string; - symbol: string; - tokenId: string; - decimals: string; - contractAddress: string; -} -export interface ERC__Transfer { - fieldOrder: string[]; - from: string; - to: string; - amount: string; - type: string; - executedAt: string; - tokenMetadata: ERC__Token; - transactionHash: string; -}"; -const ERC_TORII_TYPES: &str = "\n\t\tERC__Balance: ERC__Balance,\n\t\tERC__Token: \ - ERC__Token,\n\t\tERC__Transfer: ERC__Transfer,"; -const ERC_TORII_INIT: &str = " -\t\tERC__Balance: { -\t\t\tfieldOrder: ['balance', 'type', 'tokenmetadata'], -\t\t\tbalance: '', -\t\t\ttype: 'ERC20', -\t\t\ttokenMetadata: { -\t\t\t\tfieldOrder: ['name', 'symbol', 'tokenId', 'decimals', 'contractAddress'], -\t\t\t\tname: '', -\t\t\t\tsymbol: '', -\t\t\t\ttokenId: '', -\t\t\t\tdecimals: '', -\t\t\t\tcontractAddress: '', -\t\t\t}, -\t\t}, -\t\tERC__Token: { -\t\t\tfieldOrder: ['name', 'symbol', 'tokenId', 'decimals', 'contractAddress'], -\t\t\tname: '', -\t\t\tsymbol: '', -\t\t\ttokenId: '', -\t\t\tdecimals: '', -\t\t\tcontractAddress: '', -\t\t}, -\t\tERC__Transfer: { -\t\t\tfieldOrder: ['from', 'to', 'amount', 'type', 'executed', 'tokenMetadata'], -\t\t\tfrom: '', -\t\t\tto: '', -\t\t\tamount: '', -\t\t\ttype: 'ERC20', -\t\t\texecutedAt: '', -\t\t\ttokenMetadata: { -\t\t\t\tfieldOrder: ['name', 'symbol', 'tokenId', 'decimals', 'contractAddress'], -\t\t\t\tname: '', -\t\t\t\tsymbol: '', -\t\t\t\ttokenId: '', -\t\t\t\tdecimals: '', -\t\t\t\tcontractAddress: '', -\t\t\t}, -\t\t\ttransactionHash: '', -\t\t}, -"; - -pub(crate) struct TsErcGenerator; - -impl TsErcGenerator { - fn add_schema_type(&self, buffer: &mut Buffer, token: &Composite) { - let (_, namespace, _) = get_namespace_and_path(token); - let schema_type = format!("export interface {namespace}SchemaType extends SchemaType"); - if buffer.has(&schema_type) { - if buffer.has(ERC_TORII_TYPES) { - return; - } - - buffer.insert_after(ERC_TORII_TYPES.to_owned(), &schema_type, ",", 2); - } - } - - fn add_schema_type_init(&self, buffer: &mut Buffer, token: &Composite) { - let (_, namespace, _) = get_namespace_and_path(token); - let const_type = format!("export const schema: {namespace}SchemaType"); - if buffer.has(&const_type) { - if buffer.has(ERC_TORII_INIT) { - return; - } - buffer.insert_after(ERC_TORII_INIT.to_owned(), &const_type, ",", 2); - } - } -} - -impl BindgenModelGenerator for TsErcGenerator { - fn generate(&self, token: &Composite, buffer: &mut Buffer) -> BindgenResult { - if buffer.has(ERC_TORII_TPL) { - return Ok(String::new()); - } - - // As this generator is separated from schema.rs we need to check if schema is present in - // buffer and also adding torii types to schema to query this data through grpc - self.add_schema_type(buffer, token); - self.add_schema_type_init(buffer, token); - - Ok(ERC_TORII_TPL.to_owned()) - } -} diff --git a/crates/dojo/bindgen/src/plugins/typescript/generator/function.rs b/crates/dojo/bindgen/src/plugins/typescript/generator/function.rs index 321f971806..ddd910c8e3 100644 --- a/crates/dojo/bindgen/src/plugins/typescript/generator/function.rs +++ b/crates/dojo/bindgen/src/plugins/typescript/generator/function.rs @@ -1,8 +1,9 @@ -use cainome::parser::tokens::{CompositeType, Function, Token}; +use cainome::parser::tokens::{CompositeType, Function, StateMutability, Token}; use convert_case::{Case, Casing}; use dojo_world::contracts::naming; -use super::JsType; +use super::constants::JS_BIGNUMBERISH; +use super::{token_is_enum, JsPrimitiveInputType}; use crate::error::BindgenResult; use crate::plugins::{BindgenContractGenerator, Buffer}; use crate::DojoContract; @@ -10,15 +11,25 @@ use crate::DojoContract; pub(crate) struct TsFunctionGenerator; impl TsFunctionGenerator { fn check_imports(&self, buffer: &mut Buffer) { - if !buffer.has("import { DojoProvider } from ") { - buffer.insert(0, "import { DojoProvider } from \"@dojoengine/core\";".to_owned()); - buffer.insert(1, "import { Account } from \"starknet\";".to_owned()); + if !buffer.has("import { DojoProvider, DojoCall } from ") { + buffer.insert( + 0, + "import { DojoProvider, DojoCall } from \"@dojoengine/core\";".to_owned(), + ); + buffer.insert( + 1, + format!( + "import {{ Account, AccountInterface, {}, CairoOption, CairoCustomEnum, \ + ByteArray }} from \"starknet\";", + JS_BIGNUMBERISH + ), + ); buffer.insert(2, "import * as models from \"./models.gen\";\n".to_owned()); } } fn setup_function_wrapper_start(&self, buffer: &mut Buffer) -> usize { - let fn_wrapper = "export async function setupWorld(provider: DojoProvider) {\n"; + let fn_wrapper = "export function setupWorld(provider: DojoProvider) {\n"; if !buffer.has(fn_wrapper) { buffer.push(fn_wrapper.to_owned()); @@ -27,63 +38,107 @@ impl TsFunctionGenerator { buffer.iter().position(|b| b.contains(fn_wrapper)).unwrap() } + /// Generate string template to build function calldata. + /// * namespace - &str - Token namespace + /// * contract_name - &str - Token contract_name avoid name clashing between different systems + /// * token - &Function - cairo function token + fn build_function_calldata(&self, contract_name: &str, token: &Function) -> String { + format!( + "\tconst build_{contract_name}_{}_calldata = ({}): DojoCall => {{ +\t\treturn {{ +\t\t\tcontractName: \"{contract_name}\", +\t\t\tentrypoint: \"{}\", +\t\t\tcalldata: [{}], +\t\t}}; +\t}}; +", + token.name.to_case(Case::Camel), + self.get_function_input_args(token).join(", "), + token.name, + self.format_function_calldata(token), + ) + } + + /// Generate string template to build system function + /// We call different methods depending if token is View or External + /// * namespace - &str - Token namespace + /// * contract_name - &str - Token contract_name avoid name clashing between different systems + /// * token - &Function - cairo function token fn generate_system_function( &self, namespace: &str, contract_name: &str, token: &Function, ) -> String { - format!( - "\tconst {contract_name}_{} = async ({}) => {{ + let function_calldata = self.build_function_calldata(contract_name, token); + let function_name = format!("{contract_name}_{}", token.name.to_case(Case::Camel)); + let function_call = match token.state_mutability { + StateMutability::External => format!( + "\tconst {function_name} = async ({}) => {{ \t\ttry {{ \t\t\treturn await provider.execute( \t\t\t\tsnAccount, -\t\t\t\t{{ -\t\t\t\t\tcontractName: \"{contract_name}\", -\t\t\t\t\tentrypoint: \"{}\", -\t\t\t\t\tcalldata: [{}], -\t\t\t\t}}, +\t\t\t\tbuild_{function_name}_calldata({}), \t\t\t\t\"{namespace}\", \t\t\t); \t\t}} catch (error) {{ \t\t\tconsole.error(error); +\t\t\tthrow error; \t\t}} \t}};\n", - token.name.to_case(Case::Camel), - self.format_function_inputs(token), - token.name, - self.format_function_calldata(token) - ) + self.format_function_inputs(token), + self.format_function_calldata(token), + ), + StateMutability::View => format!( + "\tconst {function_name} = async ({}) => {{ +\t\ttry {{ +\t\t\treturn await provider.call(\"{namespace}\", build_{function_name}_calldata({})); +\t\t}} catch (error) {{ +\t\t\tconsole.error(error); +\t\t\tthrow error; +\t\t}} +\t}};\n", + self.format_function_inputs(token), + self.format_function_calldata(token), + ), + }; + format!("{}\n{}", function_calldata, function_call) } - fn format_function_inputs(&self, token: &Function) -> String { - let inputs = vec!["snAccount: Account".to_owned()]; - token - .inputs - .iter() - .fold(inputs, |mut acc, input| { - let prefix = match &input.1 { - Token::Composite(t) => { - if t.r#type == CompositeType::Enum + fn get_function_input_args(&self, token: &Function) -> Vec { + token.inputs.iter().fold(Vec::new(), |mut acc, input| { + let prefix = match &input.1 { + Token::Composite(t) => { + if !token_is_enum(t) + && (t.r#type == CompositeType::Enum || (t.r#type == CompositeType::Struct && !t.type_path.starts_with("core")) - { - "models." - } else { - "" - } + || t.r#type == CompositeType::Unknown) + { + "models." + } else { + "" } - _ => "", - }; - acc.push(format!( - "{}: {}{}", - input.0.to_case(Case::Camel), - prefix, - JsType::from(&input.1) - )); - acc - }) - .join(", ") + } + _ => "", + }; + let mut type_input = JsPrimitiveInputType::from(&input.1).to_string(); + if type_input.contains("<") { + type_input = type_input.replace("<", format!("<{}", prefix).as_str()); + } else { + type_input = format!("{}{}", prefix, type_input); + } + acc.push(format!("{}: {}", input.0.to_case(Case::Camel), type_input)); + acc + }) + } + + fn format_function_inputs(&self, token: &Function) -> String { + let inputs = match token.state_mutability { + StateMutability::External => vec!["snAccount: Account | AccountInterface".to_owned()], + StateMutability::View => Vec::new(), + }; + [inputs, self.get_function_input_args(token)].concat().join(", ") } fn format_function_calldata(&self, token: &Function) -> String { @@ -110,12 +165,15 @@ impl TsFunctionGenerator { token: &Function, buffer: &mut Buffer, ) { - let return_token = "\treturn {"; + let function_name = format!("{contract_name}_{}", token.name.to_case(Case::Camel)); + let build_calldata = format!("build{}Calldata", token.name.to_case(Case::Pascal)); + let build_calldata_sc = + format!("build_{contract_name}_{}_calldata", token.name.to_case(Case::Camel)); + let return_token = "\n\n\treturn {"; if !buffer.has(return_token) { buffer.push(format!( - "\treturn {{\n\t\t{}: {{\n\t\t\t{}: {}_{},\n\t\t}},\n\t}};\n}}", - contract_name, - token.name.to_case(Case::Camel), + "\n\n\treturn {{\n\t\t{}: {{\n\t\t\t{}: {function_name},\n\t\t\t{build_calldata}: \ + {build_calldata_sc},\n\t\t}},\n\t}};\n}}", contract_name, token.name.to_case(Case::Camel) )); @@ -138,9 +196,8 @@ impl TsFunctionGenerator { ) { if let Some(insert_pos) = buffer.get_first_before_pos(",", pos, return_idx) { let gen = format!( - "\n\t\t\t{}: {}_{},", - token.name.to_case(Case::Camel), - contract_name, + "\n\t\t\t{}: {function_name},\n\t\t\t{build_calldata}: \ + {build_calldata_sc},", token.name.to_case(Case::Camel) ); // avoid duplicating identifiers. This can occur because some contracts keeps @@ -156,11 +213,10 @@ impl TsFunctionGenerator { // if buffer has return but not contract_name, we append in this object buffer.insert_after( format!( - "\n\t\t{}: {{\n\t\t\t{}: {}_{},\n\t\t}},", + "\n\t\t{}: {{\n\t\t\t{}: {function_name},\n\t\t\t{build_calldata}: \ + {build_calldata_sc},\n\t\t}},", contract_name, - token.name.to_case(Case::Camel), - contract_name, - token.name.to_case(Case::Camel), + token.name.to_case(Case::Camel) ), return_token, ",", @@ -201,7 +257,10 @@ impl BindgenContractGenerator for TsFunctionGenerator { #[cfg(test)] mod tests { - use cainome::parser::tokens::{CoreBasic, Function, Token}; + use cainome::parser::tokens::{ + Array, Composite, CompositeInner, CompositeInnerKind, CompositeType, CoreBasic, Function, + StateMutability, Token, + }; use cainome::parser::TokenizedAbi; use dojo_world::contracts::naming; @@ -235,20 +294,60 @@ mod tests { fn test_generate_system_function() { let generator = TsFunctionGenerator {}; let function = create_change_theme_function(); - let expected = "\tconst actions_changeTheme = async (snAccount: Account, value: number) \ + let expected = "\tconst build_actions_changeTheme_calldata = (value: BigNumberish): \ + DojoCall => { +\t\treturn { +\t\t\tcontractName: \"actions\", +\t\t\tentrypoint: \"change_theme\", +\t\t\tcalldata: [value], +\t\t}; +\t}; + +\tconst actions_changeTheme = async (snAccount: Account | AccountInterface, value: BigNumberish) \ => { \t\ttry { \t\t\treturn await provider.execute( \t\t\t\tsnAccount, -\t\t\t\t{ -\t\t\t\t\tcontractName: \"actions\", -\t\t\t\t\tentrypoint: \"change_theme\", -\t\t\t\t\tcalldata: [value], -\t\t\t\t}, +\t\t\t\tbuild_actions_changeTheme_calldata(value), \t\t\t\t\"onchain_dash\", \t\t\t); \t\t} catch (error) { \t\t\tconsole.error(error); +\t\t\tthrow error; +\t\t} +\t}; +"; + + let contract = create_dojo_contract(); + assert_eq!( + expected, + generator.generate_system_function( + naming::get_namespace_from_tag(&contract.tag).as_str(), + naming::get_name_from_tag(&contract.tag).as_str(), + &function + ) + ) + } + + #[test] + fn test_generate_system_function_view() { + let generator = TsFunctionGenerator {}; + let function = create_change_theme_view_function(); + let expected = "\tconst build_actions_changeTheme_calldata = (value: BigNumberish): \ + DojoCall => { +\t\treturn { +\t\t\tcontractName: \"actions\", +\t\t\tentrypoint: \"change_theme\", +\t\t\tcalldata: [value], +\t\t}; +\t}; + +\tconst actions_changeTheme = async (value: BigNumberish) => { +\t\ttry { +\t\t\treturn await provider.call(\"onchain_dash\", build_actions_changeTheme_calldata(value)); +\t\t} catch (error) { +\t\t\tconsole.error(error); +\t\t\tthrow error; \t\t} \t}; "; @@ -268,7 +367,14 @@ mod tests { fn test_format_function_inputs() { let generator = TsFunctionGenerator {}; let function = create_change_theme_function(); - let expected = "snAccount: Account, value: number"; + let expected = "snAccount: Account | AccountInterface, value: BigNumberish"; + assert_eq!(expected, generator.format_function_inputs(&function)) + } + #[test] + fn test_format_function_inputs_view() { + let generator = TsFunctionGenerator {}; + let function = create_basic_view_function(); + let expected = ""; assert_eq!(expected, generator.format_function_inputs(&function)) } @@ -276,7 +382,24 @@ mod tests { fn test_format_function_inputs_complex() { let generator = TsFunctionGenerator {}; let function = create_change_theme_function(); - let expected = "snAccount: Account, value: number"; + let expected = "snAccount: Account | AccountInterface, value: BigNumberish"; + assert_eq!(expected, generator.format_function_inputs(&function)) + } + + #[test] + fn test_format_function_inputs_cairo_option() { + let generator = TsFunctionGenerator {}; + let function = create_function_with_option_param(); + let expected = + "snAccount: Account | AccountInterface, value: CairoOption"; + assert_eq!(expected, generator.format_function_inputs(&function)) + } + + #[test] + fn test_format_function_inputs_cairo_enum() { + let generator = TsFunctionGenerator {}; + let function = create_function_with_custom_enum(); + let expected = "snAccount: Account | AccountInterface, value: models.GatedType"; assert_eq!(expected, generator.format_function_inputs(&function)) } @@ -307,9 +430,10 @@ mod tests { generator.setup_function_wrapper_end("actions", &create_change_theme_function(), &mut buff); - let expected = "\treturn { + let expected = "\n\n\treturn { \t\tactions: { \t\t\tchangeTheme: actions_changeTheme, +\t\t\tbuildChangeThemeCalldata: build_actions_changeTheme_calldata, \t\t}, \t}; }"; @@ -322,10 +446,12 @@ mod tests { &create_increate_global_counter_function(), &mut buff, ); - let expected_2 = "\treturn { + let expected_2 = "\n\n\treturn { \t\tactions: { \t\t\tchangeTheme: actions_changeTheme, +\t\t\tbuildChangeThemeCalldata: build_actions_changeTheme_calldata, \t\t\tincreaseGlobalCounter: actions_increaseGlobalCounter, +\t\t\tbuildIncreaseGlobalCounterCalldata: build_actions_increaseGlobalCounter_calldata, \t\t}, \t}; }"; @@ -333,13 +459,16 @@ mod tests { assert_eq!(expected_2, buff[0]); generator.setup_function_wrapper_end("dojo_starter", &create_move_function(), &mut buff); - let expected_3 = "\treturn { + let expected_3 = "\n\n\treturn { \t\tactions: { \t\t\tchangeTheme: actions_changeTheme, +\t\t\tbuildChangeThemeCalldata: build_actions_changeTheme_calldata, \t\t\tincreaseGlobalCounter: actions_increaseGlobalCounter, +\t\t\tbuildIncreaseGlobalCounterCalldata: build_actions_increaseGlobalCounter_calldata, \t\t}, \t\tdojo_starter: { \t\t\tmove: dojo_starter_move, +\t\t\tbuildMoveCalldata: build_dojo_starter_move_calldata, \t\t}, \t}; }"; @@ -354,9 +483,10 @@ mod tests { generator.setup_function_wrapper_end("actions", &create_change_theme_function(), &mut buff); - let expected = "\treturn { + let expected = "\n\n\treturn { \t\tactions: { \t\t\tchangeTheme: actions_changeTheme, +\t\t\tbuildChangeThemeCalldata: build_actions_changeTheme_calldata, \t\t}, \t}; }"; @@ -404,8 +534,31 @@ mod tests { "value".to_owned(), Token::CoreBasic(CoreBasic { type_path: "core::integer::u8".to_owned() }), )], + StateMutability::External, ) } + + fn create_change_theme_view_function() -> Function { + create_test_function( + "change_theme", + vec![( + "value".to_owned(), + Token::CoreBasic(CoreBasic { type_path: "core::integer::u8".to_owned() }), + )], + StateMutability::View, + ) + } + + fn create_basic_view_function() -> Function { + Function { + name: "allowance".to_owned(), + state_mutability: cainome::parser::tokens::StateMutability::View, + inputs: vec![], + outputs: vec![], + named_outputs: vec![], + } + } + fn create_change_theme_function_camelized() -> Function { create_test_function( "changeTheme", @@ -413,20 +566,26 @@ mod tests { "value".to_owned(), Token::CoreBasic(CoreBasic { type_path: "core::integer::u8".to_owned() }), )], + StateMutability::External, ) } fn create_increate_global_counter_function() -> Function { - create_test_function("increase_global_counter", vec![]) + create_test_function("increase_global_counter", vec![], StateMutability::External) } + fn create_move_function() -> Function { - create_test_function("move", vec![]) + create_test_function("move", vec![], StateMutability::External) } - fn create_test_function(name: &str, inputs: Vec<(String, Token)>) -> Function { + fn create_test_function( + name: &str, + inputs: Vec<(String, Token)>, + state_mutability: StateMutability, + ) -> Function { Function { name: name.to_owned(), - state_mutability: cainome::parser::tokens::StateMutability::External, + state_mutability, inputs, outputs: vec![], named_outputs: vec![], @@ -440,4 +599,162 @@ mod tests { systems: vec![], } } + + fn create_function_with_option_param() -> Function { + Function { + name: "cairo_option".to_owned(), + state_mutability: cainome::parser::tokens::StateMutability::External, + inputs: vec![("value".to_owned(), Token::Composite(Composite { +type_path: "core::option::Option".to_owned(), + inners: vec![], + generic_args: vec![ + ( + "A".to_owned(), + Token::Composite( + Composite { + type_path: "tournament::ls15_components::models::tournament::GatedType".to_owned(), + inners: vec![ + CompositeInner { + index: 0, + name: "token".to_owned(), + kind: CompositeInnerKind::NotUsed, + token: Token::Composite( + Composite { + type_path: "tournament::ls15_components::models::tournament::GatedToken".to_owned(), + inners: vec![], + generic_args: vec![], + r#type: CompositeType::Unknown, + is_event: false, + alias: None, + }, + ), + }, + CompositeInner { + index: 1, + name: "tournament".to_owned(), + kind: CompositeInnerKind::NotUsed, + token: Token::Array( + Array { + type_path: "core::array::Span::".to_owned(), + inner: Box::new(Token::CoreBasic( + CoreBasic { + type_path: "core::integer::u64".to_owned(), + }, + )), + is_legacy: false, + }, + ), + }, + CompositeInner { + index: 2, + name: "address".to_owned(), + kind: CompositeInnerKind::NotUsed, + token: Token::Array( + Array { + type_path: "core::array::Span::".to_owned(), + inner: Box::new(Token::CoreBasic( + CoreBasic { + type_path: "core::starknet::contract_address::ContractAddress".to_owned(), + }, + )) , + is_legacy: false, + }, + ), + } + ], + generic_args: vec![], + r#type: CompositeType::Unknown, + is_event: false, + alias: None + } + ) + ), + ], + r#type: CompositeType::Unknown, + is_event: false, + alias: None + }))], + outputs: vec![], + named_outputs: vec![], + } + } + fn create_function_with_custom_enum() -> Function { + Function { + name: "cairo_enum".to_owned(), + state_mutability: cainome::parser::tokens::StateMutability::External, + inputs: vec![("value".to_owned(), Token::Composite(Composite { + type_path: "tournament::ls15_components::models::tournament::GatedType".to_owned(), + inners: vec![ + CompositeInner { + index: 0, + name: "token".to_owned(), + kind: CompositeInnerKind::NotUsed, + token: Token::Composite( + Composite { + type_path: "tournament::ls15_components::models::tournament::GatedType".to_owned(), + inners: vec![ + CompositeInner { + index: 0, + name: "token".to_owned(), + kind: CompositeInnerKind::NotUsed, + token: Token::Composite( + Composite { + type_path: "tournament::ls15_components::models::tournament::GatedToken".to_owned(), + inners: vec![], + generic_args: vec![], + r#type: CompositeType::Unknown, + is_event: false, + alias: None, + }, + ), + }, + CompositeInner { + index: 1, + name: "tournament".to_owned(), + kind: CompositeInnerKind::NotUsed, + token: Token::Array( + Array { + type_path: "core::array::Span::".to_owned(), + inner: Box::new(Token::CoreBasic( + CoreBasic { + type_path: "core::integer::u64".to_owned(), + }, + )), + is_legacy: false, + }, + ), + }, + CompositeInner { + index: 2, + name: "address".to_owned(), + kind: CompositeInnerKind::NotUsed, + token: Token::Array( + Array { + type_path: "core::array::Span::".to_owned(), + inner: Box::new(Token::CoreBasic( + CoreBasic { + type_path: "core::starknet::contract_address::ContractAddress".to_owned(), + }, + )) , + is_legacy: false, + }, + ), + } + ], + generic_args: vec![], + r#type: CompositeType::Unknown, + is_event: false, + alias: None + }), + } + ], + generic_args: vec![], + r#type: CompositeType::Unknown, + is_event: false, + alias: None + }))], + outputs: vec![], + named_outputs: vec![], + } + } } diff --git a/crates/dojo/bindgen/src/plugins/typescript/generator/interface.rs b/crates/dojo/bindgen/src/plugins/typescript/generator/interface.rs index f75efadf94..d4ae4670c2 100644 --- a/crates/dojo/bindgen/src/plugins/typescript/generator/interface.rs +++ b/crates/dojo/bindgen/src/plugins/typescript/generator/interface.rs @@ -1,21 +1,57 @@ -use cainome::parser::tokens::{Composite, CompositeType}; +use cainome::parser::tokens::{Composite, CompositeType, Token}; -use super::JsType; +use super::constants::{ + BIGNUMNERISH_IMPORT, CAIRO_OPTION_IMPORT, SN_IMPORT_SEARCH, WITH_FIELD_ORDER_TYPE_DEF, +}; +use super::{token_is_option, JsPrimitiveType}; use crate::error::BindgenResult; +use crate::plugins::typescript::generator::constants::CAIRO_OPTION_TOKEN; use crate::plugins::{BindgenModelGenerator, Buffer}; pub(crate) struct TsInterfaceGenerator; +impl TsInterfaceGenerator { + fn check_import(&self, token: &Composite, buffer: &mut Buffer) { + // only search for end part of the import, as we append the other imports afterward + if !buffer.has("BigNumberish } from 'starknet';") { + buffer.push(BIGNUMNERISH_IMPORT.to_owned()); + } + + // type is Option, need to import CairoOption + if token_is_option(token) { + // we directly add import if 'starknet' import is not present + if !buffer.has(SN_IMPORT_SEARCH) { + buffer.push(CAIRO_OPTION_IMPORT.to_owned()); + } else if !buffer.has(CAIRO_OPTION_TOKEN) { + // If 'starknet' import is present, we add CairoOption to the imported types + buffer.insert_after(format!(" {CAIRO_OPTION_TOKEN}"), SN_IMPORT_SEARCH, "{", 1); + } + } + } + + fn add_input_type(&self, buffer: &mut Buffer) { + if !buffer.has(WITH_FIELD_ORDER_TYPE_DEF) { + buffer.push(WITH_FIELD_ORDER_TYPE_DEF.to_owned()); + } + } +} impl BindgenModelGenerator for TsInterfaceGenerator { - fn generate(&self, token: &Composite, _buffer: &mut Buffer) -> BindgenResult { + fn generate(&self, token: &Composite, buffer: &mut Buffer) -> BindgenResult { if token.r#type != CompositeType::Struct || token.inners.is_empty() { return Ok(String::new()); } + if buffer + .has(format!("// Type definition for `{path}` struct", path = token.type_path).as_str()) + { + return Ok(String::new()); + } + + self.check_import(token, buffer); + self.add_input_type(buffer); Ok(format!( "// Type definition for `{path}` struct export interface {name} {{ -\tfieldOrder: string[]; {fields} }} ", @@ -24,7 +60,15 @@ export interface {name} {{ fields = token .inners .iter() - .map(|inner| { format!("\t{}: {};", inner.name, JsType::from(&inner.token)) }) + .map(|inner| { + if let Token::Composite(composite) = &inner.token { + if token_is_option(composite) { + self.check_import(composite, buffer); + } + } + + format!("\t{}: {};", inner.name, JsPrimitiveType::from(&inner.token)) + }) .collect::>() .join("\n") )) @@ -82,11 +126,25 @@ mod tests { assert_eq!( result, "// Type definition for `core::test::TestStruct` struct\nexport interface TestStruct \ - {\n\tfieldOrder: string[];\n\tfield1: number;\n\tfield2: number;\n\tfield3: \ - number;\n}\n" + {\n\tfield1: BigNumberish;\n\tfield2: BigNumberish;\n\tfield3: BigNumberish;\n}\n" ); } + #[test] + fn test_check_import() { + let mut buff = Buffer::new(); + let writer = TsInterfaceGenerator; + let token = create_test_struct_token(); + writer.check_import(&token, &mut buff); + assert_eq!(1, buff.len()); + let option = create_option_token(); + writer.check_import(&option, &mut buff); + assert_eq!(1, buff.len()); + let custom_enum = create_custom_enum_token(); + writer.check_import(&custom_enum, &mut buff); + assert_eq!(1, buff.len()); + } + fn create_test_struct_token() -> Composite { Composite { type_path: "core::test::TestStruct".to_owned(), @@ -116,4 +174,57 @@ mod tests { alias: None, } } + + fn create_option_token() -> Composite { + Composite { + type_path: "core::option::Option".to_owned(), + inners: vec![CompositeInner { + index: 0, + name: "value".to_owned(), + kind: CompositeInnerKind::Key, + token: Token::CoreBasic(CoreBasic { type_path: "core::felt252".to_owned() }), + }], + generic_args: vec![], + r#type: CompositeType::Struct, + is_event: false, + alias: None, + } + } + fn create_custom_enum_token() -> Composite { + Composite { + type_path: "core::test::CustomEnum".to_owned(), + inners: vec![ + CompositeInner { + index: 0, + name: "Variant1".to_owned(), + kind: CompositeInnerKind::NotUsed, + token: Token::CoreBasic(CoreBasic { type_path: "core::felt252".to_owned() }), + }, + CompositeInner { + index: 1, + name: "Variant2".to_owned(), + kind: CompositeInnerKind::NotUsed, + token: Token::Composite(Composite { + type_path: "core::test::NestedStruct".to_owned(), + inners: vec![CompositeInner { + index: 0, + name: "nested_field".to_owned(), + kind: CompositeInnerKind::Key, + token: Token::CoreBasic(CoreBasic { + type_path: "core::felt252".to_owned(), + }), + }], + generic_args: vec![], + r#type: CompositeType::Struct, + is_event: false, + alias: None, + }), + }, + ], + generic_args: vec![], + r#type: CompositeType::Enum, + is_event: false, + alias: None, + } + } } diff --git a/crates/dojo/bindgen/src/plugins/typescript/generator/mod.rs b/crates/dojo/bindgen/src/plugins/typescript/generator/mod.rs index 2c33e067c2..04060fa233 100644 --- a/crates/dojo/bindgen/src/plugins/typescript/generator/mod.rs +++ b/crates/dojo/bindgen/src/plugins/typescript/generator/mod.rs @@ -1,10 +1,18 @@ -use cainome::parser::tokens::{Composite, Token}; +use cainome::parser::tokens::{Composite, CompositeInner, CompositeType, Token}; +use constants::{ + CAIRO_BOOL, CAIRO_BYTE_ARRAY, CAIRO_CONTRACT_ADDRESS, CAIRO_FELT252, CAIRO_I128, CAIRO_OPTION, + CAIRO_OPTION_DEFAULT_VALUE, CAIRO_U128, CAIRO_U16, CAIRO_U256, CAIRO_U256_STRUCT, CAIRO_U32, + CAIRO_U64, CAIRO_U8, CAIRO_UNIT_TYPE, JS_BIGNUMBERISH, JS_BOOLEAN, JS_STRING, +}; use convert_case::{Case, Casing}; +use crate::plugins::typescript::generator::constants::CAIRO_OPTION_TYPE_PATH; + +pub(crate) mod constants; pub(crate) mod r#enum; -pub(crate) mod erc; pub(crate) mod function; pub(crate) mod interface; +pub(crate) mod models; pub(crate) mod schema; /// Get the namespace and path of a model @@ -29,140 +37,305 @@ pub(crate) fn generate_type_init(token: &Composite) -> String { token .inners .iter() - .map(|i| { - match i.token.to_composite() { - Ok(c) => { - format!("\t\t\t{}: {},", i.name, JsDefaultValue::from(c)) - } - Err(_) => { - // this will fail on core types as - // `core::starknet::contract_address::ContractAddress` - // `core::felt252` - // `core::integer::u64` - // and so son - format!("\t\t\t{}: {},", i.name, JsDefaultValue::from(&i.token)) - } - } - }) + .map(build_composite_inner_primitive_default_value) .collect::>() .join("\n") ) } +/// Checks if Token::Composite is an Option +/// * token - The token to check +pub(crate) fn token_is_option(token: &Composite) -> bool { + token.type_path.starts_with(CAIRO_OPTION_TYPE_PATH) +} + +/// Checks if Token::Composite is an custom enum (enum with nested Composite types) +/// * token - The token to check +pub(crate) fn token_is_enum(token: &Composite) -> bool { + token.r#type == CompositeType::Enum +} + +/// Type used to map cainome `Token` into javascript types in interface definition #[derive(Debug)] -pub(crate) struct JsType(String); -impl From<&str> for JsType { +pub(crate) struct JsPrimitiveType(String); + +impl From<&str> for JsPrimitiveType { fn from(value: &str) -> Self { match value { - "felt252" => JsType("number".to_owned()), - "ContractAddress" => JsType("string".to_owned()), - "ByteArray" => JsType("string".to_owned()), - "u8" => JsType("number".to_owned()), - "u16" => JsType("number".to_owned()), - "u32" => JsType("number".to_owned()), - "u64" => JsType("number".to_owned()), - "u128" => JsType("number".to_owned()), - "u256" => JsType("number".to_owned()), - "U256" => JsType("number".to_owned()), - "bool" => JsType("boolean".to_owned()), - _ => JsType(value.to_owned()), + CAIRO_FELT252 => JsPrimitiveType(JS_BIGNUMBERISH.to_owned()), + CAIRO_CONTRACT_ADDRESS => JsPrimitiveType(JS_STRING.to_owned()), + CAIRO_BYTE_ARRAY => JsPrimitiveType(JS_STRING.to_owned()), + CAIRO_U8 => JsPrimitiveType(JS_BIGNUMBERISH.to_owned()), + CAIRO_U16 => JsPrimitiveType(JS_BIGNUMBERISH.to_owned()), + CAIRO_U32 => JsPrimitiveType(JS_BIGNUMBERISH.to_owned()), + CAIRO_U64 => JsPrimitiveType(JS_BIGNUMBERISH.to_owned()), + CAIRO_U128 => JsPrimitiveType(JS_BIGNUMBERISH.to_owned()), + CAIRO_U256 => JsPrimitiveType(JS_BIGNUMBERISH.to_owned()), + CAIRO_U256_STRUCT => JsPrimitiveType(JS_BIGNUMBERISH.to_owned()), + CAIRO_I128 => JsPrimitiveType(JS_BIGNUMBERISH.to_owned()), + CAIRO_BOOL => JsPrimitiveType(JS_BOOLEAN.to_owned()), + CAIRO_UNIT_TYPE => JsPrimitiveType(JS_STRING.to_owned()), + _ => JsPrimitiveType(value.to_owned()), } } } -impl From<&Token> for JsType { +impl From<&Token> for JsPrimitiveType { fn from(value: &Token) -> Self { match value { - Token::Array(a) => JsType::from(format!("Array<{}>", JsType::from(&*a.inner)).as_str()), - Token::Tuple(t) => JsType::from( + Token::Array(a) => JsPrimitiveType::from( + format!("Array<{}>", JsPrimitiveType::from(&*a.inner)).as_str(), + ), + Token::Tuple(t) => JsPrimitiveType::from( format!( "[{}]", t.inners .iter() - .map(|i| JsType::from(i.type_name().as_str()).to_string()) + .map(|i| JsPrimitiveType::from(i.type_name().as_str()).to_string()) .collect::>() .join(", ") .as_str() ) .as_str(), ), - _ => JsType::from(value.type_name().as_str()), + Token::Composite(c) => { + if token_is_option(c) { + return JsPrimitiveType::from( + format!( + "CairoOption<{}>", + c.generic_args + .iter() + .map(|(_, t)| JsPrimitiveType::from(t).to_string()) + .collect::>() + .join(", ") + ) + .as_str(), + ); + } + if token_is_enum(c) { + // we defined a type wrapper with Enum suffix let's use it there + return JsPrimitiveType::from(format!("{}Enum", c.type_name()).as_str()); + } + return JsPrimitiveType::from(value.type_name().as_str()); + } + _ => JsPrimitiveType::from(value.type_name().as_str()), } } } -impl std::fmt::Display for JsType { +impl std::fmt::Display for JsPrimitiveType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(&self.0) } } +/// Type used to map cainome `Token` into javascript types in function definition #[derive(Debug)] -pub(crate) struct JsDefaultValue(String); -impl From<&str> for JsDefaultValue { +pub(crate) struct JsPrimitiveInputType(String); +impl std::fmt::Display for JsPrimitiveInputType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.0) + } +} +impl From<&str> for JsPrimitiveInputType { fn from(value: &str) -> Self { match value { - "felt252" => JsDefaultValue("0".to_string()), - "ContractAddress" => JsDefaultValue("\"\"".to_string()), - "ByteArray" => JsDefaultValue("\"\"".to_string()), - "u8" => JsDefaultValue("0".to_string()), - "u16" => JsDefaultValue("0".to_string()), - "u32" => JsDefaultValue("0".to_string()), - "u64" => JsDefaultValue("0".to_string()), - "u128" => JsDefaultValue("0".to_string()), - "u256" => JsDefaultValue("0".to_string()), - "U256" => JsDefaultValue("0".to_string()), - "bool" => JsDefaultValue("false".to_string()), - _ => JsDefaultValue(value.to_string()), + CAIRO_FELT252 => JsPrimitiveInputType(JS_BIGNUMBERISH.to_owned()), + CAIRO_CONTRACT_ADDRESS => JsPrimitiveInputType(JS_STRING.to_owned()), + CAIRO_BYTE_ARRAY => JsPrimitiveInputType(CAIRO_BYTE_ARRAY.to_owned()), + CAIRO_U8 => JsPrimitiveInputType(JS_BIGNUMBERISH.to_owned()), + CAIRO_U16 => JsPrimitiveInputType(JS_BIGNUMBERISH.to_owned()), + CAIRO_U32 => JsPrimitiveInputType(JS_BIGNUMBERISH.to_owned()), + CAIRO_U64 => JsPrimitiveInputType(JS_BIGNUMBERISH.to_owned()), + CAIRO_U128 => JsPrimitiveInputType(JS_BIGNUMBERISH.to_owned()), + CAIRO_U256 => JsPrimitiveInputType(JS_BIGNUMBERISH.to_owned()), + CAIRO_U256_STRUCT => JsPrimitiveInputType(JS_BIGNUMBERISH.to_owned()), + CAIRO_I128 => JsPrimitiveInputType(JS_BIGNUMBERISH.to_owned()), + CAIRO_BOOL => JsPrimitiveInputType(JS_BOOLEAN.to_owned()), + _ => JsPrimitiveInputType(value.to_owned()), } } } -impl From<&Composite> for JsDefaultValue { + +impl From<&Token> for JsPrimitiveInputType { + fn from(value: &Token) -> Self { + match value { + Token::Array(a) => JsPrimitiveInputType::from( + format!("Array<{}>", JsPrimitiveInputType::from(&*a.inner)).as_str(), + ), + Token::Tuple(t) => JsPrimitiveInputType::from( + format!( + "[{}]", + t.inners + .iter() + .map(|i| JsPrimitiveInputType::from(i.type_name().as_str()).to_string()) + .collect::>() + .join(", ") + .as_str() + ) + .as_str(), + ), + Token::Composite(c) => { + if token_is_option(c) { + return JsPrimitiveInputType::from( + format!( + "CairoOption<{}>", + c.generic_args + .iter() + .map(|(_, t)| JsPrimitiveInputType::from(t).to_string()) + .collect::>() + .join(", ") + ) + .as_str(), + ); + } + if token_is_enum(c) { + // Use CairoCustomEnum type from starknetjs + return JsPrimitiveInputType::from("CairoCustomEnum"); + } + return JsPrimitiveInputType::from(value.type_name().as_str()); + } + _ => JsPrimitiveInputType::from(value.type_name().as_str()), + } + } +} + +#[derive(Debug)] +pub(crate) struct JsPrimitiveDefaultValue(String); +impl From<&str> for JsPrimitiveDefaultValue { + fn from(value: &str) -> Self { + match value { + CAIRO_FELT252 => JsPrimitiveDefaultValue("0".to_string()), + CAIRO_CONTRACT_ADDRESS => JsPrimitiveDefaultValue("\"\"".to_string()), + CAIRO_BYTE_ARRAY => JsPrimitiveDefaultValue("\"\"".to_string()), + CAIRO_UNIT_TYPE => JsPrimitiveDefaultValue("\"\"".to_string()), + CAIRO_U8 => JsPrimitiveDefaultValue("0".to_string()), + CAIRO_U16 => JsPrimitiveDefaultValue("0".to_string()), + CAIRO_U32 => JsPrimitiveDefaultValue("0".to_string()), + CAIRO_U64 => JsPrimitiveDefaultValue("0".to_string()), + CAIRO_U128 => JsPrimitiveDefaultValue("0".to_string()), + CAIRO_U256 => JsPrimitiveDefaultValue("0".to_string()), + CAIRO_U256_STRUCT => JsPrimitiveDefaultValue("0".to_string()), + CAIRO_I128 => JsPrimitiveDefaultValue("0".to_string()), + CAIRO_BOOL => JsPrimitiveDefaultValue("false".to_string()), + CAIRO_OPTION => JsPrimitiveDefaultValue(CAIRO_OPTION_DEFAULT_VALUE.to_string()), + _ => JsPrimitiveDefaultValue(value.to_string()), + } + } +} +impl From<&Composite> for JsPrimitiveDefaultValue { fn from(value: &Composite) -> Self { match value.r#type { cainome::parser::tokens::CompositeType::Enum => { - JsDefaultValue(format!("{}.{}", value.type_name(), value.inners[0].name)) + if token_is_enum(value) { + return JsPrimitiveDefaultValue(build_custom_enum_default_value(value)); + } + match value.inners[0].token.to_composite() { + Ok(c) => JsPrimitiveDefaultValue::from(c), + Err(_) => JsPrimitiveDefaultValue(format!( + "{}.{}", + value.type_name(), + value.inners[0].name + )), + } } - cainome::parser::tokens::CompositeType::Struct => JsDefaultValue(format!( - "{{ fieldOrder: [{}], {} }}", - value.inners.iter().map(|i| format!("'{}'", i.name)).collect::>().join(", "), - value - .inners - .iter() - .map(|i| format!("{}: {},", i.name, JsDefaultValue::from(&i.token))) - .collect::>() - .join(" ") - )), - _ => JsDefaultValue::from(value.type_name().as_str()), + cainome::parser::tokens::CompositeType::Struct => { + JsPrimitiveDefaultValue(build_struct_default_value(value)) + } + _ => JsPrimitiveDefaultValue::from(value.type_name().as_str()), + } + } +} + +/// Builds the default value for an enum variant +/// * token - The enum token to build the default value for +fn build_default_enum_variant(token: &Composite) -> String { + let default_value = token.inners.iter().take(1).fold(String::new(), |_acc, i| { + format!("\n\t\t{}", build_composite_inner_primitive_default_value(i)) + }); + let undefined = token + .inners + .iter() + .skip(1) + .fold(String::new(), |acc, i| format!("{acc}\n\t\t\t\t{}: undefined,", i.name)); + + default_value + &undefined +} + +/// Builds JsPrimitiveDefaultValue from CompositeInne token +/// * inner - CompositeInner +fn build_composite_inner_primitive_default_value(inner: &CompositeInner) -> String { + match inner.token.to_composite() { + Ok(c) => { + format!("\t\t{}: {},", inner.name, JsPrimitiveDefaultValue::from(c)) + } + Err(_) => { + // this will fail on core types as + // `core::starknet::contract_address::ContractAddress` + // `core::felt252` + // `core::integer::u64` + // and so son + format!("\t\t\t{}: {},", inner.name, JsPrimitiveDefaultValue::from(&inner.token)) } } } -impl From<&Token> for JsDefaultValue { +/// Builds the default value for an enum variant +/// * token - The enum token to build the default value for +fn build_custom_enum_default_value(token: &Composite) -> String { + format!("new CairoCustomEnum({{ {} }})", build_default_enum_variant(token),) +} + +/// Builds the default value for a struct +/// * token - The enum token to build the default value for +fn build_struct_default_value(token: &Composite) -> String { + format!( + "{{ {} }}", + token + .inners + .iter() + .map(|i| format!( + "{}: {},", + i.name, + match i.token.to_composite() { + Ok(c) => { + JsPrimitiveDefaultValue::from(c) + } + Err(_) => { + JsPrimitiveDefaultValue::from(&i.token) + } + } + )) + .collect::>() + .join(" ") + ) +} + +impl From<&Token> for JsPrimitiveDefaultValue { fn from(value: &Token) -> Self { match value { - Token::Array(a) => { - JsDefaultValue::from(format!("[{}]", JsDefaultValue::from(&*a.inner)).as_str()) - } - Token::Tuple(t) => JsDefaultValue::from( + Token::Array(a) => JsPrimitiveDefaultValue::from( + format!("[{}]", JsPrimitiveDefaultValue::from(&*a.inner)).as_str(), + ), + Token::Tuple(t) => JsPrimitiveDefaultValue::from( format!( "[{}]", t.inners .iter() - .map(|i| JsDefaultValue::from(i.type_name().as_str()).to_string()) + .map(|i| JsPrimitiveDefaultValue::from(i.type_name().as_str()).to_string()) .collect::>() .join(", ") .as_str() ) .as_str(), ), - Token::Composite(c) => JsDefaultValue::from(c), - _ => JsDefaultValue::from(value.type_name().as_str()), + Token::Composite(c) => JsPrimitiveDefaultValue::from(c), + _ => JsPrimitiveDefaultValue::from(value.type_name().as_str()), } } } -impl std::fmt::Display for JsDefaultValue { +impl std::fmt::Display for JsPrimitiveDefaultValue { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(&self.0) } @@ -175,16 +348,19 @@ mod tests { Tuple, }; - use crate::plugins::typescript::generator::{generate_type_init, JsDefaultValue, JsType}; + use crate::plugins::typescript::generator::constants::JS_BIGNUMBERISH; + use crate::plugins::typescript::generator::{ + generate_type_init, JsPrimitiveDefaultValue, JsPrimitiveType, + }; - impl PartialEq for &str { - fn eq(&self, other: &JsType) -> bool { + impl PartialEq for &str { + fn eq(&self, other: &JsPrimitiveType) -> bool { self == &other.0.as_str() } } - impl PartialEq for &str { - fn eq(&self, other: &JsDefaultValue) -> bool { + impl PartialEq for &str { + fn eq(&self, other: &JsPrimitiveDefaultValue) -> bool { self == &other.0.as_str() } } @@ -192,22 +368,24 @@ mod tests { #[test] fn test_js_type_basics() { assert_eq!( - "number", - JsType::from(&Token::CoreBasic(CoreBasic { + JS_BIGNUMBERISH, + JsPrimitiveType::from(&Token::CoreBasic(CoreBasic { type_path: "core::integer::u8".to_owned() })) ); assert_eq!( - "number", - JsType::from(&Token::CoreBasic(CoreBasic { type_path: "core::felt252".to_owned() })) + JS_BIGNUMBERISH, + JsPrimitiveType::from(&Token::CoreBasic(CoreBasic { + type_path: "core::felt252".to_owned() + })) ) } #[test] fn test_tuple_type() { assert_eq!( - "[number, number]", - JsType::from(&Token::Tuple(Tuple { + format!("[{}, {}]", JS_BIGNUMBERISH, JS_BIGNUMBERISH).as_str(), + JsPrimitiveType::from(&Token::Tuple(Tuple { type_path: "(core::integer::u8,core::integer::u128)".to_owned(), inners: vec![ Token::CoreBasic(CoreBasic { type_path: "core::integer::u8".to_owned() }), @@ -220,8 +398,8 @@ mod tests { #[test] fn test_array_type() { assert_eq!( - "Array<[number, number]>", - JsType::from(&Token::Array(Array { + format!("Array<[{}, {}]>", JS_BIGNUMBERISH, JS_BIGNUMBERISH).as_str(), + JsPrimitiveType::from(&Token::Array(Array { type_path: "core::array::Span<(core::integer::u8,core::integer::u128)>".to_owned(), inner: Box::new(Token::Tuple(Tuple { type_path: "(core::integer::u8,core::integer::u128)".to_owned(), @@ -235,17 +413,93 @@ mod tests { ) } + #[test] + fn test_option_type() { + assert_eq!( + "CairoOption", + JsPrimitiveType::from(&Token::Composite(Composite { + type_path: "core::option::Option".to_owned(), + inners: vec![], + generic_args: vec![ + ( + "A".to_owned(), + Token::Composite( + Composite { + type_path: "tournament::ls15_components::models::tournament::GatedType".to_owned(), + inners: vec![ + CompositeInner { + index: 0, + name: "token".to_owned(), + kind: CompositeInnerKind::NotUsed, + token: Token::Composite( + Composite { + type_path: "tournament::ls15_components::models::tournament::GatedToken".to_owned(), + inners: vec![], + generic_args: vec![], + r#type: CompositeType::Unknown, + is_event: false, + alias: None, + }, + ), + }, + CompositeInner { + index: 1, + name: "tournament".to_owned(), + kind: CompositeInnerKind::NotUsed, + token: Token::Array( + Array { + type_path: "core::array::Span::".to_owned(), + inner: Box::new(Token::CoreBasic( + CoreBasic { + type_path: "core::integer::u64".to_owned(), + }, + )), + is_legacy: false, + }, + ), + }, + CompositeInner { + index: 2, + name: "address".to_owned(), + kind: CompositeInnerKind::NotUsed, + token: Token::Array( + Array { + type_path: "core::array::Span::".to_owned(), + inner: Box::new(Token::CoreBasic( + CoreBasic { + type_path: "core::starknet::contract_address::ContractAddress".to_owned(), + }, + )) , + is_legacy: false, + }, + ), + } + ], + generic_args: vec![], + r#type: CompositeType::Unknown, + is_event: false, + alias: None + } + ) + ), + ], + r#type: CompositeType::Unknown, + is_event: false, + alias: None })) + ) + } + #[test] fn test_default_value_basics() { assert_eq!( "0", - JsDefaultValue::from(&Token::CoreBasic(CoreBasic { + JsPrimitiveDefaultValue::from(&Token::CoreBasic(CoreBasic { type_path: "core::integer::u8".to_owned() })) ); assert_eq!( "0", - JsDefaultValue::from(&Token::CoreBasic(CoreBasic { + JsPrimitiveDefaultValue::from(&Token::CoreBasic(CoreBasic { type_path: "core::felt252".to_owned() })) ) @@ -255,7 +509,7 @@ mod tests { fn test_tuple_default_value() { assert_eq!( "[0, 0]", - JsDefaultValue::from(&Token::Tuple(Tuple { + JsPrimitiveDefaultValue::from(&Token::Tuple(Tuple { type_path: "(core::integer::u8,core::integer::u128)".to_owned(), inners: vec![ Token::CoreBasic(CoreBasic { type_path: "core::integer::u8".to_owned() }), @@ -269,7 +523,7 @@ mod tests { fn test_array_default_value() { assert_eq!( "[[0, 0]]", - JsDefaultValue::from(&Token::Array(Array { + JsPrimitiveDefaultValue::from(&Token::Array(Array { type_path: "core::array::Span<(core::integer::u8,core::integer::u128)>".to_owned(), inner: Box::new(Token::Tuple(Tuple { type_path: "(core::integer::u8,core::integer::u128)".to_owned(), @@ -283,6 +537,256 @@ mod tests { ) } + #[test] + fn test_enum_default_value() { + assert_eq!( + "new CairoCustomEnum({ \n\t\t\t\t\tUp: \"\",\n\t\t\t\tDown: undefined,\n\t\t\t\tLeft: \ + undefined,\n\t\t\t\tRight: undefined, })", + JsPrimitiveDefaultValue::from(&Token::Composite(Composite { + type_path: "dojo_starter::Direction".to_owned(), + inners: vec![ + CompositeInner { + index: 0, + name: "Up".to_owned(), + kind: CompositeInnerKind::NotUsed, + token: Token::CoreBasic(CoreBasic { type_path: "()".to_owned() }) + }, + CompositeInner { + index: 1, + name: "Down".to_owned(), + kind: CompositeInnerKind::NotUsed, + token: Token::CoreBasic(CoreBasic { type_path: "()".to_owned() }) + }, + CompositeInner { + index: 2, + name: "Left".to_owned(), + kind: CompositeInnerKind::NotUsed, + token: Token::CoreBasic(CoreBasic { type_path: "()".to_owned() }) + }, + CompositeInner { + index: 3, + name: "Right".to_owned(), + kind: CompositeInnerKind::NotUsed, + token: Token::CoreBasic(CoreBasic { type_path: "()".to_owned() }) + }, + ], + generic_args: vec![], + r#type: CompositeType::Enum, + is_event: false, + alias: None, + })) + ) + } + + #[test] + fn test_cairo_custom_enum_default_value() { + assert_eq!( + "new CairoCustomEnum({ \n\t\t\t\titem: { id: 0, xp: 0, },\n\t\t\t\taddress: \ + undefined, })", + JsPrimitiveDefaultValue::from(&Token::Composite(Composite { + type_path: "dojo_starter::Direction".to_owned(), + inners: vec![ + CompositeInner { + index: 0, + name: "item".to_owned(), + kind: CompositeInnerKind::NotUsed, + token: Token::Composite(Composite { + type_path: "dojo_starter::Item".to_owned(), + inners: vec![ + CompositeInner { + index: 0, + name: "id".to_owned(), + kind: CompositeInnerKind::NotUsed, + token: Token::CoreBasic(CoreBasic { + type_path: "core::felt252".to_owned(), + }) + }, + CompositeInner { + index: 1, + name: "xp".to_owned(), + kind: CompositeInnerKind::NotUsed, + token: Token::CoreBasic(CoreBasic { + type_path: "core::felt252".to_owned(), + }) + }, + ], + generic_args: vec![], + r#type: CompositeType::Struct, + is_event: false, + alias: None, + }) + }, + CompositeInner { + index: 1, + name: "address".to_owned(), + kind: CompositeInnerKind::NotUsed, + token: Token::CoreBasic(CoreBasic { type_path: "()".to_owned() }) + }, + ], + generic_args: vec![], + r#type: CompositeType::Enum, + is_event: false, + alias: None, + })) + ) + } + + #[test] + fn test_composite_default_value() { + assert_eq!( + "{ id: 0, xp: 0, }", + JsPrimitiveDefaultValue::from(&Token::Composite(Composite { + type_path: "dojo_starter::Item".to_owned(), + inners: vec![ + CompositeInner { + index: 0, + name: "id".to_owned(), + kind: CompositeInnerKind::NotUsed, + token: Token::CoreBasic(CoreBasic { + type_path: "core::felt252".to_owned(), + }) + }, + CompositeInner { + index: 1, + name: "xp".to_owned(), + kind: CompositeInnerKind::NotUsed, + token: Token::CoreBasic(CoreBasic { + type_path: "core::felt252".to_owned(), + }) + }, + ], + generic_args: vec![], + r#type: CompositeType::Struct, + is_event: false, + alias: None, + })) + ) + } + + #[test] + fn test_nested_composite_default_value() { + assert_eq!( + "{ id: 0, xp: 0, item: { id: 0, xp: 0, item: { id: 0, xp: 0, }, }, }", + JsPrimitiveDefaultValue::from(&Token::Composite(Composite { + type_path: "dojo_starter::Item".to_owned(), + inners: vec![ + CompositeInner { + index: 0, + name: "id".to_owned(), + kind: CompositeInnerKind::NotUsed, + token: Token::CoreBasic(CoreBasic { + type_path: "core::felt252".to_owned(), + }) + }, + CompositeInner { + index: 1, + name: "xp".to_owned(), + kind: CompositeInnerKind::NotUsed, + token: Token::CoreBasic(CoreBasic { + type_path: "core::felt252".to_owned(), + }) + }, + CompositeInner { + index: 1, + name: "item".to_owned(), + kind: CompositeInnerKind::NotUsed, + token: Token::Composite(Composite { + type_path: "dojo_starter::Item".to_owned(), + inners: vec![ + CompositeInner { + index: 0, + name: "id".to_owned(), + kind: CompositeInnerKind::NotUsed, + token: Token::CoreBasic(CoreBasic { + type_path: "core::felt252".to_owned(), + }) + }, + CompositeInner { + index: 1, + name: "xp".to_owned(), + kind: CompositeInnerKind::NotUsed, + token: Token::CoreBasic(CoreBasic { + type_path: "core::felt252".to_owned(), + }) + }, + CompositeInner { + index: 1, + name: "item".to_owned(), + kind: CompositeInnerKind::NotUsed, + token: Token::Composite(Composite { + type_path: "dojo_starter::Item".to_owned(), + inners: vec![ + CompositeInner { + index: 0, + name: "id".to_owned(), + kind: CompositeInnerKind::NotUsed, + token: Token::CoreBasic(CoreBasic { + type_path: "core::felt252".to_owned(), + }) + }, + CompositeInner { + index: 1, + name: "xp".to_owned(), + kind: CompositeInnerKind::NotUsed, + token: Token::CoreBasic(CoreBasic { + type_path: "core::felt252".to_owned(), + }) + }, + ], + generic_args: vec![], + r#type: CompositeType::Struct, + is_event: false, + alias: None, + }) + }, + ], + generic_args: vec![], + r#type: CompositeType::Struct, + is_event: false, + alias: None, + }) + }, + ], + generic_args: vec![], + r#type: CompositeType::Struct, + is_event: false, + alias: None, + })) + ) + } + + #[test] + fn test_cairo_option_default_value() { + assert_eq!( + "new CairoOption(CairoOptionVariant.None)", + JsPrimitiveDefaultValue::from(&Token::Composite(Composite { + type_path: "core::option::Option".to_owned(), + inners: vec![ + CompositeInner { + index: 0, + name: "id".to_owned(), + kind: CompositeInnerKind::NotUsed, + token: Token::CoreBasic(CoreBasic { + type_path: "core::felt252".to_owned(), + }) + }, + CompositeInner { + index: 1, + name: "xp".to_owned(), + kind: CompositeInnerKind::NotUsed, + token: Token::CoreBasic(CoreBasic { + type_path: "core::felt252".to_owned(), + }) + }, + ], + generic_args: vec![], + r#type: CompositeType::Unknown, + is_event: false, + alias: None, + })) + ) + } + #[test] fn test_generate_type_init() { let token = create_test_struct_token("TestStruct"); @@ -299,6 +803,13 @@ mod tests { \t\t}"; assert_eq!(expected, init_type); } + + #[test] + fn test_option_custom_enum_type() { + let token = create_option_custom_enum_token(); + assert_eq!("{ }", JsPrimitiveDefaultValue::from(&Token::Composite(token))) + } + fn create_test_struct_token(name: &str) -> Composite { Composite { type_path: format!("onchain_dash::{name}"), @@ -328,4 +839,54 @@ mod tests { alias: None, } } + + fn create_option_custom_enum_token() -> Composite { + Composite { + type_path: "core::option::Option".to_owned(), + inners: vec![], + generic_args: vec![( + "A".to_owned(), + Token::Composite(Composite { + type_path: "core::test::CustomEnum".to_owned(), + inners: vec![ + CompositeInner { + index: 0, + name: "Variant1".to_owned(), + kind: CompositeInnerKind::NotUsed, + token: Token::CoreBasic(CoreBasic { + type_path: "core::felt252".to_owned(), + }), + }, + CompositeInner { + index: 1, + name: "Variant2".to_owned(), + kind: CompositeInnerKind::NotUsed, + token: Token::Composite(Composite { + type_path: "core::test::NestedStruct".to_owned(), + inners: vec![CompositeInner { + index: 0, + name: "nested_field".to_owned(), + kind: CompositeInnerKind::Key, + token: Token::CoreBasic(CoreBasic { + type_path: "core::felt252".to_owned(), + }), + }], + generic_args: vec![], + r#type: CompositeType::Struct, + is_event: false, + alias: None, + }), + }, + ], + generic_args: vec![], + r#type: CompositeType::Enum, + is_event: false, + alias: None, + }), + )], + r#type: CompositeType::Struct, + is_event: false, + alias: None, + } + } } diff --git a/crates/dojo/bindgen/src/plugins/typescript/generator/models.rs b/crates/dojo/bindgen/src/plugins/typescript/generator/models.rs new file mode 100644 index 0000000000..d36704ea14 --- /dev/null +++ b/crates/dojo/bindgen/src/plugins/typescript/generator/models.rs @@ -0,0 +1,29 @@ +use cainome::parser::tokens::Composite; + +use super::get_namespace_and_path; +use crate::error::BindgenResult; +use crate::plugins::{BindgenModelGenerator, Buffer}; + +pub(crate) struct TsModelsGenerator; + +impl BindgenModelGenerator for TsModelsGenerator { + fn generate(&self, token: &Composite, buffer: &mut Buffer) -> BindgenResult { + let (ns, _namespace, type_name) = get_namespace_and_path(token); + let models_mapping = "export enum ModelsMapping {"; + if !buffer.has(models_mapping) { + buffer.push(format!( + "export enum ModelsMapping {{\n\t{type_name} = '{ns}-{type_name}',\n}}", + )); + return Ok("".to_owned()); + } + + let gen = format!("\n\t{type_name} = '{ns}-{type_name}',"); + if buffer.has(&gen) { + return Ok("".to_owned()); + } + + buffer.insert_after(gen, models_mapping, ",", 1); + + Ok("".to_owned()) + } +} diff --git a/crates/dojo/bindgen/src/plugins/typescript/generator/schema.rs b/crates/dojo/bindgen/src/plugins/typescript/generator/schema.rs index e906eb6cc3..ccd4a9eb1b 100644 --- a/crates/dojo/bindgen/src/plugins/typescript/generator/schema.rs +++ b/crates/dojo/bindgen/src/plugins/typescript/generator/schema.rs @@ -12,58 +12,86 @@ pub(crate) struct TsSchemaGenerator {} impl TsSchemaGenerator { /// Import only needs to be present once fn import_schema_type(&self, buffer: &mut Buffer) { - if !buffer.has("import type { SchemaType }") { - buffer.insert(0, "import type { SchemaType } from \"@dojoengine/sdk\";\n".to_owned()); + if !buffer.has("import type { SchemaType as ISchemaType }") { + buffer.insert( + 0, + "import type { SchemaType as ISchemaType } from \"@dojoengine/sdk\";\n".to_owned(), + ); } } /// Generates the type definition for the schema fn handle_schema_type(&self, token: &Composite, buffer: &mut Buffer) { - let (ns, namespace, type_name) = get_namespace_and_path(token); - let schema_type = format!("export interface {namespace}SchemaType extends SchemaType"); - if !buffer.has(&schema_type) { + let (ns, _namespace, type_name) = get_namespace_and_path(token); + let schema_type = "export interface SchemaType extends ISchemaType"; + if !buffer.has(schema_type) { buffer.push(format!( - "export interface {namespace}SchemaType extends SchemaType {{\n\t{ns}: \ - {{\n\t\t{}: {},\n\t}},\n}}", + "export interface SchemaType extends ISchemaType {{\n\t{ns}: {{\n\t\t{}: \ + WithFieldOrder<{}>,\n\t}},\n}}", type_name, type_name )); return; } + // check if namespace is defined in interface. if not, add it. + // next, find where namespace was defined in interface and add property to it. + if !self.namespace_is_defined(buffer, &ns) { + let gen = format!("\n\t{ns}: {{\n\t\t{type_name}: WithFieldOrder<{type_name}>,\n\t}},"); + buffer.insert_after(gen, schema_type, ",", 1); + return; + } + // type has already been initialized - let gen = format!("\n\t\t{type_name}: {type_name},"); + let gen = format!("\n\t\t{type_name}: WithFieldOrder<{type_name}>,"); if buffer.has(&gen) { return; } - // fastest way to add a field to the interface is to search for the n-1 `,` and add the + let ns_def = format!("\n\t{ns}: {{\n\t\t"); + + // fastest way to add a field to the interface is to search for the n-1 + // `,` and add the // field directly after it. // to improve this logic, we would need to either have some kind of code parsing. // we could otherwise have some intermediate representation that we pass to this generator // function. - buffer.insert_after(gen, &schema_type, ",", 2); + buffer.insert_after(gen, &ns_def, ",", 2); } /// Generates the default values for the schema fn handle_schema_const(&self, token: &Composite, buffer: &mut Buffer) { - let (ns, namespace, type_name) = get_namespace_and_path(token); - let const_type = format!("export const schema: {namespace}SchemaType"); - if !buffer.has(&const_type) { + let (ns, _namespace, type_name) = get_namespace_and_path(token); + let const_type = "export const schema: SchemaType"; + if !buffer.has(const_type) { buffer.push(format!( - "export const schema: {namespace}SchemaType = {{\n\t{ns}: {{\n\t\t{}: \ - {},\n\t}},\n}};", + "export const schema: SchemaType = {{\n\t{ns}: {{\n\t\t{}: {},\n\t}},\n}};", type_name, generate_type_init(token) )); return; } + // check if namespace is defined in interface. if not, add it. + // next, find where namespace was defined in interface and add property to it. + if !self.namespace_is_defined(buffer, &ns) { + let gen = + format!("\n\t{ns}: {{\n\t\t{}: {},\n\t}},", type_name, generate_type_init(token)); + buffer.insert_after(gen, const_type, ",", 1); + return; + } + // type has already been initialized let gen = format!("\n\t\t{type_name}: {},", generate_type_init(token)); if buffer.has(&gen) { return; } - buffer.insert_after(gen, &const_type, ",", 2); + + buffer.insert_after(gen, const_type, ",", 2); + } + + /// Check if namespace is defined in schema + fn namespace_is_defined(&self, buffer: &mut Buffer, ns: &str) -> bool { + buffer.has(format!("\n\t{ns}: {{\n\t\t").as_str()) } } @@ -74,12 +102,12 @@ impl BindgenModelGenerator for TsSchemaGenerator { } self.import_schema_type(buffer); - // in buffer search for interface named {pascal_case(namespace)}SchemaType extends - // SchemaType + // in buffer search for interface named SchemaType extends + // ISchemaType // this should be hold in a buffer item self.handle_schema_type(token, buffer); - // in buffer search for const schema: InterfaceName = named + // in buffer search for const schema: SchemaType = named // {pascal_case(namespace)}SchemaType extends SchemaType // this should be hold in a buffer item self.handle_schema_const(token, buffer); @@ -122,11 +150,14 @@ mod tests { let generator = TsSchemaGenerator {}; let mut buffer = Buffer::new(); - let token = create_test_struct_token("TestStruct"); + let token = create_test_struct_token("TestStruct", "onchain_dash"); let _result = generator.generate(&token, &mut buffer); // token is not empty, we should have an import - assert_eq!("import type { SchemaType } from \"@dojoengine/sdk\";\n", buffer[0]); + assert_eq!( + "import type { SchemaType as ISchemaType } from \"@dojoengine/sdk\";\n", + buffer[0] + ); } /// NOTE: For the following tests, we assume that the `enum.rs` and `interface.rs` generators @@ -136,11 +167,11 @@ mod tests { let generator = TsSchemaGenerator {}; let mut buffer = Buffer::new(); - let token = create_test_struct_token("TestStruct"); + let token = create_test_struct_token("TestStruct", "onchain_dash"); let _result = generator.generate(&token, &mut buffer); assert_eq!( - "export interface OnchainDashSchemaType extends SchemaType {\n\tonchain_dash: \ - {\n\t\tTestStruct: TestStruct,\n\t},\n}", + "export interface SchemaType extends ISchemaType {\n\tonchain_dash: \ + {\n\t\tTestStruct: WithFieldOrder,\n\t},\n}", buffer[1] ); } @@ -150,21 +181,40 @@ mod tests { let generator = TsSchemaGenerator {}; let mut buffer = Buffer::new(); - let token = create_test_struct_token("TestStruct"); + let token = create_test_struct_token("TestStruct", "onchain_dash"); generator.handle_schema_type(&token, &mut buffer); assert_ne!(0, buffer.len()); assert_eq!( - "export interface OnchainDashSchemaType extends SchemaType {\n\tonchain_dash: \ - {\n\t\tTestStruct: TestStruct,\n\t},\n}", + "export interface SchemaType extends ISchemaType {\n\tonchain_dash: \ + {\n\t\tTestStruct: WithFieldOrder,\n\t},\n}", buffer[0] ); - let token_2 = create_test_struct_token("AvailableTheme"); + let token_2 = create_test_struct_token("AvailableTheme", "onchain_dash"); generator.handle_schema_type(&token_2, &mut buffer); assert_eq!( - "export interface OnchainDashSchemaType extends SchemaType {\n\tonchain_dash: \ - {\n\t\tTestStruct: TestStruct,\n\t\tAvailableTheme: AvailableTheme,\n\t},\n}", + "export interface SchemaType extends ISchemaType {\n\tonchain_dash: \ + {\n\t\tTestStruct: WithFieldOrder,\n\t\tAvailableTheme: \ + WithFieldOrder,\n\t},\n}", + buffer[0] + ); + let token_3 = create_test_struct_token("Player", "combat"); + generator.handle_schema_type(&token_3, &mut buffer); + assert_eq!( + "export interface SchemaType extends ISchemaType {\n\tonchain_dash: \ + {\n\t\tTestStruct: WithFieldOrder,\n\t\tAvailableTheme: \ + WithFieldOrder,\n\t},\n\tcombat: {\n\t\tPlayer: \ + WithFieldOrder,\n\t},\n}", + buffer[0] + ); + let token_4 = create_test_struct_token("Position", "combat"); + generator.handle_schema_type(&token_4, &mut buffer); + assert_eq!( + "export interface SchemaType extends ISchemaType {\n\tonchain_dash: \ + {\n\t\tTestStruct: WithFieldOrder,\n\t\tAvailableTheme: \ + WithFieldOrder,\n\t},\n\tcombat: {\n\t\tPlayer: \ + WithFieldOrder,\n\t\tPosition: WithFieldOrder,\n\t},\n}", buffer[0] ); } @@ -173,42 +223,72 @@ mod tests { fn test_handle_schema_const() { let generator = TsSchemaGenerator {}; let mut buffer = Buffer::new(); - let token = create_test_struct_token("TestStruct"); + let token = create_test_struct_token("TestStruct", "onchain_dash"); generator.handle_schema_const(&token, &mut buffer); assert_eq!(buffer.len(), 1); assert_eq!( buffer[0], - "export const schema: OnchainDashSchemaType = {\n\tonchain_dash: {\n\t\tTestStruct: \ + "export const schema: SchemaType = {\n\tonchain_dash: {\n\t\tTestStruct: \ {\n\t\t\tfieldOrder: ['field1', 'field2', 'field3'],\n\t\t\tfield1: \ 0,\n\t\t\tfield2: 0,\n\t\t\tfield3: 0,\n\t\t},\n\t},\n};" ); - let token_2 = create_test_struct_token("AvailableTheme"); + let token_2 = create_test_struct_token("AvailableTheme", "onchain_dash"); generator.handle_schema_const(&token_2, &mut buffer); assert_eq!(buffer.len(), 1); assert_eq!( buffer[0], - "export const schema: OnchainDashSchemaType = {\n\tonchain_dash: {\n\t\tTestStruct: \ + "export const schema: SchemaType = {\n\tonchain_dash: {\n\t\tTestStruct: \ {\n\t\t\tfieldOrder: ['field1', 'field2', 'field3'],\n\t\t\tfield1: \ 0,\n\t\t\tfield2: 0,\n\t\t\tfield3: 0,\n\t\t},\n\t\tAvailableTheme: \ {\n\t\t\tfieldOrder: ['field1', 'field2', 'field3'],\n\t\t\tfield1: \ 0,\n\t\t\tfield2: 0,\n\t\t\tfield3: 0,\n\t\t},\n\t},\n};" ); + + let token_3 = create_test_struct_token("Player", "combat"); + generator.handle_schema_const(&token_3, &mut buffer); + assert_eq!(buffer.len(), 1); + assert_eq!( + buffer[0], + "export const schema: SchemaType = {\n\tonchain_dash: {\n\t\tTestStruct: \ + {\n\t\t\tfieldOrder: ['field1', 'field2', 'field3'],\n\t\t\tfield1: \ + 0,\n\t\t\tfield2: 0,\n\t\t\tfield3: 0,\n\t\t},\n\t\tAvailableTheme: \ + {\n\t\t\tfieldOrder: ['field1', 'field2', 'field3'],\n\t\t\tfield1: \ + 0,\n\t\t\tfield2: 0,\n\t\t\tfield3: 0,\n\t\t},\n\t},\n\tcombat: {\n\t\tPlayer: \ + {\n\t\t\tfieldOrder: ['field1', 'field2', 'field3'],\n\t\t\tfield1: \ + 0,\n\t\t\tfield2: 0,\n\t\t\tfield3: 0,\n\t\t},\n\t},\n};" + ); + + let token_4 = create_test_struct_token("Position", "combat"); + generator.handle_schema_const(&token_4, &mut buffer); + assert_eq!(buffer.len(), 1); + assert_eq!( + buffer[0], + "export const schema: SchemaType = {\n\tonchain_dash: {\n\t\tTestStruct: \ + {\n\t\t\tfieldOrder: ['field1', 'field2', 'field3'],\n\t\t\tfield1: \ + 0,\n\t\t\tfield2: 0,\n\t\t\tfield3: 0,\n\t\t},\n\t\tAvailableTheme: \ + {\n\t\t\tfieldOrder: ['field1', 'field2', 'field3'],\n\t\t\tfield1: \ + 0,\n\t\t\tfield2: 0,\n\t\t\tfield3: 0,\n\t\t},\n\t},\n\tcombat: {\n\t\tPlayer: \ + {\n\t\t\tfieldOrder: ['field1', 'field2', 'field3'],\n\t\t\tfield1: \ + 0,\n\t\t\tfield2: 0,\n\t\t\tfield3: 0,\n\t\t},\n\t\tPosition: {\n\t\t\tfieldOrder: \ + ['field1', 'field2', 'field3'],\n\t\t\tfield1: 0,\n\t\t\tfield2: 0,\n\t\t\tfield3: \ + 0,\n\t\t},\n\t},\n};" + ); } #[test] fn test_handle_nested_struct() { let generator = TsSchemaGenerator {}; let mut buffer = Buffer::new(); - let nested_struct = create_test_nested_struct_token("TestNestedStruct"); + let nested_struct = create_test_nested_struct_token("TestNestedStruct", "onchain_dash"); let _res = generator.generate(&nested_struct, &mut buffer); assert_eq!(buffer.len(), 3); } - fn create_test_struct_token(name: &str) -> Composite { + fn create_test_struct_token(name: &str, namespace: &str) -> Composite { Composite { - type_path: format!("onchain_dash::{name}"), + type_path: format!("{namespace}::{name}"), inners: vec![ CompositeInner { index: 0, @@ -236,18 +316,18 @@ mod tests { } } - pub fn create_test_nested_struct_token(name: &str) -> Composite { + pub fn create_test_nested_struct_token(name: &str, namespace: &str) -> Composite { Composite { - type_path: format!("onchain_dash::{name}"), + type_path: format!("{namespace}::{name}"), inners: vec![ CompositeInner { index: 0, name: "field1".to_owned(), kind: CompositeInnerKind::Key, token: Token::Array(cainome::parser::tokens::Array { - type_path: "core::array::Array::".to_owned(), + type_path: format!("core::array::Array::<{namespace}::Direction>"), inner: Box::new(Token::Composite(Composite { - type_path: "onchain_dah::Direction".to_owned(), + type_path: format!("{namespace}::Direction"), inners: vec![ CompositeInner { index: 0, @@ -302,7 +382,7 @@ mod tests { index: 1, name: "field2".to_owned(), kind: CompositeInnerKind::Key, - token: Token::Composite(create_test_struct_token("Position")), + token: Token::Composite(create_test_struct_token("Position", namespace)), }, ], generic_args: vec![], diff --git a/crates/dojo/bindgen/src/plugins/typescript/mod.rs b/crates/dojo/bindgen/src/plugins/typescript/mod.rs index d9d7f57e1d..b8058ee725 100644 --- a/crates/dojo/bindgen/src/plugins/typescript/mod.rs +++ b/crates/dojo/bindgen/src/plugins/typescript/mod.rs @@ -3,9 +3,9 @@ use std::path::PathBuf; use async_trait::async_trait; use generator::r#enum::TsEnumGenerator; -use generator::erc::TsErcGenerator; use generator::function::TsFunctionGenerator; use generator::interface::TsInterfaceGenerator; +use generator::models::TsModelsGenerator; use generator::schema::TsSchemaGenerator; use writer::{TsFileContractWriter, TsFileWriter}; @@ -31,7 +31,7 @@ impl TypescriptPlugin { Box::new(TsInterfaceGenerator {}), Box::new(TsEnumGenerator {}), Box::new(TsSchemaGenerator {}), - Box::new(TsErcGenerator {}), + Box::new(TsModelsGenerator {}), ], )), Box::new(TsFileContractWriter::new( diff --git a/crates/dojo/bindgen/src/plugins/typescript/writer.rs b/crates/dojo/bindgen/src/plugins/typescript/writer.rs index b49bc580a6..06c3e97c0d 100644 --- a/crates/dojo/bindgen/src/plugins/typescript/writer.rs +++ b/crates/dojo/bindgen/src/plugins/typescript/writer.rs @@ -20,11 +20,31 @@ impl TsFileWriter { impl BindgenWriter for TsFileWriter { fn write(&self, path: &str, data: &DojoData) -> BindgenResult<(PathBuf, Vec)> { let models_path = Path::new(path).to_owned(); - let mut models = data.models.values().collect::>(); + let models = data.models.values().collect::>(); + let events = data.events.values().collect::>(); + let mut e_composites = events + .iter() + .flat_map(|e| { + let mut composites = Vec::new(); + let mut enum_composites = + e.tokens.enums.iter().map(|e| e.to_composite().unwrap()).collect::>(); + let mut struct_composites = + e.tokens.structs.iter().map(|s| s.to_composite().unwrap()).collect::>(); + let mut func_composites = e + .tokens + .functions + .iter() + .map(|f| f.to_composite().unwrap()) + .collect::>(); + composites.append(&mut enum_composites); + composites.append(&mut struct_composites); + composites.append(&mut func_composites); + composites + }) + .filter(|c| !(c.type_path.starts_with("dojo::") || c.type_path.starts_with("core::"))) + .collect::>(); - // Sort models based on their tag to ensure deterministic output. - models.sort_by(|a, b| a.tag.cmp(&b.tag)); - let composites = models + let mut m_composites = models .iter() .flat_map(|m| { let mut composites: Vec<&Composite> = Vec::new(); @@ -46,11 +66,16 @@ impl BindgenWriter for TsFileWriter { .filter(|c| !(c.type_path.starts_with("dojo::") || c.type_path.starts_with("core::"))) .collect::>(); + // Sort models based on their tag to ensure deterministic output. + // models.sort_by(|a, b| a.tag.cmp(&b.tag)); + m_composites.sort_by(|a, b| a.type_path.cmp(&b.type_path)); + e_composites.sort_by(|a, b| a.type_path.cmp(&b.type_path)); + let code = self .generators .iter() .fold(Buffer::new(), |mut acc, g| { - composites.iter().for_each(|c| { + [m_composites.clone(), e_composites.clone()].concat().iter().for_each(|c| { match g.generate(c, &mut acc) { Ok(code) => { if !code.is_empty() { @@ -88,46 +113,58 @@ impl TsFileContractWriter { impl BindgenWriter for TsFileContractWriter { fn write(&self, path: &str, data: &DojoData) -> BindgenResult<(PathBuf, Vec)> { let models_path = Path::new(path).to_owned(); + let mut functions = data + .contracts + .values() + .collect::>() + .into_iter() + .flat_map(|c| { + c.systems + .clone() + .into_iter() + .filter(|s| { + let name = s.to_function().unwrap().name.as_str(); + ![ + "contract_name", + "namespace", + "tag", + "name_hash", + "selector", + "dojo_init", + "namespace_hash", + "world", + "dojo_name", + "upgrade", + "world_dispatcher", + ] + .contains(&name) + }) + .map(|s| match s.to_function() { + Ok(f) => (c, f.clone()), + Err(_) => { + panic!("Failed to get function out of system {:?}", &s) + } + }) + .collect::>() + }) + .collect::>(); + functions.sort_by(|(_, af), (_, bf)| af.name.cmp(&bf.name)); let code = self .generators .iter() .fold(Buffer::new(), |mut acc, g| { - data.contracts.iter().for_each(|(_, c)| { - c.systems - .iter() - .filter(|s| { - let name = s.to_function().unwrap().name.as_str(); - ![ - "contract_name", - "namespace", - "tag", - "name_hash", - "selector", - "dojo_init", - "namespace_hash", - "world", - "dojo_name", - "upgrade", - "world_dispatcher", - ] - .contains(&name) - }) - .for_each(|s| match s.to_function() { - Ok(f) => match g.generate(c, f, &mut acc) { - Ok(code) => { - if !code.is_empty() { - acc.push(code) - } - } - Err(_) => { - log::error!("Failed to generate code for system {:?}", s); - } - }, - Err(_) => { - log::error!("Failed to get function out of system {:?}", s); + functions.iter().for_each(|(c, f)| { + match g.generate(c, f, &mut acc) { + Ok(code) => { + if !code.is_empty() { + acc.push(code) } - }) + } + Err(_) => { + log::error!("Failed to generate code for function {:?}", f.name); + } + }; }); acc diff --git a/crates/dojo/bindgen/src/plugins/unity/mod.rs b/crates/dojo/bindgen/src/plugins/unity/mod.rs index 22477e2c2d..3e29f46038 100644 --- a/crates/dojo/bindgen/src/plugins/unity/mod.rs +++ b/crates/dojo/bindgen/src/plugins/unity/mod.rs @@ -344,19 +344,7 @@ public class {}_{} : ModelInstance {{ true, enum_variant, )], - CompositeType::Unknown if t.type_name() == "U256" => vec![ - ( - format!("new FieldElement({}.high).Inner", arg_name), - false, - enum_variant.clone(), - ), - ( - format!("new FieldElement({}.low).Inner", arg_name), - false, - enum_variant, - ), - ], - CompositeType::Struct => { + CompositeType::Unknown | CompositeType::Struct => { let mut tokens = vec![]; t.inners.iter().for_each(|f| { tokens.extend(handle_arg_recursive( @@ -408,7 +396,6 @@ public class {}_{} : ModelInstance {{ tokens } - CompositeType::Unknown => panic!("Unknown composite type: {:?}", t), } } Token::Array(array) => { diff --git a/crates/dojo/core-cairo-test/Scarb.lock b/crates/dojo/core-cairo-test/Scarb.lock index 08c860821b..3a5364d80a 100644 --- a/crates/dojo/core-cairo-test/Scarb.lock +++ b/crates/dojo/core-cairo-test/Scarb.lock @@ -3,7 +3,11 @@ version = 1 [[package]] name = "dojo" +<<<<<<< HEAD version = "1.0.0" +======= +version = "1.0.10" +>>>>>>> dojo/main dependencies = [ "dojo_plugin", ] diff --git a/crates/dojo/core-cairo-test/src/tests/world/metadata.cairo b/crates/dojo/core-cairo-test/src/tests/world/metadata.cairo index cce794c8b1..0471b82d62 100644 --- a/crates/dojo/core-cairo-test/src/tests/world/metadata.cairo +++ b/crates/dojo/core-cairo-test/src/tests/world/metadata.cairo @@ -9,7 +9,7 @@ fn test_set_metadata_world() { let world = world.dispatcher; let metadata = ResourceMetadata { - resource_id: 0, metadata_uri: format!("ipfs:world_with_a_long_uri_that") + resource_id: 0, metadata_uri: format!("ipfs:world_with_a_long_uri_that"), metadata_hash: 42 }; world.set_metadata(metadata.clone()); @@ -30,7 +30,7 @@ fn test_set_metadata_resource_owner() { starknet::testing::set_contract_address(bob); let metadata = ResourceMetadata { - resource_id: model_selector, metadata_uri: format!("ipfs:bob") + resource_id: model_selector, metadata_uri: format!("ipfs:bob"), metadata_hash: 42 }; drop_all_events(world.contract_address); @@ -46,6 +46,7 @@ fn test_set_metadata_resource_owner() { if let world::Event::MetadataUpdate(event) = event.unwrap() { assert(event.resource == metadata.resource_id, 'bad resource'); assert(event.uri == metadata.metadata_uri, 'bad uri'); + assert(event.hash == metadata.metadata_hash, 'bad hash'); } else { core::panic_with_felt252('no EventUpgraded event'); } @@ -70,7 +71,7 @@ fn test_set_metadata_not_possible_for_resource_writer() { starknet::testing::set_contract_address(bob); let metadata = ResourceMetadata { - resource_id: model_selector, metadata_uri: format!("ipfs:bob") + resource_id: model_selector, metadata_uri: format!("ipfs:bob"), metadata_hash: 42 }; world.set_metadata(metadata.clone()); @@ -85,7 +86,7 @@ fn test_set_metadata_not_possible_for_random_account() { let world = world.dispatcher; let metadata = ResourceMetadata { // World metadata. - resource_id: 0, metadata_uri: format!("ipfs:bob"), + resource_id: 0, metadata_uri: format!("ipfs:bob"), metadata_hash: 42 }; let bob = starknet::contract_address_const::<0xb0b>(); @@ -112,7 +113,7 @@ fn test_set_metadata_through_malicious_contract() { starknet::testing::set_contract_address(malicious_contract); let metadata = ResourceMetadata { - resource_id: model_selector, metadata_uri: format!("ipfs:bob") + resource_id: model_selector, metadata_uri: format!("ipfs:bob"), metadata_hash: 42 }; world.set_metadata(metadata.clone()); diff --git a/crates/dojo/core/Scarb.lock b/crates/dojo/core/Scarb.lock index 5a313fab2b..3e1c687548 100644 --- a/crates/dojo/core/Scarb.lock +++ b/crates/dojo/core/Scarb.lock @@ -3,7 +3,7 @@ version = 1 [[package]] name = "dojo" -version = "1.0.1" +version = "1.0.11" dependencies = [ "dojo_plugin", ] diff --git a/crates/dojo/core/Scarb.toml b/crates/dojo/core/Scarb.toml index 52d1390721..7bb45e92d6 100644 --- a/crates/dojo/core/Scarb.toml +++ b/crates/dojo/core/Scarb.toml @@ -3,7 +3,7 @@ cairo-version = "=2.8.4" edition = "2024_07" description = "The Dojo Core library for autonomous worlds." name = "dojo" -version = "1.0.1" +version = "1.0.11" [dependencies] starknet = "=2.8.4" diff --git a/crates/dojo/core/src/model/metadata.cairo b/crates/dojo/core/src/model/metadata.cairo index 512d4c14c0..acef47be19 100644 --- a/crates/dojo/core/src/model/metadata.cairo +++ b/crates/dojo/core/src/model/metadata.cairo @@ -9,6 +9,7 @@ pub struct ResourceMetadata { #[key] pub resource_id: felt252, pub metadata_uri: ByteArray, + pub metadata_hash: felt252 } pub fn default_address() -> starknet::ContractAddress { diff --git a/crates/dojo/core/src/world/world_contract.cairo b/crates/dojo/core/src/world/world_contract.cairo index 30c513bcd6..ea1a10e1ea 100644 --- a/crates/dojo/core/src/world/world_contract.cairo +++ b/crates/dojo/core/src/world/world_contract.cairo @@ -111,7 +111,8 @@ pub mod world { pub struct MetadataUpdate { #[key] pub resource: felt252, - pub uri: ByteArray + pub uri: ByteArray, + pub hash: felt252 } #[derive(Drop, starknet::Event)] @@ -356,7 +357,11 @@ pub mod world { self .emit( - MetadataUpdate { resource: metadata.resource_id, uri: metadata.metadata_uri } + MetadataUpdate { + resource: metadata.resource_id, + uri: metadata.metadata_uri, + hash: metadata.metadata_hash + } ); } diff --git a/crates/dojo/test-utils/Cargo.toml b/crates/dojo/test-utils/Cargo.toml index fbff82a6bf..f2e47f07e5 100644 --- a/crates/dojo/test-utils/Cargo.toml +++ b/crates/dojo/test-utils/Cargo.toml @@ -10,11 +10,11 @@ anyhow.workspace = true assert_fs.workspace = true async-trait.workspace = true camino.workspace = true -jsonrpsee = { workspace = true, features = [ "server" ] } katana-core = { workspace = true } katana-executor = { workspace = true, features = [ "blockifier" ] } katana-node.workspace = true katana-primitives = { workspace = true } +katana-rpc.workspace = true scarb.workspace = true scarb-ui.workspace = true serde.workspace = true diff --git a/crates/dojo/test-utils/src/sequencer.rs b/crates/dojo/test-utils/src/sequencer.rs index 6d876b3f44..ef1b023eec 100644 --- a/crates/dojo/test-utils/src/sequencer.rs +++ b/crates/dojo/test-utils/src/sequencer.rs @@ -1,16 +1,16 @@ -use std::collections::HashSet; use std::sync::Arc; -use jsonrpsee::core::Error; use katana_core::backend::Backend; use katana_core::constants::DEFAULT_SEQUENCER_ADDRESS; use katana_executor::implementation::blockifier::BlockifierFactory; use katana_node::config::dev::DevConfig; -use katana_node::config::rpc::{ApiKind, RpcConfig, DEFAULT_RPC_ADDR, DEFAULT_RPC_MAX_CONNECTIONS}; +use katana_node::config::rpc::{RpcConfig, DEFAULT_RPC_ADDR, DEFAULT_RPC_MAX_CONNECTIONS}; pub use katana_node::config::*; use katana_node::LaunchedNode; use katana_primitives::chain::ChainId; use katana_primitives::chain_spec::ChainSpec; +use katana_rpc::Error; +use rpc::RpcModulesList; use starknet::accounts::{ExecutionEncoding, SingleOwnerAccount}; use starknet::core::chain_id; use starknet::core::types::{BlockId, BlockTag, Felt}; @@ -42,7 +42,8 @@ impl TestSequencer { .await .expect("Failed to launch node"); - let url = Url::parse(&format!("http://{}", handle.rpc.addr)).expect("Failed to parse URL"); + let url = + Url::parse(&format!("http://{}", handle.rpc.addr())).expect("Failed to parse URL"); let account = handle.node.backend.chain_spec.genesis.accounts().next().unwrap(); let account = TestAccount { @@ -104,7 +105,7 @@ impl TestSequencer { } pub fn stop(self) -> Result<(), Error> { - self.handle.rpc.handle.stop() + self.handle.rpc.stop() } pub fn url(&self) -> Url { @@ -118,13 +119,14 @@ pub fn get_default_test_config(sequencing: SequencingConfig) -> Config { chain.genesis.sequencer_address = *DEFAULT_SEQUENCER_ADDRESS; let rpc = RpcConfig { - cors_origins: None, + cors_origins: Vec::new(), port: 0, addr: DEFAULT_RPC_ADDR, + apis: RpcModulesList::all(), max_connections: DEFAULT_RPC_MAX_CONNECTIONS, - apis: HashSet::from([ApiKind::Starknet, ApiKind::Dev, ApiKind::Saya, ApiKind::Torii]), max_event_page_size: Some(100), + max_proof_keys: Some(100), }; - Config { sequencing, rpc, dev, chain, ..Default::default() } + Config { sequencing, rpc, dev, chain: chain.into(), ..Default::default() } } diff --git a/crates/dojo/types/Cargo.toml b/crates/dojo/types/Cargo.toml index 88d725071c..5db93b4011 100644 --- a/crates/dojo/types/Cargo.toml +++ b/crates/dojo/types/Cargo.toml @@ -20,3 +20,4 @@ starknet-crypto.workspace = true strum.workspace = true strum_macros.workspace = true thiserror.workspace = true +indexmap.workspace = true diff --git a/crates/dojo/types/src/schema.rs b/crates/dojo/types/src/schema.rs index 5ebaea0d42..712f102225 100644 --- a/crates/dojo/types/src/schema.rs +++ b/crates/dojo/types/src/schema.rs @@ -1,9 +1,13 @@ use std::any::type_name; +use std::str::FromStr; use cainome::cairo_serde::{ByteArray, CairoSerde}; +use crypto_bigint::{Encoding, U256}; +use indexmap::IndexMap; use itertools::Itertools; use num_traits::ToPrimitive; use serde::{Deserialize, Serialize}; +use serde_json::{json, Value as JsonValue}; use starknet::core::types::Felt; use strum_macros::AsRefStr; @@ -308,6 +312,196 @@ impl Ty { } } } + + /// Convert a Ty to a JSON Value + pub fn to_json_value(&self) -> Result { + match self { + Ty::Primitive(primitive) => match primitive { + Primitive::Bool(Some(v)) => Ok(json!(*v)), + Primitive::I8(Some(v)) => Ok(json!(*v)), + Primitive::I16(Some(v)) => Ok(json!(*v)), + Primitive::I32(Some(v)) => Ok(json!(*v)), + Primitive::I64(Some(v)) => Ok(json!(v.to_string())), + Primitive::I128(Some(v)) => Ok(json!(v.to_string())), + Primitive::U8(Some(v)) => Ok(json!(*v)), + Primitive::U16(Some(v)) => Ok(json!(*v)), + Primitive::U32(Some(v)) => Ok(json!(*v)), + Primitive::U64(Some(v)) => Ok(json!(v.to_string())), + Primitive::U128(Some(v)) => Ok(json!(v.to_string())), + Primitive::USize(Some(v)) => Ok(json!(*v)), + Primitive::U256(Some(v)) => { + let bytes = v.to_be_bytes(); + let high = u128::from_be_bytes(bytes[..16].try_into().unwrap()); + let low = u128::from_be_bytes(bytes[16..].try_into().unwrap()); + Ok(json!({ + "high": high.to_string(), + "low": low.to_string() + })) + } + Primitive::Felt252(Some(v)) => Ok(json!(format!("{:#x}", v))), + Primitive::ClassHash(Some(v)) => Ok(json!(format!("{:#x}", v))), + Primitive::ContractAddress(Some(v)) => Ok(json!(format!("{:#x}", v))), + _ => Err(PrimitiveError::MissingFieldElement), + }, + Ty::Struct(s) => { + let mut obj = IndexMap::new(); + for member in &s.children { + obj.insert(member.name.clone(), member.ty.to_json_value()?); + } + Ok(json!(obj)) + } + Ty::Enum(e) => { + let option = e.option().map_err(|_| PrimitiveError::MissingFieldElement)?; + Ok(json!({ + option.name.clone(): option.ty.to_json_value()? + })) + } + Ty::Array(items) => { + let values: Result, _> = items.iter().map(|ty| ty.to_json_value()).collect(); + Ok(json!(values?)) + } + Ty::Tuple(items) => { + let values: Result, _> = items.iter().map(|ty| ty.to_json_value()).collect(); + Ok(json!(values?)) + } + Ty::ByteArray(bytes) => Ok(json!(bytes.clone())), + } + } + + /// Parse a JSON Value into a Ty + pub fn from_json_value(&mut self, value: JsonValue) -> Result<(), PrimitiveError> { + match (self, value) { + (Ty::Primitive(primitive), value) => match primitive { + Primitive::Bool(v) => { + if let JsonValue::Bool(b) = value { + *v = Some(b); + } + } + Primitive::I8(v) => { + if let JsonValue::Number(n) = value { + *v = n.as_i64().map(|n| n as i8); + } + } + Primitive::I16(v) => { + if let JsonValue::Number(n) = value { + *v = n.as_i64().map(|n| n as i16); + } + } + Primitive::I32(v) => { + if let JsonValue::Number(n) = value { + *v = n.as_i64().map(|n| n as i32); + } + } + Primitive::I64(v) => { + if let JsonValue::String(s) = value { + *v = s.parse().ok(); + } + } + Primitive::I128(v) => { + if let JsonValue::String(s) = value { + *v = s.parse().ok(); + } + } + Primitive::U8(v) => { + if let JsonValue::Number(n) = value { + *v = n.as_u64().map(|n| n as u8); + } + } + Primitive::U16(v) => { + if let JsonValue::Number(n) = value { + *v = n.as_u64().map(|n| n as u16); + } + } + Primitive::U32(v) => { + if let JsonValue::Number(n) = value { + *v = n.as_u64().map(|n| n as u32); + } + } + Primitive::U64(v) => { + if let JsonValue::String(s) = value { + *v = s.parse().ok(); + } + } + Primitive::U128(v) => { + if let JsonValue::String(s) = value { + *v = s.parse().ok(); + } + } + Primitive::USize(v) => { + if let JsonValue::Number(n) = value { + *v = n.as_u64().map(|n| n as u32); + } + } + Primitive::U256(v) => { + if let JsonValue::Object(obj) = value { + if let (Some(JsonValue::String(high)), Some(JsonValue::String(low))) = + (obj.get("high"), obj.get("low")) + { + if let (Ok(high), Ok(low)) = (high.parse::(), low.parse::()) + { + let mut bytes = [0u8; 32]; + bytes[..16].copy_from_slice(&high.to_be_bytes()); + bytes[16..].copy_from_slice(&low.to_be_bytes()); + *v = Some(U256::from_be_slice(&bytes)); + } + } + } + } + Primitive::Felt252(v) => { + if let JsonValue::String(s) = value { + *v = Felt::from_str(&s).ok(); + } + } + Primitive::ClassHash(v) => { + if let JsonValue::String(s) = value { + *v = Felt::from_str(&s).ok(); + } + } + Primitive::ContractAddress(v) => { + if let JsonValue::String(s) = value { + *v = Felt::from_str(&s).ok(); + } + } + }, + (Ty::Struct(s), JsonValue::Object(obj)) => { + for member in &mut s.children { + if let Some(value) = obj.get(&member.name) { + member.ty.from_json_value(value.clone())?; + } + } + } + (Ty::Enum(e), JsonValue::Object(obj)) => { + if let Some((name, value)) = obj.into_iter().next() { + e.set_option(&name).map_err(|_| PrimitiveError::TypeMismatch)?; + if let Some(option) = e.option { + e.options[option as usize].ty.from_json_value(value)?; + } + } + } + (Ty::Array(items), JsonValue::Array(values)) => { + let template = items[0].clone(); + items.clear(); + for value in values { + let mut item = template.clone(); + item.from_json_value(value)?; + items.push(item); + } + } + (Ty::Tuple(items), JsonValue::Array(values)) => { + if items.len() != values.len() { + return Err(PrimitiveError::TypeMismatch); + } + for (item, value) in items.iter_mut().zip(values) { + item.from_json_value(value)?; + } + } + (Ty::ByteArray(bytes), JsonValue::String(s)) => { + *bytes = s; + } + _ => return Err(PrimitiveError::TypeMismatch), + } + Ok(()) + } } #[derive(Debug)] diff --git a/crates/dojo/utils/src/env.rs b/crates/dojo/utils/src/env.rs index 3229129dfb..ec4ed6e5b8 100644 --- a/crates/dojo/utils/src/env.rs +++ b/crates/dojo/utils/src/env.rs @@ -4,3 +4,6 @@ pub const DOJO_KEYSTORE_PATH_ENV_VAR: &str = "DOJO_KEYSTORE_PATH"; pub const DOJO_KEYSTORE_PASSWORD_ENV_VAR: &str = "DOJO_KEYSTORE_PASSWORD"; pub const DOJO_ACCOUNT_ADDRESS_ENV_VAR: &str = "DOJO_ACCOUNT_ADDRESS"; pub const DOJO_WORLD_ADDRESS_ENV_VAR: &str = "DOJO_WORLD_ADDRESS"; +pub const IPFS_URL_ENV_VAR: &str = "DOJO_IPFS_URL"; +pub const IPFS_USERNAME_ENV_VAR: &str = "DOJO_IPFS_USERNAME"; +pub const IPFS_PASSWORD_ENV_VAR: &str = "DOJO_IPFS_PASSWORD"; diff --git a/crates/dojo/utils/src/lib.rs b/crates/dojo/utils/src/lib.rs index 96524d1805..71435ba34c 100644 --- a/crates/dojo/utils/src/lib.rs +++ b/crates/dojo/utils/src/lib.rs @@ -12,5 +12,5 @@ pub use tx::*; pub mod env; pub mod keystore; - +pub mod provider; pub mod signal; diff --git a/crates/dojo/utils/src/provider.rs b/crates/dojo/utils/src/provider.rs new file mode 100644 index 0000000000..9e7eadfd6a --- /dev/null +++ b/crates/dojo/utils/src/provider.rs @@ -0,0 +1,26 @@ +use starknet::core::types::{BlockId, BlockTag}; +use starknet::providers::Provider; +use tracing::trace; + +/// Check if the provider is healthy. +/// +/// This function will check if the provider is healthy by getting the latest block, +/// and returns an error otherwise. +pub async fn health_check_provider( + provider: P, +) -> anyhow::Result<(), anyhow::Error> { + match provider.get_block_with_tx_hashes(BlockId::Tag(BlockTag::Latest)).await { + Ok(block) => { + trace!( + latest_block = ?block, + "Provider health check." + ); + Ok(()) + } + Err(_) => { + let error_info = + format!("Unhealthy provider {:?}, please check your configuration.", provider); + Err(anyhow::anyhow!(error_info)) + } + } +} diff --git a/crates/dojo/utils/src/tx/declarer.rs b/crates/dojo/utils/src/tx/declarer.rs index e1b5b0f48c..62519e6f41 100644 --- a/crates/dojo/utils/src/tx/declarer.rs +++ b/crates/dojo/utils/src/tx/declarer.rs @@ -14,6 +14,7 @@ use starknet::core::types::{ BlockId, BlockTag, DeclareTransactionResult, Felt, FlattenedSierraClass, StarknetError, }; use starknet::providers::{Provider, ProviderError}; +use tracing::trace; use crate::{ FeeConfig, TransactionError, TransactionExt, TransactionResult, TransactionWaiter, TxnConfig, @@ -92,7 +93,7 @@ where match account.provider().get_class(BlockId::Tag(BlockTag::Pending), class_hash).await { Err(ProviderError::StarknetError(StarknetError::ClassHashNotFound)) => {} Ok(_) => { - tracing::trace!( + trace!( label = labeled_class.label, class_hash = format!("{:#066x}", class_hash), "Class already declared." @@ -104,7 +105,7 @@ where let casm_class_hash = labeled_class.casm_class_hash; - tracing::trace!( + trace!( label = labeled_class.label, class_hash = format!("{:#066x}", class_hash), casm_class_hash = format!("{:#066x}", casm_class_hash), @@ -127,7 +128,7 @@ where } }; - tracing::trace!( + trace!( label = labeled_class.label, transaction_hash = format!("{:#066x}", transaction_hash), class_hash = format!("{:#066x}", class_hash), diff --git a/crates/dojo/utils/src/tx/deployer.rs b/crates/dojo/utils/src/tx/deployer.rs index f449a17fc8..da15d1a5c9 100644 --- a/crates/dojo/utils/src/tx/deployer.rs +++ b/crates/dojo/utils/src/tx/deployer.rs @@ -98,7 +98,7 @@ where match provider.get_class_hash_at(BlockId::Tag(BlockTag::Pending), contract_address).await { Err(ProviderError::StarknetError(StarknetError::ContractNotFound)) => Ok(false), Ok(_) => { - tracing::trace!( + trace!( contract_address = format!("{:#066x}", contract_address), "Contract already deployed." ); diff --git a/crates/dojo/utils/src/tx/waiter.rs b/crates/dojo/utils/src/tx/waiter.rs index c98f168e38..b8016317db 100644 --- a/crates/dojo/utils/src/tx/waiter.rs +++ b/crates/dojo/utils/src/tx/waiter.rs @@ -42,7 +42,7 @@ pub enum TransactionWaitingError { /// # Examples /// /// ```ignore -/// ues url::Url; +/// use url::Url; /// use starknet::providers::jsonrpc::HttpTransport; /// use starknet::providers::JsonRpcClient; /// use starknet::core::types::TransactionFinalityStatus; diff --git a/crates/dojo/world/Cargo.toml b/crates/dojo/world/Cargo.toml index c3cd9a6d44..00154e7671 100644 --- a/crates/dojo/world/Cargo.toml +++ b/crates/dojo/world/Cargo.toml @@ -30,6 +30,7 @@ num-bigint.workspace = true [dev-dependencies] tokio.workspace = true +futures.workspace = true [features] -metadata = [ "dep:ipfs-api-backend-hyper" ] +ipfs = [ "dep:ipfs-api-backend-hyper" ] diff --git a/crates/dojo/world/src/config/environment.rs b/crates/dojo/world/src/config/environment.rs index 9387bf2799..bf18602fe1 100644 --- a/crates/dojo/world/src/config/environment.rs +++ b/crates/dojo/world/src/config/environment.rs @@ -1,5 +1,7 @@ use serde::Deserialize; +use super::IpfsConfig; + #[derive(Default, Deserialize, Clone, Debug)] pub struct Environment { pub rpc_url: Option, @@ -10,6 +12,7 @@ pub struct Environment { pub world_address: Option, pub world_block: Option, pub http_headers: Option>, + pub ipfs_config: Option, } #[derive(Debug, Clone, Deserialize)] diff --git a/crates/dojo/world/src/config/ipfs_config.rs b/crates/dojo/world/src/config/ipfs_config.rs new file mode 100644 index 0000000000..ae55ac2c2a --- /dev/null +++ b/crates/dojo/world/src/config/ipfs_config.rs @@ -0,0 +1,22 @@ +use anyhow::Result; +use serde::Deserialize; + +#[derive(Default, Deserialize, Clone, Debug)] +pub struct IpfsConfig { + pub url: String, + pub username: String, + pub password: String, +} + +impl IpfsConfig { + pub fn assert_valid(&self) -> Result<()> { + if self.url.is_empty() || self.username.is_empty() || self.password.is_empty() { + anyhow::bail!("Invalid IPFS credentials: empty values not allowed"); + } + if !self.url.starts_with("http://") && !self.url.starts_with("https://") { + anyhow::bail!("Invalid IPFS URL: must start with http:// or https://"); + } + + Ok(()) + } +} diff --git a/crates/dojo/world/src/config/metadata_config.rs b/crates/dojo/world/src/config/metadata_config.rs index f1409df0ab..43ed71a973 100644 --- a/crates/dojo/world/src/config/metadata_config.rs +++ b/crates/dojo/world/src/config/metadata_config.rs @@ -1,11 +1,13 @@ //! Metadata configuration for the world. use std::collections::HashMap; +use std::hash::{Hash, Hasher}; use serde::{Deserialize, Serialize}; +use serde_json::json; use url::Url; -use crate::config::WorldConfig; +use crate::config::{ResourceConfig, WorldConfig}; use crate::uri::Uri; /// World metadata that describes the world. @@ -33,3 +35,62 @@ impl From for WorldMetadata { } } } + +impl Hash for WorldMetadata { + fn hash(&self, state: &mut H) { + self.name.hash(state); + self.seed.hash(state); + self.description.hash(state); + self.cover_uri.hash(state); + self.icon_uri.hash(state); + self.website.hash(state); + + json!(self.socials).to_string().hash(state); + + // include icon and cover data into the hash to + // detect data changes even if the filename is the same. + if let Some(Uri::File(icon)) = &self.icon_uri { + let icon_data = std::fs::read(icon).expect("read icon failed"); + icon_data.hash(state); + }; + + if let Some(Uri::File(cover)) = &self.cover_uri { + let cover_data = std::fs::read(cover).expect("read cover failed"); + cover_data.hash(state); + }; + } +} + +/// resource metadata that describes world resources such as contracts, +/// models or events. +#[derive(Default, Serialize, Deserialize, Debug, Clone)] +pub struct ResourceMetadata { + pub name: String, + pub description: Option, + pub icon_uri: Option, +} + +impl From for ResourceMetadata { + fn from(config: ResourceConfig) -> Self { + ResourceMetadata { + name: dojo_types::naming::get_name_from_tag(&config.tag), + description: config.description, + icon_uri: config.icon_uri, + } + } +} + +impl Hash for ResourceMetadata { + fn hash(&self, state: &mut H) { + self.name.hash(state); + self.description.hash(state); + self.icon_uri.hash(state); + + // include icon and cover data into the hash to + // detect data changes even if the filename is the same. + if let Some(Uri::File(icon)) = &self.icon_uri { + let icon_data = std::fs::read(icon).expect("read icon failed"); + icon_data.hash(state); + }; + } +} diff --git a/crates/dojo/world/src/config/mod.rs b/crates/dojo/world/src/config/mod.rs index 35e0323de9..1bc23a8b66 100644 --- a/crates/dojo/world/src/config/mod.rs +++ b/crates/dojo/world/src/config/mod.rs @@ -1,13 +1,17 @@ pub mod calldata_decoder; pub mod environment; +pub mod ipfs_config; pub mod metadata_config; pub mod migration_config; pub mod namespace_config; pub mod profile_config; +pub mod resource_config; pub mod world_config; pub use environment::Environment; +pub use ipfs_config::IpfsConfig; pub use metadata_config::WorldMetadata; pub use namespace_config::NamespaceConfig; pub use profile_config::ProfileConfig; +pub use resource_config::ResourceConfig; pub use world_config::WorldConfig; diff --git a/crates/dojo/world/src/config/profile_config.rs b/crates/dojo/world/src/config/profile_config.rs index 4297a16d57..7bdf22f3ce 100644 --- a/crates/dojo/world/src/config/profile_config.rs +++ b/crates/dojo/world/src/config/profile_config.rs @@ -9,6 +9,7 @@ use toml; use super::environment::Environment; use super::migration_config::MigrationConfig; use super::namespace_config::NamespaceConfig; +use super::resource_config::ResourceConfig; use super::world_config::WorldConfig; /// Profile configuration that is used to configure the world and its environment. @@ -17,6 +18,9 @@ use super::world_config::WorldConfig; #[derive(Debug, Clone, Default, Deserialize)] pub struct ProfileConfig { pub world: WorldConfig, + pub models: Option>, + pub contracts: Option>, + pub events: Option>, pub namespace: NamespaceConfig, pub env: Option, pub migration: Option, @@ -134,6 +138,21 @@ mod tests { website = "https://example.com" socials = { "twitter" = "test", "discord" = "test" } + [[models]] + tag = "ns1-m1" + description = "This is the m1 model" + icon_uri = "ipfs://dojo/m1.png" + + [[contracts]] + tag = "ns1-c1" + description = "This is the c1 contract" + icon_uri = "ipfs://dojo/c1.png" + + [[events]] + tag = "ns1-e1" + description = "This is the e1 event" + icon_uri = "ipfs://dojo/e1.png" + [namespace] default = "test" mappings = { "test" = ["test2"] } @@ -146,6 +165,11 @@ mod tests { keystore_password = "test" world_address = "test" + [env.ipfs_config] + url = "https://ipfs.service" + username = "johndoe" + password = "123456" + [migration] skip_contracts = [ "module::my-contract" ] @@ -172,6 +196,11 @@ mod tests { assert_eq!(env.keystore_password, Some("test".to_string())); assert_eq!(env.world_address, Some("test".to_string())); + let ipfs_config = env.ipfs_config.unwrap(); + assert_eq!(ipfs_config.url, "https://ipfs.service".to_string()); + assert_eq!(ipfs_config.username, "johndoe".to_string()); + assert_eq!(ipfs_config.password, "123456".to_string()); + assert_eq!(config.world.description, Some("test".to_string())); assert_eq!( config.world.cover_uri, @@ -190,6 +219,27 @@ mod tests { ])) ); + assert!(config.models.is_some()); + let models = config.models.unwrap(); + assert_eq!(models.len(), 1); + assert_eq!(models[0].tag, "ns1-m1"); + assert_eq!(models[0].description, Some("This is the m1 model".to_string())); + assert_eq!(models[0].icon_uri, Some(Uri::from_string("ipfs://dojo/m1.png").unwrap())); + + assert!(config.contracts.is_some()); + let contracts = config.contracts.unwrap(); + assert_eq!(contracts.len(), 1); + assert_eq!(contracts[0].tag, "ns1-c1"); + assert_eq!(contracts[0].description, Some("This is the c1 contract".to_string())); + assert_eq!(contracts[0].icon_uri, Some(Uri::from_string("ipfs://dojo/c1.png").unwrap())); + + assert!(config.events.is_some()); + let events = config.events.unwrap(); + assert_eq!(events.len(), 1); + assert_eq!(events[0].tag, "ns1-e1"); + assert_eq!(events[0].description, Some("This is the e1 event".to_string())); + assert_eq!(events[0].icon_uri, Some(Uri::from_string("ipfs://dojo/e1.png").unwrap())); + assert_eq!(config.namespace.default, "test".to_string()); assert_eq!( config.namespace.mappings, diff --git a/crates/dojo/world/src/config/resource_config.rs b/crates/dojo/world/src/config/resource_config.rs new file mode 100644 index 0000000000..ba0e12dcb1 --- /dev/null +++ b/crates/dojo/world/src/config/resource_config.rs @@ -0,0 +1,10 @@ +use serde::Deserialize; + +use crate::uri::Uri; + +#[derive(Debug, Clone, Deserialize)] +pub struct ResourceConfig { + pub tag: String, + pub description: Option, + pub icon_uri: Option, +} diff --git a/crates/dojo/world/src/constants.rs b/crates/dojo/world/src/constants.rs new file mode 100644 index 0000000000..9aa55c2b1f --- /dev/null +++ b/crates/dojo/world/src/constants.rs @@ -0,0 +1,4 @@ +use starknet_crypto::Felt; + +// the world selector +pub const WORLD: Felt = Felt::ZERO; diff --git a/crates/dojo/world/src/contracts/abigen/world.rs b/crates/dojo/world/src/contracts/abigen/world.rs index bd07917a9f..eec0cfc5bf 100644 --- a/crates/dojo/world/src/contracts/abigen/world.rs +++ b/crates/dojo/world/src/contracts/abigen/world.rs @@ -393,6 +393,7 @@ impl cainome::cairo_serde::CairoSerde for FieldLayout { pub struct MetadataUpdate { pub resource: starknet::core::types::Felt, pub uri: cainome::cairo_serde::ByteArray, + pub hash: starknet::core::types::Felt, } impl cainome::cairo_serde::CairoSerde for MetadataUpdate { type RustType = Self; @@ -402,12 +403,14 @@ impl cainome::cairo_serde::CairoSerde for MetadataUpdate { let mut __size = 0; __size += starknet::core::types::Felt::cairo_serialized_size(&__rust.resource); __size += cainome::cairo_serde::ByteArray::cairo_serialized_size(&__rust.uri); + __size += starknet::core::types::Felt::cairo_serialized_size(&__rust.hash); __size } fn cairo_serialize(__rust: &Self::RustType) -> Vec { let mut __out: Vec = vec![]; __out.extend(starknet::core::types::Felt::cairo_serialize(&__rust.resource)); __out.extend(cainome::cairo_serde::ByteArray::cairo_serialize(&__rust.uri)); + __out.extend(starknet::core::types::Felt::cairo_serialize(&__rust.hash)); __out } fn cairo_deserialize( @@ -419,7 +422,9 @@ impl cainome::cairo_serde::CairoSerde for MetadataUpdate { __offset += starknet::core::types::Felt::cairo_serialized_size(&resource); let uri = cainome::cairo_serde::ByteArray::cairo_deserialize(__felts, __offset)?; __offset += cainome::cairo_serde::ByteArray::cairo_serialized_size(&uri); - Ok(MetadataUpdate { resource, uri }) + let hash = starknet::core::types::Felt::cairo_deserialize(__felts, __offset)?; + __offset += starknet::core::types::Felt::cairo_serialized_size(&hash); + Ok(MetadataUpdate { resource, uri, hash }) } } impl MetadataUpdate { @@ -625,6 +630,7 @@ impl OwnerUpdated { pub struct ResourceMetadata { pub resource_id: starknet::core::types::Felt, pub metadata_uri: cainome::cairo_serde::ByteArray, + pub metadata_hash: starknet::core::types::Felt, } impl cainome::cairo_serde::CairoSerde for ResourceMetadata { type RustType = Self; @@ -634,12 +640,14 @@ impl cainome::cairo_serde::CairoSerde for ResourceMetadata { let mut __size = 0; __size += starknet::core::types::Felt::cairo_serialized_size(&__rust.resource_id); __size += cainome::cairo_serde::ByteArray::cairo_serialized_size(&__rust.metadata_uri); + __size += starknet::core::types::Felt::cairo_serialized_size(&__rust.metadata_hash); __size } fn cairo_serialize(__rust: &Self::RustType) -> Vec { let mut __out: Vec = vec![]; __out.extend(starknet::core::types::Felt::cairo_serialize(&__rust.resource_id)); __out.extend(cainome::cairo_serde::ByteArray::cairo_serialize(&__rust.metadata_uri)); + __out.extend(starknet::core::types::Felt::cairo_serialize(&__rust.metadata_hash)); __out } fn cairo_deserialize( @@ -651,7 +659,9 @@ impl cainome::cairo_serde::CairoSerde for ResourceMetadata { __offset += starknet::core::types::Felt::cairo_serialized_size(&resource_id); let metadata_uri = cainome::cairo_serde::ByteArray::cairo_deserialize(__felts, __offset)?; __offset += cainome::cairo_serde::ByteArray::cairo_serialized_size(&metadata_uri); - Ok(ResourceMetadata { resource_id, metadata_uri }) + let metadata_hash = starknet::core::types::Felt::cairo_deserialize(__felts, __offset)?; + __offset += starknet::core::types::Felt::cairo_serialized_size(&metadata_hash); + Ok(ResourceMetadata { resource_id, metadata_uri, metadata_hash }) } } #[derive(Clone, serde::Serialize, serde::Deserialize, PartialEq, Debug)] @@ -1791,7 +1801,18 @@ impl TryFrom<&starknet::core::types::EmittedEvent> for Event { } }; data_offset += cainome::cairo_serde::ByteArray::cairo_serialized_size(&uri); - return Ok(Event::MetadataUpdate(MetadataUpdate { resource, uri })); + let hash = + match starknet::core::types::Felt::cairo_deserialize(&event.data, data_offset) { + Ok(v) => v, + Err(e) => { + return Err(format!( + "Could not deserialize field {} for {}: {:?}", + "hash", "MetadataUpdate", e + )); + } + }; + data_offset += starknet::core::types::Felt::cairo_serialized_size(&hash); + return Ok(Event::MetadataUpdate(MetadataUpdate { resource, uri, hash })); } let selector = event.keys[0]; if selector @@ -2660,7 +2681,18 @@ impl TryFrom<&starknet::core::types::Event> for Event { } }; data_offset += cainome::cairo_serde::ByteArray::cairo_serialized_size(&uri); - return Ok(Event::MetadataUpdate(MetadataUpdate { resource, uri })); + let hash = + match starknet::core::types::Felt::cairo_deserialize(&event.data, data_offset) { + Ok(v) => v, + Err(e) => { + return Err(format!( + "Could not deserialize field {} for {}: {:?}", + "hash", "MetadataUpdate", e + )); + } + }; + data_offset += starknet::core::types::Felt::cairo_serialized_size(&hash); + return Ok(Event::MetadataUpdate(MetadataUpdate { resource, uri, hash })); } let selector = event.keys[0]; if selector diff --git a/crates/dojo/world/src/diff/compare.rs b/crates/dojo/world/src/diff/compare.rs index 53224ae5f0..ce2d3f0986 100644 --- a/crates/dojo/world/src/diff/compare.rs +++ b/crates/dojo/world/src/diff/compare.rs @@ -108,6 +108,7 @@ mod tests { address: Felt::ZERO, owners: HashSet::new(), writers: HashSet::new(), + metadata_hash: Felt::ZERO, }, }); @@ -143,6 +144,7 @@ mod tests { address: Felt::ZERO, owners: HashSet::new(), writers: HashSet::new(), + metadata_hash: Felt::ZERO, }, }); @@ -192,6 +194,7 @@ mod tests { address: Felt::ZERO, owners: HashSet::new(), writers: HashSet::new(), + metadata_hash: Felt::ZERO, }, is_initialized: true, }); diff --git a/crates/dojo/world/src/diff/manifest.rs b/crates/dojo/world/src/diff/manifest.rs index a6f0755705..0bdf956a77 100644 --- a/crates/dojo/world/src/diff/manifest.rs +++ b/crates/dojo/world/src/diff/manifest.rs @@ -122,6 +122,10 @@ impl Manifest { let mut events = Vec::new(); for resource in diff.resources.values() { + if diff.profile_config.is_skipped(&resource.tag()) { + continue; + } + match resource.resource_type() { ResourceType::Contract => { contracts.push(resource_diff_to_dojo_contract(diff, resource)) diff --git a/crates/dojo/world/src/diff/mod.rs b/crates/dojo/world/src/diff/mod.rs index 799a18f19c..c32ef25503 100644 --- a/crates/dojo/world/src/diff/mod.rs +++ b/crates/dojo/world/src/diff/mod.rs @@ -12,6 +12,7 @@ use starknet::core::types::contract::SierraClass; use starknet::core::types::{BlockId, BlockTag, StarknetError}; use starknet::providers::{Provider, ProviderError}; use starknet_crypto::Felt; +use tracing::trace; use super::local::{ResourceLocal, WorldLocal}; use super::remote::{ResourceRemote, WorldRemote}; @@ -29,6 +30,8 @@ pub use resource::*; pub struct WorldStatusInfo { /// The address of the world. pub address: Felt, + /// The hash of the metadata associated to the world. + pub metadata_hash: Felt, /// The class hash of the world. pub class_hash: Felt, /// The casm class hash of the world. @@ -77,6 +80,7 @@ impl WorldDiff { let mut diff = Self { world_info: WorldStatusInfo { address: local.deterministic_world_address()?, + metadata_hash: Felt::ZERO, class_hash: local.class_hash, casm_class_hash: local.casm_class_hash, class: local.class, @@ -117,6 +121,7 @@ impl WorldDiff { world_info: WorldStatusInfo { // As the remote world was found, its address is always used. address: remote.address, + metadata_hash: remote.metadata_hash, class_hash: local.class_hash, casm_class_hash: local.casm_class_hash, class: local.class, @@ -167,7 +172,7 @@ impl WorldDiff { { Err(ProviderError::StarknetError(StarknetError::ContractNotFound)) => Ok(false), Ok(_) => { - tracing::trace!( + trace!( contract_address = format!("{:#066x}", world_address), "World already deployed." ); diff --git a/crates/dojo/world/src/diff/resource.rs b/crates/dojo/world/src/diff/resource.rs index 3a33f90fcd..a5adf7f9f2 100644 --- a/crates/dojo/world/src/diff/resource.rs +++ b/crates/dojo/world/src/diff/resource.rs @@ -113,6 +113,15 @@ impl ResourceDiff { } } + /// Returns the current metadata hash of the resource. + pub fn metadata_hash(&self) -> Felt { + match self { + ResourceDiff::Created(_) => Felt::ZERO, + ResourceDiff::Updated(_, remote) => remote.metadata_hash(), + ResourceDiff::Synced(_, remote) => remote.metadata_hash(), + } + } + pub fn abi(&self) -> Vec { match self { ResourceDiff::Created(local) => local.abi(), diff --git a/crates/dojo/world/src/lib.rs b/crates/dojo/world/src/lib.rs index 1b7dbae2d8..0c61344148 100644 --- a/crates/dojo/world/src/lib.rs +++ b/crates/dojo/world/src/lib.rs @@ -1,13 +1,14 @@ #![warn(unused_crate_dependencies)] -#[cfg(feature = "metadata")] pub mod metadata; pub mod config; +pub mod constants; pub mod contracts; pub mod diff; pub mod local; pub mod remote; +pub mod services; pub mod uri; pub mod utils; diff --git a/crates/dojo/world/src/metadata/metadata_storage.rs b/crates/dojo/world/src/metadata/metadata_storage.rs new file mode 100644 index 0000000000..fc7ec2a134 --- /dev/null +++ b/crates/dojo/world/src/metadata/metadata_storage.rs @@ -0,0 +1,116 @@ +use std::hash::{DefaultHasher, Hash, Hasher}; + +use anyhow::{Context, Result}; +use serde_json::json; +use starknet_crypto::Felt; + +use crate::config::metadata_config::{ResourceMetadata, WorldMetadata}; +use crate::services::UploadService; +use crate::uri::Uri; + +/// Helper function to compute metadata hash. +/// +/// # Arguments +/// * `data` - the data to hash. +/// +/// # Returns +/// The hash value. +fn compute_metadata_hash(data: T) -> u64 +where + T: Hash, +{ + let mut hasher = DefaultHasher::new(); + data.hash(&mut hasher); + hasher.finish() +} + +/// Helper function to process an optional URI. +/// +/// If the URI is set and refer to a local asset, this asset +/// is then uploaded using the provided UploadService. +/// In any other case, the URI is kept as it is. +/// +/// # Arguments +/// * `uri` - The URI to process +/// * `service` - The metadata service to use to upload assets. +/// +/// # Returns +/// The updated URI or a Anyhow error. +async fn upload_uri(uri: &Option, service: &mut impl UploadService) -> Result> { + if let Some(Uri::File(path)) = uri { + let data = std::fs::read(path)?; + let uploaded_uri = Uri::Ipfs(service.upload(data).await?); + Ok(Some(uploaded_uri)) + } else { + Ok(uri.clone()) + } +} + +/// Trait to be implemented by metadata structs to be +/// uploadable on a storage system. +#[allow(async_fn_in_trait)] +pub trait MetadataStorage { + /// Upload metadata using the provided service. + /// + /// # Arguments + /// * `service` - service to use to upload metadata + /// + /// # Returns + /// The uploaded metadata URI or a Anyhow error. + async fn upload(&self, service: &mut impl UploadService) -> Result; + + /// Upload metadata using the provided service, only if it has changed. + /// + /// # Arguments + /// * `service` - service to use to upload metadata + /// * `current_hash` - the hash of the previously uploaded metadata + /// + /// # Returns + /// The uploaded metadata URI or a Anyhow error. + async fn upload_if_changed( + &self, + service: &mut impl UploadService, + current_hash: Felt, + ) -> Result> + where + Self: std::hash::Hash, + { + let new_hash = compute_metadata_hash(self); + let new_hash = Felt::from_raw([0, 0, 0, new_hash]); + + if new_hash != current_hash { + let new_uri = self.upload(service).await?; + return Ok(Some((new_uri, new_hash))); + } + + Ok(None) + } +} + +#[allow(async_fn_in_trait)] +impl MetadataStorage for WorldMetadata { + async fn upload(&self, service: &mut impl UploadService) -> Result { + let mut meta = self.clone(); + + meta.icon_uri = + upload_uri(&self.icon_uri, service).await.context("Failed to upload icon URI")?; + meta.cover_uri = + upload_uri(&self.cover_uri, service).await.context("Failed to upload cover URI")?; + + let serialized = json!(meta).to_string(); + service.upload(serialized.as_bytes().to_vec()).await.context("Failed to upload metadata") + } +} + +#[allow(async_fn_in_trait)] +impl MetadataStorage for ResourceMetadata { + async fn upload(&self, service: &mut impl UploadService) -> Result { + let mut meta = self.clone(); + + meta.icon_uri = + upload_uri(&self.icon_uri, service).await.context("Failed to upload icon URI")?; + + let serialized = json!(meta).to_string(); + service.upload(serialized.as_bytes().to_vec()).await.context("Failed to upload metadata") + } +} diff --git a/crates/dojo/world/src/metadata/metadata_test.rs b/crates/dojo/world/src/metadata/metadata_test.rs index cfedcfe191..a324fd45a6 100644 --- a/crates/dojo/world/src/metadata/metadata_test.rs +++ b/crates/dojo/world/src/metadata/metadata_test.rs @@ -1,34 +1,194 @@ -// use std::collections::HashMap; -// use std::fs; -// -// use camino::Utf8PathBuf; -// use dojo_test_utils::compiler; -// use scarb::compiler::Profile; -// use scarb::ops; -// use url::Url; -// -// use crate::contracts::naming::{get_filename_from_tag, TAG_SEPARATOR}; -// use crate::manifest::{CONTRACTS_DIR, MODELS_DIR, WORLD_CONTRACT_TAG}; -// use crate::metadata::{ -// dojo_metadata_from_workspace, ArtifactMetadata, Uri, WorldMetadata, ABIS_DIR, BASE_DIR, -// MANIFESTS_DIR, -// }; -// -// #[tokio::test] -// async fn world_metadata_hash_and_upload() { -// let meta = WorldMetadata { -// name: "Test World".to_string(), -// seed: String::from("dojo_examples"), -// description: Some("A world used for testing".to_string()), -// cover_uri: Some(Uri::File("src/metadata/metadata_test_data/cover.png".into())), -// icon_uri: Some(Uri::File("src/metadata/metadata_test_data/cover.png".into())), -// website: Some(Url::parse("https://dojoengine.org").unwrap()), -// socials: Some(HashMap::from([("x".to_string(), "https://x.com/dojostarknet".to_string())])), -// artifacts: ArtifactMetadata { -// abi: Some(Uri::File("src/metadata_test_data/abi.json".into())), -// source: Some(Uri::File("src/metadata_test_data/source.cairo".into())), -// }, -// }; -// -// let _ = meta.upload().await.unwrap(); -// } +use core::str; +use std::collections::HashMap; +use std::fs; +use std::path::PathBuf; +use std::str::FromStr; + +use starknet_crypto::Felt; +use url::Url; + +use super::metadata_storage::MetadataStorage; +use crate::config::metadata_config::{ResourceMetadata, WorldMetadata}; +use crate::services::{MockUploadService, UploadService}; +use crate::uri::Uri; + +/// Helper function to create a local file absolute path +/// from a relative path. +fn test_file_path(filename: &str) -> PathBuf { + fs::canonicalize( + PathBuf::from_str(&format!("./src/metadata/metadata_test_data/{}", filename)).unwrap(), + ) + .unwrap() +} + +/// Helper function to build a WorldMetadata for tests. +fn build_world_metadata() -> WorldMetadata { + WorldMetadata { + name: "world".to_string(), + seed: "world seed".to_string(), + description: Some("world description".to_string()), + cover_uri: Some(Uri::File(test_file_path("cover.png"))), + icon_uri: Some(Uri::File(test_file_path("icon.png"))), + website: Some(Url::parse("https://my_world.com").expect("parsing failed")), + socials: Some(HashMap::from([ + ("twitter".to_string(), "twitter_url".to_string()), + ("discord".to_string(), "discord_url".to_string()), + ])), + } +} + +/// Helper function to build a ResourceMetadata for tests. +fn build_resource_metadata() -> ResourceMetadata { + ResourceMetadata { + name: "my model".to_string(), + description: Some("my model description".to_string()), + icon_uri: Some(Uri::File(test_file_path("icon.png"))), + } +} + +// Helper function to check IPFS URI. +fn assert_ipfs_uri(uri: &Option) { + if let Some(uri) = uri { + assert!(uri.to_string().starts_with("ipfs://")); + } +} + +// Helper function to check IPFS content. +async fn assert_ipfs_content(service: &MockUploadService, uri: String, path: PathBuf) { + let ipfs_data = service.get(uri).await.expect("read metadata failed"); + let expected_data = std::fs::read(path).expect("read local data failed"); + assert_eq!(ipfs_data, expected_data); +} + +#[tokio::test] +async fn test_world_metadata() { + let mut metadata_service = MockUploadService::default(); + + let world_metadata = build_world_metadata(); + + // first metadata upload without existing hash. + let res = world_metadata.upload_if_changed(&mut metadata_service, Felt::ZERO).await; + + let (current_uri, current_hash) = if let Ok(Some(res)) = res { + res + } else { + panic!("Upload failed"); + }; + + // no change => the upload is not done. + let res = world_metadata.upload_if_changed(&mut metadata_service, current_hash).await; + + assert!(res.is_ok()); + assert!(res.unwrap().is_none()); + + // different hash => metadata are reuploaded. + let res = + world_metadata.upload_if_changed(&mut metadata_service, current_hash + Felt::ONE).await; + + let (new_uri, new_hash) = if let Ok(Some(res)) = res { + res + } else { + panic!("Upload failed"); + }; + + assert_eq!(new_uri, current_uri); + assert_eq!(new_hash, current_hash); + + // read back the metadata from service to be sure it is correctly written + let read_metadata = metadata_service.get(current_uri).await.expect("read metadata failed"); + + let read_metadata = str::from_utf8(&read_metadata); + assert!(read_metadata.is_ok()); + + let read_metadata = serde_json::from_str::(read_metadata.unwrap()); + assert!(read_metadata.is_ok()); + + let read_metadata = read_metadata.unwrap(); + + assert_eq!(read_metadata.name, "world".to_string()); + assert_eq!(read_metadata.seed, "world seed".to_string()); + assert_eq!(read_metadata.description, Some("world description".to_string())); + assert_eq!(read_metadata.website, Some(Url::parse("https://my_world.com").unwrap())); + assert_eq!( + read_metadata.socials, + Some(HashMap::from([ + ("twitter".to_string(), "twitter_url".to_string()), + ("discord".to_string(), "discord_url".to_string()), + ])) + ); + + assert_ipfs_uri(&read_metadata.cover_uri); + assert_ipfs_content( + &metadata_service, + read_metadata.cover_uri.unwrap().to_string(), + fs::canonicalize(PathBuf::from_str("./src/metadata/metadata_test_data/cover.png").unwrap()) + .unwrap(), + ) + .await; + + assert_ipfs_uri(&read_metadata.icon_uri); + assert_ipfs_content( + &metadata_service, + read_metadata.icon_uri.unwrap().to_string(), + fs::canonicalize(PathBuf::from_str("./src/metadata/metadata_test_data/icon.png").unwrap()) + .unwrap(), + ) + .await; +} + +#[tokio::test] +async fn test_resource_metadata() { + let mut metadata_service = MockUploadService::default(); + + let resource_metadata = build_resource_metadata(); + + // first metadata upload without existing hash. + let res = resource_metadata.upload_if_changed(&mut metadata_service, Felt::ZERO).await; + assert!(res.is_ok()); + let res = res.unwrap(); + + assert!(res.is_some()); + let (current_uri, current_hash) = res.unwrap(); + + // no change => the upload is not done. + let res = resource_metadata.upload_if_changed(&mut metadata_service, current_hash).await; + assert!(res.is_ok()); + let res = res.unwrap(); + + assert!(res.is_none()); + + // different hash => metadata are reuploaded. + let res = + resource_metadata.upload_if_changed(&mut metadata_service, current_hash + Felt::ONE).await; + assert!(res.is_ok()); + let res = res.unwrap(); + + assert!(res.is_some()); + let (new_uri, new_hash) = res.unwrap(); + + assert_eq!(new_uri, current_uri); + assert_eq!(new_hash, current_hash); + + // read back the metadata stored on IPFS to be sure it is correctly written + let read_metadata = metadata_service.get(current_uri).await.expect("read metadata failed"); + + let read_metadata = str::from_utf8(&read_metadata); + assert!(read_metadata.is_ok()); + + let read_metadata = serde_json::from_str::(read_metadata.unwrap()); + assert!(read_metadata.is_ok()); + + let read_metadata = read_metadata.unwrap(); + + assert_eq!(read_metadata.name, "my model".to_string()); + assert_eq!(read_metadata.description, Some("my model description".to_string())); + + assert_ipfs_uri(&read_metadata.icon_uri); + assert_ipfs_content( + &metadata_service, + read_metadata.icon_uri.unwrap().to_string(), + fs::canonicalize(PathBuf::from_str("./src/metadata/metadata_test_data/icon.png").unwrap()) + .unwrap(), + ) + .await; +} diff --git a/crates/dojo/world/src/metadata/metadata_test_data/abi.json b/crates/dojo/world/src/metadata/metadata_test_data/abi.json deleted file mode 100644 index 78efed0140..0000000000 --- a/crates/dojo/world/src/metadata/metadata_test_data/abi.json +++ /dev/null @@ -1,17 +0,0 @@ -[ - { - "type": "impl", - "name": "WorldProviderImpl", - "interface_name": "dojo::world::IWorldProvider" - }, - { - "type": "struct", - "name": "dojo::world::IWorldDispatcher", - "members": [ - { - "name": "contract_address", - "type": "core::starknet::contract_address::ContractAddress" - } - ] - } -] diff --git a/crates/dojo/world/src/metadata/metadata_test_data/icon.png b/crates/dojo/world/src/metadata/metadata_test_data/icon.png new file mode 100644 index 0000000000..45ad927b14 Binary files /dev/null and b/crates/dojo/world/src/metadata/metadata_test_data/icon.png differ diff --git a/crates/dojo/world/src/metadata/metadata_test_data/source.cairo b/crates/dojo/world/src/metadata/metadata_test_data/source.cairo deleted file mode 100644 index c917342ece..0000000000 --- a/crates/dojo/world/src/metadata/metadata_test_data/source.cairo +++ /dev/null @@ -1,79 +0,0 @@ -use starknet::ContractAddress; - -#[derive(Serde, Copy, Drop, Introspect)] -enum Direction { - None, - Left, - Right, - Up, - Down, -} - -impl DirectionIntoFelt252 of Into { - fn into(self: Direction) -> felt252 { - match self { - Direction::None => 0, - Direction::Left => 1, - Direction::Right => 2, - Direction::Up => 3, - Direction::Down => 4, - } - } -} - -#[derive(Model, Copy, Drop, Serde)] -struct Moves { - #[key] - player: ContractAddress, - remaining: u8, - last_direction: Direction -} - -#[derive(Copy, Drop, Serde, Introspect)] -struct Vec2 { - x: u32, - y: u32 -} - -#[derive(Model, Copy, Drop, Serde)] -struct Position { - #[key] - player: ContractAddress, - vec: Vec2, -} - -trait Vec2Trait { - fn is_zero(self: Vec2) -> bool; - fn is_equal(self: Vec2, b: Vec2) -> bool; -} - -impl Vec2Impl of Vec2Trait { - fn is_zero(self: Vec2) -> bool { - if self.x - self.y == 0 { - return true; - } - false - } - - fn is_equal(self: Vec2, b: Vec2) -> bool { - self.x == b.x && self.y == b.y - } -} - -#[cfg(test)] -mod tests { - use super::{Position, Vec2, Vec2Trait}; - - #[test] - #[available_gas(100000)] - fn test_vec_is_zero() { - assert(Vec2Trait::is_zero(Vec2 { x: 0, y: 0 }), 'not zero'); - } - - #[test] - #[available_gas(100000)] - fn test_vec_is_equal() { - let position = Vec2 { x: 420, y: 0 }; - assert(position.is_equal(Vec2 { x: 420, y: 0 }), 'not equal'); - } -} diff --git a/crates/dojo/world/src/metadata/mod.rs b/crates/dojo/world/src/metadata/mod.rs index 16a61c9701..cd80aa8877 100644 --- a/crates/dojo/world/src/metadata/mod.rs +++ b/crates/dojo/world/src/metadata/mod.rs @@ -1,3 +1,5 @@ -//! Metadata for the world. +pub mod metadata_storage; +pub use metadata_storage::MetadataStorage; -pub mod world; +#[cfg(test)] +mod metadata_test; diff --git a/crates/dojo/world/src/metadata/world.rs b/crates/dojo/world/src/metadata/world.rs deleted file mode 100644 index be76c31f6a..0000000000 --- a/crates/dojo/world/src/metadata/world.rs +++ /dev/null @@ -1,44 +0,0 @@ -use std::io::Cursor; - -use anyhow::Result; -use ipfs_api_backend_hyper::{IpfsApi, IpfsClient, TryFromUri}; -use serde_json::json; - -use crate::config::metadata_config::WorldMetadata; -use crate::uri::Uri; - -#[cfg(test)] -#[path = "metadata_test.rs"] -mod test; - -pub const IPFS_CLIENT_URL: &str = "https://ipfs.infura.io:5001"; -pub const IPFS_USERNAME: &str = "2EBrzr7ZASQZKH32sl2xWauXPSA"; -pub const IPFS_PASSWORD: &str = "12290b883db9138a8ae3363b6739d220"; - -impl WorldMetadata { - pub async fn upload(&self) -> Result { - let mut meta = self.clone(); - let client = - IpfsClient::from_str(IPFS_CLIENT_URL)?.with_credentials(IPFS_USERNAME, IPFS_PASSWORD); - - if let Some(Uri::File(icon)) = &self.icon_uri { - let icon_data = std::fs::read(icon)?; - let reader = Cursor::new(icon_data); - let response = client.add(reader).await?; - meta.icon_uri = Some(Uri::Ipfs(format!("ipfs://{}", response.hash))) - }; - - if let Some(Uri::File(cover)) = &self.cover_uri { - let cover_data = std::fs::read(cover)?; - let reader = Cursor::new(cover_data); - let response = client.add(reader).await?; - meta.cover_uri = Some(Uri::Ipfs(format!("ipfs://{}", response.hash))) - }; - - let serialized = json!(meta).to_string(); - let reader = Cursor::new(serialized); - let response = client.add(reader).await?; - - Ok(response.hash) - } -} diff --git a/crates/dojo/world/src/remote/events_to_remote.rs b/crates/dojo/world/src/remote/events_to_remote.rs index a6404d61a0..38837b5feb 100644 --- a/crates/dojo/world/src/remote/events_to_remote.rs +++ b/crates/dojo/world/src/remote/events_to_remote.rs @@ -15,6 +15,7 @@ use tracing::trace; use super::permissions::PermissionsUpdateable; use super::{ResourceRemote, WorldRemote}; +use crate::constants::WORLD; use crate::contracts::abigen::world::{self, Event as WorldEvent}; use crate::remote::{CommonRemoteInfo, ContractRemote, EventRemote, ModelRemote, NamespaceRemote}; @@ -35,7 +36,7 @@ impl WorldRemote { // The world contract exists, we can continue and fetch the events. } Err(ProviderError::StarknetError(StarknetError::ContractNotFound)) => { - tracing::trace!(%world_address, "No remote world contract found."); + trace!(%world_address, "No remote world contract found."); return Ok(world); } Err(e) => return Err(e.into()), @@ -55,6 +56,7 @@ impl WorldRemote { world::ContractInitialized::event_selector(), world::WriterUpdated::event_selector(), world::OwnerUpdated::event_selector(), + world::MetadataUpdate::event_selector(), ]]; let filter = EventFilter { @@ -68,7 +70,7 @@ impl WorldRemote { let chunk_size = 500; - tracing::trace!( + trace!( world_address = format!("{:#066x}", world_address), chunk_size, ?filter, @@ -104,7 +106,7 @@ impl WorldRemote { for event in &events { match world::Event::try_from(event) { Ok(ev) => { - tracing::trace!(?ev, "Processing world event."); + trace!(?ev, "Processing world event."); world.match_event(ev)?; } Err(e) => { @@ -126,7 +128,7 @@ impl WorldRemote { self.class_hashes.push(e.class_hash.into()); // The creator is the world's owner, but no event emitted for that. - self.external_owners.insert(Felt::ZERO, HashSet::from([e.creator.into()])); + self.external_owners.insert(WORLD, HashSet::from([e.creator.into()])); trace!( class_hash = format!("{:#066x}", e.class_hash.0), @@ -250,6 +252,17 @@ impl WorldRemote { trace!(?e, "Owner updated."); } + WorldEvent::MetadataUpdate(e) => { + if e.resource == WORLD { + self.metadata_hash = e.hash; + } else { + // Unwrap is safe because the resource must exist in the world. + let resource = self.resources.get_mut(&e.resource).unwrap(); + trace!(?resource, "Metadata updated."); + + resource.set_metadata_hash(e.hash); + } + } _ => { // Ignore events filtered out by the event filter. } @@ -516,4 +529,37 @@ mod tests { let resource = world_remote.resources.get(&selector).unwrap(); assert_eq!(resource.as_namespace_or_panic().owners, HashSet::from([])); } + + #[tokio::test] + async fn test_metadata_updated_event() { + let mut world_remote = WorldRemote::default(); + let selector = naming::compute_selector_from_names("ns", "m1"); + + let resource = ResourceRemote::Model(ModelRemote { + common: CommonRemoteInfo::new(Felt::TWO, "ns", "m1", Felt::ONE), + }); + world_remote.add_resource(resource); + + let event = WorldEvent::MetadataUpdate(world::MetadataUpdate { + resource: selector, + uri: ByteArray::from_string("ipfs://m1").unwrap(), + hash: Felt::THREE, + }); + + world_remote.match_event(event).unwrap(); + + let resource = world_remote.resources.get(&selector).unwrap(); + assert_eq!(resource.metadata_hash(), Felt::THREE); + + let event = WorldEvent::MetadataUpdate(world::MetadataUpdate { + resource: selector, + uri: ByteArray::from_string("ipfs://m1").unwrap(), + hash: Felt::ONE, + }); + + world_remote.match_event(event).unwrap(); + + let resource = world_remote.resources.get(&selector).unwrap(); + assert_eq!(resource.metadata_hash(), Felt::ONE); + } } diff --git a/crates/dojo/world/src/remote/mod.rs b/crates/dojo/world/src/remote/mod.rs index 8a6dc15405..b76e5ba3f4 100644 --- a/crates/dojo/world/src/remote/mod.rs +++ b/crates/dojo/world/src/remote/mod.rs @@ -23,6 +23,8 @@ use crate::{ContractAddress, DojoSelector}; pub struct WorldRemote { /// The world's address used to build the remote world. pub address: Felt, + /// The hash of the metadata associated to the world. + pub metadata_hash: Felt, /// The class hashes of the world. pub class_hashes: Vec, /// The resources of the world, by dojo selector. diff --git a/crates/dojo/world/src/remote/resource.rs b/crates/dojo/world/src/remote/resource.rs index b44e217db5..9a8c584079 100644 --- a/crates/dojo/world/src/remote/resource.rs +++ b/crates/dojo/world/src/remote/resource.rs @@ -30,6 +30,8 @@ pub struct CommonRemoteInfo { pub namespace: String, /// The address of the resource. pub address: ContractAddress, + /// The hash of the stored metadata associated to the resource if any. + pub metadata_hash: Felt, /// The contract addresses that have owner permission on the resource. pub owners: HashSet, /// The contract addresses that have writer permission on the resource. @@ -80,6 +82,7 @@ impl CommonRemoteInfo { name: name.to_string(), namespace: namespace.to_string(), address, + metadata_hash: Felt::ZERO, owners: HashSet::new(), writers: HashSet::new(), } @@ -173,6 +176,26 @@ impl ResourceRemote { } } + /// Set the hash of the stored metadata associated to the resource. + pub fn set_metadata_hash(&mut self, hash: Felt) { + match self { + ResourceRemote::Contract(c) => c.common.metadata_hash = hash, + ResourceRemote::Model(m) => m.common.metadata_hash = hash, + ResourceRemote::Event(e) => e.common.metadata_hash = hash, + ResourceRemote::Namespace(_) => {} + } + } + + /// The hash of the stored metadata associated to the resource. + pub fn metadata_hash(&self) -> Felt { + match self { + ResourceRemote::Contract(c) => c.common.metadata_hash, + ResourceRemote::Model(m) => m.common.metadata_hash, + ResourceRemote::Event(e) => e.common.metadata_hash, + ResourceRemote::Namespace(_) => Felt::ZERO, + } + } + /// Push a new class hash to the resource meaning it has been upgraded. pub fn push_class_hash(&mut self, class_hash: Felt) { match self { diff --git a/crates/dojo/world/src/services/ipfs_service.rs b/crates/dojo/world/src/services/ipfs_service.rs new file mode 100644 index 0000000000..30f9fda3dc --- /dev/null +++ b/crates/dojo/world/src/services/ipfs_service.rs @@ -0,0 +1,65 @@ +use std::io::Cursor; + +use anyhow::Result; +#[cfg(test)] +use futures::TryStreamExt; +use ipfs_api_backend_hyper::{IpfsApi, TryFromUri}; + +use super::upload_service::UploadService; +use crate::config::IpfsConfig; + +/// IPFS implementation of UploadService, allowing to upload data to IPFS. +pub struct IpfsService { + client: ipfs_api_backend_hyper::IpfsClient, +} + +// impl required by clippy +impl std::fmt::Debug for IpfsService { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + write!(f, "IPFS service")?; + Ok(()) + } +} + +impl IpfsService { + /// Instanciate a new IPFS service with IPFS configuration. + /// + /// # Arguments + /// * `config` - The IPFS configuration + /// + /// # Returns + /// A new `IpfsService` is the IPFS client has been successfully + /// instanciated or a Anyhow error if not. + pub fn new(config: IpfsConfig) -> Result { + config.assert_valid()?; + + Ok(Self { + client: ipfs_api_backend_hyper::IpfsClient::from_str(&config.url)? + .with_credentials(config.username, &config.password), + }) + } +} + +#[allow(async_fn_in_trait)] +impl UploadService for IpfsService { + async fn upload(&mut self, data: Vec) -> Result { + let reader = Cursor::new(data); + let response = self + .client + .add(reader) + .await + .map_err(|e| anyhow::anyhow!("Failed to upload to IPFS: {}", e))?; + Ok(format!("ipfs://{}", response.hash)) + } + + #[cfg(test)] + async fn get(&self, uri: String) -> Result> { + let res = self + .client + .cat(&uri.replace("ipfs://", "")) + .map_ok(|chunk| chunk.to_vec()) + .try_concat() + .await?; + Ok(res) + } +} diff --git a/crates/dojo/world/src/services/mock_upload_service.rs b/crates/dojo/world/src/services/mock_upload_service.rs new file mode 100644 index 0000000000..4e04f7a2e0 --- /dev/null +++ b/crates/dojo/world/src/services/mock_upload_service.rs @@ -0,0 +1,39 @@ +use std::collections::HashMap; +use std::hash::{DefaultHasher, Hash, Hasher}; + +use anyhow::Result; + +use super::upload_service::UploadService; + +/// Mock implementation of UploadService to be used for tests only. +/// It just stores uri and data in a HashMap when `upload` is called, +/// and returns these data when `get` is called. +#[derive(Debug, Default)] +pub struct MockUploadService { + data: HashMap>, +} + +#[allow(async_fn_in_trait)] +impl UploadService for MockUploadService { + async fn upload(&mut self, data: Vec) -> Result { + let mut hasher = DefaultHasher::new(); + data.hash(&mut hasher); + let hash = hasher.finish(); + + let uri = format!("ipfs://{:x}", hash); + self.data.insert(uri.clone(), data); + + Ok(uri) + } + + #[cfg(test)] + async fn get(&self, uri: String) -> Result> { + if !uri.starts_with("ipfs://") { + return Err(anyhow::anyhow!("Invalid URI format. Expected ipfs:// prefix")); + } + self.data + .get(&uri) + .cloned() + .ok_or_else(|| anyhow::anyhow!("No data found for URI: {}", uri)) + } +} diff --git a/crates/dojo/world/src/services/mod.rs b/crates/dojo/world/src/services/mod.rs new file mode 100644 index 0000000000..0cacac303b --- /dev/null +++ b/crates/dojo/world/src/services/mod.rs @@ -0,0 +1,9 @@ +mod upload_service; +pub use upload_service::UploadService; +mod mock_upload_service; +pub use mock_upload_service::MockUploadService; + +#[cfg(feature = "ipfs")] +mod ipfs_service; +#[cfg(feature = "ipfs")] +pub use ipfs_service::IpfsService; diff --git a/crates/dojo/world/src/services/upload_service.rs b/crates/dojo/world/src/services/upload_service.rs new file mode 100644 index 0000000000..d1dcf630f1 --- /dev/null +++ b/crates/dojo/world/src/services/upload_service.rs @@ -0,0 +1,26 @@ +use anyhow::Result; + +/// UploadService trait to be implemented to upload +/// some data on a specific storage system. +#[allow(async_fn_in_trait)] +pub trait UploadService: std::marker::Send + std::marker::Sync + std::marker::Unpin { + /// Upload some bytes (`data`) to the storage system, + /// and get back a string URI. + /// + /// # Arguments + /// * `data` - bytes to upload + /// + /// # Returns + /// A string URI or a Anyhow error. + async fn upload(&mut self, data: Vec) -> Result; + + /// Read stored bytes from a URI. (for tests only) + /// + /// # Arguments + /// * `uri` - the URI of the data to read + /// + /// # Returns + /// the read bytes or a Anyhow error. + #[cfg(test)] + async fn get(&self, uri: String) -> Result>; +} diff --git a/crates/dojo/world/src/uri.rs b/crates/dojo/world/src/uri.rs index b83b358eb9..8a2a93db76 100644 --- a/crates/dojo/world/src/uri.rs +++ b/crates/dojo/world/src/uri.rs @@ -1,3 +1,4 @@ +use std::hash::{Hash, Hasher}; use std::path::PathBuf; use anyhow::Result; @@ -17,6 +18,22 @@ pub enum Uri { File(PathBuf), } +impl Hash for Uri { + fn hash(&self, state: &mut H) { + match self { + Uri::Http(url) => { + url.to_string().hash(state); + } + Uri::Ipfs(uri) => { + uri.to_string().hash(state); + } + Uri::File(path) => { + path.hash(state); + } + } + } +} + impl Serialize for Uri { fn serialize(&self, serializer: S) -> Result where @@ -40,6 +57,16 @@ impl<'de> Deserialize<'de> for Uri { } } +impl std::fmt::Display for Uri { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Uri::File(path) => write!(f, "{}", path.to_string_lossy()), + Uri::Http(url) => write!(f, "{}", url), + Uri::Ipfs(uri) => write!(f, "{}", uri), + } + } +} + impl Uri { pub fn cid(&self) -> Option<&str> { match self { diff --git a/crates/katana/cli/Cargo.toml b/crates/katana/cli/Cargo.toml index 9d58c2c038..be65483377 100644 --- a/crates/katana/cli/Cargo.toml +++ b/crates/katana/cli/Cargo.toml @@ -10,6 +10,7 @@ dojo-utils.workspace = true katana-core.workspace = true katana-node.workspace = true katana-primitives.workspace = true +katana-rpc.workspace = true katana-slot-controller = { workspace = true, optional = true } alloy-primitives.workspace = true @@ -32,7 +33,7 @@ assert_matches.workspace = true starknet.workspace = true [features] -default = [ "slot", "server" ] +default = [ "server", "slot" ] +server = [ ] slot = [ "dep:katana-slot-controller", "katana-primitives/slot" ] -server = [ ] starknet-messaging = [ "katana-node/starknet-messaging" ] diff --git a/crates/katana/cli/src/args.rs b/crates/katana/cli/src/args.rs index aa852432dc..44c146d388 100644 --- a/crates/katana/cli/src/args.rs +++ b/crates/katana/cli/src/args.rs @@ -1,9 +1,11 @@ //! Katana node CLI options and configuration. -use std::collections::HashSet; use std::path::PathBuf; +use std::sync::Arc; use alloy_primitives::U256; +#[cfg(feature = "server")] +use anyhow::bail; use anyhow::{Context, Result}; use clap::Parser; use katana_core::constants::DEFAULT_SEQUENCER_ADDRESS; @@ -13,7 +15,9 @@ use katana_node::config::dev::{DevConfig, FixedL1GasPriceConfig}; use katana_node::config::execution::ExecutionConfig; use katana_node::config::fork::ForkingConfig; use katana_node::config::metrics::MetricsConfig; -use katana_node::config::rpc::{ApiKind, RpcConfig}; +use katana_node::config::rpc::RpcConfig; +#[cfg(feature = "server")] +use katana_node::config::rpc::{RpcModuleKind, RpcModulesList}; use katana_node::config::{Config, SequencingConfig}; use katana_primitives::chain_spec::{self, ChainSpec}; use katana_primitives::genesis::allocation::DevAllocationsGenerator; @@ -22,6 +26,7 @@ use serde::{Deserialize, Serialize}; use tracing::{info, Subscriber}; use tracing_log::LogTracer; use tracing_subscriber::{fmt, EnvFilter}; +use url::Url; use crate::file::NodeArgsConfig; use crate::options::*; @@ -68,6 +73,11 @@ pub struct NodeArgs { #[arg(value_parser = katana_core::service::messaging::MessagingConfig::parse)] pub messaging: Option, + #[arg(long = "l1.provider", value_name = "URL", alias = "l1-provider")] + #[arg(help = "The Ethereum RPC provider to sample the gas prices from to enable the gas \ + price oracle.")] + pub l1_provider_url: Option, + #[command(flatten)] pub logging: LoggingOptions, @@ -134,9 +144,9 @@ impl NodeArgs { } fn init_logging(&self) -> Result<()> { - const DEFAULT_LOG_FILTER: &str = "info,tasks=debug,executor=trace,forking::backend=trace,\ - blockifier=off,jsonrpsee_server=off,hyper=off,\ - messaging=debug,node=error"; + const DEFAULT_LOG_FILTER: &str = + "pipeline=debug,stage=debug,info,tasks=debug,executor=trace,forking::backend=trace,\ + blockifier=off,jsonrpsee_server=off,hyper=off,messaging=debug,node=error"; let filter = if self.development.dev { &format!("{DEFAULT_LOG_FILTER},server=debug") @@ -162,7 +172,7 @@ impl NodeArgs { pub fn config(&self) -> Result { let db = self.db_config(); - let rpc = self.rpc_config(); + let rpc = self.rpc_config()?; let dev = self.dev_config(); let chain = self.chain_spec()?; let metrics = self.metrics_config(); @@ -170,40 +180,70 @@ impl NodeArgs { let execution = self.execution_config(); let sequencing = self.sequencer_config(); let messaging = self.messaging.clone(); - - Ok(Config { metrics, db, dev, rpc, chain, execution, sequencing, messaging, forking }) + let l1_provider_url = self.gpo_config(); + + Ok(Config { + metrics, + db, + dev, + rpc, + chain, + execution, + sequencing, + messaging, + forking, + l1_provider_url, + }) } fn sequencer_config(&self) -> SequencingConfig { SequencingConfig { block_time: self.block_time, no_mining: self.no_mining } } - fn rpc_config(&self) -> RpcConfig { - let mut apis = HashSet::from([ApiKind::Starknet, ApiKind::Torii, ApiKind::Saya]); - // only enable `katana` API in dev mode - if self.development.dev { - apis.insert(ApiKind::Dev); - } - + fn rpc_config(&self) -> Result { #[cfg(feature = "server")] { - RpcConfig { - apis, + let modules = if let Some(modules) = &self.server.http_modules { + // TODO: This check should be handled in the `katana-node` level. Right now if you + // instantiate katana programmatically, you can still add the dev module without + // enabling dev mode. + // + // We only allow the `dev` module in dev mode (ie `--dev` flag) + if !self.development.dev && modules.contains(&RpcModuleKind::Dev) { + bail!("The `dev` module can only be enabled in dev mode (ie `--dev` flag)") + } + + modules.clone() + } else { + // Expose the default modules if none is specified. + let mut modules = RpcModulesList::default(); + + // Ensures the `--dev` flag enabled the dev module. + if self.development.dev { + modules.add(RpcModuleKind::Dev); + } + + modules + }; + + Ok(RpcConfig { + apis: modules, port: self.server.http_port, addr: self.server.http_addr, max_connections: self.server.max_connections, cors_origins: self.server.http_cors_origins.clone(), max_event_page_size: Some(self.server.max_event_page_size), - } + max_proof_keys: Some(self.server.max_proof_keys), + }) } #[cfg(not(feature = "server"))] { - RpcConfig { apis, ..Default::default() } + Ok(RpcConfig::default()) } } - fn chain_spec(&self) -> Result { + fn chain_spec(&self) -> Result> { let mut chain_spec = chain_spec::DEV_UNALLOCATED.clone(); if let Some(id) = self.starknet.environment.chain_id { @@ -229,7 +269,7 @@ impl NodeArgs { katana_slot_controller::add_controller_account(&mut chain_spec.genesis)?; } - Ok(chain_spec) + Ok(Arc::new(chain_spec)) } fn dev_config(&self) -> DevConfig { @@ -295,6 +335,10 @@ impl NodeArgs { None } + fn gpo_config(&self) -> Option { + self.l1_provider_url.clone() + } + /// Parse the node config from the command line arguments and the config file, /// and merge them together prioritizing the command line arguments. pub fn with_config_file(mut self) -> Result { @@ -374,6 +418,7 @@ mod test { }; use katana_primitives::chain::ChainId; use katana_primitives::{address, felt, ContractAddress, Felt}; + use katana_rpc::cors::HeaderValue; use super::*; @@ -592,4 +637,55 @@ chain_id.Named = "Mainnet" assert_eq!(config.chain.genesis.gas_prices.strk, 8888); assert_eq!(config.chain.id, ChainId::Id(Felt::from_str("0x123").unwrap())); } + + #[test] + #[cfg(feature = "server")] + fn parse_cors_origins() { + let config = NodeArgs::parse_from([ + "katana", + "--http.cors_origins", + "*,http://localhost:3000,https://example.com", + ]) + .config() + .unwrap(); + + let cors_origins = config.rpc.cors_origins; + + assert_eq!(cors_origins.len(), 3); + assert!(cors_origins.contains(&HeaderValue::from_static("*"))); + assert!(cors_origins.contains(&HeaderValue::from_static("http://localhost:3000"))); + assert!(cors_origins.contains(&HeaderValue::from_static("https://example.com"))); + } + + #[test] + fn http_modules() { + // If the `--http.api` isn't specified, only starknet module will be exposed. + let config = NodeArgs::parse_from(["katana"]).config().unwrap(); + let modules = config.rpc.apis; + assert_eq!(modules.len(), 1); + assert!(modules.contains(&RpcModuleKind::Starknet)); + + // If the `--http.api` is specified, only the ones in the list will be exposed. + let config = NodeArgs::parse_from(["katana", "--http.api", "saya,torii"]).config().unwrap(); + let modules = config.rpc.apis; + assert_eq!(modules.len(), 2); + assert!(modules.contains(&RpcModuleKind::Saya)); + assert!(modules.contains(&RpcModuleKind::Torii)); + + // Specifiying the dev module without enabling dev mode is forbidden. + let err = + NodeArgs::parse_from(["katana", "--http.api", "starknet,dev"]).config().unwrap_err(); + assert!( + err.to_string() + .contains("The `dev` module can only be enabled in dev mode (ie `--dev` flag)") + ); + } + + #[test] + fn test_dev_api_enabled() { + let args = NodeArgs::parse_from(["katana", "--dev"]); + let config = args.config().unwrap(); + + assert!(config.rpc.apis.contains(&RpcModuleKind::Dev)); + } } diff --git a/crates/katana/cli/src/options.rs b/crates/katana/cli/src/options.rs index 64dc2ad8a7..9766232c0c 100644 --- a/crates/katana/cli/src/options.rs +++ b/crates/katana/cli/src/options.rs @@ -7,12 +7,16 @@ //! //! Currently, the merge is made at the top level of the commands. +#[cfg(feature = "server")] use std::net::IpAddr; use clap::Args; use katana_node::config::execution::{DEFAULT_INVOCATION_MAX_STEPS, DEFAULT_VALIDATION_MAX_STEPS}; +#[cfg(feature = "server")] use katana_node::config::metrics::{DEFAULT_METRICS_ADDR, DEFAULT_METRICS_PORT}; #[cfg(feature = "server")] +use katana_node::config::rpc::{RpcModulesList, DEFAULT_RPC_MAX_PROOF_KEYS}; +#[cfg(feature = "server")] use katana_node::config::rpc::{ DEFAULT_RPC_ADDR, DEFAULT_RPC_MAX_CONNECTIONS, DEFAULT_RPC_MAX_EVENT_PAGE_SIZE, DEFAULT_RPC_PORT, @@ -20,9 +24,13 @@ use katana_node::config::rpc::{ use katana_primitives::block::BlockHashOrNumber; use katana_primitives::chain::ChainId; use katana_primitives::genesis::Genesis; +#[cfg(feature = "server")] +use katana_rpc::cors::HeaderValue; use serde::{Deserialize, Serialize}; use url::Url; +#[cfg(feature = "server")] +use crate::utils::{deserialize_cors_origins, serialize_cors_origins}; use crate::utils::{parse_block_hash_or_number, parse_genesis, LogFormat}; const DEFAULT_DEV_SEED: &str = "0"; @@ -85,8 +93,18 @@ pub struct ServerOptions { /// Comma separated list of domains from which to accept cross origin requests. #[arg(long = "http.cors_origins")] #[arg(value_delimiter = ',')] + #[serde( + default, + serialize_with = "serialize_cors_origins", + deserialize_with = "deserialize_cors_origins" + )] + pub http_cors_origins: Vec, + + /// API's offered over the HTTP-RPC interface. + #[arg(long = "http.api", value_name = "MODULES")] + #[arg(value_parser = RpcModulesList::parse)] #[serde(default)] - pub http_cors_origins: Option>, + pub http_modules: Option, /// Maximum number of concurrent connections allowed. #[arg(long = "rpc.max-connections", value_name = "COUNT")] @@ -99,6 +117,12 @@ pub struct ServerOptions { #[arg(default_value_t = DEFAULT_RPC_MAX_EVENT_PAGE_SIZE)] #[serde(default = "default_page_size")] pub max_event_page_size: u64, + + /// Maximum keys for requesting storage proofs. + #[arg(long = "rpc.max-proof-keys", value_name = "SIZE")] + #[arg(default_value_t = DEFAULT_RPC_MAX_PROOF_KEYS)] + #[serde(default = "default_proof_keys")] + pub max_proof_keys: u64, } #[cfg(feature = "server")] @@ -107,9 +131,11 @@ impl Default for ServerOptions { ServerOptions { http_addr: DEFAULT_RPC_ADDR, http_port: DEFAULT_RPC_PORT, + http_cors_origins: Vec::new(), + http_modules: Some(RpcModulesList::default()), max_connections: DEFAULT_RPC_MAX_CONNECTIONS, - http_cors_origins: None, max_event_page_size: DEFAULT_RPC_MAX_EVENT_PAGE_SIZE, + max_proof_keys: DEFAULT_RPC_MAX_PROOF_KEYS, } } } @@ -372,6 +398,11 @@ fn default_page_size() -> u64 { DEFAULT_RPC_MAX_EVENT_PAGE_SIZE } +#[cfg(feature = "server")] +fn default_proof_keys() -> u64 { + katana_node::config::rpc::DEFAULT_RPC_MAX_PROOF_KEYS +} + #[cfg(feature = "server")] fn default_metrics_addr() -> IpAddr { DEFAULT_METRICS_ADDR diff --git a/crates/katana/cli/src/utils.rs b/crates/katana/cli/src/utils.rs index 28cc0ffddc..eb8457166c 100644 --- a/crates/katana/cli/src/utils.rs +++ b/crates/katana/cli/src/utils.rs @@ -15,7 +15,8 @@ use katana_primitives::genesis::constant::{ }; use katana_primitives::genesis::json::GenesisJson; use katana_primitives::genesis::Genesis; -use serde::{Deserialize, Serialize}; +use katana_rpc::cors::HeaderValue; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; use tracing::info; use crate::args::LOG_TARGET; @@ -191,6 +192,33 @@ PREFUNDED ACCOUNTS } } +pub fn serialize_cors_origins(values: &[HeaderValue], serializer: S) -> Result +where + S: Serializer, +{ + let string = values + .iter() + .map(|v| v.to_str()) + .collect::, _>>() + .map_err(serde::ser::Error::custom)? + .join(","); + + serializer.serialize_str(&string) +} + +pub fn deserialize_cors_origins<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + String::deserialize(deserializer)? + .split(',') + .map(|s| s.trim()) + .filter(|s| !s.is_empty()) + .map(HeaderValue::from_str) + .collect::, _>>() + .map_err(serde::de::Error::custom) +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/katana/contracts/Makefile b/crates/katana/contracts/Makefile index 3a9b0e5de5..792c2b7c8c 100644 --- a/crates/katana/contracts/Makefile +++ b/crates/katana/contracts/Makefile @@ -11,4 +11,13 @@ $(BUILD_DIR): ./account/src/* scarb build -p katana_account mv target/dev/$(ORIGINAL_CLASS_NAME) $(BUILD_DIR)/$(CLASS_NAME) +## ---- Piltover contracts + +ORIGINAL_CLASS_NAME := piltover_appchain$(CONTRACT_CLASS_SUFFIX) +CLASS_NAME := appchain_core_contract.json + +$(BUILD_DIR): ./piltover/src/* + cd piltover && scarb build + mv target/dev/$(ORIGINAL_CLASS_NAME) $(BUILD_DIR)/$(CLASS_NAME) + ## ---- diff --git a/crates/katana/contracts/build/appchain_core_contract.json b/crates/katana/contracts/build/appchain_core_contract.json new file mode 100644 index 0000000000..b71c4cffb6 --- /dev/null +++ b/crates/katana/contracts/build/appchain_core_contract.json @@ -0,0 +1 @@ +{"sierra_program":["0x1","0x6","0x0","0x2","0x8","0x0","0x84a","0x7b6","0x157","0x52616e6765436865636b","0x800000000000000100000000000000000000000000000000","0x436f6e7374","0x800000000000000000000000000000000000000000000002","0x1","0xe","0x2","0x496e76616c6964206e5f61637475616c5f75706461746573","0x6","0x8000000000000110000000000000000","0x800000000000000000000000000000000000000000000003","0x7","0x7c","0x553132384d756c47756172616e746565","0x8","0x9","0x75313238","0x800000000000000700000000000000000000000000000000","0x537472756374","0x800000000000000700000000000000000000000000000003","0x0","0x25e2ca4b84968c2d8b83ef476ca8549410346b00836ce79beaf538155990bb2","0x4e6f6e5a65726f","0x800000000000000700000000000000000000000000000001","0x21","0x556e696e697469616c697a6564","0x800000000000000200000000000000000000000000000001","0x6f","0x4172726179","0x800000000000000300000000000000000000000000000001","0x800000000000000300000000000000000000000000000002","0x11d42572a2f68d02771f7e7ec67f66ddcc590fa62a7dbd1eaaf065c9b126e93","0xc","0x66656c74323532","0x800000000000000f00000000000000000000000000000001","0x2ee1e2b1b89f8c495f200e4956278a4d47395fe262f27b52e5865c9524c08c3","0x456e756d","0x11c6d8087e00642489f92d2821ad6ebd6532ad1a3b6d12833da6d6810391511","0xf","0x4d","0x800000000000000300000000000000000000000000000005","0x2fa0fe4e21198b84cc7a4d3b7c5d7fe6796fecd979e7ac84cee75efca162d82","0x10","0x11","0x800000000000000300000000000000000000000000000003","0xd","0x12","0x16a4c8d7c05909052238a862d8cc3e7975bf05a07b3a69c6b28951083a6d672","0x14","0x1064596b8313597f6d64f5324cf677f2c3e7759e71eb9fbe857938be57d7180","0x13","0x15","0x32ae48d6764693e076d982989640a356d60f77ede38bc686485a133d5845fda","0x6e5f627974657320746f6f20626967","0x1000000000000000000000000000000","0x10000000000000000000000000000","0x100000000000000000000000000","0x1000000000000000000000000","0x10000000000000000000000","0x100000000000000000000","0x1000000000000000000","0x10000000000000000","0x100000000000000","0x1000000000000","0x10000000000","0x100000000","0x1000000","0x10000","0x100","0x800000000000000700000000000000000000000000000011","0x14cb65c06498f4a8e9db457528e9290f453897bdb216ce18347fff8fef2cd11","0x426f756e646564496e74","0x800000000000000700000000000000000000000000000002","0x753332","0x800000000000000700000000000000000000000000000005","0x2845996390f6fd5f35c6c2fe0009767bff4314f21e625e5c68c033241cb0dc9","0x800000000000000700000000000000000000000000000004","0x2a954fc5bde4fc30b43c7c9266f15aa28cdc3cb3e3a1c5b66144769a722dbe9","0x2b","0x2c","0x20c577e8eeff6beb1e324874baf4430a040ca8324f3d8ceae764d615af63cda","0x2d","0xf0543ea4ecb370f31d9bdb832c88d493044c9d84f8c52b0fe5941d7300e5e1","0x32","0x8000000000000000","0x753634","0x4b656363616b206c61737420696e70757420776f7264203e3762","0x3d","0x3e","0x426f78","0x313d53fcef2616901e3fd6801087e8d55f5cb59357e1fc8b603b82ae0af064c","0x3f","0x1259acba616ea55122a5f5482c05de5e2627fadf6a30dd9102a67db79341e07","0x41","0x1f59a2873c5f1a7a3081ca249a7f0b205e6e5bd6f066896be33bc8e3c32c1ab","0x42","0x496e76616c6964206f75747075745f6e5f75706461746573","0x45","0x862805ba9ffc63618192421d9e798f61932ed29cab9f96a55af1b1d8d9177e","0x46","0x496e76616c6964206e5f6d657373616765735f746f5f6c31","0x496e76616c6964206e5f6d657373616765735f746f5f6c32","0x19b9ae4ba181a54f9e7af894a81b44a60aea4c9803939708d6cc212759ee94c","0x53746f726167654261736541646472657373","0x924583257a47dd83702b92d1bcf41027fba06c39486295102ef8c82b4f8b94","0x4b","0x27ac189be2665e35a306076d813d25df74772a62033822174b340faa0a188cd","0x132da7fba42c360cdd7375338f98bc94fdeb57076d71a4d74f6a87e8a8b5ab0","0x536e617073686f74","0x1baeba72e79e9db2587cf44fedb2f3700b2075a5e8e39a562584862c4b71f62","0x50","0x51","0x4f","0xae3d6fab824e6ad9e61c0c445293639b8da977444cdcb589d2aff7b506396c","0x52","0x800000000000000300000000000000000000000000000004","0x7da71e1dc546b96d9fd53438ce53f427347947c6c30c6495690af26972951f","0x54","0x7a6c7ac3fb5de49a2c8dcc9944d6a5732f5657c36221c0c621e0a9d1bc02e6","0x281848a2ead29305005a1178671c6a7d7780cb656c57678566ea8033dbfa001","0x2b38a2c0dec5d61d904ef74f7c761865623dab2fdc54bc366262ea43d32c565","0x13520bd3ff585a5aff71104609be0dc45b8417d2918b2fcb891e7c2b0cdd531","0x1c87269cd6a0b60092e8037bbd40c67deb46e8010df6ab043ae0e1143e8a674","0x2c0b6d6df6d05e293a459065a2c8f3d4a89f557f2df8c165b79d3b19871a8cb","0x1652261cbe947bf564ad527e57c737d464b5d17befd71fae8f2ab14ac46dd45","0x398697ad8745b865eefbaa4afad41e52876386afeeb69101604d297801076ef","0x436f6e747261637441646472657373","0x800000000000000700000000000000000000000000000007","0x26beda48e712fe3212c956dbb4745ca7b022a0f0cc0e8c0d25e0c4131b7961e","0x5e","0x2d6fa3de4eef6e881130d75050d779ac7536f93cc4d7d0ec4b8f0012a0d31d","0x4fcf26fccf2443b603beea55dda009bcc565292d60f77422a6b6b4cafd9ff","0x2db340e6c609371026731f47050d3976552c89b4fbb012941663841c59d1af3","0x264029018ff7e3c0552db60eb00dd04eddf84c86e9b06640ce3731b70dc0bd7","0x1390fd803c110ac71730ece1decfc34eb1d0088e295d4f1b125dda1e0c5b9ff","0x2f299fcd816fb55f80a1424db439dd511bc7ad82a66cc659834273084e02cff","0x27f894093975d7c219019eb13b34537f76f17f53008ed7c10470e65ecf02801","0x35568983bf34e06b8e03dff7b9f9de47bc7551a325ed6e63e9ea158faa7c666","0x27e52c4726206888daafd5df6c3f35e1053e803f8844a48172b255ca33f069","0x2049c4157e50f4e4f9e1aac5f369f82789a0e612b8e0989eba981d4d0900f35","0x66","0x65","0xec","0x18ef5e2178ac6be59ceafd15e6995810f636807e02c51d309c3f65e37000fc5","0x6a","0x7533325f616464204f766572666c6f77","0x100000000000000000000000000000000","0x4f7074696f6e3a3a756e77726170206661696c65642e","0x2f23416cc60464d4158423619ba713070eb82b686c9d621a22c67bd37f6e0a9","0x70","0x52657475726e6564206461746120746f6f2073686f7274","0x150f46fb03d864e199f98f62a4bbcd080dec8c14cf1d4da54a05447cf5c7656","0x74","0x75","0x292c9ddb3b6fae4fc8ad7bb478be65dbb933c0ba77f1716f207bbc46ada5b07","0x76","0x3731d7667bf026b9960bf90daef8b575526345deb176f45e77fce4127cb8028","0x753235365f616464204f766572666c6f77","0x7b","0x7d","0x3f829a4bc463d91621ba418d447cc38c95ddc483f9ccfebae79050eb7b3dcb6","0x7e","0x25e50662218619229b3f53f1dc3253192a0f68ca423d900214253db415a90b4","0x80","0x82","0x38b507bf259d96f5c53e8ab8f187781c3d096482729ec2d57f3366318a8502f","0x83","0x84","0x3c5ce4d28d473343dbe52c630edf038a582af9574306e1d609e379cd17fc87a","0x85","0x20dcfcf53560163d81c9052504a9ba78f19d0f7972e061d35f00406cc3dbf1","0x87","0x496e76616c6964206e5f626c6f6273","0x7533325f6d756c204f766572666c6f77","0x5","0x3a83b8b7bb593ebe343e178cb6461960c41c4904f4ce1fa2311ceb91f4848c9","0x8f","0x321b4756e8d4a021fe0626b6e2633cb74e27863210848a86f1d3ea34c5a8ef9","0x91","0x93","0x3ea1097d9afe38b3dad8f42cb24d87580aec7172bf5f7f51a930b28e3494833","0x94","0xa","0x224a011038537aed66f7f74d128ab721bf80a42464aec2fc1677a24a212ad83","0x9b","0x3","0x5265656e7472616e637947756172643a207265656e7472616e742063616c6c","0x69780","0x165e64f39511287c00dbbffa4b5f1e8291fae244557fb4483faeee98fcbc924","0x800000000000000f00000000000000000000000000000003","0xa0","0x175076a3e478d199bb3c5645ff558bba7b6f9441adf8a21500ea879ff5bfd84","0xa1","0x53746174653a20696e76616c696420626c6f636b206e756d626572","0x53746174653a20696e76616c69642070726576696f757320726f6f74","0xfed86962538b8316aa56b15a5ebf51a691adc49b02fb9e742b1fcd6888ad60","0xa5","0xfe903c729956c13f663785ce1d499a52f0f5cf95931ee0bc963dff6cf6d963","0xa6","0x1945cb4f9fc617bec60c0b217151cb3099396a5db0c7c49409b9e0e85c4fbae","0xa8","0xd1e9234a5407f97edf31ede6f78d9398757ec7550bc1775a2e4bf64195adab","0xa9","0x43414e43454c4c4154494f4e5f4e4f545f524551554553544544","0x53746f7265553634202d206e6f6e20753634","0x7536345f616464204f766572666c6f77","0x43414e43454c5f414c4c4f5745445f54494d455f4f564552464c4f57","0x43414e43454c4c4154494f4e5f4e4f545f414c4c4f5745445f594554","0x21eb9c9dac8a0c77f263154323951a578904e81f20e46c3218cb7942587ed56","0x11a4edaf795586e09e79bc8a18af605ef6205fcf112e7067498f60ef982c054","0x4e4f5f4d4553534147455f544f5f43414e43454c","0x20fdb7068be7f8747cc8d1257f17180b584f6031931b3d812e173ec83d69395","0x3be21cb653a577e120092d2cff591fabab878e99c4b7022c727c67d57516cd8","0xf20bbe682ccce4d351f87b971c757a08f3de1ba673e93b6444bba1d5d1c830","0x3808c701a5d13e100ab11b6c02f91f752ecae7e420d21b56c90ec0a475cc7e5","0x494e56414c49445f4e4f4e4345","0x3b74868357065ca2f9fcfd3c23bbae27b4a77f6d9a6a32eae18e844f1cf5267","0x494e56414c49445f4d4553534147455f544f5f434f4e53554d45","0x3ae3c0242bd1c83caced6e5a82afedd0a39d6a01aa4f144085f91115f9678ee","0x2ffad8583696cb3c9b5b0470633f57abf53eef2d83772ee224628d3ac81434","0x32665c947abe83a1eae26bfeb854dc8aafb79dc81c0ccbb42e9db11769cea","0xbe","0xbb","0xb6","0xb2","0x60","0x5f","0x10203be321c62a7bd4c060d69539c1fbe065baa9e253c74d2cc48be163e259","0xc0","0x800000000000000700000000000000000000000000000006","0x3d7b6858fca4a019759239b7e5ae4375c4f227c3648f7b282d1e0f45be6d65f","0x967e9eb37c495862da2837f6b98b596ed1a02f98af3173f46a3f7e4cbb94c1","0xc3","0x436f6e6669673a206e6f74206f70657261746f72","0x436f6e6669673a20616c7265616479206f70657261746f72","0x2c4be14f60c29d8dedd01a1091ea8c1572e3277b511cfff4179da99e219457e","0x142dfc74e1f154626be92ec05c6ab84d1f7273785d3f4e7a58fe6654e67527b","0x436c61737348617368","0x142ea2d2fd5397fde7c79b95d51ea4a79991de55600cb7c1e6148f4a627dbc0","0xc9","0x358f4bf88951260abbc2ca3e111e2e32432b563fa321326f0a408b880755514","0xca","0x436c61737320686173682063616e6e6f74206265207a65726f","0x43616c6c6572206973206e6f7420746865206f776e6572","0x43616c6c657220697320746865207a65726f2061646472657373","0xb","0x67655f6f7574707574602e","0x686173683d3d2a736e6f735f6f75747075745f686173685f696e5f62726964","0x617373657274696f6e206661696c65643a2060736e6f735f6f75747075745f","0x736e6f733a20696e76616c696420636f6e6669672068617368","0x204a4855e6a132dba7a50006142d3dabb9a614f91c609a32d8afea9bf12504f","0x31448060506164e4d1df7635613bacfbea8af9c3dc85ea9a55935292a4acddc","0x104eb68e98232f2362ae8fd62c9465a5910d805fa88b305d1f7721b8727f04","0xd6","0x1ecc1916386442ed7a58f772a276477fddb70240b8db10c219005e61639bec7","0x800000000000000700000000000000000000000000000009","0x243d66a7d11b84a46f966e4cbbe1383e4c0f5d7d8c9c638884b11f758831242","0x69","0xcb","0xc4","0xbf","0x68","0x67","0xd4","0xd8","0x74584e9f10ffb1a40aa5a3582e203f6758defc4a497d1a2d5a89f274a320e9","0xda","0xdc","0x149ee8c97f9cdd259b09b6ca382e10945af23ee896a644de8c7b57da1779da7","0xdd","0x36775737a2dc48f3b19f9a1f4bc3ab9cb367d1e2e827cef96323826fd39f53f","0xdf","0x46a6158a16a947e5916b2a2ca68501a45e93d7110e81aa2d6438b1c57c879a3","0x17","0x742830292e73656375726974795f626974733e3530602e","0x6669636174696f6e735f666f725f666163745f686173682866616374292e61","0x792829207d0a2020202020202020202020202e6765745f616c6c5f76657269","0x3a2073656c662e636f6e6669672e6765745f66616374735f72656769737472","0x727944697370617463686572207b20636f6e74726163745f61646472657373","0x679ea9c5b65e40ad9da80f5a4150d36f3b6af3e88305e2e3ae5eccbc5743d9","0xe8","0x1f","0x617373657274696f6e206661696c65643a20602a4946616374526567697374","0x62797465733331","0x34f0613049045c266383e4ac7cbb97ab8e62b4aa2fb3e6193d1c2413dc35da6","0xf0","0x3560396eec4038e19aae5f46a3a54f9e2f765148d5cb5fe45469e2743875125","0x2ce4352eafa6073ab4ecf9445ae96214f99c2c33a29c01fcae68ba501d10e2c","0xf3","0x1db272a74ecc37960c3e9275c6035a460fcbd1e2c2466d0a7dbcbad6969b7d0","0x3c4930bb381033105f3ca15ccded195c90cd2af5baa0e1ceb36fde292df7652","0x80000000000000030000000000000000000000000000000f","0x33b425c09d59ffabb24db832ddbcebd2d46cd5ed48709c7639c9647d845a2ed","0xf7","0x3ced79530d961eab8cd2650c37e0ce6e62b09b57530cc19932a4894418a682b","0xf8","0x4","0x2a594b95e3522276fe0ac7ac7a7e4ad8c47eaa6223bc0fd6991aa683b7ee495","0xfb","0x2b6b19c99229f9bb40eacaca42412aa5886826db0370d2b4eccbcf0d8a9c690","0xfd","0x3cb2f98d5008ee3c98d5806d18642590f28e61529f368306790d3de548d2101","0xfe","0x506564657273656e","0x104","0x506f736569646f6e","0x106","0x53797374656d","0x108","0x7533325f737562204f766572666c6f77","0x496e646578206f7574206f6620626f756e6473","0x24ce53d258db8152433da18abe389a2d249e90024bd355a4a65c9858e271fa4","0x10e","0x3e7f3c4bb6a12499ffa6e9273a47156242a6e802675febe9824b8bd1f679d6e","0x10f","0x2bda0d002c23d20ced3d8c45905bff0b9f44d42c73ba14aeaee267c4f93bc61","0x111","0xcef8c730813130d9536fa56b99308ed22cd664d06eeb3baa4b48fe071b83ca","0x112","0x1425220ff641e1beddd94608c8fbef66e91dc5632606ce24f491b72ec6885bb","0x114","0x33c7e3c664f048f9b91ade32b9b6f2208049789ea43ddfb0f4130b34ce6cc79","0x115","0x26c65095bfb08b3274b3ac4b521f2869c41527083a4068d7a1c4a1d65d4fd37","0x2c45a9aa512e25f827510889c929da2b4fb227873669963a3f1d04a8fab4454","0x221bfd5f4fb41e1fbb1c6d1ccbe9292675550a532bc5eb87520abf1e41e4cb9","0x145cc613954179acf89d43c94ed0e091828cbddcca83f5b408785785036d36d","0xb5bead4e6ae52c02db5eed7e8c77847e0a0464a2c43ebf6aef909306904b0","0x5c9e3c4155c5daf6b4dab2e30fd404b83901058147c3117990e8a5c241a4df","0xebbde18a014f8151bceda0d5acadf19790c9eb9a708899433a2bccff4ba84b","0x11d","0x4e6f6e20436f6e747261637441646472657373","0x183a1b309b77fa43aa409ee3681db27df849965d2e5d22fb671795a0d00c912","0x436f6e6669673a206e6f74206f776e6572206f72206f70657261746f72","0x23599d49a57f3c0ce2ab859a3e14edf2626886da731027d327af94b70f27d0a","0x3288d594b9a45d15bb2fcb7903f06cdb06b27f0ba88186ec4cfaa98307cb972","0x122","0x123","0x39634dfd119414deb5b2e8d75e5740c30ed3b55dada24abd2b60f0c3e6d8d99","0x124","0x12b","0xb9","0x12c","0x128","0x1597b831feeb60c71f259624b79cf66995ea4f7e383403583674ab9c33b9cec","0x129","0x80000000000000070000000000000000000000000000000e","0x348a62b7a38c0673e61e888d83a3ac1bf334ee7361a8514593d3d9532ed8b39","0x12a","0x3342418ef16b3e2799b906b1e4e89dbb9b111332dd44f72458ce44f9895b508","0x7d4d99e9ed8d285b5c61b493cedb63976bc3d9da867933d829f49ce838b5e7","0x127","0x126","0x12d","0x130","0x7538","0x177c3fee7303058660804b7dcaf6dab6ba6b1c4aa194cbac2aaf483fd2e0334","0x53746f7261676541646472657373","0x28a1868d4e0a4c6ae678a74db4e55a60b628ba8668dc128cf0c8e418d0a7945","0x100f2d3e4f2a12a441968d33f8d36e4e4f47d1dcfe7fd47fefd4173d5600b92","0xde34324aa1d709b29de56e4149b428056584fb9ec077fec97db88b6fa6591c","0x1f5ef0e74ce255cfb3218e020e4425269d35845f07d4bea6343ad78a1f5333b","0x13a","0x1b6c4e38fb7bbf86202e9783bf1b2203cc2c2c6becc501182cb4af93aca4e7e","0x13c","0x5d56621473b32150ea57e69023bdab664833d1e812b459c6467600a40c984","0x13d","0x800000000000000f00000000000000000000000000000002","0xcc5e86243f861d2d64b08c35db21013e773ac5cf10097946fe0011304886d5","0x13f","0x4661696c656420746f20646573657269616c697a6520706172616d202331","0x4661696c656420746f20646573657269616c697a6520706172616d202332","0x4661696c656420746f20646573657269616c697a6520706172616d202333","0x4661696c656420746f20646573657269616c697a6520706172616d202334","0x4f7574206f6620676173","0x800000000000000f00000000000000000000000000000007","0x372baf89c42b69014eaf3f2676192760e9e48795a70511bef987676d4b12eda","0x147","0xd64222d8555edb2fb14f4c3cb99daa79280f5e194d864ec5e79de992bd054f","0x148","0x4275696c74696e436f737473","0x496e70757420746f6f206c6f6e6720666f7220617267756d656e7473","0x12867ecd09c884a5cf1f6d9eb0193b4695ce3bb3b2d796a8367d0c371f59cb2","0x1d49f7a4b277bf7b55a2664ce8cef5d6922b5ffb806b89644b9e0cdbbcac378","0x14d","0x13fdd7105045794a99550ae1c4ac13faa62610dfab62c16422bfcf5803baa6e","0x14e","0x9931c641b913035ae674b400b61a51476d506bbe8bba2ff8a6272790aba9e6","0x146","0x17b6ecc31946835b0d9d92c2dd7a9c14f29af0371571ae74a1b228828b2242","0x151","0x34f9bd7c6cb2dd4263175964ad75f1ff1461ddc332fbfb274e0fb2a5d7ab968","0x152","0x29d7d57c04a880978e7b3689f6218e507f3be17588744b58dc17762447ad0e7","0x154","0x4761734275696c74696e","0x3a2","0x7265766f6b655f61705f747261636b696e67","0x77697468647261775f676173","0x6272616e63685f616c69676e","0x7374727563745f6465636f6e737472756374","0x656e61626c655f61705f747261636b696e67","0x73746f72655f74656d70","0x61727261795f736e617073686f745f706f705f66726f6e74","0x656e756d5f696e6974","0x155","0x6a756d70","0x7374727563745f636f6e737472756374","0x656e756d5f6d61746368","0x64697361626c655f61705f747261636b696e67","0x756e626f78","0x61727261795f6e6577","0x72656e616d65","0x156","0x66756e6374696f6e5f63616c6c","0x153","0x150","0x64726f70","0x14f","0x75313238735f66726f6d5f66656c74323532","0x14c","0x636f6e73745f61735f696d6d656469617465","0x14b","0x61727261795f617070656e64","0x6765745f6275696c74696e5f636f737473","0x14a","0x77697468647261775f6761735f616c6c","0x149","0x736e617073686f745f74616b65","0x145","0x144","0x143","0x142","0x141","0x636c6173735f686173685f7472795f66726f6d5f66656c74323532","0x140","0x16","0x13e","0x21adb5788e32c84f69a1863d85ef9394b7bf761a0ce1190f826984e5075c371","0x13b","0x18","0x636f6e74726163745f616464726573735f746f5f66656c74323532","0x139","0x138","0x706564657273656e","0xad292db4ff05a993c318438c1b6c8a8303266af2da151aa28ccece6726f1f1","0x137","0x73746f726167655f616464726573735f66726f6d5f62617365","0x135","0x73746f726167655f726561645f73797363616c6c","0x66656c743235325f69735f7a65726f","0x134","0x626f6f6c5f6e6f745f696d706c","0x133","0x132","0x19","0x73746f726167655f626173655f616464726573735f636f6e7374","0x1fc38e80e47fef63a2bb3cd54e82088afb5043235c79dfc195ca18c3ad3af5a","0x131","0x647570","0x136","0x12f","0x2679d68052ccd03a53755ca9169677965fbd93e489df62f5f40d4f03c24f7a4","0x6765745f657865637574696f6e5f696e666f5f76325f73797363616c6c","0x12e","0x1a","0x125","0x121","0x2b7fa3184fd643e0dae8c68f176e7a315c91767ef3c1c2144e3d007f6f4eb23","0x73746f726167655f77726974655f73797363616c6c","0x120","0x11f","0x1b","0x11e","0x11c","0x11b","0x11a","0x119","0x118","0x117","0x1c","0x116","0x1d","0x1e","0x113","0x20","0x110","0xa3e35b50432cd1669313bd75e434458dd8bc8d21437d2aa29d6c256f7b13d1","0x27f5e07830ee1ad079cf8c8462d2edc585bda982dbded162e761eb7fd71d84a","0x3fd94528f836b27f28ba8d7c354705dfc5827b048ca48870ac47c9d5b9aa181","0x10d","0x66656c743235325f737562","0x7533325f7472795f66726f6d5f66656c74323532","0x61727261795f736c696365","0x61727261795f6c656e","0x7533325f6f766572666c6f77696e675f737562","0x10c","0x10b","0x616c6c6f635f6c6f63616c","0x66696e616c697a655f6c6f63616c73","0x22","0xff","0x73746f72655f6c6f63616c","0x103","0x10a","0x109","0x101","0x102","0x107","0x23","0xfc","0xfa","0x61727261795f676574","0x24","0xf9","0xf6","0xf5","0x25","0xf4","0xf2","0x26","0xf1","0xee","0xed","0xeb","0xea","0x27","0xe9","0xe7","0xe6","0xe5","0xe4","0xe3","0xe2","0xe1","0xe0","0x7374727563745f736e617073686f745f6465636f6e737472756374","0x7533325f746f5f66656c74323532","0xde","0x28","0xdb","0xd9","0x29","0xd7","0x656d69745f6576656e745f73797363616c6c","0x587f8a359f3afbadaac7e3a22b5d00fa5f08794c82353701e04afb0485d8c1","0x626f6f6c5f746f5f66656c74323532","0xd5","0xd3","0xd2","0xd1","0xd0","0xcf","0x105","0x2bd557f4ba80dfabefabe45e9b2dd35db1b9a78e96c72bc2b69b655ce47a930","0xce","0xcd","0x636c6173735f686173685f746f5f66656c74323532","0xcc","0x7265706c6163655f636c6173735f73797363616c6c","0xc8","0xc7","0xc6","0xc5","0x3f97d998859e8c6799ce31da96c5c5cd0d5d94c61e88afdfbcdcdb79c7a5689","0x66656c743235325f616464","0x2a","0xc1","0xbd","0xc2","0xbc","0xba","0xb8","0xb7","0x7536345f746f5f66656c74323532","0xb5","0xb4","0x7536345f7472795f66726f6d5f66656c74323532","0xb3","0x7536345f6571","0x236ec444c5e8e362b257b8ba6b3f8e04df5d7f0c39ff8dc4987411200cd7e3d","0x7536345f6f766572666c6f77696e675f616464","0x7536345f6f766572666c6f77696e675f737562","0xb1","0xb0","0xaf","0xae","0xad","0xac","0xab","0xaa","0xa7","0xa4","0xa3","0x2e","0xa2","0x9f","0x9e","0x68616465735f7065726d75746174696f6e","0x9d","0x2f","0x9c","0x9a","0x99","0x98","0x97","0x96","0x7533325f776964655f6d756c","0x646f776e63617374","0x30","0x95","0x31","0x92","0x90","0x8e","0x8d","0x8c","0x8b","0x8a","0x757063617374","0x33","0x88","0x34","0x86","0x35","0x81","0x7f","0x6b656363616b5f73797363616c6c","0x753132385f6f766572666c6f77696e675f616464","0x7a","0x79","0x78","0x63616c6c5f636f6e74726163745f73797363616c6c","0x36","0x77","0x73","0x7533325f6571","0x7533325f6f766572666c6f77696e675f616464","0x72","0x37","0x71","0x753132385f69735f7a65726f","0x6e","0x753132385f736166655f6469766d6f64","0x753132385f746f5f66656c74323532","0x6d","0x66656c743235325f6d756c","0x627974657333315f7472795f66726f6d5f66656c74323532","0x6c","0x6b","0x627974657333315f746f5f66656c74323532","0x64","0x63","0x62","0x61","0x5d","0x38","0x5c","0x39","0x5b","0x3a","0x5a","0x3b","0x59","0x3c","0x58","0x57","0x56","0x55","0x40","0x53","0x4e","0x636f6e74726163745f616464726573735f636f6e7374","0x23e69db976c64677f931404ca1c9ac66300309fcf9cc2887884860e1b525846","0x4c","0x4a","0x61727261795f706f705f66726f6e74","0x49","0x48","0x47","0x44","0x43","0x7533325f736166655f6469766d6f64","0x7536345f69735f7a65726f","0x7536345f736166655f6469766d6f64","0x656e756d5f66726f6d5f626f756e6465645f696e74","0x89","0x753235365f736166655f6469766d6f64","0x753132385f6d756c5f67756172616e7465655f766572696679","0x753132385f6571","0x753132385f6f766572666c6f77696e675f737562","0x37ed","0xffffffffffffffff","0x1e5","0x16f","0x174","0x1d4","0x1d0","0x18b","0x1c2","0x1b6","0x1b0","0x1bc","0x1d8","0x268","0x201","0x206","0x256","0x252","0x21e","0x243","0x23b","0x25a","0x2ec","0x285","0x28a","0x2da","0x2d6","0x2a2","0x2c7","0x2bf","0x2de","0x39c","0x309","0x30e","0x38a","0x386","0x326","0x377","0x36d","0x34e","0x353","0x35c","0x360","0x38e","0x43d","0x3b9","0x3be","0x42c","0x3c8","0x3cd","0x41a","0x3e3","0x409","0x401","0x4b8","0x461","0x4ab","0x49c","0x496","0x4a2","0x57c","0x4d4","0x4d9","0x56a","0x566","0x4f1","0x557","0x542","0x538","0x51b","0x549","0x531","0x54e","0x56e","0x5e9","0x5a0","0x5dc","0x5cf","0x5c5","0x5d4","0x6cd","0x605","0x60a","0x6ba","0x6b6","0x617","0x61c","0x6a3","0x697","0x683","0x63e","0x670","0x667","0x6be","0x77a","0x6eb","0x6f0","0x769","0x705","0x75a","0x750","0x72e","0x732","0x73d","0x744","0x826","0x797","0x79c","0x815","0x7b1","0x806","0x7fc","0x7da","0x7de","0x7e9","0x7f0","0x8dc","0x843","0x848","0x8c9","0x8c5","0x8ba","0x8a7","0x86b","0x896","0x88d","0x8cd","0x9e3","0x8fa","0x8ff","0x9d0","0x9cc","0x90c","0x911","0x9b9","0x9ad","0x999","0x925","0x92a","0x984","0x943","0x970","0x967","0x9d4","0xaea","0xa01","0xa06","0xad7","0xad3","0xa13","0xa18","0xac0","0xab4","0xaa0","0xa2c","0xa31","0xa8b","0xa4a","0xa77","0xa6e","0xadb","0xb5d","0xb56","0xb47","0xb18","0xb39","0xb32","0xbf0","0xb80","0xbe3","0xbd6","0xbd0","0xbc9","0xbda","0xce0","0xc0c","0xc11","0xccf","0xccb","0xc1e","0xc23","0xcba","0xc2d","0xc32","0xca8","0xc3c","0xc41","0xc95","0xc58","0xc83","0xc7c","0xcd3","0xd27","0xcfd","0xd09","0xd0e","0xd1c","0xd3d","0xd42","0xd94","0xd8b","0xd7e","0xd6f","0xd63","0xef","0x1337","0x130e","0x12ef","0xdee","0x132d","0x12d0","0x12aa","0x1217","0x11fa","0x11de","0x11ba","0x11a2","0x118a","0x115f","0x113e","0x1116","0x10f3","0x10d3","0x10ad","0xf71","0xf67","0xf5d","0xf53","0xf49","0xf3f","0xf35","0xf25","0xf2a","0x158","0x159","0x15a","0x1087","0x15b","0x15c","0x106d","0x15d","0x15e","0x15f","0x160","0x161","0x162","0x105e","0x163","0x164","0x165","0x166","0x167","0x168","0x169","0x16a","0x16b","0x104c","0x16c","0x16d","0x16e","0x170","0x171","0x172","0x173","0x175","0x176","0x177","0x178","0x179","0x103a","0x17a","0x17b","0x17c","0x17d","0x17e","0x17f","0x180","0x181","0x182","0x183","0x184","0x185","0x186","0x187","0x1027","0x188","0x189","0x18a","0x18c","0x18d","0x18e","0x18f","0x190","0x191","0x192","0x193","0x194","0x1013","0x195","0x196","0x197","0x198","0x199","0x19a","0x19b","0x19c","0x19d","0x19e","0x19f","0x1a0","0x1a1","0x1004","0x1a2","0x1a3","0x1a4","0x1a5","0x1a6","0x1a7","0x1a8","0x1a9","0x1aa","0x1ab","0x1ac","0x1ad","0xff3","0x1ae","0x1af","0x1b1","0x1b2","0x1b3","0x1b4","0x1b5","0x1b7","0x1b8","0x1b9","0x1ba","0x1bb","0x1bd","0x1be","0x1bf","0x1c0","0x1c1","0x1c3","0x1c4","0x1c5","0x1c6","0x1c7","0x1c8","0x1c9","0x1ca","0x1cb","0x1cc","0x10a2","0x1cd","0x1ce","0x1cf","0x1d1","0x1d2","0x1d3","0x1d5","0x1d6","0x1d7","0x1d9","0x1da","0x1db","0x1dc","0x1dd","0x1de","0x1df","0x1134","0x1e0","0x1e1","0x1e2","0x1e3","0x1e4","0x1e6","0x1e7","0x1e8","0x1e9","0x1ea","0x1eb","0x1ec","0x1ed","0x11d3","0x1ee","0x1ef","0x1f0","0x1f1","0x1f2","0x1f3","0x1f4","0x1f5","0x1f6","0x1f7","0x1f8","0x1f9","0x1fa","0x1fb","0x12a0","0x1fc","0x1fd","0x1fe","0x1ff","0x200","0x202","0x203","0x1296","0x204","0x205","0x207","0x208","0x209","0x20a","0x20b","0x128c","0x20c","0x20d","0x20e","0x20f","0x210","0x211","0x212","0x213","0x214","0x215","0x216","0x217","0x218","0x219","0x21a","0x21b","0x21c","0x21d","0x21f","0x127c","0x220","0x221","0x222","0x223","0x224","0x225","0x226","0x227","0x228","0x229","0x22a","0x22b","0x22c","0x22d","0x22e","0x22f","0x1281","0x230","0x231","0x232","0x233","0x234","0x235","0x236","0x237","0x238","0x239","0x23a","0x23c","0x23d","0x23e","0x23f","0x240","0x241","0x242","0x244","0x13bb","0x13b1","0x13a7","0x1389","0x1399","0x13c0","0x13de","0x141d","0x140d","0x1407","0x1414","0x14a5","0x1499","0x1454","0x1459","0x1488","0x147d","0x152b","0x151f","0x14da","0x14df","0x14f3","0x1514","0x162e","0x1622","0x155d","0x1639","0x160e","0x1604","0x15ee","0x15e3","0x15d2","0x15c1","0x15bb","0x15c8","0x15fa","0x1618","0x16b0","0x16a4","0x1664","0x1691","0x1699","0x1686","0x168b","0x16b7","0x179c","0x1788","0x1774","0x1764","0x1750","0x1748","0x173a","0x1759","0x188d","0x187c","0x186a","0x1804","0x1856","0x184e","0x1841","0x185f","0x18c6","0x19ce","0x19bc","0x19a8","0x198f","0x197b","0x1968","0x1955","0x194e","0x195d","0x1be0","0x1bce","0x1bba","0x1ba1","0x1b8a","0x1b78","0x1b60","0x1b48","0x1b35","0x1b1c","0x1b03","0x1aef","0x1ad8","0x1ac5","0x1abe","0x1ab1","0x1acd","0x1b56","0x1b97","0x1c0e","0x1c13","0x1ecc","0x1c1e","0x1c23","0x1eb6","0x1c2e","0x1c33","0x1ea0","0x1c3e","0x1c43","0x1e8a","0x1c4e","0x1c53","0x1e74","0x1c5e","0x1c63","0x1e5e","0x1c6e","0x1c73","0x1e48","0x1c7e","0x1c83","0x1e32","0x1c8e","0x1c93","0x1e1c","0x1c9e","0x1ca3","0x1e06","0x1cac","0x1cb1","0x1cd9","0x1cc6","0x1ce1","0x1df0","0x1ceb","0x1cf0","0x1d19","0x1d06","0x1d22","0x1dda","0x1d2c","0x1d31","0x1d5a","0x1d47","0x1d63","0x1dc4","0x1d6d","0x1d72","0x1d9b","0x1d88","0x1da4","0x1daf","0x1f9d","0x1f92","0x1f87","0x1f75","0x1f69","0x1f5d","0x1f4d","0x1f43","0x201e","0x200c","0x1ff7","0x1feb","0x1fe0","0x2003","0x2078","0x2042","0x2047","0x2069","0x205f","0x20f7","0x208f","0x2094","0x20e5","0x209f","0x20a4","0x20d2","0x20c0","0x2371","0x2365","0x2352","0x233f","0x2148","0x218e","0x2333","0x2320","0x230d","0x22fa","0x2179","0x217d","0x22ee","0x22e2","0x21cc","0x21c0","0x21b4","0x21d6","0x22ce","0x22b9","0x22a3","0x228c","0x2274","0x225b","0x2241","0x2227","0x24ba","0x24af","0x24a4","0x249b","0x245","0x246","0x247","0x23c7","0x248","0x23cb","0x249","0x24a","0x23d9","0x23df","0x23e6","0x23f8","0x24b","0x23f0","0x24c","0x2489","0x247a","0x2472","0x246c","0x24d","0x242f","0x2435","0x243d","0x244f","0x2447","0x245b","0x24e","0x24f","0x250","0x251","0x2481","0x253","0x24c4","0x254","0x255","0x2522","0x24e5","0x24ea","0x2510","0x257","0x258","0x259","0x2509","0x25b","0x25c","0x2504","0x25d","0x25e","0x25f","0x2516","0x260","0x261","0x262","0x2882","0x263","0x264","0x2871","0x27f7","0x2794","0x2784","0x265","0x26f4","0x2615","0x2557","0x255b","0x2601","0x266","0x267","0x25f5","0x269","0x2575","0x26a","0x26b","0x260f","0x26c","0x26d","0x25e5","0x26e","0x26f","0x25b5","0x25a6","0x259a","0x25c0","0x25e2","0x25d7","0x270","0x25cb","0x271","0x2698","0x272","0x273","0x261e","0x2622","0x26e3","0x2638","0x26ee","0x26d3","0x26c6","0x26b5","0x2683","0x2674","0x2668","0x268e","0x26b2","0x26a7","0x269b","0x274","0x274d","0x26fc","0x2700","0x2770","0x2738","0x2729","0x271d","0x2743","0x276d","0x2762","0x2756","0x275","0x276","0x277e","0x27c2","0x27b6","0x27ad","0x27cd","0x27f1","0x27e9","0x27dd","0x2867","0x282f","0x2821","0x2816","0x283b","0x2861","0x2857","0x2847","0x277","0x28b6","0x278","0x279","0x2897","0x27a","0x27b","0x27c","0x289c","0x27d","0x27e","0x28ab","0x27f","0x280","0x281","0x282","0x283","0x284","0x286","0x287","0x28ec","0x28f9","0x292a","0x296e","0x2970","0x2972","0x298a","0x288","0x28d9","0x289","0x28b","0x28c","0x28e9","0x28d","0x28e","0x28f","0x299c","0x290","0x291","0x292","0x293","0x294","0x295","0x296","0x297","0x298","0x2937","0x2942","0x294d","0x2958","0x2963","0x299","0x29a","0x29b","0x29c","0x29d","0x29e","0x29f","0x2a0","0x2a1","0x2a3","0x2a4","0x2a5","0x2a6","0x2a7","0x2a8","0x2a9","0x2aa","0x2ab","0x2ac","0x2ad","0x2ae","0x2af","0x2b0","0x2b1","0x2b2","0x2b3","0x2b4","0x2b5","0x2b6","0x2b7","0x2b8","0x29de","0x2b9","0x29d7","0x2ba","0x2bb","0x2bc","0x2bd","0x2be","0x2a1e","0x2a17","0x2a5b","0x2a34","0x2c0","0x2c1","0x2c2","0x2c3","0x2c4","0x2a53","0x2c5","0x2c6","0x2a49","0x2c8","0x2ac3","0x2a79","0x2c9","0x2ca","0x2cb","0x2cc","0x2a85","0x2a8a","0x2aa6","0x2a94","0x2a99","0x2aa0","0x2cd","0x2ce","0x2aaa","0x2cf","0x2d0","0x2ab8","0x2d1","0x2d2","0x2d3","0x2d4","0x2b5f","0x2d5","0x2d7","0x2d8","0x2b4f","0x2b42","0x2b35","0x2d9","0x2db","0x2b25","0x2b1f","0x2dc","0x2dd","0x2b2c","0x2df","0x2b57","0x2be2","0x2e0","0x2b91","0x2b7f","0x2e1","0x2e2","0x2b97","0x2e3","0x2e4","0x2e5","0x2e6","0x2bd7","0x2e7","0x2e8","0x2ba2","0x2ba7","0x2e9","0x2bca","0x2bb8","0x2ea","0x2eb","0x2bfc","0x2c01","0x2c98","0x2c8a","0x2c13","0x2c17","0x2c83","0x2c2e","0x2c33","0x2c73","0x2c64","0x2c44","0x2c48","0x2c5d","0x2ed","0x2ee","0x2ef","0x2f0","0x2f1","0x2f2","0x2f3","0x2cb0","0x2cb5","0x2cf4","0x2ce6","0x2cc7","0x2ccb","0x2f4","0x2f5","0x2ce0","0x2f6","0x2f7","0x2f8","0x2f9","0x2fa","0x2fb","0x2d0c","0x2d11","0x2d52","0x2d44","0x2d23","0x2d27","0x2fc","0x2fd","0x2fe","0x2d3e","0x2ff","0x300","0x301","0x302","0x303","0x2db9","0x2db0","0x2da1","0x2d7d","0x2d81","0x2d91","0x304","0x305","0x306","0x307","0x2dff","0x308","0x2dd4","0x30a","0x30b","0x30c","0x2dd9","0x30d","0x2df5","0x30f","0x310","0x311","0x2ded","0x312","0x313","0x314","0x315","0x316","0x317","0x318","0x319","0x2e96","0x2e67","0x2e61","0x2e5b","0x2e55","0x2e4f","0x2e49","0x2e45","0x31a","0x31b","0x31c","0x31d","0x2e4d","0x31e","0x31f","0x2e53","0x320","0x2e59","0x321","0x2e5f","0x322","0x2e65","0x323","0x2e6b","0x324","0x325","0x2e7e","0x2e86","0x2e9c","0x327","0x2ebb","0x328","0x2ead","0x329","0x32a","0x2eca","0x32b","0x32c","0x2f4e","0x2ee7","0x32d","0x32e","0x32f","0x330","0x2ef3","0x2ef8","0x2f30","0x2f00","0x2f05","0x2f26","0x2f22","0x331","0x332","0x2f19","0x333","0x334","0x335","0x2f35","0x336","0x2f2a","0x337","0x2f43","0x338","0x339","0x33a","0x33b","0x2fb7","0x33c","0x33d","0x33e","0x2f68","0x2f6d","0x2f72","0x2f77","0x2f7c","0x2f81","0x2f86","0x2f8b","0x2f90","0x2f95","0x2f9a","0x2f9f","0x2fa4","0x2fa9","0x2fae","0x2fb2","0x33f","0x340","0x341","0x342","0x343","0x344","0x345","0x346","0x347","0x348","0x349","0x34a","0x34b","0x34c","0x34d","0x34f","0x350","0x351","0x352","0x354","0x355","0x356","0x3013","0x357","0x358","0x359","0x3051","0x35a","0x35b","0x30a9","0x35d","0x35e","0x3102","0x35f","0x361","0x3141","0x362","0x363","0x3199","0x31e9","0x31e0","0x364","0x365","0x31d1","0x31c2","0x366","0x367","0x368","0x369","0x3241","0x3238","0x3229","0x321a","0x36a","0x325e","0x3263","0x32da","0x326e","0x3273","0x32cf","0x36b","0x36c","0x32c4","0x3283","0x3288","0x32a6","0x329d","0x32ae","0x36e","0x36f","0x32b9","0x370","0x371","0x372","0x373","0x374","0x375","0x3344","0x3310","0x32fc","0x3316","0x376","0x378","0x3338","0x379","0x37a","0x332e","0x37b","0x37c","0x37d","0x340f","0x337c","0x3369","0x3382","0x37e","0x37f","0x3403","0x338d","0x3392","0x33f0","0x339a","0x33ac","0x33a3","0x33a8","0x33dc","0x33b3","0x33b8","0x33c8","0x380","0x381","0x382","0x383","0x384","0x385","0x345d","0x3452","0x3443","0x3439","0x344c","0x3467","0x349d","0x3491","0x3483","0x34b4","0x34b9","0x3505","0x34c3","0x34c8","0x34fe","0x34d2","0x34d7","0x34f6","0x34e1","0x34e6","0x34ee","0x387","0x388","0x389","0x3535","0x3517","0x351c","0x352a","0x354c","0x3551","0x358c","0x3573","0x3562","0x3567","0x356d","0x357f","0x38b","0x3584","0x38c","0x38d","0x359f","0x35a4","0x3711","0x35ae","0x35b3","0x36ff","0x35bd","0x35c1","0x38f","0x390","0x391","0x392","0x3627","0x393","0x394","0x395","0x35f0","0x35f9","0x361f","0x3600","0x3605","0x360b","0x3624","0x396","0x397","0x398","0x364a","0x3631","0x3636","0x36eb","0x3640","0x3645","0x36d7","0x3658","0x36cc","0x399","0x36c4","0x3668","0x366c","0x39a","0x36bb","0x39b","0x36a6","0x368a","0x3698","0x3697","0x36af","0x39d","0x39e","0x39f","0x3a0","0x3a1","0x37dd","0x374a","0x3737","0x3750","0x37d1","0x375b","0x3760","0x37be","0x3768","0x377a","0x3771","0x3776","0x37aa","0x3781","0x3786","0x3796","0x3ab","0x44c","0x4c6","0x58b","0x5f7","0x6dd","0x789","0x835","0x8ec","0x9f3","0xafa","0xb6b","0xbfe","0xcee","0xd36","0xd9e","0x1358","0x13c8","0x1429","0x14af","0x1535","0x1642","0x16c0","0x17b0","0x18a2","0x19e4","0x1bf6","0x1ee2","0x1fa8","0x202d","0x2082","0x2107","0x237d","0x24cc","0x252c","0x288b","0x28c4","0x29a3","0x29e5","0x2a25","0x2a6a","0x2ad2","0x2b6b","0x2bf1","0x2ca7","0x2d03","0x2d61","0x2dc8","0x2e0d","0x2ed8","0x2f5d","0x2fc2","0x301a","0x3058","0x30b1","0x310a","0x3148","0x31a0","0x31f8","0x3250","0x32e6","0x3355","0x341f","0x346d","0x34ab","0x350b","0x3543","0x3592","0x3723","0x1d0e7","0x500c00500b00300a007009005004003008007006005004003002001000","0x500900501601501401301201101000500f00500400300200e00d00500d","0x500400300c00501b01a00d00501900500c00500b00300c005018017009","0x2502401502301301202202100502001501f01300600501e01d01900501c","0x500600500600502a01502901302800501e01d027005006005026015014","0x502401502d01303001502301302f00502e00502401502d01302c00502b","0x1a02700502b00503501501402503400503300503201502d025021005031","0x700900500400303700700900500400303600700600500400302b00501b","0x303b00700900500400303a007009005004003039007009005004003038","0x500400303e00700900500400303d00700900500400303c007009005004","0x700900500400304100700900500400304000700900500400303f007009","0x25045007009005004003044007009005004003043007009005004003042","0x5027005027005027005027005027005027005027005027005047015046","0x7015007049048027005027005027005027005027005027005027005027","0x504e01504d01300600500600500600500600504c01504b01301204a027","0x505000505301501402502700505200505101501402505000504f005006","0x3054005018017012056005007054005004003055007054005004003027","0x5004003043007054005004003044007054005004003045007054005004","0x7054005004003040007054005004003041007054005004003042007054","0x500400304f00501801705900505800500400305700700600500400303f","0x505d01501401302700505c00505b01501402500c00501805a02c00704f","0x505f01502d02502700502c00502e00505e00502401502901304f00504f","0x505e00502401502901302f00501e01d061007006005004003034005060","0x306500700600500400303400506400506301502d02502700502e005062","0x506901504901301206802700504f005067015014025066007006005004","0x1502d02502700502800506b01501402500600500600502401501401306a","0x502401502d01306f00506e01504901302100501806d02700502f00506c","0x504f00502100502401507401303400507300507201502d025071005070","0x700600500400307700700600500400303400507600507501502d025027","0x307b00700600500400307a007006005004003079007006005004003078","0x1301207f07e00700600500400307d00700600500400307c007006005004","0x508301504b013070005006005006005082005082005006005081015080","0x3085007006005004003084007006005004003070005082005082005006","0x13082005082005088015014013087007006005004003086007006005004","0x508c01501402508b01501802508a015018025082005082005089015014","0x700600500400302700509100509001501402508f00501805a08e00508d","0x15049013009005018017094007006005004003093007006005004003092","0x500400302b00704f00500400303400509600509501502d025009005024","0x502401502d01302700509900509801502d02505200501e01d097007006","0x500400309d00700600500400303400509c00509b01502d02509a005070","0x500400300500700900500400300d00509f00500c00500b00309e007006","0x1502d0130a20050a10150490130a000501806d05400501e01d015007009","0x501806d00c00501e01d0340050a40050a301502d0250270050a0005024","0x1502d0250270050a00050a80050240150740130a70050a60150490130a5","0x50ab01502d02502700504f0050a50050240150740130340050aa0050a9","0x30ae0070060050040030ad00700600500400304f00501b01a0340050ac","0x1502d0130af00704f00500400300900704f00500400300c00704f005004","0x502e00502401502d0130340050b10050b001502d02502c00502e005024","0x1502d01302100502100502401502d0130340050b30050b201502d025062","0x300500704f0050040030340050b60050b501502d0250b400502e005024","0x500400300f00704f00500400301000704f00500400300700704f005004","0x50b801502d02502700502100502e00505e0050240150290130b700704f","0x70540050040030bb0070060050040030ba00704f0050040030340050b9","0x50c10050c001502d0250270050bf0050240150be0130bd0150230130bc","0x502c0050c401502d0250c30070060050040030c2007006005004003034","0x1502d0250340050c70050c601502d0250c500507000502401502d013027","0x50cb0050ca01502d0250c900507000502401502d0130270050620050c8","0x70060050040030cc00700600500400306200501b01a02100501b01a034","0x130d00070060050040030cf0070060050040030ce0070060050040030cd","0x70540050040030060050700050060050820050820050060050d1015080","0x50060050d40150800130d300700600500400306a0050d2015049013015","0x70060050040030060050d5015049013006005070005006005082005082","0x1504b0130d80070060050040030820050540050540050d701504d0130d6","0x50db0150490130da0070060050040030700050820050820050060050d9","0x150800250700050060050060050820050820050060050dc015080013006","0x250060050240150490130e30050e20050e10050e00050df0050de0050dd","0x50060050820050e70150e601308200501b01a0340050e50050e401502d","0x50040030ea0070060050040030e90050e8015049025006005006005006","0x150490130120ee0060050ed01504901306a0050ec0150490130eb007006","0x70060050040030f30070060050040030f20050f10150490250f00050ef","0x30f70070060050040030f600704f0050040030f50070060050040030f4","0x1504d0130fa0070060050040030f90070060050040030f8007006005004","0x502100502401507401306a0050fc0150490130060050060050060050fb","0x1510002500c0050ff0150490130340050fe0050fd01502d025027005021","0x1502d013109005108005107005106005105005104005103005102005101","0x501806d08f00501e01d03400510b00510a01502d025027005021005024","0x501806d04f00500600510c00510f01507401310e00510d01504901310c","0x311300700600500400311200704f005004003111007006005004003110","0x5004003116007006005004003115007006005004003114007006005004","0x303400511900511801502d02502700511000502401502d013117007006","0x5a05400704f00500400301211c11b00700600500400311a00704f005004","0x511e00511d01502d02509900502401501f01309900501806d052005018","0x512100512001502d02500c00502401504901308200511f015049013034","0x512501512401306a00512301504901300c005006005122015014013034","0x5006005006005006005006005006005006005006005006005006005006","0x512701502d02512600502e00502401502d01302c005062005021005021","0x1502d02500600507000502401501401312900704f005004003034005128","0x1502d02502700512d0050240150be01312c01502301303400512b00512a","0x501b01a12b00501b01a12800501b01a00600501b01a03400512f00512e","0x1a13500501b01a00213413300501b01a00213213100501b01a002130121","0x502401504d01313700700600500400313600700600500400311000501b","0x1502d0250270051390050240150be013138015023013006005006005006","0x507000502401502d01302700512600513c01502d02503400513b00513a","0x514100502401501401314001502301303400513f00513e01502d02513d","0x500400300600502700514401501402503400514300514201502d025006","0x1504901306a005147015049013006005027005146015014025145007006","0x1502d025028005141005024015014013149007006005004003006005148","0x500400306a00514d01504901314c00700600500400303400514b00514a","0x502401501401302700502700515001501402514f01502301314e007006","0x1d15600501805a15500501805a03400515400515301502d025152005151","0x500600515c01515b01315a00515901504901315800501806d15700501e","0x504f00507000500900515d005006005006005006005070005009005082","0x516000515f0150e601300900505400500600515e01504d01307000504f","0x1301216400500716300500400316200501805a006005082005082005161","0x501801701500700600500400300500700600500400306a005165015049","0x516801504901306a00516701504901301216601500704f005004003006","0x516a01502d0250270051510050240150be013169007006005004003006","0x516e01502d02502700516d0050240150be01316c01502301303400516b","0x500400303400517200517101502d02502700502401517001303400516f","0x7006005004003175007006005004003174007006005004003173007006","0x50bf005179015178013070005024015049013177007006005004003176","0x1502d02502700517a0050240150be01313900512d00514100515100516d","0x500c00517f01501402517e00700600500400301217d03400517c00517b","0x1502d025181005070005024015014013027005070005180015014025027","0x502100518601502d02503400518500518401502d025034005183005182","0x501805a03400518900518801502d02518700507000502401502d013027","0x519101519001518f01518e18d00218c02700518b00518a015014025006","0x5193015007196005007195006005005194015005005193015192070005","0x519900500719600500719502700500519801519719600500519306f005","0x600500519d07000500519800600500519c00600500519b01519a196005","0x2f0ba00519f00600500519302100500519307000500519319e005005193","0x51930050071a10050071951870050051931890050051911a0005005199","0x71950270050051a21a1005005193135005005193133005005193131005","0x1830050051911a30050051990330ba00519f187005005199005007187005","0x500702b00500719502b00500519301500702b005007195181005005199","0x1a50050051930150071a500500719500c0050051980151a402b005005199","0x51a21a500500519901500500519d0050071a50050071950090050051a2","0x51a20700050051a20060050051a200c0050051a218b0050051a206f005","0x151a90340050051980310050051980060050051a81a70050051a6021005","0x519815100500519816d0050051980bf0050051980151ab1aa005005193","0x519f00c00500519317a00500519813900500519812d005005198141005","0x71951850050051980210050051ad17c0050051a21ac0050051990310ba","0x1b10050051a61b00050051a61af0050051a61ae0050051a60150071a1005","0x519f0bf0050051a20bf0050051ad0f00050051a20151b31b20050051a6","0x51991b50ba00519f0f00050051931720050051a21b40050051990340ba","0x820050051930820050051a20151b703400500519316f0050051a21b6005","0x51a60151ba1b90ba00519f16b0050051a21b80050051991120ba00519f","0x151bd1bc0050051911bc0050051a21bc0050051ad1bc0050051981bb005","0x519d1bf0050051911bf0050051a21bf0050051ad1bf0050051980151be","0x1520050071950151c30151c204f0050051931c10050051a60151c006a005","0x51990151c50150071520050071951c40050051a2152005005193005007","0x51981ca0070051c91c80ba00519f1c70050051a61c60050051a6152005","0x51cc06a0050051cc1cb0050051911cb0050051a21cb0050051ad1cb005","0x280050051980151cf06a0050051931ce0050051a61cd00500519304f005","0x6a0050051a204f0050051a20280050051910280050051a20280050051ad","0x51a21600050051a216200500519116200500519b1d10050051930151d0","0x51a61510050051a21540050051911d30050051991d20ba00519f161005","0x13100500519d0310050051a20340050051910151d61d50070051c91d4005","0x1d70050051ad1d700500519802100500519d13500500519d19e00500519d","0x1da0050051991d90ba00519f1d80050051a61d70050051911d70050051a2","0x1dc0050051ad1dc0050051981db0050051a61410050051a214b005005191","0x1dd0050051a21dd0050051ad1dd0050051981dc0050051911dc0050051a2","0x71951de0050051930150071de0050071950060050051cc1dd005005191","0x1df0050051a61de0050051991de0050051a21de0050051ad0050071de005","0x1e00050051ad0050071e00050071951e00050051930150071e0005007195","0x1430050051911e20050051991e10ba00519f1e00050051991e00050051a2","0x13f0050051911e500500519911a0ba00519f1e40ba00519f1e30ba00519f","0x1e70050051991e60ba00519f1260050051931260050051a213d005005199","0x1eb0050051981ea0070051c91e90070051c91e80070051c913b0050051a2","0x1870050071950190ba00519f1eb0050051911eb0050051a21eb0050051ad","0x71950151ec1a00050051930150071a0005007195189005005198015007","0x151f00060050051ef0060050051ee0151ed06f0050051cc0050071a0005","0x1a30050051930150071a3005007195183005005198015007181005007195","0x50071810050071951f20050051a60050071a30050071951f10050051a6","0x1210050051f31310050051f31330050051f31350050051f31100050051f3","0x519f17a0050051910151f40060050051f31280050051f312b0050051f3","0x51a20450050051a21350050051f71310050051f71f60050051991f50ba","0x51a21fa0050051a212f0050051a216d0050051a21f90050051a21f8005","0x519f1eb0050051931fd0050051a21fc0050051a21fb0050051a2139005","0x52010700050051cc2000050051a61ff0050051991330050051f71fe0ba","0x51f702e00500519302e00500519812b00500519118b005005193006005","0x51a22040050051ad2040050051982030050051992020ba00519f12b005","0x51932050050051980060050051f71280050051f7204005005191204005","0x519102e0050051a21280050051912070050051992060ba00519f205005","0x51992090ba00519f2080050051932080050051981210050051f7126005","0x51930520050052010990050051a20990050051ad11e00500519120a005","0x51a604f00500519d0500050051a205200500519105200500519b20b005","0x51a608f00500519c1210050051a20620050051a202c0050051a220c005","0x519920f0ba00519f11000500519311000500519820e0050051a620d005","0x51a62130050051a62120050051a62110050051a6119005005191210005","0x51ad1100050051f72170050051a62160050051a62150050051a6214005","0x51ef10e0050051cc1100050052192180050051cc1100050051a2110005","0x21d00500519921c0ba00519f21b00500519321b00500519801521a08f005","0x51930050071ac0050071952180050051a210e0050051a210b005005191","0x21e0050051ad00c00721e0050071951090050051981210050051911ac005","0xfe00500519122000500519921f0ba00519f21e00500519321e0050051a2","0x2240050051ad2240050051980152232220070051c9126005005198015221","0x519100900721e0050071951080050051982240050051912240050051a2","0x12b0050051a20150071ac00500719517c00500519813b00500519112f005","0x2280050051a62270050051a62260050051a61280050051a22250050051a6","0x22c0050051a60820050051cc22b0070051c922a0050051a22290050051a6","0x150071b40050071951720050051981b40050051930050071b4005007195","0x50071b600500719522f0050051a601522e0f00050051cc22d0050051a6","0x21e0050071950150071030050071950f20050051980152301b6005005193","0x2310050051ad2310050051980150071b600500719516f005005198005007","0x2320050051a22320050051ad2320050051982310050051912310050051a2","0x71951b80050051930150071b800500719516b005005198232005005191","0x1040050071950e90050051982340050051a62330050051a60050071b8005","0x51930150071d300500719515400500519800700721e005007195015007","0x51f70152362350070051c90820050051f30050071d30050071951d3005","0x71950de0050051980e50050051912380050051992370ba00519f082005","0x51a22390050051ad2390050051980ba00721e005007195015007105005","0x1da0050051930150071da00500719514b005005198239005005191239005","0x719523b0050051a604f0ba00519f23a0050051a20050071da005007195","0x51980050071050050071950df0050051981e20050051930050071e2005","0x15600500519b16000500519323c0050051a60150071e2005007195143005","0x23e00500519123e0050051a223e0050051ad23e00500519823d0050051a6","0x70071050050071950e000500519801523f0540050051a2156005005191","0x2410050051912410050051a22410050051ad2410050051982400050051a6","0x152470152462450070051c90152440540050051cc2430050051a6015242","0x51a62480050051a60ba0071050050071950e1005005198054005005193","0x51f30620050051f324c0050051a624b0050051a624a0050051a6249005","0x1e50050051930050071e500500719524e0050051a224d0050051a2021005","0x24f0050051990500ba00519f06200500519302f00500519c0210050051f7","0x51f70c90050051990050070c90050071950c90050051930cb005005191","0x51912500050051990520ba00519f02c00500519302800500519c062005","0x13d0050071950c50050051990050070c50050071950c50050051930c7005","0x519800500713d0050071950150071e500500719513f005005198015007","0x51a60050071e70050071951e70050051930150071e700500719513b005","0x51912550050051a62540050051992530ba00519f2520050051a6251005","0x150071f600500719512f00500519812d0050051a20c10050051a20c1005","0x1aa0050051cc0152572560050051a60050071f60050071951f6005005193","0x719512b0050051981ff0050051930050071ff0050071951aa0050051a2","0x2590ba00519f05e00500519305e0050051982580050051a60150071ff005","0x25c0050051a625b0050051a605e0050051a20b900500519125a005005199","0x4f00505400500726101526025f0050051a625e0050051a625d0050051a6","0x2640ba00519f0b40050051910b60050051912630050051992620ba00519f","0xb10050051912660050051990540ba00519f0b3005005191265005005199","0x51a62680050051a62670050051a6203005005193005007203005007195","0x26b0050051a626a0050051a6015007203005007195128005005198269005","0x519300c00500519c00900504f00500726c04f0050051f704f0050051f3","0x51ad0ac00500519105400500519c26e00500519926d0ba00519f0a5005","0x519f0a00050051930a80050051930a80050051980a50050051a20a5005","0x51992710ba00519f0a80050051a20aa00500519127000500519926f0ba","0x152742730050051980a00050051a20a00050051ad0a4005005191272005","0x90050051932050050051910090050051cc00d0050051a600c0050051a8","0x51982760050051a61520050051a209f0050051a601527500c005005191","0x2070050071952770050051a6207005005193015007207005007195121005","0x5200500519c0152792780050051a62080050051912050050051a2005007","0x9a00500519909c00500519127b00500519927a0ba00519f099005005193","0x500720a00500719520a00500519301500720a00500719511e005005198","0x2800ba00519f27f0050051a601527e11000500519101527d27c0050051a6","0x152842830050051a610c0050051a2015282096005005191281005005199","0x500721000500719508f0050051a80152880152872860050051a6015285","0x51a601500721000500719511900500519811000500519d210005005193","0x10e00500519301500728a00500719508f00500519421b005005191289005","0x519d08f00500519b28a00500519900500728a00500719528a005005193","0x51a221d00500519301500721d00500719510b00500519801528b08f005","0x28c0050051a610200500519921e00500519900500721d00500719521b005","0x8e0050051cc28d0050051a608200500519d08d00500519108d0050051cc","0xf000500519d0f200500519128e0050051a610300500519908e005005191","0x1050050051990e90050051910e90050051cc28f0050051a6104005005199","0xdf0050051932920050051a62910ba00519f0de0050051932900050051a6","0x2960050051a62950ba00519f0e00050051932940050051a62930ba00519f","0x2990ba00519f0e20050051932980050051a62970ba00519f0e1005005193","0x1070050051991060050051990580ba00519f0e300500519329a0050051a6","0x10900500519129c0050051a61080050051911080050051cc29b0050051a6","0x51930150072200050071950fe00500519800900500519d00c0050051cc","0x71950e500500519807600500519129d0050051990590ba00519f220005","0x719505c0ba00519f005007238005007195238005005193015007238005","0x519f24f00500519301500724f0050071950cb0050051980150070c9005","0x719502f0050051a807100500519907300500519129f00500519929e0ba","0x150072500050071950c70050051980150070c500500719500500724f005","0x50072a00050071952a00050051930150072a0005007195250005005193","0x51c90150070052a10050072500050071950280050051a82a0005005199","0x51982a30050051912a30050051a22a30050051ad2a30050051982a2007","0x71950c100500519801500721e00500719501500710200500719508d005","0x719505e005005191005007254005007195254005005193015007254005","0x719525a00500519300500725a0050071952a40050051930150072a4005","0x2b0050051ad0060050052a502e0050051912a40050051990050072a4005","0x51980b400500519801500725a0050071950b900500519802b0050051a2","0x51a60050072630050071952630050051930150072630050071950b6005","0x51980640050051912a800500519905e0ba00519f2a70050051a62a6005","0x51a60050072650050071952650050051930150072650050071950b3005","0x51980600050051912aa0050051990600ba00519f0060050051ad2a9005","0x71950050072660050071952660050051930150072660050071950b1005","0x519101500726e0050071950ac00500519826e00500519300500726e005","0x29e0050051930a700500519301500729e00500719500c0050051940a8005","0x519f00c00500519d00c00500519b29e00500519900500729e005007195","0x2700050071950aa0050051982700050051930050072700050071952aa0ba","0x2970050051a60152ab0580050051932990050051a60540050051ef015007","0x519d2930050051a62950050051a6272005005193005007272005007195","0x51a62710050051a627a0050051a62800050051a62910050051a6054005","0x51a62a90ba00519f0540050051a82640050051a60152ad0152ac26f005","0x519801500709a0050071950150072720050071950a4005005198262005","0x2590050051990620ba00519f27b00500519301500727b00500719509c005","0x5007253005007195253005005193015007253005007195052005005198","0x500727b00500719500500709a0050071950520050051a8253005005199","0x51a621f00500519921f00500519321f0050052ae23700504f005007261","0x51a62020050051a62060050051a62090050051a620f0050051a621c005","0x51a611a0050051a61e60050051a60190050051a61f50050051a61fe005","0x51981d20050051a61d90050051a61e10050051a61e30050051a61e4005","0x2810050071951c80050051a6281005005193015007281005007195096005","0x71950640ba00519f07000500519d0de0050051910de0050051cc005007","0xe00050051910e00050051cc0df0050051910df0050051cc005007220005","0xe20050051cc0e10050051a20e10050051910e10050051cc0e00050051a2","0x6f00500519d0700050051ad0e30050051910e30050051cc0e2005005191","0x1500729d00500719507600500519829d00500519300500729d005007195","0x29f00500719502b0050051f71120050051992a80ba00519f02b0050051f3","0x7300500519801500707100500719502f00500519829f005005193005007","0x2af0050051a21b90050051a200500707100500719501500729f005007195","0x51911b50050051992a70ba00519f2a80050051930050072a8005007195","0x51930050072aa0050071950150072a8005007195064005005198033005","0xf60050051a60150072aa0050071950600050051980280050051932aa005","0x1500725900500719505000500519805400500900500726101c005005193","0x112005005193005007112005007195005007259005007195259005005193","0x152b000f0050051930af0050051a600c0050051f3015007112005007195","0x1b50050071950152b20ba0050051a600c0050051ad00c0050051f70152b1","0x2a60ba00519f04f00500900500726102b00500519d1b5005005193005007","0x600050051a20150071b50050071950330050051980152b30070050051a6","0xba0050070050150152b50050150150152b40b70050051a20050050051a6","0x50af0050ba0150152b500501500701501000f00718700c0090072b5007","0x50090150090052b50050090050af0150152b50050151290150b70052b5","0x502100500c0150152b500501500701502e0050210210f60072b50070b7","0x1502b0052b50050060050100150270052b50050f600500f0150060052b5","0x2101502c0052b50050150f60150152b500501500701501502c0050150b7","0x2b500502f0050100150270052b500502e00500f01502f0052b500502c005","0x150152b50050150070150310052620330052b500702b00502e01502b005","0x1b50052b500501502b0150340052b50050330050270150152b5005015006","0x90050af0151b90052b500503400502f0151120052b500502700502c015","0x1120052b500511200503101500c0052b500500c0050330150090052b5005","0x90af1120151b90052b50051b90051b50151b50052b50051b5005034015","0x520f1e10052b50071d90051b90151d91d21c80ba2b50051b91b511200c","0x1e40050ba01511a1e40072b50051e10051c80150152b50050150070151e3","0x1f50052b50051d20050330150190052b50051c80050af0151e60052b5005","0x50150b70152020052b500511a0051d20151fe0052b50051e600500f015","0x150051e10152060052b50051e30051d90150152b5005015007015015293","0x70052b50050070051e30151c80052b50051c80050af0150150052b5005","0x20600511a0151290052b50051290051e40151d20052b50051d2005033015","0x60150152b50050150070152061291d20071c80150090052060052b5005","0x190152090052b50050150f60150152b50050310051e60150152b5005015","0x2b500500c0050330150190052b50050090050af01520f0052b5005209005","0x1f50152020052b500520f0051d20151fe0052b500502700500f0151f5005","0x51fe00502c0150152b500501500701521f0051b421c0052b5007202005","0x152370052b50052370050310150190052b50050190050af0152370052b5","0x2530051cd0520052b500705000520201505004f0072b50052370190071fe","0x72620052090152622590072b50050520052060150152b5005015007015","0x26d0052b50052590050ba0150152b50050150070150540051542640052b5","0x701527a0052a027126f0072b500726d0050090150152b5005015129015","0x2910052b500528000502f0152800052b50052710050270150152b5005015","0x29300521c0152950052b500526f00500f0152930052b500529100520f015","0x50150f60150152b500501500701501529f0050150b70152970052b5005","0x152950052b500527a00500f0150580052b500529900521f0152990052b5","0x701505c0051e70590052b50072970052370152970052b500505800521c","0x501500701506000529405e29e0072b50072950050090150152b5005015","0x100152a90052b500529e00500f0152aa0052b500505e00500c0150152b5","0xf60150152b50050150070150150e20050150b70150620052b50052aa005","0x52b500506000500f0152a80052b50050640050210150640052b5005015","0x2a60052632a70052b500706200502e0150620052b50052a80050100152a9","0x2b50052a400502f0152a40052b50052a70050270150152b5005015007015","0x282a30072b500706a04f00704f01506a0052b500506a0051b501506a005","0x152a30052b50052a30050af0150152b500501500701506f0712a00ba266","0x500c0150152b500501500701529f0052830730700072b50072a9005009","0x52b500507600501001529d0052b500507000500f0150760052b5005073","0x29b0052b50050150f60150152b500501500701501527c0050150b701529c","0x29a00501001529d0052b500529f00500f01529a0052b500529b005021015","0x2b50050150070152960052702980052b500729c00502e01529c0052b5005","0x51b50152920052b500529400502f0152940052b5005298005027015015","0x28f0e20e30ba0a40822900072b50072922a300704f0152920052b5005292","0x528e00505201528e0052b50050820280070500150152b5005015007015","0x1508e0052b500529d00500f01528c0052b50052900050af01528d0052b5","0x150152b50050150070150152580050150b701508d0052b500528d005253","0x152b50050280052590150152b500528f0052590150152b50050e2005259","0x152b500501500701501526b0050150b70151070052b50050e30050af015","0x2b50052a30050af0150152b50050280052590150152b50052960051e6015","0x52640151020052b50051060052620151060052b50050150f6015107005","0x52b500510200525301508e0052b500529d00500f01528c0052b5005107","0x152b50050710052590150152b50050150070150152580050150b701508d","0x25d0050150b70150910052b50052a00050af0150152b500506f005259015","0x2b500504f0050af0150152b50052a60051e60150152b5005015007015015","0x52640152890052b500528a00526201528a0052b50050150f6015091005","0x52b500528900525301508e0052b50052a900500f01528c0052b5005091","0x90150152b500501500701528300512b2860052b500708d00505401508d","0x50150060150152b50050150070152810050e009601c0072b500708e005","0x2860052710150152b500509600526f0150152b500501c00526d0150152b5","0x52910150152b50052640052800150152b500505900527a0150152b5005","0x1b501527c0052b500501529301527f0052b500501502b0150152b500521c","0x2b50050152970150990052b500527c27f00729501527c0052b500527c005","0x1527b0052b500509c0051d901509c0052b500509909a00729901509a005","0x50070051e301528c0052b500528c0050af0150150052b50050150051e1","0x151290052b50051290051e40151f50052b50051f50050330150070052b5","0x2b500501500701527b1291f500728c01500900527b0052b500527b00511a","0x52780050590152780052b50050150580150152b500528100526d015015","0x701500d09f0072112762770072b50072781f528c0ba05c0152780052b5","0x1505e0150a00052b500501529e0150152b50050150060150152b5005015","0x2a90150a40052b50050152aa0152730052b50050150600150a20052b5005","0x2720a42730a20a00090640150a50052b50050150620152720052b5005015","0x52b50052760050330152770052b50052770050af0150a70052b50050a5","0x51e40150070052b50050070051e30150150052b50050150051e1015276","0x52b500526400503101521c0052b500521c0050340151290052b5005129","0xb72a70152860052b50052860052a80150590052b50050590051b5015264","0x152af26e0ac2700aa0a80092b500528605926421c0a7129007015276277","0x26b0052a40150152b500501500701526a00521b26b0052b50072af0052a6","0x152672680072b500526900506a0152690052b500501502b0150152b5005","0x2b50050b10052a30150b10052b500526700502c0150152b5005268005291","0xaf0152700052b50052700051e10150b30052b5005266005028015266005","0x2b50050aa0050330150ac0052b50050ac0051e30150a80052b50050a8005","0x90050b30052b50050b300511a01526e0052b500526e0051e40150aa005","0x2650052b500526a0051d90150152b50050150070150b326e0aa0ac0a8270","0xac0051e30150a80052b50050a80050af0152700052b50052700051e1015","0x26e0052b500526e0051e40150aa0052b50050aa0050330150ac0052b5005","0x501500701526526e0aa0ac0a82700090052650052b500526500511a015","0x505900527a0150152b50052860052710150152b50050150060150152b5","0x501502b0150152b500521c0052910150152b50052640052800150152b5","0x2950150b60052b50050b60051b50150b60052b50050152a00150b40052b5","0x526325f00729901525f0052b50050152970152630052b50050b60b4007","0x150150052b50050150051e101525d0052b500525e0051d901525e0052b5","0x500d0050330150070052b50050070051e301509f0052b500509f0050af","0x525d0052b500525d00511a0151290052b50051290051e401500d0052b5","0x150152b50050150060150152b500501500701525d12900d00709f015009","0x152b500505900527a0150152b500508e00526d0150152b50052830051e6","0x52b500501502b0150152b500521c0052910150152b5005264005280015","0x25c00729501525b0052b500525b0051b501525b0052b500501507101525c","0x52b50050b925a00729901525a0052b50050152970150b90052b500525b","0x50af0150150052b50050150051e10152560052b50052580051d9015258","0x52b50051f50050330150070052b50050070051e301528c0052b500528c","0x150090052560052b500525600511a0151290052b50051290051e40151f5","0x51e60150152b50050150060150152b50050150070152561291f500728c","0x2800150152b500529500526d0150152b500521c0052910150152b500505c","0x150bf0052b500501506f0152550052b500501502b0150152b5005264005","0x50152970150c10052b50050bf2550072950150bf0052b50050bf0051b5","0x2510052b50052520051d90152520052b50050c12540072990152540052b5","0x70051e301504f0052b500504f0050af0150150052b50050150051e1015","0x1290052b50051290051e40151f50052b50051f50050330150070052b5005","0x50150070152511291f500704f0150090052510052b500525100511a015","0x2590052800150152b500521c0052910150152b50050540051e60150152b5","0x51b50150c70052b50050150700150c50052b500501502b0150152b5005","0x52b50050152970152500052b50050c70c50072950150c70052b50050c7","0x1e101524f0052b50050cb0051d90150cb0052b50052500c90072990150c9","0x2b50050070051e301504f0052b500504f0050af0150150052b5005015005","0x11a0151290052b50051290051e40151f50052b50051f5005033015007005","0x152b500501500701524f1291f500704f01500900524f0052b500524f005","0x50150051e101524e0052b50052530051d90150152b500521c005291015","0x150070052b50050070051e301504f0052b500504f0050af0150150052b5","0x524e00511a0151290052b50051290051e40151f50052b50051f5005033","0x51e60150152b500501500701524e1291f500704f01500900524e0052b5","0x7301524d0052b500501502b0150152b50051fe00526d0150152b500521f","0x2b500524c24d00729501524c0052b500524c0051b501524c0052b5005015","0x1d90152490052b500524b24a00729901524a0052b500501529701524b005","0x2b50050190050af0150150052b50050150051e10152480052b5005249005","0x1e40151f50052b50051f50050330150070052b50050070051e3015019005","0x1f50070190150090052480052b500524800511a0151290052b5005129005","0x2b500501502b0150152b50050af0052800150152b5005015007015248129","0x72950152430052b50052430051b50152430052b50050152a00150e1005","0x2b50052412400072990152400052b50050152970152410052b50052430e1","0xaf0150150052b50050150051e101523e0052b50050e00051d90150e0005","0x2b50050100050330150070052b50050070051e301500f0052b500500f005","0x900523e0052b500523e00511a0151290052b50051290051e4015010005","0x72b50070050150070050150152b500501501501523e12901000700f015","0xf0052b50050ba0050ba0150152b500501500701500c0090072b60af129","0x2b500700f0050090151290052b50051290050af0150152b5005015129015","0x210052b50050b70050270150152b50050150070150f60052b70b7010007","0x1000500f0150060052b500502e00520f01502e0052b500502100502f015","0x70150152b80050150b701502b0052b500500600521c0150270052b5005","0x1502f0052b500502c00521f01502c0052b50050150f60150152b5005015","0x702b00523701502b0052b500502f00521c0150270052b50050f600500f","0x72b500703312900729f0150152b50050150070150310052b90330052b5","0x150340052b50050340050af0150152b50050150070151120052ba1b5034","0x150060150152b50050150070151d20052bb1c81b90072b5007027005009","0x50760150152b50051c800526f0150152b50051b900526d0150152b5005","0x1b50151e10052b50050152930151d90052b500501502b0150152b50051b5","0x2b50050152970151e30052b50051e11d90072950151e10052b50051e1005","0x151e60052b500511a0051d901511a0052b50051e31e40072990151e4005","0x50070051e40150af0052b50050af0050330150340052b50050340050af","0x150070151e60070af0341290051e60052b50051e600511a0150070052b5","0x50150580150152b50051d200526d0150152b50050150060150152b5005","0x72b50070190af0340ba05c0150190052b50050190050590150190052b5","0x152090052b500501529e0150152b50050150070152062020072bc1fe1f5","0x51f50050af0150152b500520f00529c01521c20f0072b500520900529d","0x150070052b50050070051e40151fe0052b50051fe0050330151f50052b5","0x52b500705000529a01505004f23721f1292b500521c0071fe1f512929b","0x1505e0150152b50050520052980150152b50050150070152530052bd052","0x2370052b500523700503301521f0052b500521f0050af0152590052b5005","0x21f0af2940151b50052b50051b500529601504f0052b500504f0051e4015","0x2be26f0052b500726d00529201526d0542642621292b50051b525904f237","0x2b500501502b0150152b500526f0052900150152b5005015007015271005","0x2c0150152b50052800052910152912800072b500527a00506a01527a005","0x2b50052950050280152950052b50052930052a30152930052b5005291005","0x1e40152640052b50052640050330152620052b50052620050af015297005","0x2970542642621290052970052b500529700511a0150540052b5005054005","0x2b50052640050330152990052b50052620050af0150152b5005015007015","0xb701505c0052b50052710050820150590052b50050540051e4015058005","0x50af0150152b50051b50050760150152b50050150070150152bf005015","0x52b500504f0051e40150580052b50052370050330152990052b500521f","0x50af01529e0052b500505c0051d901505c0052b5005253005082015059","0x52b50050590051e40150580052b50050580050330152990052b5005299","0x2b500501500701529e05905829912900529e0052b500529e00511a015059","0x2b50050152a001505e0052b500501502b0150152b50051b5005076015015","0x152aa0052b500506005e0072950150600052b50050600051b5015060005","0x50620051d90150620052b50052aa2a90072990152a90052b5005015297","0x152060052b50052060050330152020052b50052020050af0150640052b5","0x72062021290050640052b500506400511a0150070052b50050070051e4","0x2b50051120050af0150152b500502700526d0150152b5005015007015064","0x2b50050310051e60150152b50050150070150152c00050150b70152a8005","0x50150060152a80052b50051290050af0150152b500502700526d015015","0x2a60051b50152a60052b50050150730152a70052b500501502b0150152b5","0x6a0052b50050152970152a40052b50052a62a70072950152a60052b5005","0x50af0150280052b50052a30051d90152a30052b50052a406a007299015","0x52b50050070051e40150af0052b50050af0050330152a80052b50052a8","0x2b50050150070150280070af2a81290050280052b500502800511a015007","0x2b50050152a00152a00052b500501502b0150152b50050ba005280015015","0x1506f0052b50050712a00072950150710052b50050710051b5015071005","0x50730051d90150730052b500506f0700072990150700052b5005015297","0x1500c0052b500500c0050330150090052b50050090050af01529f0052b5","0x700c00912900529f0052b500529f00511a0150070052b50050070051e4","0xc0072c10090af0072b50070070050070050150152b500501501501529f","0x2b50050151290150100052b50051290050ba0150152b500501500701500f","0x52c20f60b70072b50070100050090150af0052b50050af0050af015015","0x502e00502f01502e0052b50050f60050270150152b5005015007015021","0x1502b0052b50050b700500f0150270052b500500600520f0150060052b5","0x150152b50050150070150152c30050150b701502c0052b500502700521c","0x2b500502100500f0150330052b500502f00521f01502f0052b50050150f6","0x52c40310052b500702c00523701502c0052b500503300521c01502b005","0x1b90052c51121b50072b50070310af0070e30150152b5005015007015034","0x2b500702b0050090151b50052b50051b50050af0150152b5005015007015","0x26d0150152b50050150060150152b50050150070151d90052c61d21c8007","0x150152b50051120050e20150152b50051d200526f0150152b50051c8005","0x52b50051e30051b50151e30052b50050152930151e10052b500501502b","0x729901511a0052b50050152970151e40052b50051e31e10072950151e3","0x2b50050150051e10150190052b50051e60051d90151e60052b50051e411a","0x1e40150090052b50050090050330151b50052b50051b50050af015015005","0xba0091b50150af0050190052b500501900511a0150ba0052b50050ba005","0x152b50051d900526d0150152b50050150060150152b5005015007015019","0x91b50ba05c0151f50052b50051f50050590151f50052b5005015058015","0x50150600150152b50050150070152092060072c72021fe0072b50071f5","0x152020052b50052020050330151fe0052b50051fe0050af01520f0052b5","0x511200528f0150ba0052b50050ba0051e40150150052b50050150051e1","0x5004f23721f21c0af2b500511220f0ba0152021fe00928e0151120052b5","0x528c0150152b50050150070152530052c80520052b500705000528d015","0x2642620072b500525900506a0152590052b500501502b0150152b5005052","0x50540052a30150540052b500526400502c0150152b5005262005291015","0x152370052b50052370051e101526f0052b500526d00502801526d0052b5","0x504f0051e401521f0052b500521f00503301521c0052b500521c0050af","0x701526f04f21f21c2370af00526f0052b500526f00511a01504f0052b5","0x2370052b50052370051e10152710052b50052530051d90150152b5005015","0x4f0051e401521f0052b500521f00503301521c0052b500521c0050af015","0x1527104f21f21c2370af0052710052b500527100511a01504f0052b5005","0x1527a0052b500501502b0150152b50051120050e20150152b5005015007","0x528027a0072950152800052b50052800051b50152800052b50050152a0","0x152950052b50052912930072990152930052b50050152970152910052b5","0x52060050af0150150052b50050150051e10152970052b50052950051d9","0x150ba0052b50050ba0051e40152090052b50052090050330152060052b5","0x152b50050150070152970ba2092060150af0052970052b500529700511a","0x2c90050150b70152990052b50051b90050af0150152b500502b00526d015","0x2b500502b00526d0150152b50050340051e60150152b5005015007015015","0x2b500501502b0150152b50050150060152990052b50050af0050af015015","0x72950150590052b50050590051b50150590052b5005015073015058005","0x2b500505c29e00729901529e0052b500501529701505c0052b5005059058","0xaf0150150052b50050150051e10150600052b500505e0051d901505e005","0x2b50050ba0051e40150090052b50050090050330152990052b5005299005","0x150070150600ba0092990150af0050600052b500506000511a0150ba005","0x152a00152aa0052b500501502b0150152b50051290052800150152b5005","0x52b50052a92aa0072950152a90052b50052a90051b50152a90052b5005","0x51d90152a80052b50050620640072990150640052b5005015297015062","0x52b500500c0050af0150150052b50050150051e10152a70052b50052a8","0x511a0150ba0052b50050ba0051e401500f0052b500500f00503301500c","0x50150152b50050150150152a70ba00f00c0150af0052a70052b50052a7","0xba0150152b500501500701500f00c0072ca0090af0072b5007007005007","0xaf0052b50050af0050af0150152b50050151290150100052b5005129005","0x270150152b50050150070150210052cb0f60b70072b5007010005009015","0x2b500500600520f0150060052b500502e00502f01502e0052b50050f6005","0xb701502c0052b500502700521c01502b0052b50050b700500f015027005","0x521f01502f0052b50050150f60150152b50050150070150152cc005015","0x52b500503300521c01502b0052b500502100500f0150330052b500502f","0xe30150152b50050150070150340052cd0310052b500702c00523701502c","0x50af0150152b50050150070151b90052ce1121b50072b50070310af007","0x150070151d90052cf1d21c80072b500702b0050090151b50052b50051b5","0x1d200526f0150152b50051c800526d0150152b50050150060150152b5005","0x152930151e10052b500501502b0150152b50051120050e20150152b5005","0x52b50051e31e10072950151e30052b50051e30051b50151e30052b5005","0x51d90151e60052b50051e411a00729901511a0052b50050152970151e4","0x52b50051b50050af0150150052b50050150051e10150190052b50051e6","0x511a0150ba0052b50050ba0051e40150090052b50050090050330151b5","0x60150152b50050150070150190ba0091b50150af0050190052b5005019","0x590151f50052b50050150580150152b50051d900526d0150152b5005015","0x2060072d02021fe0072b50071f50091b50ba05c0151f50052b50051f5005","0x2b50051fe0050af01520f0052b50050150600150152b5005015007015209","0x1e40150150052b50050150051e10152020052b50052020050330151fe005","0x152021fe00908e0151120052b500511200528f0150ba0052b50050ba005","0x2d10520052b500705000528d01505004f23721f21c0af2b500511220f0ba","0x2b500501502b0150152b500505200528c0150152b5005015007015253005","0x2c0150152b50052620052910152642620072b500525900506a015259005","0x2b500526d00502801526d0052b50050540052a30150540052b5005264005","0x3301521c0052b500521c0050af0152370052b50052370051e101526f005","0x2b500526f00511a01504f0052b500504f0051e401521f0052b500521f005","0x52530051d90150152b500501500701526f04f21f21c2370af00526f005","0x1521c0052b500521c0050af0152370052b50052370051e10152710052b5","0x527100511a01504f0052b500504f0051e401521f0052b500521f005033","0x1120050e20150152b500501500701527104f21f21c2370af0052710052b5","0x51b50152800052b50050152a001527a0052b500501502b0150152b5005","0x52b50050152970152910052b500528027a0072950152800052b5005280","0x1e10152970052b50052950051d90152950052b5005291293007299015293","0x2b50052090050330152060052b50052060050af0150150052b5005015005","0xaf0052970052b500529700511a0150ba0052b50050ba0051e4015209005","0xaf0150152b500502b00526d0150152b50050150070152970ba209206015","0x1e60150152b50050150070150152d20050150b70152990052b50051b9005","0x2990052b50050af0050af0150152b500502b00526d0150152b5005034005","0x590052b50050150730150580052b500501502b0150152b5005015006015","0x1529701505c0052b50050590580072950150590052b50050590051b5015","0x52b500505e0051d901505e0052b500505c29e00729901529e0052b5005","0x50330152990052b50052990050af0150150052b50050150051e1015060","0x52b500506000511a0150ba0052b50050ba0051e40150090052b5005009","0x2b50051290052800150152b50050150070150600ba0092990150af005060","0x52a90051b50152a90052b50050152a00152aa0052b500501502b015015","0x150640052b50050152970150620052b50052a92aa0072950152a90052b5","0x150051e10152a70052b50052a80051d90152a80052b5005062064007299","0xf0052b500500f00503301500c0052b500500c0050af0150150052b5005","0xc0150af0052a70052b50052a700511a0150ba0052b50050ba0051e4015","0x2d30090af0072b50070070050070050150152b50050150150152a70ba00f","0x151290150100052b50051290050ba0150152b500501500701500f00c007","0xf60b70072b50070100050090150af0052b50050af0050af0150152b5005","0x502f01502e0052b50050f60050270150152b50050150070150210052d4","0x52b50050b700500f0150270052b500500600520f0150060052b500502e","0x2b50050150070150152d50050150b701502c0052b500502700521c01502b","0x2100500f0150330052b500502f00521f01502f0052b50050150f6015015","0x310052b500702c00523701502c0052b500503300521c01502b0052b5005","0x2d71121b50072b50070310af0070e30150152b50050150070150340052d6","0x2b0050090151b50052b50051b50050af0150152b50050150070151b9005","0x152b50050150060150152b50050150070151d90052d81d21c80072b5007","0x2b50051120050e20150152b50051d200526f0150152b50051c800526d015","0x51e30051b50151e30052b50050152930151e10052b500501502b015015","0x1511a0052b50050152970151e40052b50051e31e10072950151e30052b5","0x150051e10150190052b50051e60051d90151e60052b50051e411a007299","0x90052b50050090050330151b50052b50051b50050af0150150052b5005","0x1b50150af0050190052b500501900511a0150ba0052b50050ba0051e4015","0x51d900526d0150152b50050150060150152b50050150070150190ba009","0xba05c0151f50052b50051f50050590151f50052b50050150580150152b5","0x8d0150152b50050150070152092060072d92021fe0072b50071f50091b5","0x52b500521c00510601521c0052b500501510701520f0052b5005112005","0x528a0150152b500523700509101504f2370072b500521f00510201521f","0x52b50050520051b50150520052b500505000502f0150500052b500504f","0x2590052b50052590051b50152592530072b500520f0520150ba289015052","0x1c0150540052b50052640052830152642620072b50052591fe007286015","0x2b500526f0052810150152b500526d00509601526f26d0072b5005054005","0x990152800052b500527a00527c01527a0052b500527100527f015271005","0x52b50052530051e10152910052b500529100509a0152910052b5005015","0x2930ba2b50072802910ba20212909c0152620052b50052620050af015253","0x150152b50050151290150152b50050150070150590582990ba2da297295","0x52950051e40152930052b50052930050330152970052b50052970051b5","0x150152b500501500701505c0052db0152b500729700527b0152950052b5","0x2b500505e00527701505e0052b500529e00527801529e0052b50050150f6","0x2b500505c0052760150152b50050150070150152dc0050150b7015060005","0x2a90052770152a90052b50052aa00509f0152aa0052b50050150f6015015","0x150640052b500506000500d0150620052b500501502b0150600052b5005","0x70152a70052dd2a80052b50070640050a00150640052b5005064005277","0x1b50152a60052b50050150a20150152b50052a80051e60150152b5005015","0x1e60150152b50050150070150152de0050150b70152a40052b50052a6005","0x2a40052b500506a0051b501506a0052b50050152730150152b50052a7005","0x52a300506a0152a30052b50052a40620072950150152b5005015006015","0x150710052b50052a000502c0150152b50050280052910152a00280072b5","0x52530051e10150700052b500506f00502801506f0052b50050710052a3","0x152930052b50052930050330152620052b50052620050af0152530052b5","0x2932622530af0050700052b500507000511a0152950052b50052950051e4","0x590730072990150730052b50050152970150152b5005015007015070295","0x2530052b50052530051e10150760052b500529f0051d901529f0052b5005","0x580051e40152990052b50052990050330152620052b50052620050af015","0x150760582992622530af0050760052b500507600511a0150580052b5005","0x1529d0052b500501502b0150152b50051120050e20150152b5005015007","0x529c29d00729501529c0052b500529c0051b501529c0052b50050152a0","0x152980052b500529b29a00729901529a0052b500501529701529b0052b5","0x52060050af0150150052b50050150051e10152960052b50052980051d9","0x150ba0052b50050ba0051e40152090052b50052090050330152060052b5","0x152b50050150070152960ba2092060150af0052960052b500529600511a","0x2df0050150b70152940052b50051b90050af0150152b500502b00526d015","0x2b500502b00526d0150152b50050340051e60150152b5005015007015015","0x2b500501502b0150152b50050150060152940052b50050af0050af015015","0x72950152900052b50052900051b50152900052b5005015073015292005","0x2b50050820e30072990150e30052b50050152970150820052b5005290292","0xaf0150150052b50050150051e101528f0052b50050e20051d90150e2005","0x2b50050ba0051e40150090052b50050090050330152940052b5005294005","0x1500701528f0ba0092940150af00528f0052b500528f00511a0150ba005","0x152a001528e0052b500501502b0150152b50051290052800150152b5005","0x52b500528d28e00729501528d0052b500528d0051b501528d0052b5005","0x51d901508d0052b500528c08e00729901508e0052b500501529701528c","0x52b500500c0050af0150150052b50050150051e10151070052b500508d","0x511a0150ba0052b50050ba0051e401500f0052b500500f00503301500c","0x50150152b50050150150151070ba00f00c0150af0051070052b5005107","0xba0150152b500501500701500f00c0072e00090af0072b5007007005007","0xaf0052b50050af0050af0150152b50050151290150100052b5005129005","0x270150152b50050150070150210052e10f60b70072b5007010005009015","0x2b500500600520f0150060052b500502e00502f01502e0052b50050f6005","0xb701502c0052b500502700521c01502b0052b50050b700500f015027005","0x521f01502f0052b50050150f60150152b50050150070150152e2005015","0x52b500503300521c01502b0052b500502100500f0150330052b500502f","0x90150152b50050150070150340052e30310052b500702c00523701502c","0x1120050270150152b50050150070151b90052e41121b50072b500702b005","0x1d90052b50051d200520f0151d20052b50051c800502f0151c80052b5005","0x50150b70151e30052b50051d900521c0151e10052b50051b500500f015","0x51e400521f0151e40052b50050150f60150152b50050150070150152e5","0x151e30052b500511a00521c0151e10052b50051b900500f01511a0052b5","0x1e10050090150152b50050150070150190052e61e60052b50071e3005237","0x152b50050150060150152b50050150070152020052e71fe1f50072b5007","0x2b50051e600527a0150152b50051fe00526f0150152b50051f500526d015","0x2b50050152930152060052b500501502b0150152b500503100527a015015","0x1520f0052b50052092060072950152090052b50052090051b5015209005","0x521f0051d901521f0052b500520f21c00729901521c0052b5005015297","0x150af0052b50050af0050af0150150052b50050150051e10152370052b5","0x523700511a0150ba0052b50050ba0051e40150090052b5005009005033","0x20200526d0150152b50050150070152370ba0090af0150af0052370052b5","0x5c01504f0052b500504f00505901504f0052b50050150580150152b5005","0x150152b50050150070152592530072e80520500072b500704f0090af0ba","0x500052b50050500050af0152620052b50050150600150152b5005015006","0xba0051e40150150052b50050150051e10150520052b5005052005033015","0x1e60052b50051e60051b50150310052b50050310051b50150ba0052b5005","0x28d01527126f26d0542640af2b50051e60312620ba01505205000c0a4015","0x527a00528c0150152b50050150070152800052e927a0052b5007271005","0x2910152952930072b500529100506a0152910052b500501502b0150152b5","0x52b50052970052a30152970052b500529500502c0150152b5005293005","0x50af01526d0052b500526d0051e10150580052b5005299005028015299","0x52b500526f0051e40150540052b50050540050330152640052b5005264","0x501500701505826f05426426d0af0050580052b500505800511a01526f","0xaf01526d0052b500526d0051e10150590052b50052800051d90150152b5","0x2b500526f0051e40150540052b50050540050330152640052b5005264005","0x1500701505926f05426426d0af0050590052b500505900511a01526f005","0x3100527a0150152b50051e600527a0150152b50050150060150152b5005","0x51b501529e0052b50050152a001505c0052b500501502b0150152b5005","0x52b500501529701505e0052b500529e05c00729501529e0052b500529e","0x1e10152a90052b50052aa0051d90152aa0052b500505e060007299015060","0x2b50052590050330152530052b50052530050af0150150052b5005015005","0xaf0052a90052b50052a900511a0150ba0052b50050ba0051e4015259005","0x51e60150152b50050150060150152b50050150070152a90ba259253015","0x2b0150152b500503100527a0150152b50051e100526d0150152b5005019","0x640052b50050640051b50150640052b50050150700150620052b5005015","0x2a70072990152a70052b50050152970152a80052b5005064062007295015","0x52b50050150051e10152a40052b50052a60051d90152a60052b50052a8","0x51e40150090052b50050090050330150af0052b50050af0050af015015","0x2a40ba0090af0150af0052a40052b50052a400511a0150ba0052b50050ba","0x150152b50050340051e60150152b50050150060150152b5005015007015","0x2a30052b500501507301506a0052b500501502b0150152b500502b00526d","0x152970150280052b50052a306a0072950152a30052b50052a30051b5015","0x52b50050710051d90150710052b50050282a00072990152a00052b5005","0x50330150af0052b50050af0050af0150150052b50050150051e101506f","0x52b500506f00511a0150ba0052b50050ba0051e40150090052b5005009","0x2b50051290052800150152b500501500701506f0ba0090af0150af00506f","0x50730051b50150730052b50050152a00150700052b500501502b015015","0x150760052b500501529701529f0052b50050730700072950150730052b5","0x150051e101529c0052b500529d0051d901529d0052b500529f076007299","0xf0052b500500f00503301500c0052b500500c0050af0150150052b5005","0xc0150af00529c0052b500529c00511a0150ba0052b50050ba0051e4015","0x2ea0af1290072b50070050150070050150152b500501501501529c0ba00f","0x50af01500f0052b50050ba0050ba0150152b500501500701500c009007","0x150070150f60052eb0b70100072b500700f0050090151290052b5005129","0x1502b0150152b50050b700526f0150152b500501000526d0150152b5005","0x1502e0052b500502e0051b501502e0052b50050152930150210052b5005","0x60270072990150270052b50050152970150060052b500502e021007295","0x1290052b50051290050af01502c0052b500502b0051d901502b0052b5005","0x2c00511a0150070052b50050070051e40150af0052b50050af005033015","0xf600526d0150152b500501500701502c0070af12912900502c0052b5005","0x5c01502f0052b500502f00505901502f0052b50050150580150152b5005","0x150152b50050150070151b50340072ec0310330072b500702f0af1290ba","0x2b50051b90050a70151b90052b50051120050a50151120052b5005015272","0x27f0151d90052b50051d20050aa0150152b50051c80050a80151d21c8007","0x51e300527c0151e31e10072b50051e10052700151e10052b50051d9005","0x11a0050ac0150152b500501512901511a0052b50050150990151e40052b5","0x52b50051e400526e0151e60052b50051e600509a0151e611a0072b5005","0x190ba2b50071e41e600703112909c0150330052b50050330050af0151e4","0x20f0052b50050152af0150152b50050150070152092062020ba2ed1fe1f5","0x503301521c0052b500520f1e100726a0151e10052b50051e100526b015","0x52b500521c00526e01511a0052b500511a00509a0150190052b5005019","0x21f0ba2b500721c11a1f501912909c0151fe0052b50051fe0051b501521c","0x150152b50050150060150152b50050150070152530520500ba2ee04f237","0x52620052680152620052b500504f1fe0072690152590052b500501502b","0x26f26d0072b50050540050b10150152b50052640052670150542640072b5","0x2590072950152710052b50052710051b50152710052b500526d00502f015","0x52b50052800051b50152800052b500526f00502f01527a0052b5005271","0x152952930072b500529100506a0152910052b500528027a007295015280","0x2b50052970052a30152970052b500529500502c0150152b5005293005291","0x330150330052b50050330050af0150580052b5005299005028015299005","0x2b500505800511a0152370052b50052370051e401521f0052b500521f005","0x2b50051fe00527a0150152b500501500701505823721f033129005058005","0x503401505c0052b50050520051e40150590052b5005050005033015015","0x52660150152b50050150070150152ef0050150b701529e0052b5005253","0x150590052b50052020050330150152b50051e10050b30150152b500511a","0x2b500501500601529e0052b500520900503401505c0052b50052060051e4","0x51d90150600052b500529e05e00729901505e0052b5005015297015015","0x52b50050590050330150330052b50050330050af0152aa0052b5005060","0x331290052aa0052b50052aa00511a01505c0052b500505c0051e4015059","0x50152a00152a90052b500501502b0150152b50050150070152aa05c059","0x640052b50050622a90072950150620052b50050620051b50150620052b5","0x2a70051d90152a70052b50050642a80072990152a80052b5005015297015","0x1b50052b50051b50050330150340052b50050340050af0152a60052b5005","0x1b50341290052a60052b50052a600511a0150070052b50050070051e4015","0x2b500501502b0150152b50050ba0052800150152b50050150070152a6007","0x729501506a0052b500506a0051b501506a0052b50050152a00152a4005","0x2b50052a30280072990150280052b50050152970152a30052b500506a2a4","0x330150090052b50050090050af0150710052b50052a00051d90152a0005","0x2b500507100511a0150070052b50050070051e401500c0052b500500c005","0x70070050070050150152b500501501501507100700c009129005071005","0x2b50051290050ba0150152b500501500701500f00c0072f00090af0072b5","0x100050090150af0052b50050af0050af0150152b5005015129015010005","0x2b50050f60050270150152b50050150070150210052f10f60b70072b5007","0xf0150270052b500500600520f0150060052b500502e00502f01502e005","0x152f20050150b701502c0052b500502700521c01502b0052b50050b7005","0x52b500502f00521f01502f0052b50050150f60150152b5005015007015","0x523701502c0052b500503300521c01502b0052b500502100500f015033","0x70310af0070e30150152b50050150070150340052f30310052b500702c","0x52b50051b50050af0150152b50050150070151b90052f41121b50072b5","0x150152b50050150070151d90052f51d21c80072b500702b0050090151b5","0x150152b50051d200526f0150152b50051c800526d0150152b5005015006","0x1e30052b50050152930151e10052b500501502b0150152b50051120050e2","0x152970151e40052b50051e31e10072950151e30052b50051e30051b5015","0x52b50051e60051d90151e60052b50051e411a00729901511a0052b5005","0x50330151b50052b50051b50050af0150150052b50050150051e1015019","0x52b500501900511a0150ba0052b50050ba0051e40150090052b5005009","0x152b50050150060150152b50050150070150190ba0091b50150af005019","0x2b50051f50050590151f50052b50050150580150152b50051d900526d015","0x150070152092060072f62021fe0072b50071f50091b50ba05c0151f5005","0x20f0ba2b50070ba2020072650151fe0052b50051fe0050af0150152b5005","0x52b500521f0050b40150152b500501500701505004f2370ba2f721f21c","0x2642622592530af2b50050520052630150520052b500521f0050b601521f","0x52640050e20150152b500525900525e0150152b500525300525f015054","0x1fe0050af01526d0052b50050150600150152b500505400527a0150152b5","0x150052b50050150051e101520f0052b500520f0050330151fe0052b5005","0x1fe00925d0152620052b500526200528f01521c0052b500521c0051e4015","0x52b500729100525c01529128027a27126f0af2b500526226d21c01520f","0x152992970072b500529300525b0150152b50050150070152950052f8293","0x150070150590052f90580052b50072990050a00150152b50052970050b9","0x1502b0150152b50051120050e20150152b50050580051e60150152b5005","0x1529e0052b500529e0051b501529e0052b500501525a01505c0052b5005","0x26f0050af0150600052b500527a0051e101505e0052b500529e05c007295","0x620052b50052800051e40152a90052b50052710050330152aa0052b5005","0x152b50050150070150152fa0050150b70150640052b500505e005034015","0x2b500511200508d0152a80052b50050152580150152b50050590051e6015","0x509a0152a40052b50050150990152a60052b50052a800527c0152a7005","0x2a62a42802710af2560152a60052b50052a600526e0152a40052b50052a4","0x1502b0150152b50050150070150712a00280ba2fb2a306a0072b50072a7","0x152b50050700052910150730700072b500506f00506a01506f0052b5005","0x760050280150760052b500529f0052a301529f0052b500507300502c015","0x26f0052b500526f0050af01527a0052b500527a0051e101529d0052b5005","0x29d00511a0152a30052b50052a30051e401506a0052b500506a005033015","0x51e10150152b500501500701529d2a306a26f27a0af00529d0052b5005","0x52b500502800503301529b0052b500526f0050af01529c0052b500527a","0x150b70152960052b50050710050340152980052b50052a00051e401529a","0x2950052550150152b50051120050e20150152b50050150070150152fc005","0x600052b500527a0051e10150152b50052940050bf0152922940072b5005","0x2800051e40152a90052b50052710050330152aa0052b500526f0050af015","0x70150152fa0050150b70150640052b50052920050340150620052b5005","0x150600052b50050150051e10150152b50051120050e20150152b5005015","0x504f0051e40152a90052b50052370050330152aa0052b50051fe0050af","0x1529c0052b50050600050c10150640052b50050500050340150620052b5","0x506200525201529a0052b50052a900525401529b0052b50052aa005264","0x2990152900052b50050152970152960052b50050640052510152980052b5","0x529c0051e10150e30052b50050820051d90150820052b5005296290007","0x1529a0052b500529a00503301529b0052b500529b0050af01529c0052b5","0x29a29b29c0af0050e30052b50050e300511a0152980052b50052980051e4","0x2b500501502b0150152b50051120050e20150152b50050150070150e3298","0x729501528f0052b500528f0051b501528f0052b50050152a00150e2005","0x2b500528e28d00729901528d0052b500501529701528e0052b500528f0e2","0xaf0150150052b50050150051e101508e0052b500528c0051d901528c005","0x2b50050ba0051e40152090052b50052090050330152060052b5005206005","0x1500701508e0ba2092060150af00508e0052b500508e00511a0150ba005","0xb701508d0052b50051b90050af0150152b500502b00526d0150152b5005","0x526d0150152b50050340051e60150152b50050150070150152fd005015","0x2b0150152b500501500601508d0052b50050af0050af0150152b500502b","0x1060052b50051060051b50151060052b50050150730151070052b5005015","0x910072990150910052b50050152970151020052b5005106107007295015","0x52b50050150051e10152890052b500528a0051d901528a0052b5005102","0x51e40150090052b500500900503301508d0052b500508d0050af015015","0x2890ba00908d0150af0052890052b500528900511a0150ba0052b50050ba","0x2860052b500501502b0150152b50051290052800150152b5005015007015","0x2832860072950152830052b50052830051b50152830052b50050152a0015","0x2810052b500501c0960072990150960052b500501529701501c0052b5005","0xc0050af0150150052b50050150051e101527f0052b50052810051d9015","0xba0052b50050ba0051e401500f0052b500500f00503301500c0052b5005","0x2b500501501501527f0ba00f00c0150af00527f0052b500527f00511a015","0x2b500501500701500c0090072fe0af1290072b5007005015007005015015","0x50090151290052b50051290050af01500f0052b50050ba0050ba015015","0x501000526d0150152b50050150070150f60052ff0b70100072b500700f","0x50152930150210052b500501502b0150152b50050b700526f0150152b5","0x60052b500502e02100729501502e0052b500502e0051b501502e0052b5","0x2b0051d901502b0052b50050060270072990150270052b5005015297015","0xaf0052b50050af0050330151290052b50051290050af01502c0052b5005","0xaf12912900502c0052b500502c00511a0150070052b50050070051e4015","0x2b50050150580150152b50050f600526d0150152b500501500701502c007","0x330072b500702f0af1290ba05c01502f0052b500502f00505901502f005","0xc50151120052b50050152580150152b50050150070151b5034007300031","0x51c80052500151d21c80072b50051b90050c70151b90052b5005112005","0x27c0151e10052b50051d900527f0151d90052b50051d20050c90150152b5","0x52b50051e400509a0151e40052b50050150990151e30052b50051e1005","0x12909c0150330052b50050330050af0151e30052b50051e300526e0151e4","0x2b50050150070152021fe1f50ba3010191e611a0ba2b50071e31e4007031","0x51e401511a0052b500511a0050330150190052b50050190051b5015015","0x701520f0053022092060072b50070190330070e30151e60052b50051e6","0x1521f0052b500520900508d01521c0052b500501502b0150152b5005015","0x529101505004f0072b500523700506a0152370052b500521f21c007295","0x2530052b50050520052a30150520052b500505000502c0150152b500504f","0x11a0050330152060052b50052060050af0152590052b5005253005028015","0x2590052b500525900511a0151e60052b50051e60051e401511a0052b5005","0x152620052b500501502b0150152b50050150070152591e611a206129005","0x52642620072950152640052b50052640051b50152640052b50050150cb","0x1526f0052b500511a00503301526d0052b500520f0050af0150540052b5","0x3030050150b701527a0052b50050540050340152710052b50051e60051e4","0x51f500503301526d0052b50050330050af0150152b5005015007015015","0x1527a0052b50052020050340152710052b50051fe0051e401526f0052b5","0x52910051d90152910052b500527a2800072990152800052b5005015297","0x1526f0052b500526f00503301526d0052b500526d0050af0152930052b5","0x27126f26d1290052930052b500529300511a0152710052b50052710051e4","0x52b50050152a00152950052b500501502b0150152b5005015007015293","0x2970152990052b50052972950072950152970052b50052970051b5015297","0x2b50050590051d90150590052b50052990580072990150580052b5005015","0x1e40151b50052b50051b50050330150340052b50050340050af01505c005","0x5c0071b503412900505c0052b500505c00511a0150070052b5005007005","0x29e0052b500501502b0150152b50050ba0052800150152b5005015007015","0x5e29e00729501505e0052b500505e0051b501505e0052b50050152a0015","0x2a90052b50050602aa0072990152aa0052b50050152970150600052b5005","0xc0050330150090052b50050090050af0150620052b50052a90051d9015","0x620052b500506200511a0150070052b50050070051e401500c0052b5005","0x72b50070ba0050070050150152b500501501501506200700c009129005","0xb70052b50050af0050ba0150152b500501500701501000f00730400c009","0x2b50070b70050090150090052b50050090050af0150152b5005015129015","0x60052b50050210050270150152b500501500701502e0053050210f6007","0xf600500f01502b0052b500502700520f0150270052b500500600502f015","0x70150153060050150b701502f0052b500502b00521c01502c0052b5005","0x150310052b500503300521f0150330052b50050150f60150152b5005015","0x702f00523701502f0052b500503100521c01502c0052b500502e00500f","0x72b50070340090070e30150152b50050150070151b50053070340052b5","0x151120052b50051120050af0150152b50050150070151c80053081b9112","0x50270150152b50050150070151e10053091d91d20072b500702c005009","0x52b50051e400520f0151e40052b50051e300502f0151e30052b50051d9","0x150b70150190052b500511a00521c0151e60052b50051d200500f01511a","0x1f500521f0151f50052b50050150f60150152b500501500701501530a005","0x190052b50051fe00521c0151e60052b50051e100500f0151fe0052b5005","0x502c0150152b500501500701520600530b2020052b5007019005237015","0x52b50052090050310151120052b50051120050af0152090052b50051e6","0x30c21f0052b500721c00520201521c20f0072b50052091120071fe015209","0x520901505004f0072b500521f0052060150152b5005015007015237005","0x2b500504f0050ba0150152b500501500701525300530d0520052b5007050","0x152b500501500701505400530e2642620072b5007259005009015259005","0x152b500526400526f0150152b500526200526d0150152b5005015006015","0x2b50051b90050e20150152b500520200527a0150152b5005052005280015","0x526f0051b501526f0052b500501529301526d0052b500501502b015015","0x1527a0052b50050152970152710052b500526f26d00729501526f0052b5","0x150051e10152910052b50052800051d90152800052b500527127a007299","0x70052b50050070051e301520f0052b500520f0050af0150150052b5005","0x29100511a0151290052b50051290051e401500c0052b500500c005033015","0x26d0150152b500501500701529112900c00720f0150090052910052b5005","0x2930052b50052930050590152930052b50050150580150152b5005054005","0x2b500501500701505829900730f2972950072b500729300c20f0ba05c015","0x2b50052950050af0150590052b50050152aa0150152b5005015006015015","0x1e30150150052b50050150051e10152970052b5005297005033015295005","0x2b50051b900528f0151290052b50051290051e40150070052b5005007005","0x24f0150520052b50050520050310152020052b50052020051b50151b9005","0x152a92aa06005e29e05c0092b50050522021b9059129007015297295010","0x501502b0150152b50050150070150640053100620052b50072a900524e","0x150152b50052a700524c0152a62a70072b500506200524d0152a80052b5","0x506a0050b10150152b50052a400526701506a2a40072b50052a6005268","0x52b50052a02a80072950152a00052b50052a300502f0150282a30072b5","0x6a0150700052b500506f07100729501506f0052b500502800502f015071","0x2b500529f00502c0150152b500507300529101529f0730072b5005070005","0x1e101529c0052b500529d00502801529d0052b50050760052a3015076005","0x2b50050600051e301505c0052b500505c0050af01505e0052b500505e005","0x11a0152aa0052b50052aa0051e401529e0052b500529e005033015060005","0x152b500501500701529c2aa29e06005c05e00900529c0052b500529c005","0x5c0050af01505e0052b500505e0051e101529b0052b50050640051d9015","0x29e0052b500529e0050330150600052b50050600051e301505c0052b5005","0x5c05e00900529b0052b500529b00511a0152aa0052b50052aa0051e4015","0x520052800150152b50050150060150152b500501500701529b2aa29e060","0x1502b0150152b50051b90050e20150152b500520200527a0150152b5005","0x152980052b50052980051b50152980052b50050152a001529a0052b5005","0x2962940072990152940052b50050152970152960052b500529829a007295","0x150052b50050150051e10152900052b50052920051d90152920052b5005","0x580050330150070052b50050070051e30152990052b50052990050af015","0x2900052b500529000511a0151290052b50051290051e40150580052b5005","0x152b50050150060150152b5005015007015290129058007299015009005","0x2b500520200527a0150152b500504f0052800150152b50052530051e6015","0x2b500501506f0150820052b500501502b0150152b50051b90050e2015015","0x150e20052b50050e30820072950150e30052b50050e30051b50150e3005","0x528e0051d901528e0052b50050e228f00729901528f0052b5005015297","0x1520f0052b500520f0050af0150150052b50050150051e101528d0052b5","0x51290051e401500c0052b500500c0050330150070052b50050070051e3","0x1528d12900c00720f01500900528d0052b500528d00511a0151290052b5","0x27a0150152b50051b90050e20150152b50050150060150152b5005015007","0x52b50050150051e101528c0052b50052370051d90150152b5005202005","0x50330150070052b50050070051e301520f0052b500520f0050af015015","0x52b500528c00511a0151290052b50051290051e401500c0052b500500c","0x2b50050150060150152b500501500701528c12900c00720f01500900528c","0x51b90050e20150152b50051e600526d0150152b50052060051e6015015","0x8d0051b501508d0052b500501507001508e0052b500501502b0150152b5","0x1060052b50050152970151070052b500508d08e00729501508d0052b5005","0x51e10150910052b50051020051d90151020052b5005107106007299015","0x52b50050070051e30151120052b50051120050af0150150052b5005015","0x511a0151290052b50051290051e401500c0052b500500c005033015007","0x150152b500501500701509112900c0071120150090050910052b5005091","0x153110050150b701528a0052b50051c80050af0150152b500502c00526d","0x152b500502c00526d0150152b50051b50051e60150152b5005015007015","0x52b500501502b0150152b500501500601528a0052b50050090050af015","0x2890072950152860052b50052860051b50152860052b5005015073015289","0x52b500528301c00729901501c0052b50050152970152830052b5005286","0x50af0150150052b50050150051e10152810052b50050960051d9015096","0x52b500500c0050330150070052b50050070051e301528a0052b500528a","0x150090052810052b500528100511a0151290052b50051290051e401500c","0x2b0150152b50050af0052800150152b500501500701528112900c00728a","0x27c0052b500527c0051b501527c0052b50050152a001527f0052b5005015","0x9a00729901509a0052b50050152970150990052b500527c27f007295015","0x52b50050150051e101527b0052b500509c0051d901509c0052b5005099","0x50330150070052b50050070051e301500f0052b500500f0050af015015","0x52b500527b00511a0151290052b50051290051e40150100052b5005010","0x70050070050150152b500501501501527b12901000700f01500900527b","0x51290050ba0150152b500501500701500f00c0073120090af0072b5007","0x50090150af0052b50050af0050af0150152b50050151290150100052b5","0x50f60050270150152b50050150070150210053130f60b70072b5007010","0x150270052b500500600520f0150060052b500502e00502f01502e0052b5","0x3140050150b701502c0052b500502700521c01502b0052b50050b700500f","0x2b500502f00521f01502f0052b50050150f60150152b5005015007015015","0x23701502c0052b500503300521c01502b0052b500502100500f015033005","0x702b0050090150152b50050150070150340053150310052b500702c005","0x150152b50050150060150152b50050150070151b90053161121b50072b5","0x152b500503100527a0150152b500511200526f0150152b50051b500526d","0x2b50051d20051b50151d20052b50050152930151c80052b500501502b015","0x2990151e10052b50050152970151d90052b50051d21c80072950151d2005","0x50150051e10151e40052b50051e30051d90151e30052b50051d91e1007","0x150090052b50050090050330150af0052b50050af0050af0150150052b5","0x90af0150af0051e40052b50051e400511a0150ba0052b50050ba0051e4","0x2b50051b900526d0150152b50050150060150152b50050150070151e40ba","0xaf0ba05c01511a0052b500511a00505901511a0052b5005015058015015","0x1524b0150152b50050150070151fe1f50073170191e60072b500711a009","0x2090072b50052060052490152060052b500520200524a0152020052b5005","0x21c00502f01521c0052b500520f0050e10150152b500520900524801520f","0x2b500503121f0150ba28901521f0052b500521f0051b501521f0052b5005","0x500072b500504f1e600728601504f0052b500504f0051b501504f237007","0x2400152622590072b50052530052410152530052b5005052005243015052","0x52b500526400527f0152640052b50052620050e00150152b5005259005","0x26f00509a01526f0052b500501509901526d0052b500505400527c015054","0x500052b50050500050af0152370052b50052370051e101526f0052b5005","0x70152952932910ba31828027a2710ba2b500726d26f0ba01912909c015","0x23e0152800052b50052800051b50150152b50050151290150152b5005015","0x527a0051e40152710052b50052710050330152972800072b5005280005","0x150152b50050150070152990053190152b500729700527b01527a0052b5","0x52b500505800523d0150580052b50050150f60150152b500528000527a","0x2b500501500701501531a0050150b701505c0052b5005059005156015059","0x29e00515601529e0052b500528000523c0150152b5005299005276015015","0x2aa0600072b500505c0050df01505e0052b500501502b01505c0052b5005","0x701506200531b2a90052b50072aa0052390150152b500506000523b015","0x1b50150640052b50050150a20150152b50052a90051e60150152b5005015","0x52a80050340152a80052b500506405e0072950150640052b5005064005","0x2b50050152730150152b500501500701501531c0050150b70152a70052b5","0x152a40052b50052a605e0072950152a60052b50052a60051b50152a6005","0x2a30050340152a30052b500506a2a400729501506a0052b500506200502f","0x152a00280072b50052a700506a0150152b50050150060152a70052b5005","0x2b50050710052a30150710052b50052a000502c0150152b5005028005291","0xaf0152370052b50052370051e10150700052b500506f00502801506f005","0x2b500527a0051e40152710052b50052710050330150500052b5005050005","0x1500701507027a2710502370af0050700052b500507000511a01527a005","0x1529f0052b50052950730072990150730052b50050152970150152b5005","0x50500050af0152370052b50052370051e10150760052b500529f0051d9","0x152930052b50052930051e40152910052b50052910050330150500052b5","0x152b50050150070150762932910502370af0050760052b500507600511a","0x52b50050152a001529d0052b500501502b0150152b500503100527a015","0x29701529b0052b500529c29d00729501529c0052b500529c0051b501529c","0x2b50052980051d90152980052b500529b29a00729901529a0052b5005015","0x330151f50052b50051f50050af0150150052b50050150051e1015296005","0x2b500529600511a0150ba0052b50050ba0051e40151fe0052b50051fe005","0x2b50050150060150152b50050150070152960ba1fe1f50150af005296005","0x2b500501502b0150152b500502b00526d0150152b50050340051e6015015","0x72950152920052b50052920051b50152920052b5005015073015294005","0x2b50052900820072990150820052b50050152970152900052b5005292294","0xaf0150150052b50050150051e10150e20052b50050e30051d90150e3005","0x2b50050ba0051e40150090052b50050090050330150af0052b50050af005","0x150070150e20ba0090af0150af0050e20052b50050e200511a0150ba005","0x152a001528f0052b500501502b0150152b50051290052800150152b5005","0x52b500528e28f00729501528e0052b500528e0051b501528e0052b5005","0x51d901508e0052b500528d28c00729901528c0052b500501529701528d","0x52b500500c0050af0150150052b50050150051e101508d0052b500508e","0x511a0150ba0052b50050ba0051e401500f0052b500500f00503301500c","0x50150152b500501501501508d0ba00f00c0150af00508d0052b500508d","0xba0150152b500501500701500f00c00731d0090af0072b5007007005007","0xaf0052b50050af0050af0150152b50050151290150100052b5005129005","0x270150152b500501500701502100531e0f60b70072b5007010005009015","0x2b500500600520f0150060052b500502e00502f01502e0052b50050f6005","0xb701502c0052b500502700521c01502b0052b50050b700500f015027005","0x521f01502f0052b50050150f60150152b500501500701501531f005015","0x52b500503300521c01502b0052b500502100500f0150330052b500502f","0x90150152b50050150070150340053200310052b500702c00523701502c","0x50150060150152b50050150070151b90053211121b50072b500702b005","0x3100527a0150152b500511200526f0150152b50051b500526d0150152b5","0x51b50151d20052b50050152930151c80052b500501502b0150152b5005","0x52b50050152970151d90052b50051d21c80072950151d20052b50051d2","0x1e10151e40052b50051e30051d90151e30052b50051d91e10072990151e1","0x2b50050090050330150af0052b50050af0050af0150150052b5005015005","0xaf0051e40052b50051e400511a0150ba0052b50050ba0051e4015009005","0x526d0150152b50050150060150152b50050150070151e40ba0090af015","0x1511a0052b500511a00505901511a0052b50050150580150152b50051b9","0x152b50050150070151fe1f50073220191e60072b500711a0090af0ba05c","0x52060052490152060052b500520200524a0152020052b50050150de015","0x1521c0052b500520f0050e10150152b500520900524801520f2090072b5","0x21f0150ba28901521f0052b500521f0051b501521f0052b500521c00502f","0x504f1e600728601504f0052b500504f0051b501504f2370072b5005031","0x2590072b50052530052410152530052b50050520052430150520500072b5","0x26400527f0152640052b50052620050e00150152b5005259005240015262","0x1526f0052b500501509901526d0052b500505400527c0150540052b5005","0x50500050af0152370052b50052370051e101526f0052b500526f00509a","0x2932910ba32328027a2710ba2b500726d26f0ba01912909c0150500052b5","0x52b50052800051b50150152b50050151290150152b5005015007015295","0x1e40152710052b50052710050330152972800072b500528000523e015280","0x50150070152990053240152b500729700527b01527a0052b500527a005","0x580051050150580052b50050150f60150152b500528000527a0150152b5","0x70150153250050150b701505c0052b50050590050e50150590052b5005","0x1529e0052b50052800052380150152b50052990052760150152b5005015","0x2b500505c00523a01505e0052b500501502b01505c0052b500529e0050e5","0x53262a90052b50072aa0051040150152b50050600050e90152aa060007","0x52b50050150a20150152b50052a90051e60150152b5005015007015062","0x340152a80052b500506405e0072950150640052b50050640051b5015064","0x2730150152b50050150070150153270050150b70152a70052b50052a8005","0x2b50052a605e0072950152a60052b50052a60051b50152a60052b5005015","0x152a30052b500506a2a400729501506a0052b500506200502f0152a4005","0x72b50052a700506a0150152b50050150060152a70052b50052a3005034","0x52a30150710052b50052a000502c0150152b50050280052910152a0028","0x52b50052370051e10150700052b500506f00502801506f0052b5005071","0x51e40152710052b50052710050330150500052b50050500050af015237","0x7027a2710502370af0050700052b500507000511a01527a0052b500527a","0x2b50052950730072990150730052b50050152970150152b5005015007015","0xaf0152370052b50052370051e10150760052b500529f0051d901529f005","0x2b50052930051e40152910052b50052910050330150500052b5005050005","0x150070150762932910502370af0050760052b500507600511a015293005","0x152a001529d0052b500501502b0150152b500503100527a0150152b5005","0x52b500529c29d00729501529c0052b500529c0051b501529c0052b5005","0x51d90152980052b500529b29a00729901529a0052b500501529701529b","0x52b50051f50050af0150150052b50050150051e10152960052b5005298","0x511a0150ba0052b50050ba0051e40151fe0052b50051fe0050330151f5","0x60150152b50050150070152960ba1fe1f50150af0052960052b5005296","0x2b0150152b500502b00526d0150152b50050340051e60150152b5005015","0x2920052b50052920051b50152920052b50050150730152940052b5005015","0x820072990150820052b50050152970152900052b5005292294007295015","0x52b50050150051e10150e20052b50050e30051d90150e30052b5005290","0x51e40150090052b50050090050330150af0052b50050af0050af015015","0xe20ba0090af0150af0050e20052b50050e200511a0150ba0052b50050ba","0x28f0052b500501502b0150152b50051290052800150152b5005015007015","0x28e28f00729501528e0052b500528e0051b501528e0052b50050152a0015","0x8e0052b500528d28c00729901528c0052b500501529701528d0052b5005","0xc0050af0150150052b50050150051e101508d0052b500508e0051d9015","0xba0052b50050ba0051e401500f0052b500500f00503301500c0052b5005","0x2b500501501501508d0ba00f00c0150af00508d0052b500508d00511a015","0x2b500501500701501000f00732800c0090072b50070ba005007005015015","0x50090050af0150152b50050151290150b70052b50050af0050ba015015","0x2b500501500701502e0053290210f60072b50070b70050090150090052b5","0x520f0150270052b500500600502f0150060052b5005021005027015015","0x52b500502b00521c01502c0052b50050f600500f01502b0052b5005027","0x330052b50050150f60150152b500501500701501532a0050150b701502f","0x3100521c01502c0052b500502e00500f0150310052b500503300521f015","0x2b50050150070151b500532b0340052b500702f00523701502f0052b5005","0x152b50050150070151c800532c1b91120072b50070340090070e3015015","0x1d20050310151120052b50051120050af0151d20052b500502c00502c015","0x2b50071e10052020151e11d90072b50051d21120071fe0151d20052b5005","0x1e611a0072b50051e30052060150152b50050150070151e400532d1e3005","0x50ba0150152b50050150070151f500532e0190052b50071e6005209015","0x1500701520900532f2062020072b50071fe0050090151fe0052b500511a","0x20600526f0150152b500520200526d0150152b50050150060150152b5005","0x1502b0150152b50051b90050e20150152b50050190052800150152b5005","0x1521c0052b500521c0051b501521c0052b500501529301520f0052b5005","0x21f2370072990152370052b500501529701521f0052b500521c20f007295","0x150052b50050150051e10150500052b500504f0051d901504f0052b5005","0xc0050330150070052b50050070051e30151d90052b50051d90050af015","0x500052b500505000511a0151290052b50051290051e401500c0052b5005","0x152b50050150060150152b500501500701505012900c0071d9015009005","0x2b50050520050590150520052b50050150580150152b500520900526d015","0x150070152642620073302592530072b500705200c1d90ba05c015052005","0x330152530052b50052530050af0150540052b50050152aa0150152b5005","0x2b50050070051e30150150052b50050150051e10152590052b5005259005","0x310151b90052b50051b900528f0151290052b50051290051e4015007005","0x92b50050191b905412900701525925300f2340150190052b5005019005","0x150070152950053312930052b500729100523301529128027a27126f26d","0x150582990072b50052930052320152970052b500501502b0150152b5005","0x505900506a0150590052b50050582970072950150152b500529900524c","0x1505e0052b500529e00502c0150152b500505c00529101529e05c0072b5","0x52710051e10152aa0052b50050600050280150600052b500505e0052a3","0x1527a0052b500527a0051e301526d0052b500526d0050af0152710052b5","0x52aa00511a0152800052b50052800051e401526f0052b500526f005033","0x51d90150152b50050150070152aa28026f27a26d2710090052aa0052b5","0x52b500526d0050af0152710052b50052710051e10152a90052b5005295","0x51e401526f0052b500526f00503301527a0052b500527a0051e301526d","0x28026f27a26d2710090052a90052b50052a900511a0152800052b5005280","0x2b50051b90050e20150152b50050190052800150152b50050150070152a9","0x50640051b50150640052b50050152a00150620052b500501502b015015","0x152a70052b50050152970152a80052b50050640620072950150640052b5","0x150051e10152a40052b50052a60051d90152a60052b50052a82a7007299","0x70052b50050070051e30152620052b50052620050af0150150052b5005","0x2a400511a0151290052b50051290051e40152640052b5005264005033015","0x60150152b50050150070152a41292640072620150090052a40052b5005","0xe20150152b500511a0052800150152b50051f50051e60150152b5005015","0x152a30052b500501507001506a0052b500501502b0150152b50051b9005","0x50152970150280052b50052a306a0072950152a30052b50052a30051b5","0x6f0052b50050710051d90150710052b50050282a00072990152a00052b5","0x70051e30151d90052b50051d90050af0150150052b50050150051e1015","0x1290052b50051290051e401500c0052b500500c0050330150070052b5005","0x501500701506f12900c0071d901500900506f0052b500506f00511a015","0x51e40051d90150152b50051b90050e20150152b50050150060150152b5","0x151d90052b50051d90050af0150150052b50050150051e10150700052b5","0x51290051e401500c0052b500500c0050330150070052b50050070051e3","0x1507012900c0071d90150090050700052b500507000511a0151290052b5","0x730052b50051c80050af0150152b500502c00526d0150152b5005015007","0x150152b50051b50051e60150152b50050150070150153320050150b7015","0x152b50050150060150730052b50050090050af0150152b500502c00526d","0x2b50050760051b50150760052b500501507301529f0052b500501502b015","0x29901529c0052b500501529701529d0052b500507629f007295015076005","0x50150051e101529a0052b500529b0051d901529b0052b500529d29c007","0x150070052b50050070051e30150730052b50050730050af0150150052b5","0x529a00511a0151290052b50051290051e401500c0052b500500c005033","0x52800150152b500501500701529a12900c00707301500900529a0052b5","0x1b50152960052b50050152a00152980052b500501502b0150152b50050af","0x2b50050152970152940052b50052962980072950152960052b5005296005","0x150820052b50052900051d90152900052b5005294292007299015292005","0x50070051e301500f0052b500500f0050af0150150052b50050150051e1","0x151290052b50051290051e40150100052b50050100050330150070052b5","0x2b500501501501508212901000700f0150090050820052b500508200511a","0x2b500501500701501000f00733300c0090072b50070ba005007005015015","0x50090050af0150152b50050151290150b70052b50050af0050ba015015","0x2b500501500701502e0053340210f60072b50070b70050090150090052b5","0x520f0150270052b500500600502f0150060052b5005021005027015015","0x52b500502b00521c01502c0052b50050f600500f01502b0052b5005027","0x330052b50050150f60150152b50050150070150153350050150b701502f","0x3100521c01502c0052b500502e00500f0150310052b500503300521f015","0x2b50050150070151b50053360340052b500702f00523701502f0052b5005","0x152b50050150070151c80053371b91120072b50070340090070e3015015","0x1e10053381d91d20072b500702c0050090151120052b50051120050af015","0x2b50051e300502f0151e30052b50051d90050270150152b5005015007015","0x21c0151e60052b50051d200500f01511a0052b50051e400520f0151e4005","0xf60150152b50050150070150153390050150b70150190052b500511a005","0x52b50051e100500f0151fe0052b50051f500521f0151f50052b5005015","0x20600533a2020052b50070190052370150190052b50051fe00521c0151e6","0x2b50051120050af0152090052b50051e600502c0150152b5005015007015","0x21c20f0072b50052091120071fe0152090052b5005209005031015112005","0x52060150152b500501500701523700533b21f0052b500721c005202015","0x1500701525300533c0520052b500705000520901505004f0072b500521f","0x2642620072b50072590050090152590052b500504f0050ba0150152b5005","0x502f01526d0052b50052640050270150152b500501500701505400533d","0x52b500526200500f0152710052b500526f00520f01526f0052b500526d","0x2b500501500701501533e0050150b70152800052b500527100521c01527a","0x5400500f0152930052b500529100521f0152910052b50050150f6015015","0x2950052b50072800052370152800052b500529300521c01527a0052b5005","0x53400582990072b500727a0050090150152b500501500701529700533f","0x152b500529900526d0150152b50050150060150152b5005015007015059","0x2b50050520052800150152b500529500527a0150152b500505800526f015","0x2b500501502b0150152b50051b90050e20150152b500520200527a015015","0x729501529e0052b500529e0051b501529e0052b500501529301505c005","0x2b500505e0600072990150600052b500501529701505e0052b500529e05c","0xaf0150150052b50050150051e10152a90052b50052aa0051d90152aa005","0x2b500500c0050330150070052b50050070051e301520f0052b500520f005","0x90052a90052b50052a900511a0151290052b50051290051e401500c005","0x150152b500505900526d0150152b50050150070152a912900c00720f015","0x6200c20f0ba05c0150620052b50050620050590150620052b5005015058","0x2b50050150060150152b50050150070152a62a70073412a80640072b5007","0x2a80050330150640052b50050640050af0152a40052b50050152aa015015","0x70052b50050070051e30150150052b50050150051e10152a80052b5005","0x2020051b50151b90052b50051b900528f0151290052b50051290051e4015","0x2950052b50052950051b50150520052b50050520050310152020052b5005","0x2a00282a306a0092b50052950522021b92a41290070152a80640b7231015","0x150152b50050150070150730053420700052b500706f00523301506f071","0x507600524c01529d0760072b500507000523201529f0052b500501502b","0x29a29b0072b500529c00506a01529c0052b500529d29f0072950150152b5","0x52980052a30152980052b500529a00502c0150152b500529b005291015","0x150280052b50050280051e10152940052b50052960050280152960052b5","0x52a30050330152a00052b50052a00051e301506a0052b500506a0050af","0x52940052b500529400511a0150710052b50050710051e40152a30052b5","0x52b50050730051d90150152b50050150070152940712a32a006a028009","0x51e301506a0052b500506a0050af0150280052b50050280051e1015292","0x52b50050710051e40152a30052b50052a30050330152a00052b50052a0","0x150070152920712a32a006a0280090052920052b500529200511a015071","0x520052800150152b500529500527a0150152b50050150060150152b5005","0x1502b0150152b50051b90050e20150152b500520200527a0150152b5005","0x150820052b50050820051b50150820052b50050152a00152900052b5005","0xe30e20072990150e20052b50050152970150e30052b5005082290007295","0x150052b50050150051e101528e0052b500528f0051d901528f0052b5005","0x2a60050330150070052b50050070051e30152a70052b50052a70050af015","0x28e0052b500528e00511a0151290052b50051290051e40152a60052b5005","0x152b50050150060150152b500501500701528e1292a60072a7015009005","0x2b50050520052800150152b500527a00526d0150152b50052970051e6015","0x2b500501502b0150152b50051b90050e20150152b500520200527a015015","0x729501528c0052b500528c0051b501528c0052b500501507101528d005","0x2b500508e08d00729901508d0052b500501529701508e0052b500528c28d","0xaf0150150052b50050150051e10151060052b50051070051d9015107005","0x2b500500c0050330150070052b50050070051e301520f0052b500520f005","0x90051060052b500510600511a0151290052b50051290051e401500c005","0x1e60150152b50050150060150152b500501500701510612900c00720f015","0x150152b500504f0052800150152b50051b90050e20150152b5005253005","0x910052b500501506f0151020052b500501502b0150152b500520200527a","0x1529701528a0052b50050911020072950150910052b50050910051b5015","0x52b50052860051d90152860052b500528a2890072990152890052b5005","0x51e301520f0052b500520f0050af0150150052b50050150051e1015283","0x52b50051290051e401500c0052b500500c0050330150070052b5005007","0x1500701528312900c00720f0150090052830052b500528300511a015129","0x20200527a0150152b50051b90050e20150152b50050150060150152b5005","0x150150052b50050150051e101501c0052b50052370051d90150152b5005","0x500c0050330150070052b50050070051e301520f0052b500520f0050af","0x501c0052b500501c00511a0151290052b50051290051e401500c0052b5","0x150152b50050150060150152b500501500701501c12900c00720f015009","0x152b50051b90050e20150152b50051e600526d0150152b50052060051e6","0x2b50052810051b50152810052b50050150700150960052b500501502b015","0x29901527c0052b500501529701527f0052b5005281096007295015281005","0x50150051e101509a0052b50050990051d90150990052b500527f27c007","0x150070052b50050070051e30151120052b50051120050af0150150052b5","0x509a00511a0151290052b50051290051e401500c0052b500500c005033","0x526d0150152b500501500701509a12900c00711201500900509a0052b5","0x70150153430050150b701509c0052b50051c80050af0150152b500502c","0xaf0150152b500502c00526d0150152b50051b50051e60150152b5005015","0x1527b0052b500501502b0150152b500501500601509c0052b5005009005","0x527827b0072950152780052b50052780051b50152780052b5005015073","0x1509f0052b50052772760072990152760052b50050152970152770052b5","0x509c0050af0150150052b50050150051e101500d0052b500509f0051d9","0x1500c0052b500500c0050330150070052b50050070051e301509c0052b5","0x709c01500900500d0052b500500d00511a0151290052b50051290051e4","0x501502b0150152b50050af0052800150152b500501500701500d12900c","0x2950150a20052b50050a20051b50150a20052b50050152a00150a00052b5","0x52730a40072990150a40052b50050152970152730052b50050a20a0007","0x150150052b50050150051e10150a50052b50052720051d90152720052b5","0x50100050330150070052b50050070051e301500f0052b500500f0050af","0x50a50052b50050a500511a0151290052b50051290051e40150100052b5","0x2b50070ba0050070050150152b50050150150150a512901000700f015009","0x52b50050af0050ba0150152b500501500701501000f00734400c009007","0x70b70050090150090052b50050090050af0150152b50050151290150b7","0x52b50050210050270150152b500501500701502e0053450210f60072b5","0x500f01502b0052b500502700520f0150270052b500500600502f015006","0x150153460050150b701502f0052b500502b00521c01502c0052b50050f6","0x310052b500503300521f0150330052b50050150f60150152b5005015007","0x2f00523701502f0052b500503100521c01502c0052b500502e00500f015","0x2b50070340090070e30150152b50050150070151b50053470340052b5007","0x1120052b50051120050af0150152b50050150070151c80053481b9112007","0x270150152b50050150070151e10053491d91d20072b500702c005009015","0x2b50051e400520f0151e40052b50051e300502f0151e30052b50051d9005","0xb70150190052b500511a00521c0151e60052b50051d200500f01511a005","0x521f0151f50052b50050150f60150152b500501500701501534a005015","0x52b50051fe00521c0151e60052b50051e100500f0151fe0052b50051f5","0x2c0150152b500501500701520600534b2020052b5007019005237015019","0x2b50052090050310151120052b50051120050af0152090052b50051e6005","0x21f0052b500721c00520201521c20f0072b50052091120071fe015209005","0x20901505004f0072b500521f0052060150152b500501500701523700534c","0x504f0050ba0150152b500501500701525300534d0520052b5007050005","0x2b500501500701505400534e2642620072b50072590050090152590052b5","0x520f01526f0052b500526d00502f01526d0052b5005264005027015015","0x52b500527100521c01527a0052b500526200500f0152710052b500526f","0x2910052b50050150f60150152b500501500701501534f0050150b7015280","0x29300521c01527a0052b500505400500f0152930052b500529100521f015","0x2b50050150070152970053502950052b50072800052370152800052b5005","0x150152b50050150070150590053510582990072b500727a005009015015","0x150152b500505800526f0150152b500529900526d0150152b5005015006","0x152b500520200527a0150152b50050520052800150152b500529500527a","0x52b500501529301505c0052b500501502b0150152b50051b90050e2015","0x29701505e0052b500529e05c00729501529e0052b500529e0051b501529e","0x2b50052aa0051d90152aa0052b500505e0600072990150600052b5005015","0x1e301520f0052b500520f0050af0150150052b50050150051e10152a9005","0x2b50051290051e401500c0052b500500c0050330150070052b5005007005","0x70152a912900c00720f0150090052a90052b50052a900511a015129005","0x590150620052b50050150580150152b500505900526d0150152b5005015","0x2a70073522a80640072b500706200c20f0ba05c0150620052b5005062005","0x2a40052b50050152aa0150152b50050150060150152b50050150070152a6","0x150051e10152a80052b50052a80050330150640052b50050640050af015","0x1290052b50051290051e40150070052b50050070051e30150150052b5005","0x520050310152020052b50052020051b50151b90052b50051b900528f015","0x1290070152a80640b70f00152950052b50052950051b50150520052b5005","0x2b500706f00523301506f0712a00282a306a0092b50052950522021b92a4","0x23201529f0052b500501502b0150152b5005015007015073005353070005","0x529d29f0072950150152b500507600524c01529d0760072b5005070005","0x150152b500529b00529101529a29b0072b500529c00506a01529c0052b5","0x52960050280152960052b50052980052a30152980052b500529a00502c","0x1506a0052b500506a0050af0150280052b50050280051e10152940052b5","0x50710051e40152a30052b50052a30050330152a00052b50052a00051e3","0x152940712a32a006a0280090052940052b500529400511a0150710052b5","0x52b50050280051e10152920052b50050730051d90150152b5005015007","0x50330152a00052b50052a00051e301506a0052b500506a0050af015028","0x52b500529200511a0150710052b50050710051e40152a30052b50052a3","0x2b50050150060150152b50050150070152920712a32a006a028009005292","0x520200527a0150152b50050520052800150152b500529500527a015015","0x50152a00152900052b500501502b0150152b50051b90050e20150152b5","0xe30052b50050822900072950150820052b50050820051b50150820052b5","0x28f0051d901528f0052b50050e30e20072990150e20052b5005015297015","0x2a70052b50052a70050af0150150052b50050150051e101528e0052b5005","0x1290051e40152a60052b50052a60050330150070052b50050070051e3015","0x28e1292a60072a701500900528e0052b500528e00511a0151290052b5005","0x150152b50052970051e60150152b50050150060150152b5005015007015","0x152b500520200527a0150152b50050520052800150152b500527a00526d","0x52b500501507101528d0052b500501502b0150152b50051b90050e2015","0x29701508e0052b500528c28d00729501528c0052b500528c0051b501528c","0x2b50051070051d90151070052b500508e08d00729901508d0052b5005015","0x1e301520f0052b500520f0050af0150150052b50050150051e1015106005","0x2b50051290051e401500c0052b500500c0050330150070052b5005007005","0x701510612900c00720f0150090051060052b500510600511a015129005","0x50e20150152b50052530051e60150152b50050150060150152b5005015","0x2b0150152b500520200527a0150152b500504f0052800150152b50051b9","0x910052b50050910051b50150910052b500501506f0151020052b5005015","0x2890072990152890052b500501529701528a0052b5005091102007295015","0x52b50050150051e10152830052b50052860051d90152860052b500528a","0x50330150070052b50050070051e301520f0052b500520f0050af015015","0x52b500528300511a0151290052b50051290051e401500c0052b500500c","0x2b50050150060150152b500501500701528312900c00720f015009005283","0x52370051d90150152b500520200527a0150152b50051b90050e2015015","0x1520f0052b500520f0050af0150150052b50050150051e101501c0052b5","0x51290051e401500c0052b500500c0050330150070052b50050070051e3","0x1501c12900c00720f01500900501c0052b500501c00511a0151290052b5","0x26d0150152b50052060051e60150152b50050150060150152b5005015007","0x150960052b500501502b0150152b50051b90050e20150152b50051e6005","0x52810960072950152810052b50052810051b50152810052b5005015070","0x150990052b500527f27c00729901527c0052b500501529701527f0052b5","0x51120050af0150150052b50050150051e101509a0052b50050990051d9","0x1500c0052b500500c0050330150070052b50050070051e30151120052b5","0x711201500900509a0052b500509a00511a0151290052b50051290051e4","0x1c80050af0150152b500502c00526d0150152b500501500701509a12900c","0x1b50051e60150152b50050150070150153540050150b701509c0052b5005","0x601509c0052b50050090050af0150152b500502c00526d0150152b5005","0x1b50152780052b500501507301527b0052b500501502b0150152b5005015","0x2b50050152970152770052b500527827b0072950152780052b5005278005","0x1500d0052b500509f0051d901509f0052b5005277276007299015276005","0x50070051e301509c0052b500509c0050af0150150052b50050150051e1","0x151290052b50051290051e401500c0052b500500c0050330150070052b5","0x2b500501500701500d12900c00709c01500900500d0052b500500d00511a","0x2b50050152a00150a00052b500501502b0150152b50050af005280015015","0x152730052b50050a20a00072950150a20052b50050a20051b50150a2005","0x52720051d90152720052b50052730a40072990150a40052b5005015297","0x1500f0052b500500f0050af0150150052b50050150051e10150a50052b5","0x51290051e40150100052b50050100050330150070052b50050070051e3","0x150a512901000700f0150090050a50052b50050a500511a0151290052b5","0x1500c0090073550af1290072b50070050150070050150152b5005015015","0x52b50050af0050330151290052b50051290050af0150152b5005015007","0x1000f0ba2b50050ba0af1290ba0f20150ba0052b50050ba0050310150af","0x22f0150152b50050150070150210053560f60052b50070b70051030150b7","0x701502b0053570270052b500700600522d01500602e0072b50050f6005","0x2f0072b500702c00500901502c0052b500502e0050ba0150152b5005015","0x526f0150152b500502f00526d0150152b5005015007015031005358033","0x2930150340052b500501502b0150152b500502700522c0150152b5005033","0x2b50051b50340072950151b50052b50051b50051b50151b50052b5005015","0x1d90151c80052b50051121b90072990151b90052b5005015297015112005","0x2b500501000503301500f0052b500500f0050af0151d20052b50051c8005","0x1290051d20052b50051d200511a0150070052b50050070051e4015010005","0x150580150152b500503100526d0150152b50050150070151d200701000f","0x2b50071d901000f0ba05c0151d90052b50051d90050590151d90052b5005","0x1e60052b50050150620150152b500501500701511a1e40073591e31e1007","0x70051e40151e30052b50051e30050330151e10052b50051e10050af015","0x271e60071e31e10af2280150270052b50050270052290150070052b5005","0x701520900535a2060052b50072020052270152021fe1f50191292b5005","0x6a01520f0052b500501502b0150152b50052060052260150152b5005015","0x2b500521f00502c0150152b500521c00529101521f21c0072b500520f005","0xaf0150500052b500504f00502801504f0052b50052370052a3015237005","0x2b50051fe0051e40151f50052b50051f50050330150190052b5005019005","0x50150070150501fe1f50191290050500052b500505000511a0151fe005","0x330150190052b50050190050af0150520052b50052090051d90150152b5","0x2b500505200511a0151fe0052b50051fe0051e40151f50052b50051f5005","0x2b500502700522c0150152b50050150070150521fe1f5019129005052005","0x52590051b50152590052b50050152a00152530052b500501502b015015","0x152640052b50050152970152620052b50052592530072950152590052b5","0x1e40050af01526d0052b50050540051d90150540052b5005262264007299","0x70052b50050070051e401511a0052b500511a0050330151e40052b5005","0x152b500501500701526d00711a1e412900526d0052b500526d00511a015","0x52b500501502b0150152b500502e0052800150152b500502b0051e6015","0x26f0072950152710052b50052710051b50152710052b500501507301526f","0x52b500527a2800072990152800052b500501529701527a0052b5005271","0x503301500f0052b500500f0050af0152930052b50052910051d9015291","0x52b500529300511a0150070052b50050070051e40150100052b5005010","0x52b50050210051d90150152b500501500701529300701000f129005293","0x51e40150100052b500501000503301500f0052b500500f0050af015295","0x1529500701000f1290052950052b500529500511a0150070052b5005007","0x152970052b500501502b0150152b50050ba0052800150152b5005015007","0x52992970072950152990052b50052990051b50152990052b50050152a0","0x1505c0052b50050580590072990150590052b50050152970150580052b5","0x500c0050330150090052b50050090050af01529e0052b500505c0051d9","0x529e0052b500529e00511a0150070052b50050070051e401500c0052b5","0x1290072b50070050150070050150152b500501501501529e00700c009129","0x1500f0052b50050ba0050ba0150152b500501500701500c00900735b0af","0x150f600535c0b70100072b500700f0050090151290052b50051290050af","0x150152b50050b700526f0150152b500501000526d0150152b5005015007","0x52b500502e0051b501502e0052b50050152930150210052b500501502b","0x72990150270052b50050152970150060052b500502e02100729501502e","0x2b50051290050af01502c0052b500502b0051d901502b0052b5005006027","0x11a0150070052b50050070051e40150af0052b50050af005033015129005","0x26d0150152b500501500701502c0070af12912900502c0052b500502c005","0x2f0052b500502f00505901502f0052b50050150580150152b50050f6005","0x2b50050150070151b503400735d0310330072b500702f0af1290ba05c015","0x1b90052410151b90052b50051120052430151120052b5005015225015015","0x1d90052b50051d20050e00150152b50051c80052400151d21c80072b5005","0x50150990151e30052b50051e100527c0151e10052b50051d900527f015","0x526e0151e40052b50051e400509a0150152b50050151290151e40052b5","0x1e31e400703112909c0150330052b50050330050af0151e30052b50051e3","0x151080150152b50050150070152021fe1f50ba35e0191e611a0ba2b5007","0x20f0072b50052090052410152090052b50052060052430152060052b5005","0x21f00527f01521f0052b500521c0050e00150152b500520f00524001521c","0x150500052b500501509901504f0052b500523700527c0152370052b5005","0x504f00526e0150500052b500505000509a01511a0052b500511a005033","0x2b500704f0501e611a12909c0150190052b50050190051b501504f0052b5","0x2b50050152240150152b50050150070150542642620ba35f2592530520ba","0x1527a2710072b500526f00524101526f0052b500526d00524301526d005","0x2b500528000527f0152800052b500527a0050e00150152b5005271005240","0x50330152950052b50050150990152930052b500529100527c015291005","0x52b500529300526e0152950052b500529500509a0150520052b5005052","0x2970ba2b500729329525305212909c0152590052b50052590051b5015293","0x150152b50050150060150152b500501500701529e05c0590ba360058299","0x600052200150600052b50050582590190ba0fe01505e0052b500501502b","0x620ba2b50052a900521e0150152b50052aa0051090152a92aa0072b5005","0x2950152a70052b50052a70051b50152a70052b500506200502f0152a8064","0x52a40051b50152a40052b500506400502f0152a60052b50052a705e007","0x2a30052b50052a800502f01506a0052b50052a42a60072950152a40052b5","0x506a0150280052b50052a306a0072950152a30052b50052a30051b5015","0x52b500507100502c0150152b50052a00052910150712a00072b5005028","0x50af0150730052b50050700050280150700052b500506f0052a301506f","0x52b50052990051e40152970052b50052970050330150330052b5005033","0x2b50050150070150732992970331290050730052b500507300511a015299","0x50590050330150152b500525900527a0150152b500501900527a015015","0x1529d0052b500529e0050340150760052b500505c0051e401529f0052b5","0x330150152b500501900527a0150152b50050150070150153610050150b7","0x2b50050540050340150760052b50052640051e401529f0052b5005262005","0x2b50051f50050330150152b50050150070150153610050150b701529d005","0x601529d0052b50052020050340150760052b50051fe0051e401529f005","0x29b0052b500529d29c00729901529c0052b50050152970150152b5005015","0x29f0050330150330052b50050330050af01529a0052b500529b0051d9015","0x29a0052b500529a00511a0150760052b50050760051e401529f0052b5005","0x152980052b500501502b0150152b500501500701529a07629f033129005","0x52962980072950152960052b50052960051b50152960052b50050152a0","0x152900052b50052942920072990152920052b50050152970152940052b5","0x51b50050330150340052b50050340050af0150820052b50052900051d9","0x50820052b500508200511a0150070052b50050070051e40151b50052b5","0x2b0150152b50050ba0052800150152b50050150070150820071b5034129","0xe20052b50050e20051b50150e20052b50050152a00150e30052b5005015","0x28e00729901528e0052b500501529701528f0052b50050e20e3007295015","0x52b50050090050af01528c0052b500528d0051d901528d0052b500528f","0x511a0150070052b50050070051e401500c0052b500500c005033015009","0x70050150152b500501501501528c00700c00912900528c0052b500528c","0x50ba0150152b500501500701500c0090073620af1290072b5007005015","0x151290052b50051290050af0150152b500501512901500f0052b50050ba","0x50270150152b50050150070150f60053630b70100072b500700f005009","0x52b500502e00520f01502e0052b500502100502f0150210052b50050b7","0x150b701502b0052b500500600521c0150270052b500501000500f015006","0x2c00521f01502c0052b50050150f60150152b5005015007015015364005","0x2b0052b500502f00521c0150270052b50050f600500f01502f0052b5005","0x70e30150152b50050150070150310053650330052b500702b005237015","0x340050af0150152b50050150070151120053661b50340072b5007033129","0x50150070151d20053671c81b90072b50070270050090150340052b5005","0x20f0151e10052b50051d900502f0151d90052b50051c80050270150152b5","0x2b50051e300521c0151e40052b50051b900500f0151e30052b50051e1005","0x52b50050150f60150152b50050150070150153680050150b701511a005","0x521c0151e40052b50051d200500f0150190052b50051e600521f0151e6","0x50150070151fe0053691f50052b500711a00523701511a0052b5005019","0x152b500501500701520900536a2062020072b50071e40050090150152b5","0x21c00520f01521c0052b500520f00502f01520f0052b5005206005027015","0x4f0052b500521f00521c0152370052b500520200500f01521f0052b5005","0x150500052b50050150f60150152b500501500701501536b0050150b7015","0x505200521c0152370052b500520900500f0150520052b500505000521f","0x152b500501500701525900536c2530052b500704f00523701504f0052b5","0x270150152b500501500701505400536d2642620072b5007237005009015","0x2b500526f00520f01526f0052b500526d00502f01526d0052b5005264005","0xb70152800052b500527100521c01527a0052b500526200500f015271005","0x521f0152910052b50050150f60150152b500501500701501536e005015","0x52b500529300521c01527a0052b500505400500f0152930052b5005291","0x90150152b500501500701529700536f2950052b5007280005237015280","0x50150060150152b50050150070150590053700582990072b500727a005","0x29500527a0150152b500505800526f0150152b500529900526d0150152b5","0x50e20150152b50051f500527a0150152b500525300527a0150152b5005","0x1b501529e0052b500501529301505c0052b500501502b0150152b50051b5","0x2b500501529701505e0052b500529e05c00729501529e0052b500529e005","0x152a90052b50052aa0051d90152aa0052b500505e060007299015060005","0x50070051e40150af0052b50050af0050330150340052b50050340050af","0x150070152a90070af0341290052a90052b50052a900511a0150070052b5","0x50590150620052b50050150580150152b500505900526d0150152b5005","0x2a62a70073712a80640072b50070620af0340ba05c0150620052b5005062","0x152a40052b500501529e0150152b50050150060150152b5005015007015","0x280052b50050152aa0152a30052b500501506001506a0052b500501505e","0x2a306a2a40090640150710052b50050150620152a00052b50050152a9015","0x52a80050330150640052b50050640050af01506f0052b50050712a0028","0x151b50052b50051b500528f0150070052b50050070051e40152a80052b5","0x52950051b50152530052b50052530051b50151f50052b50051f50051b5","0x29f0730701292b50052952531f51b506f0072a806400f10b0152950052b5","0x2a40150152b500501500701529c00537229d0052b50070760052a6015076","0x29a0072b500529b00506a01529b0052b500501502b0150152b500529d005","0x2960052a30152960052b500529800502c0150152b500529a005291015298","0x700052b50050700050af0152920052b50052940050280152940052b5005","0x29200511a01529f0052b500529f0051e40150730052b5005073005033015","0x29c0051d90150152b500501500701529229f0730701290052920052b5005","0x730052b50050730050330150700052b50050700050af0152900052b5005","0x730701290052900052b500529000511a01529f0052b500529f0051e4015","0x2b500529500527a0150152b50050150060150152b500501500701529029f","0x51b50050e20150152b50051f500527a0150152b500525300527a015015","0xe30051b50150e30052b50050152a00150820052b500501502b0150152b5","0x28f0052b50050152970150e20052b50050e30820072950150e30052b5005","0x50af01528d0052b500528e0051d901528e0052b50050e228f007299015","0x52b50050070051e40152a60052b50052a60050330152a70052b50052a7","0x2b500501500701528d0072a62a712900528d0052b500528d00511a015007","0x2b500527a00526d0150152b50052970051e60150152b5005015006015015","0x51b50050e20150152b50051f500527a0150152b500525300527a015015","0x8e0051b501508e0052b500501507101528c0052b500501502b0150152b5","0x1070052b500501529701508d0052b500508e28c00729501508e0052b5005","0x50af0151020052b50051060051d90151060052b500508d107007299015","0x52b50050070051e40150af0052b50050af0050330150340052b5005034","0x2b50050150070151020070af0341290051020052b500510200511a015007","0x2b50051b50050e20150152b50052590051e60150152b5005015006015015","0x2b500501502b0150152b50051f500527a0150152b500523700526d015015","0x729501528a0052b500528a0051b501528a0052b500501506f015091005","0x2b50052892860072990152860052b50050152970152890052b500528a091","0x330150340052b50050340050af01501c0052b50052830051d9015283005","0x2b500501c00511a0150070052b50050070051e40150af0052b50050af005","0x152b50050150060150152b500501500701501c0070af03412900501c005","0x2b50051e400526d0150152b50051b50050e20150152b50051fe0051e6015","0x52810051b50152810052b50050150700150960052b500501502b015015","0x1527c0052b500501529701527f0052b50052810960072950152810052b5","0x340050af01509a0052b50050990051d90150990052b500527f27c007299","0x70052b50050070051e40150af0052b50050af0050330150340052b5005","0x152b500501500701509a0070af03412900509a0052b500509a00511a015","0x3730050150b701509c0052b50051120050af0150152b500502700526d015","0x2b500502700526d0150152b50050310051e60150152b5005015007015015","0x2b500501502b0150152b500501500601509c0052b50051290050af015015","0x72950152780052b50052780051b50152780052b500501507301527b005","0x2b50052772760072990152760052b50050152970152770052b500527827b","0x3301509c0052b500509c0050af01500d0052b500509f0051d901509f005","0x2b500500d00511a0150070052b50050070051e40150af0052b50050af005","0x2b50050ba0052800150152b500501500701500d0070af09c12900500d005","0x50a20051b50150a20052b50050152a00150a00052b500501502b015015","0x150a40052b50050152970152730052b50050a20a00072950150a20052b5","0x90050af0150a50052b50052720051d90152720052b50052730a4007299","0x70052b50050070051e401500c0052b500500c0050330150090052b5005","0x152b50050150060150a500700c0091290050a50052b50050a500511a015","0x152b500501500701500f00c0073740090af0072b5007005015007005015","0x527b0150af0052b50050af0050af0150101290072b500512900523e015","0x152b500512900527a0150152b50050150070150b70053750152b5007010","0x510e0150210052b50050f600700710c0150f60052b50050ba00521d015","0x52b50050090050330150af0052b50050af0050af01502e0052b5005021","0x152b500501500701502e0090af0ba00502e0052b500502e00521b015009","0x2b50050151290150060052b50050070050ba0150152b50050b7005276015","0x150152b500501500701502c00537602b0270072b5007006005009015015","0x503300520f0150330052b500502f00502f01502f0052b500502b005027","0x151b50052b500503100521c0150340052b500502700500f0150310052b5","0x21f0151120052b50050150f60150152b50050150070150153770050150b7","0x2b50051b900521c0150340052b500502c00500f0151b90052b5005112005","0x53781d20052b50071b50052370151c80052b500503400502c0151b5005","0x2b50051d20ba0072950150152b50050150060150152b50050150070151d9","0xaf0151e40052b50051e31290071100151e30052b50050152730151e1005","0x2b50051c80050310150090052b50050090050330150af0052b50050af005","0x1120151e40052b50051e40051b50151e10052b50051e10050340151c8005","0x150070150191e611a0ba0050191e611a0ba2b50051e41e11c80090af0af","0xba0052910150152b500512900527a0150152b50050150060150152b5005","0x1fe0052b50051f51c800710c0151f50052b50051d90050190150152b5005","0x90050330150af0052b50050af0050af0152020052b50051fe00510e015","0x150070152020090af0ba0052020052b500520200521b0150090052b5005","0x52800150152b50050ba0052910150152b500512900527a0150152b5005","0x1b50152090052b50050152a00152060052b500501502b0150152b5005007","0x2b500501529701520f0052b50052092060072950152090052b5005209005","0x152370052b500521f00521801521f0052b500520f21c00729901521c005","0x523700521b01500f0052b500500f00503301500c0052b500500c0050af","0x70050090150070052b50050050050ba01523700f00c0ba0052370052b5","0x2b500512900500c0150152b50050150070150af0053791290ba0072b5007","0xb701500f0052b500500900501001500c0052b50050ba00500f015009005","0x50210150100052b50050150f60150152b500501500701501537a005015","0x52b50050b700501001500c0052b50050af00500f0150b70052b5005010","0x2e0150210052b50050f600502c0150f600c0072b500500c00521701500f","0x502e0050270150152b500501500701500600537b02e0052b500700f005","0x1502b0052b500502b0051b501502b0052b500502700502f0150270052b5","0x2800150152b500501500701503300537c02f02c0072b500702b015007216","0xc0072b500500c0052170150310052b50050150990150152b5005021005","0x2150150310052b500503100509a0151b502f0072b500502f0050ac015034","0x150152b50050150070151c800537d1b91120072b50071b503103402c129","0x2f0050ac0151d90052b50051d20052140151d200c0072b500500c005217","0x52b50051b900500f0151d90052b50051d900509a0151e102f0072b5005","0x50150070151e611a00737e1e41e30072b50071e11d91120ba2130151b9","0x150070151fe00537f1f50190072b50071e402f00c1e31292150150152b5","0x152060052b50052020052120152020052b50051b900502c0150152b5005","0x20f00511901520f0052b50052062090072110152090052b50051f500502c","0x21c0052b500521c0052100150190052b50050190050af01521c0052b5005","0x1502b0150152b50051b900526d0150152b500501500701521c019007005","0x152370052b50052370051b50152370052b500501520e01521f0052b5005","0x4f0500072990150500052b500501529701504f0052b500523721f007295","0x1fe0052b50051fe0050af0152530052b500505200520d0150520052b5005","0x2660150152b50050150070152531fe0070052530052b5005253005210015","0x150152b500500c00526d0150152b50051b900526d0150152b50051e6005","0x2620052b500501508f0152590052b500501502b0150152b500502f005266","0x152970152640052b50052622590072950152620052b50052620051b5015","0x52b500526d00520d01526d0052b50052640540072990150540052b5005","0x11a00700526f0052b500526f00521001511a0052b500511a0050af01526f","0x2b500500c00526d0150152b500502f0052660150152b500501500701526f","0x527a0051b501527a0052b500501520e0152710052b500501502b015015","0x152910052b50050152970152800052b500527a27100729501527a0052b5","0x1c80050af0152950052b500529300520d0152930052b5005280291007299","0x50150070152951c80070052950052b50052950052100151c80052b5005","0x29700520c0152970052b50050150f60150152b500500c00526d0150152b5","0x52b50050580051190150580052b50052990210072110152990052b5005","0x330070050590052b50050590052100150330052b50050330050af015059","0x2b500500c00526d0150152b50050060051e60150152b5005015007015059","0x2100721101529e0052b500505c00520c01505c0052b50050150f6015015","0x52b50050150050af0150600052b500505e00511901505e0052b500529e","0xf60052b500501520b0150600150070050600052b5005060005210015015","0x52b500501520a0150270052b500501511e01502e0052b5005015380015","0x2b50050153800150340052b500501511e0150330052b500501538001502c","0x50153800151d90052b50050152080151c80052b500501520b015112005","0x1511e0150190052b500501520701511a0052b50050151210151e30052b5","0x20401520f0052b50050152050152060052b50050152050151fe0052b5005","0x23721f21c0092b50050af0051260150152b50050150060150152b5005015","0x150050052b50050050050330150150052b50050150050af01505205004f","0x2622592531292b50050501290050151291280151290052b50051290051e4","0x2650150152b500501500701526d0053810540052b5007264005203015264","0x152b50050150070152932912800ba38227a27126f0ba2b5007262259007","0x2950052630152950052b500527a0050b601527a0052b500527a0050b4015","0x529900525e0150152b500529700525f01505c0590582992970af2b5005","0x2530050af0150152b500505c00527a0150152b50050590050e20150152b5","0x70052b50050070051e101526f0052b500526f0050330152530052b5005","0x25300925d0150580052b500505800528f0152710052b50052710051e4015","0x2b500502b02c00720001506002102b05e29e0af2b500505823727100726f","0x3832aa0052b500706000525c0150210052b500502102e00712b01502b005","0x50a00150640620072b50052aa00525b0150152b50050150070152a9005","0x2b50052a80051e60150152b50050150070152a70053842a80052b5007064","0x51c800512f0150152b50051d900512d0150152b50052060051ff015015","0x540050450150152b500521f0051f60150152b50050620050b90150152b5","0x524c0150152b50050330051fb0150152b500521c00529c0150152b5005","0x27a0150152b50050190051f80150152b50050520051fc0150152b500504f","0x150152b50051120051fb0150152b500511a0051310150152b500500f005","0x152b500500c0052800150152b50051e30051fb0150152b500503400522a","0x2b50051fe00522a0150152b500520f0051ff0150152b5005010005271015","0x50090052910150152b500502700522a0150152b50050f600512f015015","0x2a40051b50152a40052b500501525a0152a60052b500501502b0150152b5","0x52b500529e0050af01506a0052b50052a42a60072950152a40052b5005","0x51e40152a00052b500502b0051e10150280052b500505e0050330152a3","0x150153850050150b701506f0052b500506a0050340150710052b5005021","0x150700052b50050150580150152b50052a70051e60150152b5005015007","0x2b50050150a20150760052b50050150a201529f0730072b500500900506a","0x1529b0052b500529c29d0760ba0fe01529c0052b50050150a201529d005","0x505e00503301529e0052b500529e0050af01529a0052b500529f00502c","0x150700052b50050700050590150ba0052b50050ba0051e301505e0052b5","0x5e29e0091fd01529a0052b500529a00503101529b0052b500529b005133","0x60052b50050060270071350152940062962981292b500529a29b0700ba","0x151f90150152b50050150070152900053862920052b50072940051fa015","0x52b50050e30050ba0150e300c0072b500500c0051f20150820052b5005","0x28e28f0072b50070820e22980ba1f10150820052b500508200509a0150e2","0x502701528e0052b500528e0051eb0150152b500501500701528d005387","0x2b500508e00528001508d08e0072b500529200513901528c0052b500528e","0x71100151070052b50051070051b50151070052b500528c00502f015015","0x2b500528f0050af0151060052b50051060051b50151060052b500510708d","0x12f0150152b50050150070151020053880152b500710600527b01528f005","0x1528a0052b50050150a20150910052b50050150580150152b50050f6005","0x528628928a0ba0fe0152860052b50050150a20152890052b50050150a2","0x152960052b500529600503301528f0052b500528f0050af0152830052b5","0x52830051330150910052b50050910050590150060052b50050060051e3","0x1c0052b500501c00503101501c00c0072b500500c0051f20152830052b5","0x1fe00713501527f1f52810961292b500501c28309100629628f0091fd015","0x501500701527c0053891e40052b500727f0051fa0151f50052b50051f5","0x330150960052b50050960050af0150990052b500507300513b0150152b5","0x51e411a00713d0150990052b50050990051e70152810052b5005281005","0x727b0051e501527b09c09a0ba2b50050992810960ba13f0151e40052b5","0x152770052b50050152720150152b500501500701527800538a1e60052b5","0x9f0051e201500d09f0072b50052760051430152760052b5005277005141","0x150a20052b50050a000527f0150a00052b500500d0051e00150152b5005","0x50150990150a40052b500527300527c0152730a20072b50050a2005270","0x9a0150a52720072b50052720050ac0150152b50050151290152720052b5","0x51e60190071df0150a40052b50050a400526e0150a50052b50050a5005","0x2700aa0ba38b2020a80a70ba2b50070a40a502109c12909c0151e60052b5","0x2b50050a200526b01526e0052b50050152af0150152b50050150070150ac","0x150a70052b50050a70050330152af0052b500526e0a200726a0150a2005","0x2022060071de0152af0052b50052af00526e0152720052b500527200509a","0x2690ba38c20926a26b0ba2b50072af2720a80a712909c0152020052b5005","0x501000f0071dd0150152b50050150060150152b5005015007015267268","0x1526b0052b500526b00503301509a0052b500509a0050af0150b10052b5","0x50b10051dc01500c0052b500500c00503101526a0052b500526a0051e4","0xb100c26a26b09a0af1db0152090052b500520920f0071de0150b10052b5","0x26500514b0151e10052b50051e11e300712b0152651e10b32661292b5005","0x72b50051e60051da0150152b50050150070150b400538d1d20052b5007","0x25c25d25e25f0062b50052630051d70150152b50050b60051d80152630b6","0x1525125a0072b500525a00523e0152522540c10bf25525625825a0b925b","0x1d90071d40150c50052b50050c50051b50150c50052b5005209251007110","0x2b50050150070150c700538e0152b50070c500527b0151d20052b50051d2","0x51390150c90052b50052022500072950152500052b500501502b015015","0x2b500524f0c90072950150152b50050cb00528001524f0cb0072b50051e4","0x29101524b24c0072b500524e00506a01524d0052b500501505801524e005","0x152490052b50050150a201524a0052b50050150a20150152b500524c005","0x24b00502c0150e10052b500524824924a0ba0fe0152480052b50050150a2","0xb30052b50050b30050330152660052b50052660050af0152430052b5005","0xe100513301524d0052b500524d0050590151f50052b50051f50051e3015","0xe124d1f50b32660091fd0152430052b50052430050310150e10052b5005","0x51fa0150310052b50050310340071350150e00312402411292b5005243","0x52b50050152580150152b500501500701523d00538f23e0052b50070e0","0x25001523b0df0072b500523c0050c701523c0052b50051560050c5015156","0x52b500523900527f0152390052b500523b0050c90150152b50050df005","0xe500509a0150e50052b50050150990151050052b50050de00527c0150de","0x71050e51e124012909c0151050052b500510500526e0150e50052b5005","0xe90051b50150152b50050150070152332341040ba3900e923a2380ba2b5","0x23a0052b500523a0051e40152380052b50052380050330150e90052b5005","0x150152b50050150070150f00053912312320072b50070e92410070e3015","0x52310051510150152b50050f20052800151030f20072b500523e005139","0x152380052b50052380050330152320052b50052320050af01522f0052b5","0x51030051b501522f0052b500522f00515201523a0052b500523a0051e4","0x12b0152291b522c22d1292b500510322f23a2382320af1540151030052b5","0x70152270053922280052b50072290051d30151b50052b50051b5112007","0x2250072b50052260051600152260052b50052280051610150152b5005015","0x522400509a0152240052b50050150990150152b5005225005158015108","0x150070151090053932200fe0072b500722410822d0ba15a0152240052b5","0x1521e0052b50052200051550152200052b500522000515d0150152b5005","0x10c0051620150152b500510b00527a01510c21d10b0ba2b500521e005157","0x9a01521b0052b50050151ce01510e0052b500521d0051d10150152b5005","0x10e21b0fe0ba21301510e0052b500510e00509a01521b0052b500521b005","0x52180052660150152b50050150070152162170073942181100072b5007","0x540050450150152b500521f0051f60150152b50050620050b90150152b5","0x524c0150152b50050330051fb0150152b500521c00529c0150152b5005","0x27a0150152b500525e00527a0150152b50050520051fc0150152b500504f","0x150152b50052540051cb0150152b50052520051630150152b500525f005","0x152b500525500527a0150152b50050bf0052910150152b50050c1005291","0x2b500525a00527a0150152b500525800527a0150152b500525600527a015","0x525c00527a0150152b500525b00527a0150152b50050b900527a015015","0x50151c60150152b50051d20051c70150152b500525d00527a0150152b5","0x151c40152130052b50050150990152140052b50050150a20152150052b5","0x52b50052132142150ba1cd0152110052b50050151c10152120052b5005","0x51b50151190052b50051190051bf0151100052b50051100050af015119","0x2112121191101291bc0152110052b500521100509a0152120052b5005212","0x501500701508f00539520d0052b500720e0051bb01520e2100072b5005","0x1b80150152b500520b0051e601520b20c0072b500520d00516b0150152b5","0x2100052b50052100050af01511e0052b50050151c10153800052b5005015","0x11e00509a0153800052b50053800051b501520c0052b500520c0051bf015","0x2080051bb01520820a0072b500511e38020c2101291bc01511e0052b5005","0x72b500512100516b0150152b50050150070152070053961210052b5007","0x50151c10151260052b500501516d0150152b50052040051e6015204205","0x152050052b50052050051bf01520a0052b500520a0050af0151280052b5","0x20520a1291bc0151280052b500512800509a0151260052b50051260051b5","0x70151ff00539712b0052b50072000051bb0152002030072b5005128126","0x152b500512f0051e601512f12d0072b500512b00516b0150152b5005015","0x2b50052030050af0150450052b50050151c10151f60052b500501516f015","0x9a0151f60052b50051f60051b501512d0052b500512d0051bf015203005","0x1bb0151fc1fb0072b50050451f612d2031291bc0150450052b5005045005","0x51f800516b0150152b50050150070151310053981f80052b50071fc005","0x1c10151fd0052b50050151b60150152b50051330051e601513322a0072b5","0x52b500522a0051bf0151fb0052b50051fb0050af0151350052b5005015","0x1291bc0151350052b500513500509a0151fd0052b50051fd0051b501522a","0x1f10053991f20052b50071f90051bb0151f91fa0072b50051351fd22a1fb","0x51390051e60151391eb0072b50051f200516b0150152b5005015007015","0x1fa0050af0151e70052b50050151b401513b0052b50050151720150152b5","0x13b0052b500513b0051b50151eb0052b50051eb0051bf0151fa0052b5005","0x13f13d0072b50051e713b1eb1fa1291bc0151e70052b50051e700509a015","0x1502b0150152b500501500701514100539a1e50052b500713f0051bb015","0x151e20052b50051e20051b50151e20052b50050151b20151430052b5005","0x51e60151df1b90072b50051e500516b0151e00052b50051e2143007295","0x72b50051b90051b00151b90052b50051b91c80071b10150152b50051df","0x1850151dc1dd0072b50051dd0051ae0150152b50051de0051af0151dd1de","0x51da0052660150152b500514b00527a0151da14b1db0ba2b50051dc005","0x151d70052b50051d800517c0151d81db0072b50051db00517a0150152b5","0x1d41e00072950151d40052b50051d40051b50151d40052b50051d70051ac","0x13d0052b500513d0050af0151520052b50051db0051aa0151510052b5005","0x1510050340151520052b50051520051a701522c0052b500522c005033015","0x151290151611d31540ba2b500515115222c13d1291a50151510052b5005","0x152b500501500701515800539b1600052b50071610051810150152b5005","0x1dd0051ae0150152b500515d0051e601515d15a0072b5005160005183015","0x51570051a30151d11621570ba2b50051550051850151551dd0072b5005","0x72950151ce0052b500516200502f0150152b50051d10052660150152b5","0x1cb0051a30151c61c71cb0ba2b50051dd0051850151630052b50051ce15a","0x1ac0151c40052b50051c60051d10150152b50051c700527a0150152b5005","0x51cd0050340151cd0052b50051c11630072950151c10052b50051c4005","0x51dd0051a10150152b500501500701501539c0050150b70151bf0052b5","0x340150152b50051bc0050bf0151bb1bc0072b50051580052550150152b5","0x1516b0052b50050152970150152b50050150060151bf0052b50051bb005","0x1540050af01516d0052b50051b80051870151b80052b50051bf16b007299","0x2b0052b500502b0051e10151d30052b50051d30050330151540052b5005","0x16d0051890151b50052b50051b50051e40150310052b50050310051e3015","0x12f0150152b500501500701516d1b503102b1d315400900516d0052b5005","0x52b500513d0050af01516f0052b50051410051870150152b50051c8005","0x51e301502b0052b500502b0051e101522c0052b500522c00503301513d","0x52b500516f0051890151b50052b50051b50051e40150310052b5005031","0x51c800512f0150152b500501500701516f1b503102b22c13d00900516f","0x330151fa0052b50051fa0050af0151b60052b50051f10051870150152b5","0x2b50050310051e301502b0052b500502b0051e101522c0052b500522c005","0x90051b60052b50051b60051890151b50052b50051b50051e4015031005","0x150152b50051c800512f0150152b50050150070151b61b503102b22c1fa","0x522c0050330151fb0052b50051fb0050af0151720052b5005131005187","0x150310052b50050310051e301502b0052b500502b0051e101522c0052b5","0x2b22c1fb0090051720052b50051720051890151b50052b50051b50051e4","0x1ff0051870150152b50051c800512f0150152b50050150070151721b5031","0x22c0052b500522c0050330152030052b50052030050af0151b40052b5005","0x1b50051e40150310052b50050310051e301502b0052b500502b0051e1015","0x1b41b503102b22c2030090051b40052b50051b40051890151b50052b5005","0x52b50052070051870150152b50051c800512f0150152b5005015007015","0x51e101522c0052b500522c00503301520a0052b500520a0050af0151b2","0x52b50051b50051e40150310052b50050310051e301502b0052b500502b","0x150070151b21b503102b22c20a0090051b20052b50051b20051890151b5","0xaf0151b10052b500508f0051870150152b50051c800512f0150152b5005","0x2b500502b0051e101522c0052b500522c0050330152100052b5005210005","0x1890151b50052b50051b50051e40150310052b50050310051e301502b005","0x152b50050150070151b11b503102b22c2100090051b10052b50051b1005","0x52b500501502b0150152b50051c800512f0150152b5005216005266015","0x1ae00518b0151ae0052b50051d20051a00151af0052b500501502b0151b0","0x17c0072b500517a00519e01517a0052b50051850051960151850052b5005","0x22c0050330152170052b50052170050af0150152b500517c0050000151ac","0x1b00052b50051b00050340151ac0052b50051ac00539d01522c0052b5005","0x1aa0ba2b50051af1b01ac22c2170af39e0151af0052b50051af005034015","0x151830053a01810052b50071a500539f0150152b50050151290151a51a7","0x51870051e60151871a11a30ba2b50051810053a10150152b5005015007","0x6a0150152b50051890052910151a01890072b50051a300506a0150152b5","0x2b50051a000502c0150152b500518b00529101519618b0072b50051a1005","0x72b500700019e1b51a71293a20150000052b500519600502c01519e005","0x150152b50050150060150152b50050150070153a23a139f0ba3a339e39d","0x3a40052b50052522540c10bf25525625825a0b925b25c25d25e25f0063a4","0x39e0051e401539d0052b500539d0050330151aa0052b50051aa0050af015","0x3a405239e39d1aa0af2280153a40052b50053a400522901539e0052b5005","0x70153ab0053aa3a90052b50073a80052270153a83a73a63a51292b5005","0x9f0153ad0052b50050150f60153ac0052b50050153a50150152b5005015","0x2b50053ac00527c0153af0052b50053ae0053a60153ae0052b50053ad005","0x526e0153b10052b50053b100509a0153b10052b50050150990153b0005","0x3b03b13a73a60af2560153af0052b50053af0051b50153b00052b50053b0","0x152250150152b50050150070153b73b63b50ba3b43b33b20072b50073af","0x3b90072b50053b80053a80153b80052b50052b70053a70152b70052b5005","0x3bb00527f0153bb0052b50053ba0053ab0150152b50053b90053a90153ba","0x153bd0052b50050150990153bc0052b50052b800527c0152b80052b5005","0x53bc00526e0153bd0052b50053bd00509a0153b20052b50053b2005033","0x3c33c20ba3c13c03bf3be0ba2b50073bc3bd3b33b212909c0153bc0052b5","0x2b50053c50053a70153c50052b50050151080150152b50050150070153c4","0x3ab0150152b50053c70053a90153c83c70072b50053c60053a80153c6005","0x2b50053ca00527c0153ca0052b50053c900527f0153c90052b50053c8005","0x509a0153be0052b50053be0050330153cc0052b50050150990153cb005","0x52b50053c00051b50153cb0052b50053cb00526e0153cc0052b50053cc","0x153d33d23d10ba3d03cf3ce3cd0ba2b50073cb3cc3bf3be12909c0153c0","0x3d40052b50052bb0053a70152bb0052b50050152240150152b5005015007","0x3d60053ab0150152b50053d50053a90153d63d50072b50053d40053a8015","0x3d90052b50053d800527c0153d80052b50053d700527f0153d70052b5005","0x53da00509a0153cd0052b50053cd0050330153da0052b5005015099015","0x153cf0052b50053cf0051b50153d90052b50053d900526e0153da0052b5","0x150070153e03df3de0ba3dd3dc02f3db0ba2b50073d93da3ce3cd12909c","0xba3ac0153e20052b500501502b0153e10052b500501502b0150152b5005","0x53e400519e0153e40052b50053e30053ad0153e30052b50053dc3cf3c0","0x153a50052b50053a50050af0150152b50053e50050000153e63e50072b5","0x53e10050340153e60052b50053e600539d0153db0052b50053db005033","0x2f0052b500502f03300712b0153e20052b50053e20050340153e10052b5","0x2b50073e900539f0153e93e83e70ba2b50053e23e13e63db3a50af39e015","0x3ee3ed0ba2b50053ea0053a10150152b50050150070153ec0053eb3ea005","0x52910153f13f00072b50053ed00506a0150152b50053ef0051e60153ef","0x152b50053f20052910153f33f20072b50053ee00506a0150152b50053f0","0x3e81293a20153f50052b50053f300502c0153f40052b50053f100502c015","0x150152b50050150070152be3fa3f90ba3f83f73f60072b50073f53f402f","0x53a90053af0150152b50053fc0051e60153fc3fb0072b50050540053ae","0x3fd3fb04f06221f21c0090640150152b50053fe0051e60153fe3fd0072b5","0x4000052b50052bd3ff0073b00152bd0052b50050150f60153ff0052b5005","0x3f60050330153e70052b50053e70050af0154010052b50054000053b1015","0x310052b50050310051e301502b0052b500502b0051e10153f60052b5005","0x3f63e70090054010052b50054010051890153f70052b50053f70051e4015","0x50450150152b500521f0051f60150152b50050150070154013f703102b","0x24c0150152b50053a90052260150152b500521c00529c0150152b5005054","0x154020052b50050152970150152b50050620050b90150152b500504f005","0x3e70050af0154040052b50054030051870154030052b50052be402007299","0x2b0052b500502b0051e10153f90052b50053f90050330153e70052b5005","0x4040051890153fa0052b50053fa0051e40150310052b50050310051e3015","0x1f60150152b50050150070154043fa03102b3f93e70090054040052b5005","0x150152b500521c00529c0150152b50050540050450150152b500521f005","0x152b50050620050b90150152b500504f00524c0150152b50053a9005226","0x3e80050330153e70052b50053e70050af0152bf0052b50053ec005187015","0x310052b50050310051e301502b0052b500502b0051e10153e80052b5005","0x3e83e70090052bf0052b50052bf00518901502f0052b500502f0051e4015","0x50450150152b500521f0051f60150152b50050150070152bf02f03102b","0x24c0150152b50053a90052260150152b500521c00529c0150152b5005054","0x150152b50053c000527a0150152b50050620050b90150152b500504f005","0x4050052b50050152970150152b50050330051fb0150152b50053cf00527a","0x50af0154070052b50054060051870154060052b50053e0405007299015","0x52b500502b0051e10153de0052b50053de0050330153a50052b50053a5","0x51890153df0052b50053df0051e40150310052b50050310051e301502b","0x150152b50050150070154073df03102b3de3a50090054070052b5005407","0x152b500521c00529c0150152b50050540050450150152b500521f0051f6","0x2b50050620050b90150152b500504f00524c0150152b50053a9005226015","0x2b50050152970150152b50050330051fb0150152b50053c000527a015015","0x152bc0052b50054090051870154090052b50053d3408007299015408005","0x502b0051e10153d10052b50053d10050330153a50052b50053a50050af","0x153d20052b50053d20051e40150310052b50050310051e301502b0052b5","0x2b50050150070152bc3d203102b3d13a50090052bc0052b50052bc005189","0x521c00529c0150152b50050540050450150152b500521f0051f6015015","0x620050b90150152b500504f00524c0150152b50053a90052260150152b5","0x729901540a0052b50050152970150152b50050330051fb0150152b5005","0x2b50053a50050af01540c0052b500540b00518701540b0052b50053c440a","0x1e301502b0052b500502b0051e10153c20052b50053c20050330153a5005","0x2b500540c0051890153c30052b50053c30051e40150310052b5005031005","0x21f0051f60150152b500501500701540c3c303102b3c23a500900540c005","0x52260150152b500521c00529c0150152b50050540050450150152b5005","0x1fb0150152b50050620050b90150152b500504f00524c0150152b50053a9","0x52b50053b740d00729901540d0052b50050152970150152b5005033005","0x50330153a50052b50053a50050af01540f0052b500540e00518701540e","0x52b50050310051e301502b0052b500502b0051e10153b50052b50053b5","0x3a500900540f0052b500540f0051890153b60052b50053b60051e4015031","0x450150152b500521f0051f60150152b500501500701540f3b603102b3b5","0x150152b500504f00524c0150152b500521c00529c0150152b5005054005","0x52b50053ab0051870150152b50050330051fb0150152b50050620050b9","0x51e10153a60052b50053a60050330153a50052b50053a50050af015410","0x52b50053a70051e40150310052b50050310051e301502b0052b500502b","0x150070154103a703102b3a63a50090054100052b50054100051890153a7","0x50450150152b500521f0051f60150152b50050620050b90150152b5005","0x24c0150152b50050330051fb0150152b500521c00529c0150152b5005054","0x150152b500525e00527a0150152b50050520051fc0150152b500504f005","0x152b50052540051cb0150152b50052520051630150152b500525f00527a","0x2b500525500527a0150152b50050bf0052910150152b50050c1005291015","0x525a00527a0150152b500525800527a0150152b500525600527a015015","0x25c00527a0150152b500525b00527a0150152b50050b900527a0150152b5","0x1e40154110052b500539f0050330150152b500525d00527a0150152b5005","0x154140050150b70154130052b50053a20050340154120052b50053a1005","0x152b500521f0051f60150152b50050620050b90150152b5005015007015","0x2b50050330051fb0150152b500521c00529c0150152b5005054005045015","0x525e00527a0150152b50050520051fc0150152b500504f00524c015015","0x2540051cb0150152b50052520051630150152b500525f00527a0150152b5","0x527a0150152b50050bf0052910150152b50050c10052910150152b5005","0x27a0150152b500525800527a0150152b500525600527a0150152b5005255","0x150152b500525b00527a0150152b50050b900527a0150152b500525a005","0x72b50051830052550150152b500525d00527a0150152b500525c00527a","0x51e40154110052b50051a70050330150152b50054150050bf015416415","0x150152b50050150060154130052b50054160050340154120052b50051b5","0x52ba0051870152ba0052b50054134170072990154170052b5005015297","0x154110052b50054110050330151aa0052b50051aa0050af0154180052b5","0x54120051e40150310052b50050310051e301502b0052b500502b0051e1","0x1541841203102b4111aa0090054180052b50054180051890154120052b5","0x150152b50050620050b90150152b50051c800512f0150152b5005015007","0x152b500521c00529c0150152b50050540050450150152b500521f0051f6","0x2b50050520051fc0150152b500504f00524c0150152b50050330051fb015","0x52520051630150152b500525f00527a0150152b500525e00527a015015","0xbf0052910150152b50050c10052910150152b50052540051cb0150152b5","0x527a0150152b500525600527a0150152b500525500527a0150152b5005","0x27a0150152b50050b900527a0150152b500525a00527a0150152b5005258","0x150152b500525d00527a0150152b500525c00527a0150152b500525b005","0x41a0052b500501520e0154190052b500501502b0150152b50051d20051c7","0x152970152b90052b500541a41900729501541a0052b500541a0051b5015","0x52b500541c00518701541c0052b50052b941b00729901541b0052b5005","0x51e101522c0052b500522c0050330151090052b50051090050af01541d","0x52b50051b50051e40150310052b50050310051e301502b0052b500502b","0x1500701541d1b503102b22c10900900541d0052b500541d0051890151b5","0x51f60150152b50050620050b90150152b50051c800512f0150152b5005","0x1fb0150152b500521c00529c0150152b50050540050450150152b500521f","0x150152b50050520051fc0150152b500504f00524c0150152b5005033005","0x152b50052520051630150152b500525f00527a0150152b500525e00527a","0x2b50050bf0052910150152b50050c10052910150152b50052540051cb015","0x525800527a0150152b500525600527a0150152b500525500527a015015","0x25b00527a0150152b50050b900527a0150152b500525a00527a0150152b5","0x51c70150152b500525d00527a0150152b500525c00527a0150152b5005","0x22d0052b500522d0050af0152c00052b50052270051870150152b50051d2","0x310051e301502b0052b500502b0051e101522c0052b500522c005033015","0x2c00052b50052c00051890151b50052b50051b50051e40150310052b5005","0x2b500525d00527a0150152b50050150070152c01b503102b22c22d009005","0x50620050b90150152b50051c800512f0150152b50051d20051c7015015","0x21c00529c0150152b50050540050450150152b500521f0051f60150152b5","0x51fc0150152b500504f00524c0150152b50050330051fb0150152b5005","0x1630150152b500525f00527a0150152b500525e00527a0150152b5005052","0x150152b50050c10052910150152b50052540051cb0150152b5005252005","0x152b500525600527a0150152b500525500527a0150152b50050bf005291","0x2b50050b900527a0150152b500525a00527a0150152b500525800527a015","0x51120051fb0150152b500525c00527a0150152b500525b00527a015015","0x50150cb01541e0052b500501502b0150152b500523e0053b20150152b5","0x4200052b500541f41e00729501541f0052b500541f0051b501541f0052b5","0x23a0051e40154220052b50052380050330154210052b50050f00050af015","0x70150154250050150b70154240052b50054200050340154230052b5005","0x12f0150152b50051d20051c70150152b500525d00527a0150152b5005015","0x150152b500521f0051f60150152b50050620050b90150152b50051c8005","0x152b50050330051fb0150152b500521c00529c0150152b5005054005045","0x2b500525e00527a0150152b50050520051fc0150152b500504f00524c015","0x52540051cb0150152b50052520051630150152b500525f00527a015015","0x25500527a0150152b50050bf0052910150152b50050c10052910150152b5","0x527a0150152b500525800527a0150152b500525600527a0150152b5005","0x27a0150152b500525b00527a0150152b50050b900527a0150152b500525a","0x150152b500523e0053b20150152b50051120051fb0150152b500525c005","0x52340051e40154220052b50051040050330154210052b50052410050af","0x2990154260052b50050152970154240052b50052330050340154230052b5","0x54210050af0154280052b50054270051870154270052b5005424426007","0x1502b0052b500502b0051e10154220052b50054220050330154210052b5","0x54280051890154230052b50054230051e40150310052b50050310051e3","0x527a0150152b500501500701542842303102b4224210090054280052b5","0xb90150152b50051c800512f0150152b50051d20051c70150152b500525d","0x150152b50050540050450150152b500521f0051f60150152b5005062005","0x152b500504f00524c0150152b50050330051fb0150152b500521c00529c","0x2b500525f00527a0150152b500525e00527a0150152b50050520051fc015","0x50c10052910150152b50052540051cb0150152b5005252005163015015","0x25600527a0150152b500525500527a0150152b50050bf0052910150152b5","0x527a0150152b500525a00527a0150152b500525800527a0150152b5005","0x1fb0150152b500525c00527a0150152b500525b00527a0150152b50050b9","0x52b50052410050af0154290052b500523d0051870150152b5005112005","0x51e301502b0052b500502b0051e10152400052b5005240005033015241","0x52b50054290051890151e10052b50051e10051e40150310052b5005031","0x50c70052760150152b50050150070154291e103102b240241009005429","0x1c800512f0150152b50051d20051c70150152b500525d00527a0150152b5","0x50450150152b500521f0051f60150152b50050620050b90150152b5005","0x24c0150152b50050330051fb0150152b500521c00529c0150152b5005054","0x150152b500525e00527a0150152b50050520051fc0150152b500504f005","0x152b50052540051cb0150152b50052520051630150152b500525f00527a","0x2b500525500527a0150152b50050bf0052910150152b50050c1005291015","0x525a00527a0150152b500525800527a0150152b500525600527a015015","0x25c00527a0150152b500525b00527a0150152b50050b900527a0150152b5","0x53b20150152b500503400522a0150152b50051120051fb0150152b5005","0x3b301542a0052b500501502b0150152b500520200527a0150152b50051e4","0x2b50052b642a0072950152b60052b50052b60051b50152b60052b5005015","0x18701542d0052b500542b42c00729901542c0052b500501529701542b005","0x2b50050b30050330152660052b50052660050af01542e0052b500542d005","0x1e40151f50052b50051f50051e301502b0052b500502b0051e10150b3005","0x1f502b0b326600900542e0052b500542e0051890151e10052b50051e1005","0x51e60053b50150152b50051e40053b20150152b500501500701542e1e1","0x21f0051f60150152b50050620050b90150152b50051c800512f0150152b5","0x51fb0150152b500521c00529c0150152b50050540050450150152b5005","0x27a0150152b50050520051fc0150152b500504f00524c0150152b5005033","0x150152b50051120051fb0150152b500520900527a0150152b5005202005","0x52b50050b40051870150152b50051d900512d0150152b500503400522a","0x51e10150b30052b50050b30050330152660052b50052660050af01542f","0x52b50051e10051e40151f50052b50051f50051e301502b0052b500502b","0x1500701542f1e11f502b0b326600900542f0052b500542f0051890151e1","0x512d0150152b50051e60053b50150152b50051e40053b20150152b5005","0x1f60150152b50050620050b90150152b50051c800512f0150152b50051d9","0x150152b500521c00529c0150152b50050540050450150152b500521f005","0x152b50050520051fc0150152b500504f00524c0150152b50050330051fb","0x2b50051120051fb0150152b500500f00527a0150152b500520200527a015","0x500c0052800150152b50051e30051fb0150152b500503400522a015015","0x2690050330150152b500520f0051ff0150152b50050100052710150152b5","0x4320052b50052670050340154310052b50052680051e40154300052b5005","0x150152b50051e40053b20150152b50050150070150154330050150b7015","0x152b50051c800512f0150152b50051d900512d0150152b50051e60053b5","0x2b50050540050450150152b500521f0051f60150152b50050620050b9015","0x504f00524c0150152b50050330051fb0150152b500521c00529c015015","0xf00527a0150152b50052720052660150152b50050520051fc0150152b5","0x51fb0150152b500503400522a0150152b50051120051fb0150152b5005","0x1ff0150152b50050100052710150152b500500c0052800150152b50051e3","0x150152b50052060051ff0150152b50050a20050b30150152b500520f005","0x50ac0050340154310052b50052700051e40154300052b50050aa005033","0x4340072990154340052b50050152970150152b50050150060154320052b5","0x52b500509a0050af0154360052b50054350051870154350052b5005432","0x51e301502b0052b500502b0051e10154300052b500543000503301509a","0x52b50054360051890154310052b50054310051e40151f50052b50051f5","0x51e40053b20150152b50050150070154364311f502b43009a009005436","0x1c800512f0150152b50051d900512d0150152b50052060051ff0150152b5","0x50450150152b500521f0051f60150152b50050620050b90150152b5005","0x24c0150152b50050330051fb0150152b500521c00529c0150152b5005054","0x150152b500500f00527a0150152b50050520051fc0150152b500504f005","0x152b50051e30051fb0150152b500503400522a0150152b50051120051fb","0x2b500520f0051ff0150152b50050100052710150152b500500c005280015","0x9a0050af0154370052b50052780051870150152b50050190051f8015015","0x2b0052b500502b0051e101509c0052b500509c00503301509a0052b5005","0x4370051890150210052b50050210051e40151f50052b50051f50051e3015","0x2910150152b50050150070154370211f502b09c09a0090054370052b5005","0x150152b50051d900512d0150152b50052060051ff0150152b5005073005","0x152b500521f0051f60150152b50050620050b90150152b50051c800512f","0x2b50050330051fb0150152b500521c00529c0150152b5005054005045015","0x50190051f80150152b50050520051fc0150152b500504f00524c015015","0x3400522a0150152b50051120051fb0150152b500500f00527a0150152b5","0x52710150152b500500c0052800150152b50051e30051fb0150152b5005","0x1870150152b500511a0051310150152b500520f0051ff0150152b5005010","0x2b50052810050330150960052b50050960050af0154380052b500527c005","0x1e40151f50052b50051f50051e301502b0052b500502b0051e1015281005","0x1f502b2810960090054380052b50054380051890150210052b5005021005","0x50730052910150152b50051020052760150152b5005015007015438021","0x1c800512f0150152b50051d900512d0150152b50052060051ff0150152b5","0x50450150152b500521f0051f60150152b50050620050b90150152b5005","0x24c0150152b50050330051fb0150152b500521c00529c0150152b5005054","0x150152b50050190051f80150152b50050520051fc0150152b500504f005","0x152b50051120051fb0150152b500511a0051310150152b500500f00527a","0x2b500500c0052800150152b50051e30051fb0150152b500503400522a015","0x51fe00522a0150152b500520f0051ff0150152b5005010005271015015","0x501509901543a0052b50050150a20154390052b50050151c60150152b5","0xba1cd01543d0052b50050151c101543c0052b50050153b601543b0052b5","0x543e0051bf01528f0052b500528f0050af01543e0052b500543b43a439","0x1543d0052b500543d00509a01543c0052b500543c0051b501543e0052b5","0x4424410052b50074400051bb01544043f0072b500543d43c43e28f1291bc","0x51e60154454440072b500544100516b0150152b5005015007015443005","0xaf0154470052b50050151c10154460052b50050153b70150152b5005445","0x2b50054460051b50154440052b50054440051bf01543f0052b500543f005","0x72b500544744644443f1291bc0154470052b500544700509a015446005","0x150152b500501500701544b00544a4490052b50074480051bb0154482c2","0x2b50050152b70150152b50052c30051e60152c344c0072b500544900516b","0x51bf0152c20052b50052c20050af01544e0052b50050153b801544d005","0x52b500544e00509a01544d0052b500544d0051b501544c0052b500544c","0x52b50074500051bb01545044f0072b500544e44d44c2c21291bc01544e","0x151b20154540052b500501502b0150152b5005015007015453005452451","0x52b50054554540072950154550052b50054550051b50154550052b5005","0x71b10150152b50054570051e60154570b70072b500545100516b015456","0x54580051af0154594580072b50050b70051b00150b70052b50050b70f6","0x45c45b0ba2b500545a00518501545a4590072b50054590051ae0150152b5","0x545b00517a0150152b500545d0052660150152b500545c00527a01545d","0x4600052b500545f0051ac01545f0052b500545e00517c01545e45b0072b5","0x51aa0154610052b50054604560072950154600052b50054600051b5015","0x52b500529600503301544f0052b500544f0050af0154620052b500545b","0x1291a50154610052b50054610050340154620052b50054620051a7015296","0x2c60051810150152b50050151290152c64644630ba2b500546146229644f","0x72b50054650051830150152b50050150070154670054664650052b5007","0x18501546a4590072b50054590051ae0150152b50054690051e6015469468","0x546d0052660150152b500546b0051a301546d46c46b0ba2b500546a005","0x1546f0052b500546e46800729501546e0052b500546c00502f0150152b5","0x47100527a0150152b50054700051a30154724714700ba2b5005459005185","0x154740052b50054730051ac0154730052b50054720051d10150152b5005","0x50150b70154760052b50054750050340154750052b500547446f007295","0x54670052550150152b50054590051a10150152b5005015007015015477","0x154760052b50054790050340150152b50054780050bf0154794780072b5","0x52b500547647a00729901547a0052b50050152970150152b5005015006","0x50330154630052b50054630050af01547c0052b500547b00518701547b","0x52b50050060051e301502b0052b500502b0051e10154640052b5005464","0x46300900547c0052b500547c0051890150210052b50050210051e4015006","0x1870150152b50050f600512f0150152b500501500701547c02100602b464","0x2b500529600503301544f0052b500544f0050af01547d0052b5005453005","0x1e40150060052b50050060051e301502b0052b500502b0051e1015296005","0x602b29644f00900547d0052b500547d0051890150210052b5005021005","0x544b0051870150152b50050f600512f0150152b500501500701547d021","0x152960052b50052960050330152c20052b50052c20050af01547e0052b5","0x50210051e40150060052b50050060051e301502b0052b500502b0051e1","0x1547e02100602b2962c200900547e0052b500547e0051890150210052b5","0x47f0052b50054430051870150152b50050f600512f0150152b5005015007","0x2b0051e10152960052b500529600503301543f0052b500543f0050af015","0x210052b50050210051e40150060052b50050060051e301502b0052b5005","0x501500701547f02100602b29643f00900547f0052b500547f005189015","0x1d900512d0150152b50052060051ff0150152b50050730052910150152b5","0x51f60150152b50050620050b90150152b50051c800512f0150152b5005","0x1fb0150152b500521c00529c0150152b50050540050450150152b500521f","0x150152b50050520051fc0150152b500504f00524c0150152b5005033005","0x152b500511a0051310150152b500500f00527a0150152b50050190051f8","0x2b50051e30051fb0150152b500503400522a0150152b50051120051fb015","0x520f0051ff0150152b50050100052710150152b500500c005280015015","0xf600512f0150152b50052920053b20150152b50051fe00522a0150152b5","0x51b50154810052b500501520e0154800052b500501502b0150152b5005","0x52b50050152970154820052b50054814800072950154810052b5005481","0xaf0154840052b50054830051870154830052b50054822c80072990152c8","0x2b500502b0051e10152960052b500529600503301528d0052b500528d005","0x1890150210052b50050210051e40150060052b50050060051e301502b005","0x152b500501500701548402100602b29628d0090054840052b5005484005","0x2b50051d900512d0150152b50052060051ff0150152b5005073005291015","0x521f0051f60150152b50050620050b90150152b50051c800512f015015","0x330051fb0150152b500521c00529c0150152b50050540050450150152b5","0x51f80150152b50050520051fc0150152b500504f00524c0150152b5005","0x1fb0150152b500511a0051310150152b500500f00527a0150152b5005019","0x150152b50051e30051fb0150152b500503400522a0150152b5005112005","0x152b500520f0051ff0150152b50050100052710150152b500500c005280","0x2b50052900051870150152b50050f600512f0150152b50051fe00522a015","0x1e10152960052b50052960050330152980052b50052980050af015485005","0x2b50050210051e40150060052b50050060051e301502b0052b500502b005","0x701548502100602b2962980090054850052b5005485005189015021005","0x12f0150152b50051d900512d0150152b50052060051ff0150152b5005015","0x150152b500521f0051f60150152b50050090052910150152b50051c8005","0x152b50050330051fb0150152b500521c00529c0150152b5005054005045","0x2b50050190051f80150152b50050520051fc0150152b500504f00524c015","0x51120051fb0150152b500511a0051310150152b500500f00527a015015","0xc0052800150152b50051e30051fb0150152b500503400522a0150152b5","0x522a0150152b500520f0051ff0150152b50050100052710150152b5005","0x2550150152b500502700522a0150152b50050f600512f0150152b50051fe","0x2b500529e0050af0150152b50054860050bf0154874860072b50052a9005","0x1e40152a00052b500502b0051e10150280052b500505e0050330152a3005","0x153850050150b701506f0052b50054870050340150710052b5005021005","0x152b500520f0051ff0150152b500502700522a0150152b5005015007015","0x2b50052060051ff0150152b50050f600512f0150152b50051fe00522a015","0x50090052910150152b50051c800512f0150152b50051d900512d015015","0x21c00529c0150152b50050540050450150152b500521f0051f60150152b5","0x51fc0150152b500504f00524c0150152b50050330051fb0150152b5005","0x1310150152b500500f00527a0150152b50050190051f80150152b5005052","0x150152b500503400522a0150152b50051120051fb0150152b500511a005","0x152b50050100052710150152b500500c0052800150152b50051e30051fb","0x2b50052370050b90150152b500502e0051fb0150152b500502c0053b9015","0x51e10150280052b50052800050330152a30052b50052530050af015015","0x52b50052930050340150710052b50052910051e40152a00052b5005007","0x51870154890052b500506f4880072990154880052b500501529701506f","0x52b50050280050330152a30052b50052a30050af0152c70052b5005489","0x51e40150ba0052b50050ba0051e30152a00052b50052a00051e1015028","0x710ba2a00282a30090052c70052b50052c70051890150710052b5005071","0x2b500520f0051ff0150152b500502700522a0150152b50050150070152c7","0x52060051ff0150152b50050f600512f0150152b50051fe00522a015015","0x90052910150152b50051c800512f0150152b50051d900512d0150152b5","0x51fb0150152b500521c00529c0150152b500521f0051f60150152b5005","0x1f80150152b50050520051fc0150152b500504f00524c0150152b5005033","0x150152b500511a0051310150152b500500f00527a0150152b5005019005","0x152b50051e30051fb0150152b500503400522a0150152b50051120051fb","0x2b500502c0053b90150152b50050100052710150152b500500c005280015","0x526d0051870150152b50052370050b90150152b500502e0051fb015015","0x152590052b50052590050330152530052b50052530050af01548a0052b5","0x52620051e40150ba0052b50050ba0051e30150070052b50050070051e1","0x1548a2620ba00725925300900548a0052b500548a0051890152620052b5","0x52b50051290050c50151290052b50050153ba0150152b50050ba00529c","0x50c90150152b500500900525001500c0090072b50050af0050c70150af","0x52b500501000527c0150100052b500500f00527f01500f0052b500500c","0xb700526e0150f60052b50050f600509a0150f60052b50050150990150b7","0x270ba48b00602e0210ba2b50070b70f600700512909c0150b70052b5005","0x210050330150060052b50050060051b50150152b500501500701502c02b","0x72b50070060150070e301502e0052b500502e0051e40150210052b5005","0x1502f0052b500502f0050af0150152b500501500701503100548c03302f","0x2b50050150070151d21c81b90ba48d1121b50340ba2b500702e021007265","0x52630151d90052b50051120050b60151120052b50051120050b4015015","0x1e300525e0150152b50051e100525f0151e611a1e41e31e10af2b50051d9","0x528f0150152b50051e600527a0150152b500511a0050e20150152b5005","0x2b500501900508d0150191e40072b50051e40053bb0151e40052b50051e4","0x27b0151b50052b50051b50051e40150340052b50050340050330151f5005","0x2b50050330050e20150152b50050150070151fe00548e0152b50071f5005","0x2b50050152b80152020052b500501502b0150152b50051e40050e2015015","0x152090052b50052062020072950152060052b50052060051b5015206005","0x521c0053bc01521c0052b500520920f00729901520f0052b5005015297","0x150340052b500503400503301502f0052b500502f0050af01521f0052b5","0x1b503402f12900521f0052b500521f0053bd0151b50052b50051b50051e4","0x2b50051e400508d0150152b50051fe0052760150152b500501500701521f","0x150500052b500504f23700711001504f0052b500503300508d015237005","0x1500701505200548f0152b500705000527b0150500052b50050500051b5","0x3bf0152590052b50052530053be0152530052b50050150f60150152b5005","0x2b500503400503301502f0052b500502f0050af0152620052b5005259005","0x1290052620052b50052620053bd0151b50052b50051b50051e4015034005","0x1502b0150152b50050520052760150152b50050150070152621b503402f","0x150540052b50050540051b50150540052b50050153c00152640052b5005","0x26d26f00729901526f0052b500501529701526d0052b5005054264007295","0x2f0052b500502f0050af01527a0052b50052710053bc0152710052b5005","0x27a0053bd0151b50052b50051b50051e40150340052b5005034005033015","0x330050e20150152b500501500701527a1b503402f12900527a0052b5005","0x152910052b50051d22800072990152800052b50050152970150152b5005","0x51b900503301502f0052b500502f0050af0152930052b50052910053bc","0x52930052b50052930053bd0151c80052b50051c80051e40151b90052b5","0xcb0152950052b500501502b0150152b50050150070152931c81b902f129","0x2b50052972950072950152970052b50052970051b50152970052b5005015","0x1e40150590052b50050210050330150580052b50050310050af015299005","0x154900050150b701529e0052b500529900503401505c0052b500502e005","0x2b50050270050330150580052b50050150050af0150152b5005015007015","0x29701529e0052b500502c00503401505c0052b500502b0051e4015059005","0x2b50050600053bc0150600052b500529e05e00729901505e0052b5005015","0x1e40150590052b50050590050330150580052b50050580050af0152aa005","0x2aa05c0590581290052aa0052b50052aa0053bd01505c0052b500505c005","0x150152b50050150060150152b50050152040150090052b5005015380015","0xf00527b01500f0052b500500c0053c301500c1290072b50051290053c2","0x150152b50050ba0051f60150152b50050150070150100054910152b5007","0xb70052b500501502b0150152b50050090051fb0150152b5005129005076","0xf60b70072950150f60052b50050f60051b50150f60052b50050153c4015","0x60052b500502102e00729901502e0052b50050152970150210052b5005","0x50050330150150052b50050150050af0150270052b50050060053c5015","0x270052b50050270053c60150070052b50050070051e40150050052b5005","0x150152b50050100052760150152b5005015007015027007005015129005","0x4920af02c0072b500702b0070050ba3c701502b1290072b50051290053c2","0x1502b0150340052b500501502b0150152b500501500701503103302f0ba","0x1b90052b50051120053c90151120052b50051290053c80151b50052b5005","0x50000151d91d20072b50051c800519e0151c80052b50051b90053ca015","0x2c0052b500502c0050330150150052b50050150050af0150152b50051d2","0x1b50050340150340052b50050340050340151d90052b50051d900539d015","0x341d902c0150af39e0150af0052b50050af00900712b0151b50052b5005","0x52b50071e400539f0150152b50050151290151e41e31e10ba2b50051b5","0x1fe1f50190ba2b500511a0053a10150152b50050150070151e600549311a","0x2020052910152062020072b500501900506a0150152b50051fe0051e6015","0x150152b500520900529101520f2090072b50051f500506a0150152b5005","0xaf1e31293a201521f0052b500520f00502c01521c0052b500520600502c","0x60150152b50050150070152530520500ba49404f2370072b500721f21c","0x2620052b50052590ba0073cb0152590052b50050150f60150152b5005015","0x2370050330151e10052b50051e10050af0152640052b50052620053cc015","0x2640052b50052640053c601504f0052b500504f0051e40152370052b5005","0x150152b50050ba0051f60150152b500501500701526404f2371e1129005","0x525300503401526d0052b50050520051e40150540052b5005050005033","0x50ba0051f60150152b50050150070150154950050150b701526f0052b5","0x330150152b50052710050bf01527a2710072b50051e60052550150152b5","0x2b500527a00503401526d0052b50050af0051e40150540052b50051e3005","0x26f2800072990152800052b50050152970150152b500501500601526f005","0x1e10052b50051e10050af0152930052b50052910053c50152910052b5005","0x2930053c601526d0052b500526d0051e40150540052b5005054005033015","0xba0051f60150152b500501500701529326d0541e11290052930052b5005","0x152970150152b50050090051fb0150152b50051290050760150152b5005","0x52b50052970053c50152970052b50050312950072990152950052b5005","0x51e401502f0052b500502f0050330150150052b50050150050af015299","0x1529903302f0151290052990052b50052990053c60150330052b5005033","0x500c00529c01500f00c0072b500500900529d0150090052b500501529e","0x1e40150050052b50050050050330150150052b50050150050af0150152b5","0x210f60b70101292b500500f0ba00501512929b0150ba0052b50050ba005","0x52980150152b500501500701500600549602e0052b500702100529a015","0x52b500502700508d0150270af0072b50050af0053bb0150152b500502e","0x2f0053ce01502f0052b500502c0053cd01502c0052b500501510701502b","0x340052b50050310053d10150152b50050330053cf0150310330072b5005","0x70ba2890151b50052b50051b50051b50151b50052b500503400502f015","0x1b90100072860151b90052b50051b90051b50151b91120072b500502b1b5","0x72b50051d90053d30151d90052b50051d20053d20151d21c80072b5005","0x527f0151e40052b50051e30053d40150152b50051e10052bb0151e31e1","0x190052b50050150990151e60052b500511a00527c01511a0052b50051e4","0x1c80050af0151120052b50051120051e10150190052b500501900509a015","0x2060ba4972021fe1f50ba2b50071e60190f60b712909c0151c80052b5005","0x1f50050330152020052b50052020051b50150152b500501500701520f209","0x4980152b500720200527b0151fe0052b50051fe0051e40151f50052b5005","0x521f00527801521f0052b50050150f60150152b500501500701521c005","0x150070150154990050150b701504f0052b50052370052770152370052b5","0x509f0150500052b50050150f60150152b500521c0052760150152b5005","0x52b500504f00500d01504f0052b50050520052770150520052b5005050","0x26200549a2590052b50072530050a00152530052b5005253005277015253","0x52b50050af00508d0150152b50052590051e60150152b5005015007015","0x26d0053ce01526d0052b50050540053cd0150540052b5005015107015264","0x27a0052b50052710053d10150152b500526f0053cf01527126f0072b5005","0x1120ba2890152800052b50052800051b50152800052b500527a00502f015","0x2931c80072860152930052b50052930051b50152932910072b5005264280","0x580052b50052990052780152990052b50050150f60152972950072b5005","0x501509901505c0052b500529700527c0150590052b50050580053a6015","0x150590052b50050590051b501529e0052b500529e00509a01529e0052b5","0x1fe1f50af2560152950052b50052950050af0152910052b50052910051e1","0x150152b50050150070150622a92aa0ba49b06005e0072b500705905c29e","0x52a80053d60152a80052b50050641290073d50150640052b50050150f6","0x1505e0052b500505e0050330152950052b50052950050af0152a70052b5","0x52a70053d70150600052b50050600051e40152910052b50052910051e1","0x1290050b90150152b50050150070152a706029105e2950af0052a70052b5","0x152a40052b50050622a60072990152a60052b50050152970150152b5005","0x52aa0050330152950052b50052950050af01506a0052b50052a40053d8","0x152a90052b50052a90051e40152910052b50052910051e10152aa0052b5","0x152b500501500701506a2a92912aa2950af00506a0052b500506a0053d7","0x2b50050af0050e20150152b50051290050b90150152b50052620051e6015","0x50280051b50150280052b50050153d90152a30052b500501502b015015","0x150710052b50050152970152a00052b50050282a30072950150280052b5","0x1c80050af0150700052b500506f0053d801506f0052b50052a0071007299","0x1120052b50051120051e10151f50052b50051f50050330151c80052b5005","0x1f51c80af0050700052b50050700053d70151fe0052b50051fe0051e4015","0xaf0050e20150152b50051290050b90150152b50050150070150701fe112","0x1529f0052b500520f0730072990150730052b50050152970150152b5005","0x52060050330151c80052b50051c80050af0150760052b500529f0053d8","0x152090052b50052090051e40151120052b50051120051e10152060052b5","0x152b50050150070150762091122061c80af0050760052b50050760053d7","0x2b50050060053d80150152b50050af0050e20150152b50051290050b9015","0x1e10150b70052b50050b70050330150100052b50050100050af01529d005","0x2b500529d0053d70150f60052b50050f60051e40150070052b5005007005","0x900529d0150090052b500501529e01529d0f60070b70100af00529d005","0x150052b50050150050af0150152b500500c00529c01500f00c0072b5005","0x1512929b0150ba0052b50050ba0051e40150050052b5005005005033015","0x549c02e0052b500702100529a0150210f60b70101292b500500f0ba005","0x2b50050af0053bb0150152b500502e0052980150152b5005015007015006","0x3cd01502c0052b500501510701502b0052b500502700508d0150270af007","0x50330053cf0150310330072b500502f0053ce01502f0052b500502c005","0x1b50151b50052b500503400502f0150340052b50050310053d10150152b5","0x51b50151b91120072b500502b1b50070ba2890151b50052b50051b5005","0x51d20053d20151d21c80072b50051b90100072860151b90052b50051b9","0x150152b50051e10052bb0151e31e10072b50051d90053d30151d90052b5","0x511a00527c01511a0052b50051e400527f0151e40052b50051e30053d4","0x1e10150190052b500501900509a0150190052b50050150990151e60052b5","0x190f60b712909c0151c80052b50051c80050af0151120052b5005112005","0x1b50150152b500501500701520f2092060ba49d2021fe1f50ba2b50071e6","0x2b50051fe0051e40151f50052b50051f50050330152020052b5005202005","0xf60150152b500501500701521c00549e0152b500720200527b0151fe005","0x52b50052370052770152370052b500521f00527801521f0052b5005015","0x152b500521c0052760150152b500501500701501549f0050150b701504f","0x50520052770150520052b500505000509f0150500052b50050150f6015","0x152530052b50052530052770152530052b500504f00500d01504f0052b5","0x2590051e60150152b50050150070152620054a02590052b50072530050a0","0x1502b0150152b50050af0050e20150152b50051290050b90150152b5005","0x150540052b50050540051b50150540052b50050153da0152640052b5005","0x26d26f00729901526f0052b500501529701526d0052b5005054264007295","0x1c80052b50051c80050af01527a0052b50052710053d80152710052b5005","0x1fe0051e40151120052b50051120051e10151f50052b50051f5005033015","0x1527a1fe1121f51c80af00527a0052b500527a0053d70151fe0052b5005","0x2800052b50050af00508d0150152b50052620051e60150152b5005015007","0x52930053ce0152930052b50052910053cd0152910052b5005015107015","0x152990052b50052970053d10150152b50052950053cf0152972950072b5","0x581120ba2890150580052b50050580051b50150580052b500529900502f","0x505c1c800728601505c0052b500505c0051b501505c0590072b5005280","0x152aa0052b500506000509f0150600052b50050150f601505e29e0072b5","0x2b50050150990150620052b500505e00527c0152a90052b50052aa0053a6","0x1e10152a90052b50052a90051b50150640052b500506400509a015064005","0x641fe1f50af25601529e0052b500529e0050af0150590052b5005059005","0xf60150152b500501500701506a2a42a60ba4a12a72a80072b50072a9062","0x2b50050280053d60150280052b50052a31290073d50152a30052b5005015","0x1e10152a80052b50052a800503301529e0052b500529e0050af0152a0005","0x2b50052a00053d70152a70052b50052a70051e40150590052b5005059005","0x51290050b90150152b50050150070152a02a70592a829e0af0052a0005","0x3d801506f0052b500506a0710072990150710052b50050152970150152b5","0x2b50052a600503301529e0052b500529e0050af0150700052b500506f005","0x3d70152a40052b50052a40051e40150590052b50050590051e10152a6005","0x150152b50050150070150702a40592a629e0af0050700052b5005070005","0x730052b50050152970150152b50051290050b90150152b50050af0050e2","0x50af0150760052b500529f0053d801529f0052b500520f073007299015","0x52b50051120051e10152060052b50052060050330151c80052b50051c8","0x1c80af0050760052b50050760053d70152090052b50052090051e4015112","0x50b90150152b50050af0050e20150152b5005015007015076209112206","0x100052b50050100050af01529d0052b50050060053d80150152b5005129","0xf60051e40150070052b50050070051e10150b70052b50050b7005033015","0x1529d0f60070b70100af00529d0052b500529d0053d70150f60052b5005","0x150152b50050152040150b70052b500501520a01500f0052b5005015380","0x270060ba4a202e0210f60ba2b50070ba0050072650150152b5005015006","0x502e0050b601502e0052b500502e0050b40150152b500501500701502b","0x502f00525f0151b503403103302f0af2b500502c00526301502c0052b5","0x1b500527a0150152b50050340050e20150152b500503300525e0150152b5","0x150f60052b50050f60050330150150052b50050150050af0150152b5005","0x503100528f0150210052b50050210051e40150070052b50050070051e1","0x1d21c80101b91120af2b50050311290210070f601500925d0150310052b5","0x1e10054a31d90052b50071d200525c0150100052b50050100b7007200015","0x71e40050a00151e41e30072b50051d900525b0150152b5005015007015","0x150152b500511a0051e60150152b50050150070151e60054a411a0052b5","0x152b50050af00527a0150152b50051e30050b90150152b500500f0051fb","0x52b500501525a0150190052b500501502b0150152b500500900527a015","0xaf0151fe0052b50051f50190072950151f50052b50051f50051b50151f5","0x2b50050100051e10152060052b50051b90050330152020052b5005112005","0xb701521c0052b50051fe00503401520f0052b50051c80051e4015209005","0x152720150152b50051e60051e60150152b50050150070150154a5005015","0x4f0072b50052370051430152370052b500521f00514101521f0052b5005","0x5200527f0150520052b50050500051e00150152b500504f0051e2015050","0x52b500525900527c0152592530072b50052530052700152530052b5005","0x2b50052640050ac0150152b50050151290152640052b5005015099015262","0x152620052b500526200526e0150540052b500505400509a015054264007","0x1500701529128027a0ba4a627126f26d0ba2b50072620541c81b912909c","0x26a0152530052b500525300526b0152930052b50050152af0150152b5005","0x526400509a01526d0052b500526d0050330152950052b5005293253007","0x152710052b50052710051b50152950052b500529500526e0152640052b5","0x1500701529e05c0590ba4a70582992970ba2b500729526426f26d12909c","0x1506005e0072b500505e00527001505e0052b50050152720150152b5005","0x2b50052a90050ac0152a90052b50050150990152aa0052b500506000527c","0x2970052b50052970050330150640af0072b50050af00523e0150622a9007","0x580051b50152aa0052b50052aa00526e0150620052b500506200509a015","0x2a60ba4a82a72a80072b50070642aa0622992970af2560150580052b5005","0x505e00526b0152a30052b50050152af0150152b500501500701506a2a4","0x90072b500500900523e0150280052b50052a305e00726a01505e0052b5","0x526e0152a90052b50052a900509a0152a80052b50052a80050330152a0","0xba4a906f0710072b50072a00282a92a72a80af2560150280052b5005028","0x72650150710052b50050710050330150152b500501500701529f073070","0x150152b500501500701529a29b29c0ba4aa29d00c0760ba2b500706f071","0x2b500501502b0152980052b500529d0050b601529d0052b500529d0050b4","0xe30822902920af2b50052980052630152940052b500501502b015296005","0x50e30050e20150152b500529000525e0150152b500529200525f0150e2","0x52b50050090580af2710820af3db0150152b50050e200527a0150152b5","0x519e01528d0052b500528e0053de01528e0052b500528f0053dc01528f","0x52b50051120050af0150152b500528c00500001508e28c0072b500528d","0x503401508e0052b500508e00539d0150760052b5005076005033015112","0x2b500500c00f00712b0152940052b50052940050340152960052b5005296","0x501512901510610708d0ba2b500529429608e0761120af39e01500c005","0x150152b50050150070150910054ab1020052b500710600539f0150152b5","0x28a00506a0150152b50052860051e601528628928a0ba2b50051020053a1","0x960072b500528900506a0150152b500528300529101501c2830072b5005","0x28100502c01527f0052b500501c00502c0150152b5005096005291015281","0x27b09c0ba4ac09a0990072b500727c27f00c1071293a201527c0052b5005","0x2770052b50050150f60150152b50050150060150152b5005015007015278","0x50af01509f0052b50052760053d60152760052b50052771e30073d5015","0x52b50050100051e10150990052b500509900503301508d0052b500508d","0x8d0af00509f0052b500509f0053d701509a0052b500509a0051e4015010","0x50330150152b50051e30050b90150152b500501500701509f09a010099","0x52b50052780050340150a00052b500527b0051e401500d0052b500509c","0x152b50051e30050b90150152b50050150070150154ad0050150b70150a2","0x1070050330150152b50052730050bf0150a42730072b5005091005255015","0xa20052b50050a40050340150a00052b500500c0051e401500d0052b5005","0x2b50050a22720072990152720052b50050152970150152b5005015006015","0x3301508d0052b500508d0050af0150a70052b50050a50053d80150a5005","0x2b50050a00051e40150100052b50050100051e101500d0052b500500d005","0x150070150a70a001000d08d0af0050a70052b50050a70053d70150a0005","0x1e30050b90150152b500505800527a0150152b50050150060150152b5005","0x527a0150152b500527100527a0150152b50050af00527a0150152b5005","0x2990150a80052b50050152970150152b500500f0051fb0150152b5005009","0x51120050af0152700052b50050aa0053d80150aa0052b500529a0a8007","0x150100052b50050100051e101529c0052b500529c0050330151120052b5","0x1029c1120af0052700052b50052700053d701529b0052b500529b0051e4","0x51e30050b90150152b500505800527a0150152b500501500701527029b","0x900527a0150152b500527100527a0150152b50050af00527a0150152b5","0x1e40150ac0052b50050700050330150152b500500f0051fb0150152b5005","0x154ae0050150b70152af0052b500529f00503401526e0052b5005073005","0x152b50051e30050b90150152b500505800527a0150152b5005015007015","0x2b500500900527a0150152b500527100527a0150152b50050af00527a015","0x52a90052660150152b500505e0050b30150152b500500f0051fb015015","0x3401526e0052b50052a40051e40150ac0052b50052a60050330150152b5","0x1526b0052b50050152970150152b50050150060152af0052b500506a005","0x1120050af0152690052b500526a0053d801526a0052b50052af26b007299","0x100052b50050100051e10150ac0052b50050ac0050330151120052b5005","0xac1120af0052690052b50052690053d701526e0052b500526e0051e4015","0x1e30050b90150152b500500f0051fb0150152b500501500701526926e010","0x527a0150152b500527100527a0150152b50050af00527a0150152b5005","0x2670052b500505c0051e40152680052b50050590050330150152b5005009","0x152b50050150070150154af0050150b70150b10052b500529e005034015","0x2b50050af00527a0150152b50051e30050b90150152b500500f0051fb015","0x52530050b30150152b500500900527a0150152b5005264005266015015","0x340152670052b50052800051e40152680052b500527a0050330150152b5","0x152660052b50050152970150152b50050150060150b10052b5005291005","0x1120050af0152650052b50050b30053d80150b30052b50050b1266007299","0x100052b50050100051e10152680052b50052680050330151120052b5005","0x2681120af0052650052b50052650053d70152670052b50052670051e4015","0x900527a0150152b500500f0051fb0150152b5005015007015265267010","0x150b60b40072b50051e10052550150152b50050af00527a0150152b5005","0x2b50051b90050330152020052b50051120050af0150152b50050b40050bf","0x3401520f0052b50051c80051e40152090052b50050100051e1015206005","0x27a0150152b50050150070150154a50050150b701521c0052b50050b6005","0x150152b500500f0051fb0150152b500500900527a0150152b50050af005","0x52b50050150050af0150152b50051290050b90150152b50050b70053b9","0x51e40152090052b50050070051e10152060052b5005006005033015202","0x2630052b500501529701521c0052b500502b00503401520f0052b5005027","0x50af01525e0052b500525f0053d801525f0052b500521c263007299015","0x52b50052090051e10152060052b50052060050330152020052b5005202","0x2020af00525e0052b500525e0053d701520f0052b500520f0051e4015209","0x1500c0052b50050090050c50150090052b50050153ba01525e20f209206","0x50100050c90150152b500500f00525001501000f0072b500500c0050c7","0x150210052b50050f600527c0150f60052b50050b700527f0150b70052b5","0x2b500502100526e01502e0052b500502e00509a01502e0052b5005015099","0x3302f02c0ba4b002b0270060ba2b500702102e0ba00512909c015021005","0x2b500500600503301502b0052b500502b0051b50150152b5005015007015","0x340310072b500702b0150070e30150270052b50050270051e4015006005","0x8d0151120af0072b50050af0053bb0150152b50050150070151b50054b1","0x51c81b90071100151c80052b500503400508d0151b90052b5005112005","0x150310052b50050310050af0151d20052b50051d20051b50151d20052b5","0x50af0050e20150152b50050150070151d90054b20152b50071d200527b","0x50af0151e30052b50051e10052780151e10052b50050150f60150152b5","0x52b50050070051e101511a0052b50050060050330151e40052b5005031","0x150b70151f50052b50051e30052770150190052b50050270051e40151e6","0xaf00508d0150152b50051d90052760150152b50050150070150154b3005","0x152060052b50052020051060152020052b50050151070151fe0052b5005","0x520f00528a0150152b500520900509101520f2090072b5005206005102","0x1521f0052b500521f0051b501521f0052b500521c00502f01521c0052b5","0x28601504f0052b500504f0051b501504f2370072b50051fe21f0070ba289","0x25300501c0152530052b50050520052830150520500072b500504f031007","0x2640052b50052620052810150152b50052590050960152622590072b5005","0x501509901526d0052b500505400527c0150540052b500526400527f015","0x152370052b50052370051e101526f0052b500526f00509a01526f0052b5","0x28027a2710ba2b500726d26f02700612909c0150500052b50050500050af","0x152800052b50052800051b50150152b50050150070152952932910ba4b4","0x728000527b01527a0052b500527a0051e40152710052b5005271005033","0x2780152990052b50050150f60150152b50050150070152970054b50152b5","0x154b60050150b70150590052b50050580052770150580052b5005299005","0x5c0052b50050150f60150152b50052970052760150152b5005015007015","0x5900500d0150590052b500529e00527701529e0052b500505c00509f015","0x11a0052b50052710050330151e40052b50050500050af01505e0052b5005","0x5e0052770150190052b500527a0051e40151e60052b50052370051e1015","0x52b50050600053e00150600052b50051f51290073df0151f50052b5005","0x51e101511a0052b500511a0050330151e40052b50051e40050af0152aa","0x52b50052aa0053e10150190052b50050190051e40151e60052b50051e6","0x2b50051290050b90150152b50050150070152aa0191e611a1e40af0052aa","0x53e20150620052b50052952a90072990152a90052b5005015297015015","0x52b50052910050330150500052b50050500050af0150640052b5005062","0x53e10152930052b50052930051e40152370052b50052370051e1015291","0xe20150152b50050150070150642932372910500af0050640052b5005064","0x152a80052b500501502b0150152b50051290050b90150152b50050af005","0x52a72a80072950152a70052b50052a70051b50152a70052b50050150cb","0x1506a0052b50050060050330152a40052b50051b50050af0152a60052b5","0x4b70050150b70150280052b50052a60050340152a30052b50050270051e4","0x2b50051290050b90150152b50050af0050e20150152b5005015007015015","0x51e401506a0052b500502c0050330152a40052b50050150050af015015","0x2a00052b50050152970150280052b50050330050340152a30052b500502f","0x50af01506f0052b50050710053e20150710052b50050282a0007299015","0x52b50050070051e101506a0052b500506a0050330152a40052b50052a4","0x2a40af00506f0052b500506f0053e10152a30052b50052a30051e4015007","0x3800150210052b500501511e0150b70052b500501520501506f2a300706a","0x1502f0052b50050153e301502b0052b50050152050150060052b5005015","0x3a70150330052b50050153e40150152b50050150060150152b5005015204","0x50340053a90151b50340072b50050310053a80150310052b5005033005","0x27c0151b90052b500511200527f0151120052b50051b50053ab0150152b5","0x52b50051d200509a0151d20052b50050150990151c80052b50051b9005","0x1d90ba2b50071c81d212900512909c0151c80052b50051c800526e0151d2","0x190052b50050153e40150152b50050150070151e611a1e40ba4b81e31e1","0x51e30051b50151fe0052b50050152730151f50052b500501900527c015","0x152020052b50050150990150100052b50051fe1e30073e50151e30052b5","0x50330152060100072b500501000523e0150100052b50050100b70071de","0x52b50051f500526e0152020052b500520200509a0151d90052b50051d9","0x1523721f21c0ba4b920f2090072b50072061f52021e11d90af2560151f5","0x2b500720f2090072650152090052b50052090050330150152b5005015007","0x50500050b40150152b50050150070152592530520ba4ba05002e04f0ba","0x542640af2b50052620052630152620052b50050500050b60150500052b5","0x50e20150152b500505400525e0150152b500526400525f01526f26d02c","0x150150052b50050150050af0150152b500526f00527a0150152b500526d","0x2c02f0073e60150ba0052b50050ba0051e301504f0052b500504f005033","0x52b500527100528f01527102c0072b500502c0053bb01502c0052b5005","0x23e01527a0052b500527a00528f01527a0090072b50050090053bb015271","0x500f0051f20152800052b50052800051b501528000c0072b500500c005","0x100072b500501000523e0152910052b500529100503101529100f0072b5","0x3e701502e0052b500502e00600712b0152930052b50052930051b5015293","0x210071350152990f62972951292b500529329128027a2710ba04f01500f","0x50150070150590054bb0580052b50072990053e80150f60052b50050f6","0x580053e901529e0052b500501502b01505c0052b500501502b0150152b5","0x72b500502700523e0150270052b500502702b0071de0150270052b5005","0xf06000c00902c05e0093ea0150600100072b500501000523e01505e027","0x620052b50052a90053ed0152a90052b50052aa0053ec0152aa0052b5005","0x2950050af0150152b50050640050000152a80640072b500506200519e015","0x2a80052b50052a800539d0152970052b50052970050330152950052b5005","0x2950af39e01529e0052b500529e00503401505c0052b500505c005034015","0x2a400539f0150152b50050151290152a42a62a70ba2b500529e05c2a8297","0xba2b500506a0053a10150152b50050150070152a30054bc06a0052b5007","0x1507006f0072b500502800506a0150152b50050710051e60150712a0028","0x507300529101529f0730072b50052a000506a0150152b500506f005291","0x3a201529d0052b500529f00502c0150760052b500507000502c0150152b5","0x2b500501500701529629829a0ba4bd29b29c0072b500729d07602e2a6129","0x2b50052940053ee0152940052b500501524b0150152b5005015006015015","0x3f10150152b50052900053f00150822900072b50052920053ef015292005","0x2b500502700523e0150e20052b50050e300502f0150e30052b5005082005","0x72b500528f0e20070ba2890150e20052b50050e20051b501528f027007","0x8e28c0072b500528d2a700728601528d0052b500528d0051b501528d28e","0x501000523e0151070052b500501509901508d0052b500508e00527c015","0x1070052b500510700509a01529c0052b500529c0050330151060100072b5","0x29c0af25601528c0052b500528c0050af01528e0052b500528e0051e1015","0x152b500501500701528628928a0ba4be0911020072b500710608d10729b","0x3f301501c0052b50052830af0073f20152830052b5005010027007269015","0x2b500510200503301528c0052b500528c0050af0150960052b500501c005","0x1e40150f60052b50050f60051e301528e0052b500528e0051e1015102005","0xf628e10228c0090050960052b50050960053f40150910052b5005091005","0x502700527a0150152b500501000527a0150152b5005015007015096091","0x2810072990152810052b50050152970150152b50050af00524c0150152b5","0x52b500528c0050af01527c0052b500527f0053f501527f0052b5005286","0x51e301528e0052b500528e0051e101528a0052b500528a00503301528c","0x52b500527c0053f40152890052b50052890051e40150f60052b50050f6","0x501000527a0150152b500501500701527c2890f628e28a28c00900527c","0x29a0050330150152b50050af00524c0150152b500502700527a0150152b5","0x9c0052b500529600503401509a0052b50052980051e40150990052b5005","0x150152b500501000527a0150152b50050150070150154bf0050150b7015","0x72b50052a30052550150152b50050af00524c0150152b500502700527a","0x51e40150990052b50052a60050330150152b500527b0050bf01527827b","0x150152b500501500601509c0052b500527800503401509a0052b500502e","0x52760053f50152760052b500509c2770072990152770052b5005015297","0x150990052b50050990050330152a70052b50052a70050af01509f0052b5","0x509a0051e40150f60052b50050f60051e30150070052b50050070051e1","0x1509f09a0f60070992a700900509f0052b500509f0053f401509a0052b5","0x150152b500502c0050e20150152b500501000527a0150152b5005015007","0x152b500502b0051ff0150152b50050090050e20150152b50050af00524c","0x2b50050590053f50150152b500500c00527a0150152b500500f005280015","0x1e10152970052b50052970050330152950052b50052950050af01500d005","0x2b500502e0051e40150f60052b50050f60051e30150070052b5005007005","0x701500d02e0f600729729500900500d0052b500500d0053f401502e005","0x3f60150152b500500f0052800150152b500501000527a0150152b5005015","0x150152b50050090050e20150152b50050af00524c0150152b500502f005","0x152b500502100522a0150152b500500c00527a0150152b500502b0051ff","0x52590a00072990150a00052b50050152970150152b50050060051fb015","0x150150052b50050150050af0152730052b50050a20053f50150a20052b5","0x50ba0051e30150070052b50050070051e10150520052b5005052005033","0x52730052b50052730053f40152530052b50052530051e40150ba0052b5","0x152b500501000527a0150152b50050150070152732530ba007052015009","0x2b50050af00524c0150152b500502f0053f60150152b500500f005280015","0x500c00527a0150152b500502b0051ff0150152b50050090050e2015015","0x50152970150152b50050060051fb0150152b500502100522a0150152b5","0xa50052b50052720053f50152720052b50052370a40072990150a40052b5","0x70051e101521c0052b500521c0050330150150052b50050150050af015","0x21f0052b500521f0051e40150ba0052b50050ba0051e30150070052b5005","0x50150070150a521f0ba00721c0150090050a50052b50050a50053f4015","0x2f0053f60150152b500500f0052800150152b50050b70051ff0150152b5","0x51ff0150152b50050090050e20150152b50050af00524c0150152b5005","0x1fb0150152b500502100522a0150152b500500c00527a0150152b500502b","0x52b50051e60a70072990150a70052b50050152970150152b5005006005","0x50330150150052b50050150050af0150aa0052b50050a80053f50150a8","0x52b50050ba0051e30150070052b50050070051e10151e40052b50051e4","0x150090050aa0052b50050aa0053f401511a0052b500511a0051e40150ba","0x150f60052b50050152050150100052b500501520a0150aa11a0ba0071e4","0x2c0052b50050152050150270052b500501538001502e0052b500501511e","0x152b50050152040150340052b50050153e30150330052b5005015380015","0x1b90ba4c011202f1b50ba2b50071290050072650150152b5005015006015","0x1120050b60151120052b50051120050b40150152b50050150070151d21c8","0x1e100525f01511a1e40311e31e10af2b50051d90052630151d90052b5005","0x527a0150152b50051e40050e20150152b50051e300525e0150152b5005","0x1b50052b50051b50050330150150052b50050150050af0150152b500511a","0x528f0151e60090072b50050090053bb0150ba0052b50050ba0051e3015","0x2b50050310053bb0150310052b50050310340073e60151e60052b50051e6","0x1f500c0072b500500c0051f20150190052b500501900528f015019031007","0x93f701502f0052b500502f03300712b0151f50052b50051f5005031015","0x2b500502102e0071350152060212021fe1292b50051f50191e60ba1b5015","0x150152b500501500701520f0054c12090052b50072060053e8015021005","0x2b500521c0053ee01521c0052b50050150de0150b70052b50052090053e9","0x3f10150152b50052370053f001504f2370072b500521f0053ef01521f005","0x50b70f60071de0150520052b500505000502f0150500052b500504f005","0x520052b50050520051b50152530b70072b50050b700523e0150b70052b5","0x152590052b50052590051b501525900f0072b50052530520070ba289015","0x53a80150540052b50052640053a70152642620072b50052591fe007286","0x52b500526f0053ab0150152b500526d0053a901526f26d0072b5005054","0x150990152800052b500527a00527c01527a0052b500527100527f015271","0x52b500500f0100072000152910052b500529100509a0152910052b5005","0x2930ba2b500728029102f20212909c0152620052b50052620050af01500f","0x2b500502b02c0071de0150152b50050150070152992972950ba4c202b006","0x152930052b500529300503301505802b0072b500502b00523e01502b005","0x70150590054c30152b500705800527b0150060052b500500602700712b","0x27a0150152b50050af00524c0150152b50050b700527a0150152b5005015","0x150152b50050310050e20150152b50050090050e20150152b500502b005","0x29e0052b50050153f901505c0052b500501502b0150152b500500c005280","0x1529701505e0052b500529e05c00729501529e0052b500529e0051b5015","0x52b50052aa0053fa0152aa0052b500505e0600072990150600052b5005","0x51e10152930052b50052930050330152620052b50052620050af0152a9","0x52b50050060051e40150210052b50050210051e301500f0052b500500f","0x150070152a900602100f2932620090052a90052b50052a90052be015006","0x1502b0150620052b500501502b0150152b50050590052760150152b5005","0xc0310092a81293fb0152a80b70072b50050b700523e0150640052b5005","0x2a40052b50052a60053ed0152a60052b50052a70053fc0152a70052b5005","0x2620050af0150152b500506a0050000152a306a0072b50052a400519e015","0x2a30052b50052a300539d0152930052b50052930050330152620052b5005","0x2620af39e0150640052b50050640050340150620052b5005062005034015","0x7100539f0150152b50050151290150712a00280ba2b50050640622a3293","0xba2b500506f0053a10150152b50050150070150700054c406f0052b5007","0x1529c29d0072b500507300506a0150152b50050760051e601507629f073","0x529b00529101529a29b0072b500529f00506a0150152b500529d005291","0x3a20152960052b500529a00502c0152980052b500529c00502c0150152b5","0x2b50050150070150e30822900ba4c52922940072b50072962980062a0129","0x2b50050e20053ee0150e20052b50050150de0150152b5005015006015015","0x3f10150152b500528e0053f001528d28e0072b500528f0053ef01528f005","0x2b50050b700523e01508e0052b500528c00502f01528c0052b500528d005","0x72b500508d08e00f0ba28901508e0052b500508e0051b501508d0b7007","0x911020072b50051060280072860151060052b50051060051b5015106107","0x28902b0071100152890052b500501527301528a0052b500509100527c015","0x152940052b50052940050330152830052b50050150990152860052b5005","0x51070051e10152860052b50052860051b50152830052b500528300509a","0x728628a2832922940af2560151020052b50051020050af0151070052b5","0xb70af0073fd0150152b500501500701527c27f2810ba4c609601c0072b5","0x1020052b50051020050af01509a0052b50050990053fe0150990052b5005","0x210051e30151070052b50051070051e101501c0052b500501c005033015","0x9a0052b500509a0052be0150960052b50050960051e40150210052b5005","0x2b50050b700527a0150152b500501500701509a09602110701c102009005","0x27c09c00729901509c0052b50050152970150152b50050af00524c015015","0x1020052b50051020050af0152780052b500527b0053fa01527b0052b5005","0x210051e30151070052b50051070051e10152810052b5005281005033015","0x2780052b50052780052be01527f0052b500527f0051e40150210052b5005","0x2b50050b700527a0150152b500501500701527827f021107281102009005","0x52900050330150152b500502b00527a0150152b50050af00524c015015","0x1509f0052b50050e30050340152760052b50050820051e40152770052b5","0x24c0150152b50050b700527a0150152b50050150070150154c70050150b7","0xd0072b50050700052550150152b500502b00527a0150152b50050af005","0x60051e40152770052b50052a00050330150152b500500d0050bf0150a0","0x2970150152b500501500601509f0052b50050a00050340152760052b5005","0x2b50052730053fa0152730052b500509f0a20072990150a20052b5005015","0x1e10152770052b50052770050330150280052b50050280050af0150a4005","0x2b50052760051e40150210052b50050210051e301500f0052b500500f005","0x70150a427602100f2770280090050a40052b50050a40052be015276005","0x27a0150152b500500c0052800150152b50050310050e20150152b5005015","0x150152b50050090050e20150152b50050af00524c0150152b50050b7005","0x2720052b50050152970150152b500502c0051ff0150152b50050270051fb","0x50af0150a70052b50050a50053fa0150a50052b5005299272007299015","0x52b500500f0051e10152950052b50052950050330152620052b5005262","0x52be0152970052b50052970051e40150210052b50050210051e301500f","0x150152b50050150070150a729702100f2952620090050a70052b50050a7","0x152b50050af00524c0150152b500500c0052800150152b50050310050e2","0x2b500502c0051ff0150152b50050270051fb0150152b50050090050e2015","0x520f0053fa0150152b50050f60051ff0150152b50050100053b9015015","0x152020052b50052020050330151fe0052b50051fe0050af0150a80052b5","0x502f0051e40150210052b50050210051e30150070052b50050070051e1","0x150a802f0210072021fe0090050a80052b50050a80052be01502f0052b5","0x150152b50050100053b90150152b50050f60051ff0150152b5005015007","0x152b50050af00524c0150152b500500c0052800150152b50050340053f6","0x2b500502c0051ff0150152b50050270051fb0150152b50050090050e2015","0x2b50050152970150152b50050330051fb0150152b500502e00522a015015","0x150ac0052b50052700053fa0152700052b50051d20aa0072990150aa005","0x50070051e10151b90052b50051b90050330150150052b50050150050af","0x151c80052b50051c80051e40150ba0052b50050ba0051e30150070052b5","0x2b50050153800150ac1c80ba0071b90150090050ac0052b50050ac0052be","0x501520a0150270052b500501511e01502e0052b50050152050150f6005","0x152040150340052b50050153e30150330052b500501538001502c0052b5","0x27b0151b50100072b500501000523e0150152b50050150060150152b5005","0x2b500500f0052800150152b50050150070151120054c80152b50071b5005","0x50af00524c0150152b500502e0051ff0150152b50050340053f6015015","0xc00527a0150152b500502c0053b90150152b50050090050e20150152b5","0x522a0150152b50050f60051fb0150152b500501000527a0150152b5005","0x3ff0151b90052b500501502b0150152b50050330051fb0150152b5005027","0x2b50051c81b90072950151c80052b50051c80051b50151c80052b5005015","0x3fa0151e10052b50051d21d90072990151d90052b50050152970151d2005","0x2b50050050050330150150052b50050150050af0151e30052b50051e1005","0x1e40150ba0052b50050ba0051e30150070052b50050070051e1015005005","0xba0070050150090051e30052b50051e30052be0151290052b5005129005","0x1290050072650150152b50051120052760150152b50050150070151e3129","0x50b40150152b50050150070151f50191e60ba4c911a02f1e40ba2b5007","0xaf2b50051fe0052630151fe0052b500511a0050b601511a0052b500511a","0x150152b500520600525e0150152b500520200525f01520f209031206202","0x52b50050150050af0150152b500520f00527a0150152b50052090050e2","0x73e60150ba0052b50050ba0051e30151e40052b50051e4005033015015","0x521c00528f01521c0310072b50050310053bb0150310052b5005031034","0x21f0052b500521f00528f01521f0090072b50050090053bb01521c0052b5","0x51f20152370052b50052370051b501523700c0072b500500c00523e015","0x2b500501000523e01504f0052b500504f00503101504f00f0072b500500f","0x2f0052b500502f03300712b0150500052b50050500051b5015050010007","0x1350152590062530521292b500505004f23721f21c0ba1e401500f3e7015","0x70152640054ca2620052b50072590053e80150060052b5005006027007","0x150540052b500501524b0150210052b50052620053e90150152b5005015","0x26f0053f001527126f0072b500526d0053ef01526d0052b50050540053ee","0x152800052b500527a00502f01527a0052b50052710053f10150152b5005","0x51b50152910210072b500502100523e0150210052b500502102e0071de","0x2950051b50152952930072b50052912800070ba2890152800052b5005280","0x2b50052990053a70152992970072b50052950520072860152950052b5005","0x3ab0150152b50050590053a901505c0590072b50050580053a8015058005","0x2b500505e00527c01505e0052b500529e00527f01529e0052b500505c005","0x51e10152aa0052b50052aa00509a0152aa0052b5005015099015060005","0x602aa02f25312909c0152970052b50052970050af0152930052b5005293","0x523e0150152b50050150070152a62a72a80ba4cb0640622a90ba2b5007","0x52a40640071100150640052b50050640051b50152a40100072b5005010","0x152a90052b50052a900503301506a0052b500506a0051b501506a0052b5","0x150070152a30054cc0152b500706a00527b0150620052b50050620051e4","0x1507307006f0ba4cd0712a00280ba2b50070622a90072650150152b5005","0x52b50050710050b60150710052b50050710050b40150152b5005015007","0x152b500529d00525e01529a29b29c29d0760af2b500529f00526301529f","0x2b500529a00527a0150152b500529b0050e20150152b500529c0050e2015","0x154010152980052b50050760054000150760052b50050760052bd015015","0x2920072b50052940054030152940052b50052960054020152960052b5005","0x8200502f0150820052b50052900052bf0150152b5005292005404015290","0x52b50050e30051b50150e20210072b500502100523e0150e30052b5005","0x28f0052b500528f0051b501528f02b0072b50050e20e32930ba2890150e3","0x8d08e28c0ba2b500529800540501528d28e0072b500528f297007286015","0x2b500508e0054070150152b500508d0050e20150152b500528c005406015","0x50330151020052b50050150990151060052b500528d00527c015107005","0x52b50051070051b50151020052b500510200509a0150280052b5005028","0x25601528e0052b500528e0050af01502b0052b500502b02c007200015107","0x501500701528628928a0ba4ce0b70910072b50071071061022a00280af","0x2100523e01501c0052b500501502b0152830052b500501502b0150152b5","0x4090152810052b500501000f00c0090310960094080150960210072b5005","0x2b500527c00519e01527c0052b500527f0053ed01527f0052b5005281005","0x3301528e0052b500528e0050af0150152b500509900500001509a099007","0x2b500528300503401509a0052b500509a00539d0150910052b5005091005","0x150b70052b50050b70f600712b01501c0052b500501c005034015283005","0x150152b500501512901527827b09c0ba2b500501c28309a09128e0af39e","0x2770053a10150152b50050150070152760054cf2770052b500727800539f","0x72b500509f00506a0150152b50050a00051e60150a000d09f0ba2b5005","0x2910152720a40072b500500d00506a0150152b50050a20052910152730a2","0x52b500527200502c0150a50052b500527300502c0150152b50050a4005","0x701526e0ac2700ba4d00aa0a80072b50070a70a50b727b1293a20150a7","0x152af0052b50050210af0073fd0150152b50050150060150152b5005015","0x50a800503301509c0052b500509c0050af01526b0052b50052af0053fe","0x150060052b50050060051e301502b0052b500502b0051e10150a80052b5","0x2b0a809c00900526b0052b500526b0052be0150aa0052b50050aa0051e4","0xaf00524c0150152b500502100527a0150152b500501500701526b0aa006","0x152690052b50050ac0051e401526a0052b50052700050330150152b5005","0x150152b50050150070150154d10050150b70152680052b500526e005034","0x72b50052760052550150152b50050af00524c0150152b500502100527a","0x51e401526a0052b500527b0050330150152b50052670050bf0150b1267","0x150152b50050150060152680052b50050b10050340152690052b50050b7","0x50b30053fa0150b30052b50052682660072990152660052b5005015297","0x1526a0052b500526a00503301509c0052b500509c0050af0152650052b5","0x52690051e40150060052b50050060051e301502b0052b500502b0051e1","0x1526526900602b26a09c0090052650052b50052650052be0152690052b5","0x150152b500502100527a0150152b500500f0052800150152b5005015007","0x152b50050090050e20150152b50050310050e20150152b50050af00524c","0x2b50050f60051fb0150152b500501000527a0150152b500500c00527a015","0x53fa0150b60052b50052860b40072990150b40052b5005015297015015","0x52b500528a00503301528e0052b500528e0050af0152630052b50050b6","0x51e40150060052b50050060051e301502b0052b500502b0051e101528a","0x28900602b28a28e0090052630052b50052630052be0152890052b5005289","0x2b500502100527a0150152b500500f0052800150152b5005015007015263","0x50090050e20150152b50050310050e20150152b50050af00524c015015","0xf60051fb0150152b500501000527a0150152b500500c00527a0150152b5","0x729901525f0052b50050152970150152b500502c0053b90150152b5005","0x2b50052970050af01525d0052b500525e0053fa01525e0052b500507325f","0x1e30152930052b50052930051e101506f0052b500506f005033015297005","0x2b500525d0052be0150700052b50050700051e40150060052b5005006005","0x2a30052760150152b500501500701525d07000629306f29700900525d005","0x524c0150152b500502100527a0150152b500500f0052800150152b5005","0x27a0150152b50050090050e20150152b50050310050e20150152b50050af","0x150152b50050f60051fb0150152b500501000527a0150152b500500c005","0x25b0052b50050152bc01525c0052b500501502b0150152b500502c0053b9","0x152970150b90052b500525b25c00729501525b0052b500525b0051b5015","0x52b50052580053fa0152580052b50050b925a00729901525a0052b5005","0x51e10152a90052b50052a90050330152970052b50052970050af015256","0x52b50050620051e40150060052b50050060051e30152930052b5005293","0x150070152560620062932a92970090052560052b50052560052be015062","0x524c0150152b500502100527a0150152b500500f0052800150152b5005","0x27a0150152b50050090050e20150152b50050310050e20150152b50050af","0x150152b50050f60051fb0150152b500501000527a0150152b500500c005","0x2b50052a62550072990152550052b50050152970150152b500502c0053b9","0x330152970052b50052970050af0150c10052b50050bf0053fa0150bf005","0x2b50050060051e30152930052b50052930051e10152a80052b50052a8005","0x90050c10052b50050c10052be0152a70052b50052a70051e4015006005","0x150152b500500f0052800150152b50050150070150c12a70062932a8297","0x152b50050090050e20150152b50050310050e20150152b50050af00524c","0x2b50050f60051fb0150152b500501000527a0150152b500500c00527a015","0x52640053fa0150152b500502e0051ff0150152b500502c0053b9015015","0x152530052b50052530050330150520052b50050520050af0152540052b5","0x502f0051e40150060052b50050060051e30150070052b50050070051e1","0x1525402f0060072530520090052540052b50052540052be01502f0052b5","0x150152b50050340053f60150152b500500f0052800150152b5005015007","0x152b50050090050e20150152b50050af00524c0150152b500502e0051ff","0x2b500501000527a0150152b500500c00527a0150152b500502c0053b9015","0x50330051fb0150152b500502700522a0150152b50050f60051fb015015","0x3fa0152510052b50051f52520072990152520052b50050152970150152b5","0x2b50051e60050330150150052b50050150050af0150c50052b5005251005","0x1e40150ba0052b50050ba0051e30150070052b50050070051e10151e6005","0xba0071e60150090050c50052b50050c50052be0150190052b5005019005","0x501511e01502e0052b50050152050150f60052b500501520a0150c5019","0x153e30150330052b500501538001502c0052b50050153800150270052b5","0x50072650150152b50050150060150152b50050152040150340052b5005","0xb40150152b50050150070151d21c81b90ba4d211202f1b50ba2b5007129","0x2b50051d90052630151d90052b50051120050b60151120052b5005112005","0x152b50051e300525e0150152b50051e100525f01511a1e40311e31e10af","0x2b50050150050af0150152b500511a00527a0150152b50051e40050e2015","0x3e60150ba0052b50050ba0051e30151b50052b50051b5005033015015005","0x1e600528f0151e60310072b50050310053bb0150310052b5005031034007","0x52b500501900528f0150190090072b50050090053bb0151e60052b5005","0x1f20151f50052b50051f50051b50151f500c0072b500500c00523e015019","0x501000523e0151fe0052b50051fe0050310151fe00f0072b500500f005","0x52b500502f03300712b0152020052b50052020051b50152020100072b5","0x1520f0062092061292b50052021fe1f50191e60ba1b501500f3e701502f","0x1521f0054d321c0052b500720f0053e80150060052b5005006027007135","0x2370052b500501524b0150210052b500521c0053e90150152b5005015007","0x53f00150520500072b500504f0053ef01504f0052b50052370053ee015","0x2590052b500525300502f0152530052b50050520053f10150152b5005050","0x1b50152620210072b500502100523e0150210052b500502102e0071de015","0x51b50150542640072b50052622590070ba2890152590052b5005259005","0x526f0053a701526f26d0072b50050542060072860150540052b5005054","0x150152b500527a0053a901528027a0072b50052710053a80152710052b5","0x529300527c0152930052b500529100527f0152910052b50052800053ab","0x1e10152970052b500529700509a0152970052b50050150990152950052b5","0x29702f20912909c01526d0052b500526d0050af0152640052b5005264005","0x23e0150152b500501500701505e29e05c0ba4d40590582990ba2b5007295","0x600590071100150590052b50050590051b50150600100072b5005010005","0x2990052b50052990050330152aa0052b50052aa0051b50152aa0052b5005","0x70152a90054d50152b50072aa00527b0150580052b50050580051e4015","0x150640052b50050620054020150620052b50050154010150152b5005015","0x52a70052bf0150152b50052a80054040152a72a80072b5005064005403","0x6a0210072b500502100523e0152a40052b50052a600502f0152a60052b5","0x152a30b70072b500506a2a42640ba2890152a40052b50052a40051b5015","0x540a0152a00280072b50052a326d0072860152a30052b50052a30051b5","0x2b500506f00540c01507006f0072b500507100540b0150710052b50052a0","0x527c01529f0052b500507300527f0150730052b500507000540d015015","0x29d0052b500529d00509a01529d0052b50050150990150760052b500529f","0x12909c0150280052b50050280050af0150b70052b50050b70f6007200015","0x2b50050150070152942962980ba4d629a29b29c0ba2b500707629d058299","0x51e401529c0052b500529c00503301529a0052b500529a0051b5015015","0x70150820054d72902920072b500729a02800740e01529b0052b500529b","0xe22900072b50052900054100150e30052b500501540f0150152b5005015","0x70150154d80152b50070e30e20074110152920052b50052920050af015","0x1528e0052b500528f00540a01528f0052b50050154120150152b5005015","0x528c00540d0150152b500528d00540c01528c28d0072b500528e00540b","0x151070052b500508d00527c01508d0052b500508e00527f01508e0052b5","0x2b500510700526e0151060052b500510600509a0151060052b5005015099","0x2832862890ba4d928a0911020ba2b500710710629b29c12909c015107005","0x2b500510200503301528a0052b500528a0051b50150152b5005015007015","0x9601c0072b500728a29200740e0150910052b50050910051e4015102005","0x41301527f2900072b50052900054100150152b50050150070152810054da","0x150152b500501500701509c09a0074db09927c0072b500709627f01c0ba","0x4dc2772780072b500729027b27c0ba41501527b0990072b5005099005410","0x2780050af0150152b50052770054060150152b500501500701509f276007","0xa42730a20ba4dd0a002b00d0ba2b50070911020072650152780052b5005","0x2b50050a00050b60150a00052b50050a00050b40150152b5005015007015","0x2b50050a700525e0152700aa0a80a70a50af2b5005272005263015272005","0x527000527a0150152b50050aa0050e20150152b50050a80050e2015015","0x4050150ac0052b50050a50054000150a50052b50050a50052bd0150152b5","0x526b0050e20150152b500526e00540601526b2af26e0ba2b50050ac005","0x12b01500d0052b500500d0050330152af0052b50052af0054160150152b5","0x74de26926a0072b50070992af2780ba41501502b0052b500502b02c007","0x2b500501502b0150152b50052690054060150152b5005015007015267268","0x4170150b30210072b500502100523e0152660052b500501502b0150b1005","0x150b40052b50052650052ba0152650052b500501000f00c0090310b3009","0x26300500001525f2630072b50050b600519e0150b60052b50050b40053ed","0x1500d0052b500500d00503301526a0052b500526a0050af0150152b5005","0x52660050340150b10052b50050b100503401525f0052b500525f00539d","0x1512901525c25d25e0ba2b50052660b125f00d26a0af39e0152660052b5","0x152b50050150070150b90054df25b0052b500725c00539f0150152b5005","0x506a0150152b50052560051e601525625825a0ba2b500525b0053a1015","0x72b500525800506a0150152b50052550052910150bf2550072b500525a","0x502c0152520052b50050bf00502c0150152b50050c10052910152540c1","0x2500ba4e00c70c50072b500725125202b25d1293a20152510052b5005254","0x52b500501524b0150152b50050150060150152b50050150070150cb0c9","0x3f001524c24d0072b500524e0053ef01524e0052b500524f0053ee01524f","0x52b500524b00502f01524b0052b500524c0053f10150152b500524d005","0x28901524a0052b500524a0051b50152490210072b500502100523e01524a","0x72860150e10052b50050e10051b50150e12480072b500524924a0b70ba","0x2b50050150a20152400052b500524100527c0152412430072b50050e125e","0x509a0150c50052b50050c500503301523e0052b50050150990150e0005","0x52b50052480051e10150e00052b50050e00051b501523e0052b500523e","0x72b50070e024023e0c70c50af2560152430052b50052430050af015248","0x2b50050210af0073fd0150152b500501500701523b0df23c0ba4e115623d","0x330152430052b50052430050af0150de0052b50052390053fe015239005","0x2b50050060051e30152480052b50052480051e101523d0052b500523d005","0x90050de0052b50050de0052be0151560052b50051560051e4015006005","0x150152b500502100527a0150152b50050150070150de15600624823d243","0x2b500523b1050072990151050052b50050152970150152b50050af00524c","0x330152430052b50052430050af0152380052b50050e50053fa0150e5005","0x2b50050060051e30152480052b50052480051e101523c0052b500523c005","0x90052380052b50052380052be0150df0052b50050df0051e4015006005","0x150152b500502100527a0150152b50050150070152380df00624823c243","0x2b50050c90051e401523a0052b50052500050330150152b50050af00524c","0x50150070150154e20050150b70151040052b50050cb0050340150e9005","0xb90052550150152b50050af00524c0150152b500502100527a0150152b5","0x23a0052b500525d0050330150152b50052340050bf0152332340072b5005","0x50150060151040052b50052330050340150e90052b500502b0051e4015","0x3fa0152310052b50051042320072990152320052b50050152970150152b5","0x2b500523a00503301525e0052b500525e0050af0150f00052b5005231005","0x1e40150060052b50050060051e30150b70052b50050b70051e101523a005","0x60b723a25e0090050f00052b50050f00052be0150e90052b50050e9005","0x502100527a0150152b50052670054060150152b50050150070150f00e9","0x90050e20150152b50050310050e20150152b50050af00524c0150152b5","0x52800150152b500501000527a0150152b500500c00527a0150152b5005","0x1b50151030052b50050154180150f20052b500501502b0150152b500500f","0x2b500501529701522f0052b50051030f20072950151030052b5005103005","0x152290052b500522c0053fa01522c0052b500522f22d00729901522d005","0x50b70051e101500d0052b500500d0050330152680052b50052680050af","0x1502b0052b500502b0051e40150060052b50050060051e30150b70052b5","0x2b500501500701522902b0060b700d2680090052290052b50052290052be","0x50310050e20150152b50050af00524c0150152b500502100527a015015","0x990054060150152b500500c00527a0150152b50050090050e20150152b5","0x51fb0150152b500500f0052800150152b500501000527a0150152b5005","0x2270052b50050a42280072990152280052b50050152970150152b500502c","0xa20050330152780052b50052780050af0152260052b50052270053fa015","0x60052b50050060051e30150b70052b50050b70051e10150a20052b5005","0xa22780090052260052b50052260052be0152730052b50052730051e4015","0x527a0150152b500509f0054060150152b50050150070152262730060b7","0xe20150152b50050310050e20150152b50050af00524c0150152b5005021","0x150152b50050990054060150152b500500c00527a0150152b5005009005","0x152b500502c0051fb0150152b500500f0052800150152b500501000527a","0x2b50051080051b50151080052b50050154190152250052b500501502b015","0x2990150fe0052b50050152970152240052b5005108225007295015108005","0x52760050af0151090052b50052200053fa0152200052b50052240fe007","0x150b70052b50050b70051e10151020052b50051020050330152760052b5","0x51090052be0150910052b50050910051e40150060052b50050060051e3","0x54060150152b50050150070151090910060b71022760090051090052b5","0xe20150152b50050af00524c0150152b500502100527a0150152b500509c","0x150152b500500c00527a0150152b50050090050e20150152b5005031005","0x152b500500f0052800150152b500501000527a0150152b5005290005406","0x52b500501541a01521e0052b500501502b0150152b500502c0051fb015","0x29701521d0052b500510b21e00729501510b0052b500510b0051b501510b","0x2b500510e0053fa01510e0052b500521d10c00729901510c0052b5005015","0x1e10151020052b500510200503301509a0052b500509a0050af01521b005","0x2b50050910051e40150060052b50050060051e30150b70052b50050b7005","0x701521b0910060b710209a00900521b0052b500521b0052be015091005","0xe20150152b50050af00524c0150152b500502100527a0150152b5005015","0x150152b500500c00527a0150152b50050090050e20150152b5005031005","0x152b500500f0052800150152b500501000527a0150152b5005290005406","0x52b50050152b90151100052b500501502b0150152b500502c0051fb015","0xaf0152170052b50052181100072950152180052b50052180051b5015218","0x2b50050910051e40152150052b50051020050330152160052b5005281005","0x50150070150154e30050150b70152130052b5005217005034015214005","0x310050e20150152b50050af00524c0150152b500502100527a0150152b5","0x54060150152b500500c00527a0150152b50050090050e20150152b5005","0x1fb0150152b500500f0052800150152b500501000527a0150152b5005290","0x52b50052890050330152160052b50052920050af0150152b500502c005","0x152970152130052b50052830050340152140052b50052860051e4015215","0x52b50052110053fa0152110052b50052132120072990152120052b5005","0x51e10152150052b50052150050330152160052b50052160050af015119","0x52b50052140051e40150060052b50050060051e30150b70052b50050b7","0x150070151192140060b72152160090051190052b50051190052be015214","0x50e20150152b50050af00524c0150152b500502100527a0150152b5005","0x4060150152b500500c00527a0150152b50050090050e20150152b5005031","0x150152b500500f0052800150152b500501000527a0150152b5005290005","0x20e0052b500501541b0152100052b500501502b0150152b500502c0051fb","0x1529701520d0052b500520e21000729501520e0052b500520e0051b5015","0x52b500520c0053fa01520c0052b500520d08f00729901508f0052b5005","0x51e101529c0052b500529c0050330152920052b50052920050af01520b","0x52b500529b0051e40150060052b50050060051e30150b70052b50050b7","0x1500701520b29b0060b729c29200900520b0052b500520b0052be01529b","0x50e20150152b50050af00524c0150152b500502100527a0150152b5005","0x27a0150152b500500c00527a0150152b50050090050e20150152b5005031","0x150152b500502c0051fb0150152b500500f0052800150152b5005010005","0x52b500511e0051b501511e0052b50050152b90153800052b500501502b","0x330152080052b50050820050af01520a0052b500511e38000729501511e","0x2b500520a0050340152070052b500529b0051e40151210052b500529c005","0x2b500502100527a0150152b50050150070150154e40050150b7015205005","0x50090050e20150152b50050310050e20150152b50050af00524c015015","0x1000527a0150152b500502c0051fb0150152b500500c00527a0150152b5","0x330152080052b50050280050af0150152b500500f0052800150152b5005","0x2b50052940050340152070052b50052960051e40151210052b5005298005","0x3fa0151260052b50052052040072990152040052b5005015297015205005","0x2b50051210050330152080052b50052080050af0151280052b5005126005","0x1e40150060052b50050060051e30150b70052b50050b70051e1015121005","0x60b71212080090051280052b50051280052be0152070052b5005207005","0x502100527a0150152b50052a90052760150152b5005015007015128207","0x90050e20150152b50050310050e20150152b50050af00524c0150152b5","0x527a0150152b500502c0051fb0150152b500500c00527a0150152b5005","0x2b0150152b50050f60053b90150152b500500f0052800150152b5005010","0x2000052b50052000051b50152000052b50050152bc0152030052b5005015","0x1ff0072990151ff0052b500501529701512b0052b5005200203007295015","0x52b500526d0050af01512f0052b500512d0053fa01512d0052b500512b","0x51e30152640052b50052640051e10152990052b500529900503301526d","0x52b500512f0052be0150580052b50050580051e40150060052b5005006","0x502100527a0150152b500501500701512f05800626429926d00900512f","0x310050e20150152b50050af00524c0150152b50050f60053b90150152b5","0x51fb0150152b500500c00527a0150152b50050090050e20150152b5005","0x2970150152b500500f0052800150152b500501000527a0150152b500502c","0x2b50050450053fa0150450052b500505e1f60072990151f60052b5005015","0x1e101505c0052b500505c00503301526d0052b500526d0050af0151fb005","0x2b500529e0051e40150060052b50050060051e30152640052b5005264005","0x70151fb29e00626405c26d0090051fb0052b50051fb0052be01529e005","0xe20150152b50050af00524c0150152b50050f60053b90150152b5005015","0x150152b500500c00527a0150152b50050090050e20150152b5005031005","0x152b500500f0052800150152b500501000527a0150152b500502c0051fb","0x52060050af0151fc0052b500521f0053fa0150152b500502e0051ff015","0x150070052b50050070051e10152090052b50052090050330152060052b5","0x51fc0052be01502f0052b500502f0051e40150060052b50050060051e3","0x51ff0150152b50050150070151fc02f0060072092060090051fc0052b5","0x24c0150152b50050f60053b90150152b50050340053f60150152b500502e","0x150152b500500c00527a0150152b50050090050e20150152b50050af005","0x152b500500f0052800150152b500501000527a0150152b500502c0051fb","0x52b50050152970150152b50050330051fb0150152b500502700522a015","0xaf01522a0052b50051310053fa0151310052b50051d21f80072990151f8","0x2b50050070051e10151b90052b50051b90050330150150052b5005015005","0x2be0151c80052b50051c80051e40150ba0052b50050ba0051e3015007005","0x52b500501520501522a1c80ba0071b901500900522a0052b500522a005","0x2b500501520501500f0052b50050152050150090052b5005015205015129","0x501541d0150060052b500501541d0150210052b500501541c0150b7005","0x152050150310052b500501520501502f0052b500501520501502b0052b5","0x2050151d20052b50050152050151b90052b50050152050151b50052b5005","0x50ba0150152b50050150060150152b50050152040151e10052b5005015","0x150070151e60054e511a1e40072b50071e30050090151e30052b5005007","0x151f50052b500501900502f0150190052b500511a0050270150152b5005","0x51fe00521c0152020052b50051e400500f0151fe0052b50051f500520f","0x2b50050150f60150152b50050150070150154e60050150b70152060052b5","0x21c0152020052b50051e600500f01520f0052b500520900521f015209005","0x1500701521c0054e70100052b50072060052370152060052b500520f005","0x21f0072b50072020050090150100052b50050100b70071de0150152b5005","0x2f0150500052b50052370050270150152b500501500701504f0054e8237","0x2b500521f00500f0152530052b500505200520f0150520052b5005050005","0x50150070150154e90050150b70152620052b500525300521c015259005","0x500f0150540052b500526400521f0152640052b50050150f60150152b5","0x52b50072620052370152620052b500505400521c0152590052b500504f","0x1500c0052b500500c00f0071de0150152b500501500701526d0054ea00c","0x50270150152b500501500701527a0054eb27126f0072b5007259005009","0x52b500529100520f0152910052b500528000502f0152800052b5005271","0x150b70152970052b500529300521c0152950052b500526f00500f015293","0x29900521f0152990052b50050150f60150152b50050150070150154ec005","0x2970052b500505800521c0152950052b500527a00500f0150580052b5005","0x71de0150152b50050150070150590054ed0ba0052b5007297005237015","0x701505e0054ee29e05c0072b50072950050090150ba0052b50050ba129","0x2aa0052b500506000502f0150600052b500529e0050270150152b5005015","0x2a900521c0150620052b500505c00500f0152a90052b50052aa00520f015","0x50150f60150152b50050150070150154ef0050150b70150640052b5005","0x150620052b500505e00500f0152a70052b50052a800521f0152a80052b5","0x70152a60054f00af0052b50070640052370150640052b50052a700521c","0x72b50070620050090150af0052b50050af0090071de0150152b5005015","0x150280052b500506a0050270150152b50050150070152a30054f106a2a4","0x52a400500f0150710052b50052a000520f0152a00052b500502800502f","0x150070150154f20050150b70150700052b500507100521c01506f0052b5","0xf01529f0052b500507300521f0150730052b50050150f60150152b5005","0x2b50070700052370150700052b500529f00521c01506f0052b50052a3005","0x1d90052b50051d91e10071de0150152b50050150070150760054f31d9005","0x270150152b500501500701529b0054f429c29d0072b500706f005009015","0x2b500529800520f0152980052b500529a00502f01529a0052b500529c005","0xb70152920052b500529600521c0152940052b500529d00500f015296005","0x521f0152900052b50050150f60150152b50050150070150154f5005015","0x52b500508200521c0152940052b500529b00500f0150820052b5005290","0x1de0150152b50050150070150e30054f61c80052b5007292005237015292","0x1528e0054f728f0e20072b50072940050090151c80052b50051c81d2007","0x52b500528d00502f01528d0052b500528f0050270150152b5005015007","0x521c01508d0052b50050e200500f01508e0052b500528c00520f01528c","0x150f60150152b50050150070150154f80050150b70151070052b500508e","0x8d0052b500528e00500f0151020052b500510600521f0151060052b5005","0x150910054f91120052b50071070052370151070052b500510200521c015","0x2b500708d0050090151120052b50051121b90071de0150152b5005015007","0x2830052b50052890050270150152b50050150070152860054fa28928a007","0x28a00500f0150960052b500501c00520f01501c0052b500528300502f015","0x70150154fb0050150b701527f0052b500509600521c0152810052b5005","0x150990052b500527c00521f01527c0052b50050150f60150152b5005015","0x727f00523701527f0052b500509900521c0152810052b500528600500f","0x52b50050341b50071de0150152b500501500701509a0054fc0340052b5","0x150152b50050150070152780054fd27b09c0072b5007281005009015034","0x527600520f0152760052b500527700502f0152770052b500527b005027","0x150a00052b500509f00521c01500d0052b500509c00500f01509f0052b5","0x21f0150a20052b50050150f60150152b50050150070150154fe0050150b7","0x2b500527300521c01500d0052b500527800500f0152730052b50050a2005","0x150152b50050150070150a40054ff0330052b50070a00052370150a0005","0xa70055000a52720072b500700d0050090150330052b50050330310071de","0x2b50050a800502f0150a80052b50050a50050270150152b5005015007015","0x21c0150ac0052b500527200500f0152700052b50050aa00520f0150aa005","0xf60150152b50050150070150155010050150b701526e0052b5005270005","0x52b50050a700500f01526b0052b50052af00521f0152af0052b5005015","0x26a00550202c0052b500726e00523701526e0052b500526b00521c0150ac","0x70ac00500901502c0052b500502c02f0071de0150152b5005015007015","0x52b500526800500c0150152b50050150070152670055032682690072b5","0x150b70150b30052b50050b10050100152660052b500526900500f0150b1","0x2650050210152650052b50050150f60150152b5005015007015015504005","0xb30052b50050b40050100152660052b500526700500f0150b40052b5005","0x50270150152b50050150070152630055050b60052b50070b300502e015","0x25d0052b500526600502c01525e0052b500501502b01525f0052b50050b6","0x50050330150150052b50050150050af01525c0052b500525f00502f015","0x25e0052b500525e00503401525d0052b500525d0050310150050052b5005","0x25b0ba2b500525c25e25d0050150af11201525c0052b500525c0051b5015","0x150152b50050150070152560055062580052b500725a0051b901525a0b9","0x25b0050af0150c10052b50052550050ba0150bf2550072b50052580051c8","0x2510052b50050c100500f0152520052b50050b90050330152540052b5005","0x152b50050150070150155070050150b70150c50052b50050bf0051d2015","0x2b500500c00527a0150152b50050af00527a0150152b50050ba00527a015","0x500600541e0150152b50050210052c00150152b500501000527a015015","0x3300527a0150152b500502c00527a0150152b500502b00541e0150152b5","0x527a0150152b500511200527a0150152b500503400527a0150152b5005","0x150c70052b500525600541f0150152b50051d900527a0150152b50051c8","0x50c70054200150b90052b50050b900503301525b0052b500525b0050af","0x2b50052630051e60150152b50050150070150c70b925b0ba0050c70052b5","0x150050af0150c90052b50052500050190152500052b50050150f6015015","0x2510052b500526600500f0152520052b50050050050330152540052b5005","0x150cb0055080270052b50070c50051f50150c50052b50050c90051d2015","0x270052b500502702b0074210150152b50050151290150152b5005015007","0xc0150152b500501500701524d00550924e24f0072b5007251005009015","0x2b500524c00501001524b0052b500524f00500f01524c0052b500524e005","0x52b50050150f60150152b500501500701501550a0050150b701524a005","0x501001524b0052b500524d00500f0152480052b5005249005021015249","0x501500701524300550b0e10052b500724a00502e01524a0052b5005248","0x501502b0152410052b50050e10050270150152b50050150060150152b5","0x1523e0052b500524100502f0150e00052b500524b00502c0152400052b5","0x50e00050310152520052b50052520050330152540052b50052540050af","0x1523e0052b500523e0051b50152400052b50052400050340150e00052b5","0x52b500723c0051b901523c15623d0ba2b500523e2400e02522540af112","0x150de2390072b50050df0051c80150152b500501500701523b00550c0df","0x51560050330150e50052b500523d0050af0151050052b50052390050ba","0x150e90052b50050de0051d201523a0052b500510500500f0152380052b5","0x27a0150152b50050ba00527a0150152b500501500701501550d0050150b7","0x150152b500501000527a0150152b500500c00527a0150152b50050af005","0x152b50050270052910150152b500500600541e0150152b50050210052c0","0x2b500503400527a0150152b500503300527a0150152b500502c00527a015","0x51d900527a0150152b50051c800527a0150152b500511200527a015015","0x3301523d0052b500523d0050af0151040052b500523b00541f0150152b5","0x1510415623d0ba0051040052b50051040054200151560052b5005156005","0xf60150152b50052430051e60150152b50050150060150152b5005015007","0x52b50052540050af0152330052b50052340050190152340052b5005015","0x51d201523a0052b500524b00500f0152380052b50052520050330150e5","0x501500701523200550e02e0052b50070e90051f50150e90052b5005233","0x500901502e0052b500502e0060074210150152b50050151290150152b5","0x50f000500c0150152b50050150070150f200550f0f02310072b500723a","0x1522d0052b500510300501001522f0052b500523100500f0151030052b5","0x2101522c0052b50050150f60150152b50050150070150155100050150b7","0x2b500522900501001522f0052b50050f200500f0152290052b500522c005","0x150152b50050150070152270055112280052b500722d00502e01522d005","0x2250052b50050154220152260052b50052280050270150152b5005015006","0xe50050af0152240052b500522600502f0151080052b500522f00502c015","0x1080052b50051080050310152380052b50052380050330150e50052b5005","0xe50af4240152240052b50052240051b50152250052b5005225005423015","0x551221e0052b50071090054260151092200fe0ba2b5005224225108238","0x21d0050ba01510c21d0072b500521e0054270150152b500501500701510b","0x1100052b500522000503301521b0052b50050fe0050af01510e0052b5005","0x50150b70152170052b500510c0054280152180052b500510e00500f015","0x50af00527a0150152b50050ba00527a0150152b5005015007015015513","0x210052c00150152b500501000527a0150152b500500c00527a0150152b5","0x527a0150152b50050270052910150152b500502e0052910150152b5005","0x27a0150152b500503400527a0150152b500503300527a0150152b500502c","0x150152b50051d900527a0150152b50051c800527a0150152b5005112005","0x52200050330150fe0052b50050fe0050af0152160052b500510b00541f","0x50150070152162200fe0ba0052160052b50052160054200152200052b5","0x2b50050150f60150152b50052270051e60150152b50050150060150152b5","0x3301521b0052b50050e50050af0152140052b5005215005429015215005","0x2b50052140054280152180052b500522f00500f0151100052b5005238005","0x150152b50050150070152130055140f60052b500721700542a015217005","0x2b50072180050090150f60052b50050f60210072b60150152b5005015129","0x2100052b500521100500c0150152b5005015007015119005515211212007","0x50150b701520d0052b500521000501001520e0052b500521200500f015","0x508f00502101508f0052b50050150f60150152b5005015007015015516","0x1520d0052b500520c00501001520e0052b500511900500f01520c0052b5","0x50150060150152b500501500701538000551720b0052b500720d00502e","0x502c01520a0052b500501542b01511e0052b500520b0050270150152b5","0x52b500521b0050af0151210052b500511e00502f0152080052b500520e","0x542c0152080052b50052080050310151100052b500511000503301521b","0x20a20811021b0af42d0151210052b50051210051b501520a0052b500520a","0x70151280055181260052b500720400542e0152042052070ba2b5005121","0x52b50052030050ba0152002030072b500512600542f0150152b5005015","0x500f01512d0052b50052050050330151ff0052b50052070050af01512b","0x150155190050150b70151f60052b500520000543001512f0052b500512b","0x150152b500501000527a0150152b500500c00527a0150152b5005015007","0x152b50050270052910150152b500502e0052910150152b50050f60051cb","0x2b500503400527a0150152b500503300527a0150152b500502c00527a015","0x51d900527a0150152b50051c800527a0150152b500511200527a015015","0x12800541f0150152b50050ba00527a0150152b50050af00527a0150152b5","0x2050052b50052050050330152070052b50052070050af0150450052b5005","0x150152b50050150070150452052070ba0050450052b5005045005420015","0x151fb0052b50050150f60150152b50053800051e60150152b5005015006","0x51100050330151ff0052b500521b0050af0151fc0052b50051fb005431","0x151f60052b50051fc00543001512f0052b500520e00500f01512d0052b5","0x701522a00551a1310052b50071f60054320151f80052b500512f00502c","0xf602e02702c0330341121c81d90af0ba00c0100063a40150152b5005015","0x2b50051fd1f80074350151fd0052b50051330054340151330052b5005131","0x330151ff0052b50051ff0050af0151fa0052b5005135005436015135005","0x151fa12d1ff0ba0051fa0052b50051fa00542001512d0052b500512d005","0x150152b50050f60051cb0150152b500501000527a0150152b5005015007","0x152b500502c00527a0150152b50050270052910150152b500502e005291","0x2b500511200527a0150152b500503400527a0150152b500503300527a015","0x50af00527a0150152b50051d900527a0150152b50051c800527a015015","0x22a0054370150152b500500c00527a0150152b50050ba00527a0150152b5","0x52b50051f20054360151f20052b50051f91f80074350151f90052b5005","0x542001512d0052b500512d0050330151ff0052b50051ff0050af0151f1","0xba00527a0150152b50050150070151f112d1ff0ba0051f10052b50051f1","0x527a0150152b500500c00527a0150152b50050af00527a0150152b5005","0x27a0150152b50050270052910150152b500502e0052910150152b5005010","0x150152b500503400527a0150152b500503300527a0150152b500502c005","0x152b50051d900527a0150152b50051c800527a0150152b500511200527a","0x52130054370151eb0052b500521800502c0150152b50050210052c0015","0x1e70052b500513b00543601513b0052b50051391eb0074350151390052b5","0x1e70054200151100052b500511000503301521b0052b500521b0050af015","0x50ba00527a0150152b50050150070151e711021b0ba0051e70052b5005","0x1000527a0150152b500500c00527a0150152b50050af00527a0150152b5","0x527a0150152b50050270052910150152b50050210052c00150152b5005","0x27a0150152b500503400527a0150152b500503300527a0150152b500502c","0x150152b50051d900527a0150152b50051c800527a0150152b5005112005","0x2b500523200543701513d0052b500523a00502c0150152b500500600541e","0x151410052b50051e50054360151e50052b500513f13d00743501513f005","0x51410054200152380052b50052380050330150e50052b50050e50050af","0x2b50050ba00527a0150152b50050150070151412380e50ba0051410052b5","0x501000527a0150152b500500c00527a0150152b50050af00527a015015","0x2c00527a0150152b500500600541e0150152b50050210052c00150152b5","0x527a0150152b500503400527a0150152b500503300527a0150152b5005","0x41e0150152b50051d900527a0150152b50051c800527a0150152b5005112","0x52b50050cb0054370151430052b500525100502c0150152b500502b005","0xaf0151df0052b50051e00054360151e00052b50051e21430074350151e2","0x2b50051df0054200152520052b50052520050330152540052b5005254005","0x152b50050ba00527a0150152b50050150070151df2522540ba0051df005","0x2b500501000527a0150152b500500c00527a0150152b50050af00527a015","0x502b00541e0150152b500500600541e0150152b50050210052c0015015","0x11200527a0150152b500503400527a0150152b500503300527a0150152b5","0x51ff0150152b50051d900527a0150152b50051c800527a0150152b5005","0x1dd0052b500526a0054370151de0052b50050ac00502c0150152b500502f","0x50af0151db0052b50051dc0054360151dc0052b50051dd1de007435015","0x52b50051db0054200150050052b50050050050330150150052b5005015","0x150152b50050ba00527a0150152b50050150070151db0050150ba0051db","0x152b500501000527a0150152b500500c00527a0150152b50050af00527a","0x2b500502b00541e0150152b500500600541e0150152b50050210052c0015","0x511200527a0150152b500503400527a0150152b500502f0051ff015015","0x310051ff0150152b50051d900527a0150152b50051c800527a0150152b5","0x151da0052b50050a400543701514b0052b500500d00502c0150152b5005","0x150050af0151d70052b50051d80054360151d80052b50051da14b007435","0x1d70052b50051d70054200150050052b50050050050330150150052b5005","0x27a0150152b50050ba00527a0150152b50050150070151d70050150ba005","0x150152b500501000527a0150152b500500c00527a0150152b50050af005","0x152b500502b00541e0150152b500500600541e0150152b50050210052c0","0x2b500511200527a0150152b50050310051ff0150152b500502f0051ff015","0x51b50051ff0150152b50051d900527a0150152b50051c800527a015015","0x4350151510052b500509a0054370151d40052b500528100502c0150152b5","0x50150050af0151540052b50051520054360151520052b50051511d4007","0x51540052b50051540054200150050052b50050050050330150150052b5","0x527a0150152b50050ba00527a0150152b50050150070151540050150ba","0x2c00150152b500501000527a0150152b500500c00527a0150152b50050af","0x150152b500502b00541e0150152b500500600541e0150152b5005021005","0x152b50051b50051ff0150152b50050310051ff0150152b500502f0051ff","0x2b50051b90051ff0150152b50051d900527a0150152b50051c800527a015","0x74350151610052b50050910054370151d30052b500508d00502c015015","0x2b50050150050af0151580052b50051600054360151600052b50051611d3","0xba0051580052b50051580054200150050052b5005005005033015015005","0xaf00527a0150152b50050ba00527a0150152b5005015007015158005015","0x52c00150152b500501000527a0150152b500500c00527a0150152b5005","0x1ff0150152b500502b00541e0150152b500500600541e0150152b5005021","0x150152b50051b50051ff0150152b50050310051ff0150152b500502f005","0x152b50051d20051ff0150152b50051d900527a0150152b50051b90051ff","0x15a00743501515d0052b50050e300543701515a0052b500529400502c015","0x52b50050150050af0151570052b50051550054360151550052b500515d","0x150ba0051570052b50051570054200150050052b5005005005033015015","0x50af00527a0150152b50050ba00527a0150152b5005015007015157005","0x210052c00150152b500501000527a0150152b500500c00527a0150152b5","0x51ff0150152b500502b00541e0150152b500500600541e0150152b5005","0x1ff0150152b50051b50051ff0150152b50050310051ff0150152b500502f","0x150152b50051e10051ff0150152b50051d20051ff0150152b50051b9005","0x1d11620074350151d10052b50050760054370151620052b500506f00502c","0x150052b50050150050af0151630052b50051ce0054360151ce0052b5005","0x50150ba0051630052b50051630054200150050052b5005005005033015","0x2b500500c00527a0150152b50050ba00527a0150152b5005015007015163","0x500600541e0150152b50050210052c00150152b500501000527a015015","0x310051ff0150152b500502f0051ff0150152b500502b00541e0150152b5","0x51ff0150152b50051b90051ff0150152b50051b50051ff0150152b5005","0x2c0150152b50050090051ff0150152b50051e10051ff0150152b50051d2","0x51c71cb0074350151c70052b50052a60054370151cb0052b5005062005","0x150150052b50050150050af0151c40052b50051c60054360151c60052b5","0x1c40050150ba0051c40052b50051c40054200150050052b5005005005033","0x152b500500c00527a0150152b50050090051ff0150152b5005015007015","0x2b500500600541e0150152b50050210052c00150152b500501000527a015","0x50310051ff0150152b500502f0051ff0150152b500502b00541e015015","0x1d20051ff0150152b50051b90051ff0150152b50051b50051ff0150152b5","0x502c0150152b50051290051ff0150152b50051e10051ff0150152b5005","0x2b50051cd1c10074350151cd0052b50050590054370151c10052b5005295","0x330150150052b50050150050af0151bc0052b50051bf0054360151bf005","0x151bc0050150ba0051bc0052b50051bc0054200150050052b5005005005","0x150152b50050090051ff0150152b50051290051ff0150152b5005015007","0x152b500500600541e0150152b50050210052c00150152b500501000527a","0x2b50050310051ff0150152b500502f0051ff0150152b500502b00541e015","0x51d20051ff0150152b50051b90051ff0150152b50051b50051ff015015","0x25900502c0150152b500500f0051ff0150152b50051e10051ff0150152b5","0x52b500516b1bb00743501516b0052b500526d0054370151bb0052b5005","0x50330150150052b50050150050af01516d0052b50051b80054360151b8","0x701516d0050150ba00516d0052b500516d0054200150050052b5005005","0x1ff0150152b50050090051ff0150152b50051290051ff0150152b5005015","0x150152b500500600541e0150152b50050210052c00150152b500500f005","0x152b50050310051ff0150152b500502f0051ff0150152b500502b00541e","0x2b50051d20051ff0150152b50051b90051ff0150152b50051b50051ff015","0x520200502c0150152b50050b70051ff0150152b50051e10051ff015015","0x1720052b50051b616f0074350151b60052b500521c00543701516f0052b5","0x50050330150150052b50050150050af0151b40052b5005172005436015","0x151080151b40050150ba0051b40052b50051b40054200150050052b5005","0xc0072b50050090053a80150090052b50050af0053a70150af0052b5005","0x1000527f0150100052b500500f0053ab0150152b500500c0053a901500f","0x150210052b50050150990150f60052b50050b700527c0150b70052b5005","0x700512909c0150f60052b50050f600526e0150210052b500502100509a","0x150152b500501500701502f02c02b0ba51b02700602e0ba2b50070f6021","0x52b50050152730150310052b500503300527c0150330052b5005015108","0x990151b50052b50050340270073e50150270052b50050270051b5015034","0x52b500511200509a01502e0052b500502e0050330151120052b5005015","0xaf2560151b50052b50051b50051b50150310052b500503100526e015112","0x2b50050150070151e11d91d20ba51c1c81b90072b50071b503111200602e","0x1e40053a80151e40052b50051e30053a70151e30052b5005015108015015","0x190052b50051e60053ab0150152b500511a0053a90151e611a0072b5005","0x50150990151fe0052b50051f500527c0151f50052b500501900527f015","0x152020052b500520200509a0151b90052b50051b90050330152020052b5","0x20f2092060ba2b50071fe2021c81b912909c0151fe0052b50051fe00526e","0x5004f0062b50051290051d70150152b500501500701523721f21c0ba51d","0x150152b500505200527a01529128027a27126f26d054264262259253052","0x152b500505400527a0150152b500526400527a0150152b500525900527a","0x2b50052710052910150152b500526f00527a0150152b500526d00527a015","0x52910051630150152b50052800051cb0150152b500527a005291015015","0x152930052b500525320f00711001520f0052b500520f0051b50150152b5","0x52090051e40152060052b50052060050330152930052b50052930051b5","0x150152b500501500701529500551e0152b500729300527b0152090052b5","0x52b50050150990152990052b500529700527c0152970052b5005015224","0xaf2560152990052b500529900526e0150580052b500505800509a015058","0x2b500501500701506005e29e0ba51f05c0590072b5007262299058209206","0x2a90053a80152a90052b50052aa0053a70152aa0052b5005015225015015","0x2a80052b50050640053ab0150152b50050620053a90150640620072b5005","0x50150990152a60052b50052a700527c0152a70052b50052a800527f015","0x152a40052b50052a400509a0150590052b50050590050330152a40052b5","0x282a306a0ba2b50072a62a405c05912909c0152a60052b50052a600526e","0x150280052b50050280051b50150152b500501500701506f0712a00ba520","0x6a0050330150700052b50050700051b50150700052b500504f028007110","0x5210152b500707000527b0152a30052b50052a30051e401506a0052b5005","0x529f00527c01529f0052b50050152250150152b5005015007015073005","0x26e01529d0052b500529d00509a01529d0052b50050150990150760052b5","0x52229b29c0072b500705007629d2a306a0af2560150760052b5005076005","0x74380152940052b50050150f60150152b500501500701529629829a0ba","0x2b50050150050af0152900052b50052920054390152920052b50052940ba","0x43a01529b0052b500529b0051e401529c0052b500529c005033015015005","0x1fc0150152b500501500701529029b29c0151290052900052b5005290005","0x52b50052960820072990150820052b50050152970150152b50050ba005","0x50330150150052b50050150050af0150e20052b50050e300543b0150e3","0x52b50050e200543a0152980052b50052980051e401529a0052b500529a","0x152b50050730052760150152b50050150070150e229829a0151290050e2","0x52b500501502b0150152b500505000527a0150152b50050ba0051fc015","0x28f00729501528e0052b500528e0051b501528e0052b500501543c01528f","0x52b500528d28c00729901528c0052b500501529701528d0052b500528e","0x50330150150052b50050150050af01508d0052b500508e00543b01508e","0x52b500508d00543a0152a30052b50052a30051e401506a0052b500506a","0x152b50050ba0051fc0150152b500501500701508d2a306a01512900508d","0x52b50050152970150152b500504f00527a0150152b500505000527a015","0xaf0151020052b500510600543b0151060052b500506f107007299015107","0x2b50050710051e40152a00052b50052a00050330150150052b5005015005","0x50150070151020712a00151290051020052b500510200543a015071005","0x4f00527a0150152b500505000527a0150152b50050ba0051fc0150152b5","0x1528a0052b50050600910072990150910052b50050152970150152b5005","0x529e0050330150150052b50050150050af0152890052b500528a00543b","0x52890052b500528900543a01505e0052b500505e0051e401529e0052b5","0x1fc0150152b50052950052760150152b500501500701528905e29e015129","0x150152b500504f00527a0150152b500505000527a0150152b50050ba005","0x2830052b500501543d0152860052b500501502b0150152b500526200527a","0x1529701501c0052b50052832860072950152830052b50052830051b5015","0x52b500528100543b0152810052b500501c0960072990150960052b5005","0x51e40152060052b50052060050330150150052b50050150050af01527f","0x1527f20920601512900527f0052b500527f00543a0152090052b5005209","0x150152b500512900522c0150152b50050ba0051fc0150152b5005015007","0x509900543b0150990052b500523727c00729901527c0052b5005015297","0x1521c0052b500521c0050330150150052b50050150050af01509a0052b5","0x21f21c01512900509a0052b500509a00543a01521f0052b500521f0051e4","0x2b500512900522c0150152b50050ba0051fc0150152b500501500701509a","0x543b01527b0052b50051e109c00729901509c0052b5005015297015015","0x52b50051d20050330150150052b50050150050af0152780052b500527b","0x151290052780052b500527800543a0151d90052b50051d90051e40151d2","0x12900522c0150152b50050ba0051fc0150152b50050150070152781d91d2","0x152760052b500502f2770072990152770052b50050152970150152b5005","0x502b0050330150150052b50050150050af01509f0052b500527600543b","0x509f0052b500509f00543a01502c0052b500502c0051e401502b0052b5","0x1000f0092b50050ba0051260150152b500501500601509f02c02b015129","0x50052b50050050050330150150052b50050150050af01502e0210f60b7","0x150af43e0151290052b500512900528f0150070052b50050070051e4015","0x52302f0052b500702c00543f01502c02b0270061292b500512900f007005","0x2b50050154400150310052b50050154120150152b5005015007015033005","0x990151120052b500503100527c0151b50052b5005034005407015034005","0x52b500511200526e0151b90052b50051b900509a0151b90052b5005015","0x72b50071b51121b902b0270af2560151b50052b50051b50051b5015112","0x1e40052b50050152250150152b50050150070151e31e11d90ba5241d21c8","0x2b50050151290151e60052b500501509901511a0052b50051e400527c015","0x526e0151e60052b50051e600509a0151c80052b50051c8005033015015","0xba5251f50190072b50070af11a1e61d21c80af25601511a0052b500511a","0x20900527c0152090052b50050151080150152b50050150070152062021fe","0x150190052b500501900503301521c0052b500501509901520f0052b5005","0x1f50190af25601520f0052b500520f00526e01521c0052b500521c00509a","0x150152b500501500701505205004f0ba52623721f0072b500700920f21c","0x52b50050150990152590052b500525300527c0152530052b5005015224","0x526e0152620052b500526200509a01521f0052b500521f005033015262","0xba5270542640072b500700c25926223721f0af2560152590052b5005259","0x502f0054410150152b50050150060150152b500501500701527126f26d","0x2e0210f60b701027a0090640150152b50052800051e601528027a0072b5","0x2950052b50052932910073b00152930052b50050150f60152910052b5005","0x2640050330150060052b50050060050af0152970052b50052950053b1015","0x2970052b50052970051890150540052b50050540051e40152640052b5005","0x150152b50050100051f60150152b5005015007015297054264006129005","0x152b50050210054440150152b500502e0051fc0150152b500502f005443","0x2b500526d0050330150152b50050b70050b90150152b50050f600524c015","0xb70150590052b50052710050340150580052b500526f0051e4015299005","0x54430150152b50050100051f60150152b5005015007015015528005015","0x24c0150152b50050210054440150152b500502e0051fc0150152b500502f","0x150152b500500c00527a0150152b50050b70050b90150152b50050f6005","0x50520050340150580052b50050500051e40152990052b500504f005033","0x50100051f60150152b50050150070150155280050150b70150590052b5","0x210054440150152b500502e0051fc0150152b500502f0054430150152b5","0x527a0150152b50050b70050b90150152b50050f600524c0150152b5005","0x152990052b50051fe0050330150152b500500900527a0150152b500500c","0x2b50050150060150590052b50052060050340150580052b50052020051e4","0x518701529e0052b500505905c00729901505c0052b5005015297015015","0x52b50052990050330150060052b50050060050af01505e0052b500529e","0x612900505e0052b500505e0051890150580052b50050580051e4015299","0x2f0054430150152b50050100051f60150152b500501500701505e058299","0x524c0150152b50050210054440150152b500502e0051fc0150152b5005","0x27a0150152b500500c00527a0150152b50050b70050b90150152b50050f6","0x150600052b50050152970150152b50050af00527a0150152b5005009005","0x60050af0152a90052b50052aa0051870152aa0052b50051e3060007299","0x1e10052b50051e10051e40151d90052b50051d90050330150060052b5005","0x152b50050150070152a91e11d90061290052a90052b50052a9005189015","0x2b50050210054440150152b500502e0051fc0150152b50050100051f6015","0x500c00527a0150152b50050b70050b90150152b50050f600524c015015","0x330051870150152b50050af00527a0150152b500500900527a0150152b5","0x270052b50050270050330150060052b50050060050af0150620052b5005","0x270061290050620052b500506200518901502b0052b500502b0051e4015","0x53d30150af0052b50051290053d20151290052b50050153a501506202b","0x52b500500c0053d40150152b50050090052bb01500c0090072b50050af","0x150990150b70052b500501000527c0150100052b500500f00527f01500f","0xb70052b50050b700526e0150f60052b50050f600509a0150f60052b5005","0x701502c02b0270ba52900602e0210ba2b50070b70f600700512909c015","0x210052b50050210050330150060052b50050060051b50150152b5005015","0x701502f00552a0152b500700600527b01502e0052b500502e0051e4015","0x150310052b50050330052780150330052b50050150f60150152b5005015","0x150152b500501500701501552b0050150b70150340052b5005031005277","0x52b50051b500509f0151b50052b50050150f60150152b500502f005276","0x52770151b90052b500503400500d0150340052b5005112005277015112","0x50150070151d200552c1c80052b50071b90050a00151b90052b50051b9","0x50150f60151d90052b50050153a50150152b50051c80051e60150152b5","0x151e40052b50051e30053a60151e30052b50051e10052780151e10052b5","0x2b50051e600509a0151e60052b500501509901511a0052b50051d900527c","0x2560151e40052b50051e40051b501511a0052b500511a00526e0151e6005","0x50150070152062021fe0ba52d1f50190072b50071e411a1e602e0210af","0x44601520f0052b50052090ba0074450152090052b50050150f60150152b5","0x2b50050190050330150150052b50050150050af01521c0052b500520f005","0x12900521c0052b500521c0054470151f50052b50051f50051e4015019005","0x152970150152b50050ba0054440150152b500501500701521c1f5019015","0x52b50052370052c20152370052b500520621f00729901521f0052b5005","0x51e40151fe0052b50051fe0050330150150052b50050150050af01504f","0x1504f2021fe01512900504f0052b500504f0054470152020052b5005202","0x150152b50050ba0054440150152b50051d20051e60150152b5005015007","0x52b50050520051b50150520052b50050154480150500052b500501502b","0x72990152590052b50050152970152530052b5005052050007295015052","0x2b50050150050af0152640052b50052620052c20152620052b5005253259","0x44701502e0052b500502e0051e40150210052b5005021005033015015005","0x4440150152b500501500701526402e0210151290052640052b5005264005","0x52b500502c0540072990150540052b50050152970150152b50050ba005","0x50330150150052b50050150050af01526f0052b500526d0052c201526d","0x52b500526f00544701502b0052b500502b0051e40150270052b5005027","0x2b50070050150070050150152b500501500601526f02b02701512900526f","0xba2b500512900521e0150152b500501500701501000f00752e00c009007","0x50af0150152b500501512901502e0052b50050af0050ba0150210f60b7","0x1500701502b00552f0270060072b500702e0050090150090052b5005009","0x1502f0052b500500600500f01502c0052b500502700500c0150152b5005","0x150152b50050150070150155300050150b70150330052b500502c005010","0x2b500502b00500f0150340052b50050310050210150310052b50050150f6","0x55311b50052b500703300502e0150330052b500503400501001502f005","0x51b900502f0151b90052b50051b50050270150152b5005015007015112","0x1d91d20072b500702f0050090151c80052b50051c80051b50151c80052b5","0x500f0151e30052b50051d900500c0150152b50050150070151e1005532","0x150155330050150b701511a0052b50051e30050100151e40052b50051d2","0x190052b50051e60050210151e60052b50050150f60150152b5005015007","0x1e400502c01511a0052b50050190050100151e40052b50051e100500f015","0x2b50050150070152020055341fe0052b500711a00502e0151f50052b5005","0x520600502f0152060052b50051fe0050270150152b5005015006015015","0x2090052b50052090051b501520f0052b50051c80b70073e50152090052b5","0x51b501520f0052b500520f0051b501521c0052b50052090f60073e5015","0x1505004f23721f1292b500502121c20f00712944901521c0052b500521c","0x2370051b501521f0052b500521f0051e30150520ba0072b50050ba00544b","0x500052b50050500051b501504f0052b500504f0051b50152370052b5005","0x2b50050150070152642620075352592530072b500705200c0090ba05c015","0x152530052b50052530050af0150540052b500505004f2370ba0fe015015","0x50ba00505901521f0052b500521f0051e30152590052b5005259005033","0x151f50052b50051f50050310150540052b50050540051330150ba0052b5","0x27126f26d12900527a27126f26d1292b50051f50540ba21f2592530091fd","0x2b500523700527a0150152b50051f50052800150152b500501500701527a","0x505000527a0150152b500504f00527a0150152b50050ba00544c015015","0x2910051b50152910052b50050152a00152800052b500501502b0150152b5","0x2950052b50050152970152930052b50052912800072950152910052b5005","0x50af0152990052b50052970052c30152970052b5005293295007299015","0x52b500521f0051e30152640052b50052640050330152620052b5005262","0x2b500501500701529921f2642621290052990052b500529900544d01521f","0x2b50050ba00544c0150152b50052020051e60150152b5005015006015015","0x73e50150590052b50050152730150580052b50051c80b70073e5015015","0x2b500505c0051b50150580052b50050580051b501505c0052b50050590f6","0x6000527a0152aa06005e29e1292b500502105c05800712944901505c005","0x152a90052b500505e1f500744e0150152b50052aa00527a0150152b5005","0x500c0050330150090052b50050090050af0150620052b50052a900544f","0x50620052b500506200544d01529e0052b500529e0051e301500c0052b5","0x51e60150152b50050150060150152b500501500701506229e00c009129","0x3e50150640052b50050152730150152b50050ba00544c0150152b5005112","0x2a80071294490152a80052b50052a80051b50152a80052b50050640b7007","0x6a00527a0150152b50052a400527a01506a2a42a62a71292b50050210f6","0x280052b50052a62a300744e0152a30052b500502f00502c0150152b5005","0xc0050330150090052b50050090050af0152a00052b500502800544f015","0x2a00052b50052a000544d0152a70052b50052a70051e301500c0052b5005","0x150152b50050af0052800150152b50050150070152a02a700c009129005","0x710052b500501502b0150152b50051290051090150152b50050ba00544c","0x6f07100729501506f0052b500506f0051b501506f0052b50050152a0015","0x29f0052b50050700730072990150730052b50050152970150700052b5005","0x1000503301500f0052b500500f0050af0150760052b500529f0052c3015","0x760052b500507600544d0150070052b50050070051e30150100052b5005","0x90052b500501541d0151290052b500501520501507600701000f129005","0x52b50050152050150b70052b500501541d01500f0052b500501541c015","0x152b50050150060150152b50050152040150060052b500501541d015021","0x52b500501509901502b0052b50050154500150270052b500501502b015","0x330150150052b50050150050af01502f0052b500502b02c00745101502c","0x2b50050070051e701502f0052b500502f0054530150050052b5005005005","0x2b500502700702f0050150af4540150270052b5005027005034015007005","0x2b50050150070151120055361b50052b50070340054550150340310330ba","0x151e11d91d21c81292b50051b50054560151b90052b500501502b015015","0x152b50051e10051e60150152b50051d90052910150152b50051c8005457","0x51e31e40074510151e40052b50050150990151e30052b5005015458015","0x150310052b50050310050330150330052b50050330050af01511a0052b5","0x51b90050340151d20052b50051d20051e701511a0052b500511a005453","0x54550151f50191e60ba2b50051b91d211a0310330af4540151b90052b5","0x2b50051fe0054560150152b50050150070152020055371fe0052b50071f5","0x150152b500520f0051e60150152b500520600545701520f010209206129","0x1545901521f21c0072b500501000506a0150100052b50050100b7007421","0x2b500723721f1e60ba1f10152370052b500523700509a0152370052b5005","0x500052b50050500051eb0150152b500501500701505200553805004f007","0x1545a0152592530072b500521c00506a0150ba0052b5005050005027015","0x52b50050ba1290071de0152620052b500526200509a0152620052b5005","0x2b500501500701526d0055390542640072b500726225904f0ba1f10150ba","0x523e0150f60052b50050540050270150540052b50050540051eb015015","0x2b50052640050af0152710052b500526f00502f01526f0ba0072b50050ba","0x553a0152b500727100527b0150f60052b50050f60210071de015264005","0x50190050330152800052b50052640050af0150152b500501500701527a","0x1500701501553b0050150b70152930052b50052090051e70152910052b5","0x1545b0152950052b500501502b0150152b500527a0052760150152b5005","0x580052b50052972990074510152990052b50050150990152970052b5005","0x580054530150190052b50050190050330152640052b50052640050af015","0x2950052b50052950050340152090052b50052090051e70150580052b5005","0x2b500729e00545501529e05c0590ba2b50052952090580192640af454015","0x2a92aa1292b500505e0054560150152b500501500701506000553c05e005","0x6200506a0150152b50050640051e60150152b50052aa005457015064062","0x152a60052b500501545c0150152b50052a80052910152a72a80072b5005","0x553d06a2a40072b50072a62a70590ba1f10152a60052b50052a600509a","0x506a00502701506a0052b500506a0051eb0150152b50050150070152a3","0x152a00052b50052a00051b50152a00052b500502800502f0150280052b5","0x1f90150152b500501500701507000553e06f0710072b50072a02a4007216","0x2b500506f07300745d0150730052b500507300509a0150730052b5005015","0x29d0760072b500729f07100745e01529f0052b500529f00541601529f005","0x501509901529b0052b500501502b0150152b500501500701529c00553f","0xac01529829a0072b500529a0050ac0150152b500501512901529a0052b5","0x2980760ba2130152980052b500529800509a01529629d0072b500529d005","0x2920052660150152b50050150070150822900075402922940072b5007296","0xac0150e30052b50052940050af0150152b500529a0052660150152b5005","0x5410050150b70150e20052b50050e200509a0150e229d0072b500529d005","0x2b50052900050af0150152b50050820052660150152b5005015007015015","0xe20074510150152b50050150060150e20052b500529a00509a0150e3005","0x52b500505c0050330150e30052b50050e30050af01528f0052b500529d","0x50340152a90052b50052a90051e701528f0052b500528f00545301505c","0x1528c28d28e0ba2b500529b2a928f05c0e30af45401529b0052b500529b","0x8e0054560150152b500501500701508d00554208e0052b500728c005455","0x2b50051020052910150152b50051070054570150911021061071292b5005","0x28d0050330152800052b500528e0050af0150152b50050910051e6015015","0x2b50052932912800ba45f0152930052b50051060051e70152910052b5005","0x2b500501500701501c0055432830052b500728600546001528628928a0ba","0x1502e0af0072b50052810054620152810960072b5005283005461015015","0x900742101527c0052b500527f00502f01527f0ba0072b50050ba00523e","0x2b500727c00527b01502e0052b500502e0060074210150af0052b50050af","0x1509a0f60072b50050f600523e0150152b5005015007015099005544015","0x528900503301528a0052b500528a0050af01509c0052b500509a00502f","0x1509c0052b500509c0051b50150960052b50050960051e70152890052b5","0x2760052b500727700546401527727827b0ba2b500509c09628928a129463","0x23e01500c00d0072b50052760052c60150152b500501500701509f005545","0x527b0050af0150a20052b50050a000502f0150a00f60072b50050f6005","0x1500d0052b500500d0051e70152780052b500527800503301527b0052b5","0x27b12946501500c0052b500500c00f0072b60150a20052b50050a20051b5","0xa70055460a50052b50072720054670152720a42730ba2b50050a200d278","0x52730050af0150aa0a80072b50050a50054680150152b5005015007015","0x1526e0052b50050a80051e70150ac0052b50050a40050330152700052b5","0x5470050150b701526b0052b50050aa00542c0152af0052b500500c005423","0x2b50052530052910150152b500500c0051cb0150152b5005015007015015","0x50af0052910150152b500502e0052910150152b50050f600527a015015","0x50af01526a0052b50050a70054690150152b50050ba00527a0150152b5","0x52b500526a00546a0150a40052b50050a40050330152730052b5005273","0x150152b50050ba00527a0150152b500501500701526a0a42730ba00526a","0x152b50050f600527a0150152b50052530052910150152b50050af005291","0x2b500509f0054690150152b500500f0052c00150152b500502e005291015","0x46a0152780052b500527800503301527b0052b500527b0050af015269005","0x52760150152b500501500701526927827b0ba0052690052b5005269005","0x42b0152680052b50050154220150152b500500f0052c00150152b5005099","0x52b50052890050330152700052b500528a0050af0152670052b5005015","0x542c0152af0052b500526800542301526e0052b50050960051e70150ac","0x52b50050150990152660b10072b500525300506a01526b0052b5005267","0xb42650072b50070b32662700ba1f10150b30052b50050b300509a0150b3","0x50270150b40052b50050b40051eb0150152b50050150070150b6005548","0x52b500501545c01525e25f0072b50050b100506a0152630052b50050b4","0xba1f10152630052b50052630051b501525d0052b500525d00509a01525d","0x51eb0150152b50050150070150b900554925b25c0072b500725d25e265","0x72b500525f00506a01525a0052b500525b00502701525b0052b500525b","0x51b50152550052b500525500509a0152550052b500501545b015256258","0x1525400554a0c10bf0072b500725525625c0ba1f101525a0052b500525a","0x52b50050c10050270150c10052b50050c10051eb0150152b5005015007","0x509a0150c70052b50050154500150c52510072b500525800506a015252","0x70c70c50bf0ba1f10152520052b50052520051b50150c70052b50050c7","0x52b50050c90051eb0150152b50050150070150cb00554b0c92500072b5","0x1f901524d24e0072b500525100506a01524f0052b50050c90050270150c9","0x52b500524f0051b501524c0052b500524c00509a01524c0052b5005015","0x2b500501500701524900554c24a24b0072b500724c24d2500ba1f101524f","0x506a0152480052b500524a00502701524a0052b500524a0051eb015015","0x52b500524100509a0152410052b500501546b0152430e10072b500524e","0xe02400072b500724124324b0ba1f10152480052b50052480051b5015241","0x50270150e00052b50050e00051eb0150152b500501500701523e00554d","0x52b500501546c01523c1560072b50050e100506a01523d0052b50050e0","0xba1f101523d0052b500523d0051b50150df0052b50050df00509a0150df","0x51eb0150152b50050150070150de00554e23923b0072b50070df23c240","0x72b500515600506a0151050052b50052390050270152390052b5005239","0x23a00509a01523a0052b500501546d0150152b50050e50052910152380e5","0x2b500723a23823b0ba1f10151050052b50051050051b501523a0052b5005","0x1040052b50051040051eb0150152b500501500701523400554f1040e9007","0x25a00502f0152320052b500526300502f0152330052b5005104005027015","0xf20052b500524f00502f0150f00052b500525200502f0152310052b5005","0x10500502f01522f0052b500523d00502f0151030052b500524800502f015","0x2290052b50050ba00502f01522c0052b500523300502f01522d0052b5005","0x22922c22d22f1030f20f02312320063a40152280052b50050f600502f015","0x46f0152260052b500522726e00746e0152270052b500526b2af02e0af228","0x2b50050ac0050330150e90052b50050e90050af0152250052b5005226005","0x2b50050150070152250ac0e90ba0052250052b500522500546a0150ac005","0x526b0051630150152b500526e0051d80150152b500525200527a015015","0xaf0052910150152b500502e0052910150152b50052af0051cb0150152b5","0x527a0150152b50050f600527a0150152b500525a00527a0150152b5005","0x27a0150152b500510500527a0150152b500526300527a0150152b50050ba","0x150152b500524f00527a0150152b500524800527a0150152b500523d005","0x52b50052240051b50152240052b500501520e0151080052b500501502b","0x72990152200052b50050152970150fe0052b5005224108007295015224","0x2b50052340050af01521e0052b50051090054690151090052b50050fe220","0xba00521e0052b500521e00546a0150ac0052b50050ac005033015234005","0x26e0051d80150152b500525200527a0150152b500501500701521e0ac234","0x52910150152b50052af0051cb0150152b500526b0051630150152b5005","0x27a0150152b500525a00527a0150152b50050af0052910150152b500502e","0x150152b500526300527a0150152b50050ba00527a0150152b50050f6005","0x152b500524800527a0150152b500523d00527a0150152b5005156005291","0x52b500501520e01510b0052b500501502b0150152b500524f00527a015","0x29701510c0052b500521d10b00729501521d0052b500521d0051b501521d","0x2b500521b00546901521b0052b500510c10e00729901510e0052b5005015","0x46a0150ac0052b50050ac0050330150de0052b50050de0050af015110005","0x527a0150152b50050150070151100ac0de0ba0051100052b5005110005","0x1cb0150152b500526b0051630150152b500526e0051d80150152b5005252","0x150152b50050af0052910150152b500502e0052910150152b50052af005","0x152b50050ba00527a0150152b50050f600527a0150152b500525a00527a","0x2b50050e10052910150152b500524f00527a0150152b500526300527a015","0x2b500501520e0152180052b500501502b0150152b500524800527a015015","0x152160052b50052172180072950152170052b50052170051b5015217005","0x52140054690152140052b50052162150072990152150052b5005015297","0x150ac0052b50050ac00503301523e0052b500523e0050af0152130052b5","0x27a0150152b50050150070152130ac23e0ba0052130052b500521300546a","0x150152b500526b0051630150152b500526e0051d80150152b5005252005","0x152b50050af0052910150152b500502e0052910150152b50052af0051cb","0x2b50050ba00527a0150152b50050f600527a0150152b500525a00527a015","0x524e0052910150152b500524f00527a0150152b500526300527a015015","0x2110051b50152110052b500501520e0152120052b500501502b0150152b5","0x2100052b50050152970151190052b50052112120072950152110052b5005","0x50af01520d0052b500520e00546901520e0052b5005119210007299015","0x52b500520d00546a0150ac0052b50050ac0050330152490052b5005249","0x150152b500525200527a0150152b500501500701520d0ac2490ba00520d","0x152b50052af0051cb0150152b500526b0051630150152b500526e0051d8","0x2b500525a00527a0150152b50050af0052910150152b500502e005291015","0x526300527a0150152b50050ba00527a0150152b50050f600527a015015","0x501520e01508f0052b500501502b0150152b50052510052910150152b5","0x20b0052b500520c08f00729501520c0052b500520c0051b501520c0052b5","0x11e00546901511e0052b500520b3800072990153800052b5005015297015","0xac0052b50050ac0050330150cb0052b50050cb0050af01520a0052b5005","0x150152b500501500701520a0ac0cb0ba00520a0052b500520a00546a015","0x152b500526b0051630150152b500526e0051d80150152b5005258005291","0x2b50050af0052910150152b500502e0052910150152b50052af0051cb015","0x50ba00527a0150152b50050f600527a0150152b500525a00527a015015","0x501520e0152080052b500501502b0150152b500526300527a0150152b5","0x2070052b50051212080072950151210052b50051210051b50151210052b5","0x2040054690152040052b50052072050072990152050052b5005015297015","0xac0052b50050ac0050330152540052b50052540050af0151260052b5005","0x150152b50050150070151260ac2540ba0051260052b500512600546a015","0x152b500526b0051630150152b500526e0051d80150152b500526300527a","0x2b50050af0052910150152b500502e0052910150152b50052af0051cb015","0x50ba00527a0150152b50050f600527a0150152b500525f005291015015","0x2030051b50152030052b500501520e0151280052b500501502b0150152b5","0x12b0052b50050152970152000052b50052031280072950152030052b5005","0x50af01512d0052b50051ff0054690151ff0052b500520012b007299015","0x52b500512d00546a0150ac0052b50050ac0050330150b90052b50050b9","0x150152b50050b10052910150152b500501500701512d0ac0b90ba00512d","0x152b50052af0051cb0150152b500526b0051630150152b500526e0051d8","0x2b50050ba00527a0150152b50050af0052910150152b500502e005291015","0x2b500501520e01512f0052b500501502b0150152b50050f600527a015015","0x150450052b50051f612f0072950151f60052b50051f60051b50151f6005","0x51fc0054690151fc0052b50050451fb0072990151fb0052b5005015297","0x150ac0052b50050ac0050330150b60052b50050b60050af0151f80052b5","0x27a0150152b50050150070151f80ac0b60ba0051f80052b50051f800546a","0x150152b50052530052910150152b500500f0052c00150152b50050ba005","0x152b500500900541e0150152b500500600541e0150152b50050f600527a","0x28900503301528a0052b500528a0050af0151310052b500501c005469015","0x1500701513128928a0ba0051310052b500513100546a0152890052b5005","0x52c00150152b500500900541e0150152b50050ba00527a0150152b5005","0x41e0150152b50050f600527a0150152b50052530052910150152b500500f","0x52b500528e0050af01522a0052b500508d0054690150152b5005006005","0x28e0ba00522a0052b500522a00546a01528d0052b500528d00503301528e","0x500900541e0150152b50050ba00527a0150152b500501500701522a28d","0xf600527a0150152b50052530052910150152b500500f0052c00150152b5","0x1502b0150152b50052a90051d80150152b500500600541e0150152b5005","0x151fd0052b50051fd0051b50151fd0052b50050154700151330052b5005","0x1351fa0072990151fa0052b50050152970151350052b50051fd133007295","0x29c0052b500529c0050af0151f20052b50051f90054690151f90052b5005","0x5c29c0ba0051f20052b50051f200546a01505c0052b500505c005033015","0x2b500500900541e0150152b50050ba00527a0150152b50050150070151f2","0x50f600527a0150152b50052530052910150152b500500f0052c0015015","0x501502b0150152b50052a90051d80150152b500500600541e0150152b5","0x2950151eb0052b50051eb0051b50151eb0052b50050154710151f10052b5","0x513913b00729901513b0052b50050152970151390052b50051eb1f1007","0x150700052b50050700050af01513d0052b50051e70054690151e70052b5","0x13d05c0700ba00513d0052b500513d00546a01505c0052b500505c005033","0x152b500500900541e0150152b50050ba00527a0150152b5005015007015","0x2b50050f600527a0150152b50052530052910150152b500500f0052c0015","0x2b500501502b0150152b50052a90051d80150152b500500600541e015015","0x72950151e50052b50051e50051b50151e50052b500501520e01513f005","0x2b50051411430072990151430052b50050152970151410052b50051e513f","0x330152a30052b50052a30050af0151e00052b50051e20054690151e2005","0x151e005c2a30ba0051e00052b50051e000546a01505c0052b500505c005","0x150152b500500900541e0150152b50050ba00527a0150152b5005015007","0x152b50050f600527a0150152b50052530052910150152b500500f0052c0","0x50590050af0151df0052b50050600054690150152b500500600541e015","0x51df0052b50051df00546a01505c0052b500505c0050330150590052b5","0x541e0150152b50050ba00527a0150152b50050150070151df05c0590ba","0x1ff0150152b50052530052910150152b500500f0052c00150152b5005009","0x150152b50052090051d80150152b500500600541e0150152b5005021005","0x52b50051dd0051b50151dd0052b500501520e0151de0052b500501502b","0x72990151db0052b50050152970151dc0052b50051dd1de0072950151dd","0x2b500526d0050af0151da0052b500514b00546901514b0052b50051dc1db","0xba0051da0052b50051da00546a0150190052b500501900503301526d005","0x900541e0150152b500521c0052910150152b50050150070151da01926d","0x51ff0150152b50051290051ff0150152b500500f0052c00150152b5005","0x2b0150152b50052090051d80150152b500500600541e0150152b5005021","0x1d70052b50051d70051b50151d70052b500501520e0151d80052b5005015","0x1510072990151510052b50050152970151d40052b50051d71d8007295015","0x52b50050520050af0151540052b50051520054690151520052b50051d4","0x520ba0051540052b500515400546a0150190052b5005019005033015052","0x500f0052c00150152b500500900541e0150152b5005015007015154019","0x600541e0150152b50050210051ff0150152b50051290051ff0150152b5","0xaf0151d30052b50052020054690150152b50050b700541e0150152b5005","0x2b50051d300546a0150190052b50050190050330151e60052b50051e6005","0x152b50050b700541e0150152b50050150070151d30191e60ba0051d3005","0x2b50051290051ff0150152b500500f0052c00150152b500500900541e015","0x51120054690150152b500500600541e0150152b50050210051ff015015","0x150310052b50050310050330150330052b50050330050af0151610052b5","0x150090052b50050153800151610310330ba0051610052b500516100546a","0x1f20150152b50050150060150152b500501520401500f0052b5005015472","0x50b70052140150b70052b50050100050ba0150100ba0072b50050ba005","0xf60052b500500c00547401500c0052b500500c00f00747301500c0052b5","0x2b50050150050af01502e0052b50050150990150210052b5005015475015","0x4760150ba0052b50050ba0050310150050052b5005005005033015015005","0xba0050150af47801502e0052b500502e00509a0150210052b5005021005","0x1502f00555002c0052b500702b00547901502b0270060ba2b500502e021","0x310ba2b500502c00547b0150330052b500501547a0150152b5005015007","0x3100547c0150152b50051b50051e60150152b50050340052660151b5034","0x1c80052b50051b900547e0150152b500511200547d0151b91120072b5005","0x1c800547f0150270052b50050270050330150060052b50050060050af015","0x50331c80270061294810150330052b50050330054800151c80052b5005","0x50150070151e40055511e30052b50071e10054820151e11d91d20ba2b5","0x150152b500511a0054830150191e611a0ba2b50051e30052c80150152b5","0x1fe0052b50050150990151f50052b500501540f0150152b50050190051e6","0x1e60054800151d90052b50051d90050330151d20052b50051d20050af015","0x1fe0052b50051fe00509a0151f50052b50051f50054160151e60052b5005","0x2b50072090054850152092062020ba2b50051fe1f51e61d91d20af484015","0x23721f0072b500520f0054860150152b500501500701521c00555220f005","0x4f00548801505004f0072b500521f0054870150152b50052370051e6015","0xba2b50070520072060ba2c70150520052b50050500054890150152b5005","0x52b50050154750150152b50050150070150542642620ba5532590af253","0x55401526f0052b500525926d00748a0152590052b50052590052a801526d","0x2b500527100555501527a0f60072b50050f60055550152710052b5005015","0x2910052b50052910052a80152910052b500528027a007050015280271007","0x1290152972950072b50051290055560152930052b500529126f00748a015","0x52b50050af00900712b0152530052b50052530050330150152b5005015","0x5570582990072b500729520200704f0152930052b50052930054760150af","0x50af01505e0052b50050155540150152b500501500701529e05c0590ba","0x52b500505e0055580152aa0052b50050580055580150600052b5005299","0x52b50050590050af0150152b50050150070150155590050150b70152a9","0x70500152a90052b500505c0055580152aa0052b500529e005558015060","0x506229300748a0150620052b50050620052a80150620052b50052a92aa","0x2710052b50052710055580152a72a80072b500529700555a0150640052b5","0x55c2a42a60072b50072a72710600ba55b0150640052b5005064005476015","0x2800509f0150280052b50050150f60150152b50050150070152a306a007","0x6f0052b50052a40055580150710052b50052a60050af0152a00052b5005","0x152b500501500701501555d0050150b70150700052b50052a0005277015","0x506a0050af01529f0052b50050730052780150730052b50050150f6015","0x150700052b500529f00527701506f0052b50052a30055580150710052b5","0x152b500501500701529b29c00755e29d0760072b50072a80f60710ba55b","0x6f0055580152980052b500529d00555801529a0052b50050760050af015","0x701501555f0050150b70152940052b50050700052770152960052b5005","0x152920052b50052920055580152920052b50050155600150152b5005015","0x152b50050150070150e20e30075610822900072b500729206f29c0ba55b","0x820055580152980052b500529b00555801529a0052b50052900050af015","0x701501555f0050150b70152940052b50050700052770152960052b5005","0x27801528f0052b50050150f60150152b50050700055620150152b5005015","0x2b500529b00555801529a0052b50050e30050af01528e0052b500528f005","0xa00152940052b500528e0052770152960052b50050e2005558015298005","0x2b50050150060150152b500501500701528c00556328d0052b5007294005","0x52a801508e0052b50052962980070500150152b500528d0051e6015015","0x52b500501547a01508d0052b500508e06400748a01508e0052b500508e","0x547e0150152b500510600547d0151021060072b500508d00547c015107","0x52b500525300503301529a0052b500529a0050af0150910052b5005102","0x1294810151070052b50051070054800150910052b500509100547f015253","0x55642830052b500728600548201528628928a0ba2b500510709125329a","0x548301527f2810960ba2b50052830052c80150152b500501500701501c","0x9901527c0052b500501540f0150152b500527f0051e60150152b5005096","0x52b500528900503301528a0052b500528a0050af0150990052b5005015","0x509a01527c0052b500527c0054160152810052b5005281005480015289","0x1527b09c09a0ba2b500509927c28128928a0af4840150990052b5005099","0x2780054860150152b50050150070152770055652780052b500727b005485","0xd0072b50052760054870150152b500509f0051e601509f2760072b5005","0x9c0ba2c70150a20052b50050a00054890150152b500500d0054880150a0","0x150152b50050150070150a80a70a50ba5662720a42730ba2b50070a20af","0x50aa00555a0150ac2700072b500527200555a0150aa0052b5005015567","0x5580150ac0052b50050ac0055580150152b50050151290152af26e0072b5","0x2b50050a40051e40152730052b50052730050330152af0052b50052af005","0x26b0072b50072af0ac09a0ba55b0152700052b50052700055580150a4005","0x9f0152670052b50050150f60150152b500501500701526826900756826a","0x2b500526a0055580152660052b500526b0050af0150b10052b5005267005","0x50150070150155690050150b70152650052b50050b10052770150b3005","0x50af0150b60052b50050b40052780150b40052b50050150f60150152b5","0x52b50050b60052770150b30052b50052680055580152660052b5005269","0x25f2630072b500726e2702660ba55b01526e0052b500526e005558015265","0x55801525c0052b50052630050af0150152b500501500701525d25e00756a","0x2b50052650052770150b90052b50050b300555801525b0052b500525f005","0x52b50050155600150152b500501500701501556b0050150b701525a005","0x2552560072b50072580b325e0ba55b0152580052b5005258005558015258","0x55801525c0052b50052560050af0150152b50050150070150c10bf00756c","0x2b50052650052770150b90052b500525500555801525b0052b500525d005","0x2b50052650055620150152b500501500701501556b0050150b701525a005","0xbf0050af0152520052b50052540052780152540052b50050150f6015015","0xb90052b50050c100555801525b0052b500525d00555801525c0052b5005","0x150c500556d2510052b500725a0050a001525a0052b5005252005277015","0x500150152b50052510051e60150152b50050150060150152b5005015007","0x525000556f0152500052b50050c700556e0150c70052b50050b925b007","0x152730052b500527300503301525c0052b500525c0050af0150c90052b5","0xa427325c1290050c90052b50050c90055700150a40052b50050a40051e4","0x152b50050c50051e60150152b50050150060150152b50050150070150c9","0x52b500501502b0150152b500525b0052590150152b50050b9005259015","0xcb00729501524f0052b500524f0051b501524f0052b50050155710150cb","0x52b500524e24d00729901524d0052b500501529701524e0052b500524f","0x503301525c0052b500525c0050af01524b0052b500524c0052c501524c","0x52b500524b0055700150a40052b50050a40051e40152730052b5005273","0x52b500509a0050af0150152b500501500701524b0a427325c12900524b","0x50340152480052b50050a70051e40152490052b50050a500503301524a","0x52550150152b50050150070150155720050150b70150e10052b50050a8","0x52b500509a0050af0150152b50052430050bf0152412430072b5005277","0x50340152480052b50050af0051e40152490052b500509c00503301524a","0x52550150152b50050150070150155720050150b70150e10052b5005241","0x52b500528a0050af0150152b50052400050bf0150e02400072b500501c","0x50340152480052b50050af0051e40152490052b500528900503301524a","0x52b50050e123e00729901523e0052b50050152970150e10052b50050e0","0x503301524a0052b500524a0050af0151560052b500523d0052c501523d","0x52b50051560055700152480052b50052480051e40152490052b5005249","0x150152b50050150060150152b500501500701515624824924a129005156","0x152b50052960052590150152b500506400547d0150152b500528c0051e6","0x52b500501557101523c0052b500501502b0150152b5005298005259015","0x29701523b0052b50050df23c0072950150df0052b50050df0051b50150df","0x2b50050de0052c50150de0052b500523b2390072990152390052b5005015","0x1e40152530052b500525300503301529a0052b500529a0050af015105005","0x1050af25329a1290051050052b50051050055700150af0052b50050af005","0x152b50050f60052590150152b50051290055730150152b5005015007015","0x52620050330150e50052b50052020050af0150152b50050090051fb015","0x150e90052b500505400503401523a0052b50052640051e40152380052b5","0x2590150152b50051290055730150152b50050150070150155740050150b7","0x1040072b500521c0052550150152b50050090051fb0150152b50050f6005","0x2060050330150e50052b50052020050af0150152b50051040050bf015234","0xe90052b500523400503401523a0052b50050070051e40152380052b5005","0x150152b50051290055730150152b50050150070150155740050150b7015","0x72b50051e40052550150152b50050090051fb0150152b50050f6005259","0x50330150e50052b50051d20050af0150152b50052330050bf015232233","0x52b500523200503401523a0052b50050070051e40152380052b50051d9","0x152b50051290055730150152b50050150070150155740050150b70150e9","0x2b500502f0052550150152b50050090051fb0150152b50050f6005259015","0x330150e50052b50050060050af0150152b50052310050bf0150f0231007","0x2b50050f000503401523a0052b50050070051e40152380052b5005027005","0x2c50151030052b50050e90f20072990150f20052b50050152970150e9005","0x2b50052380050330150e50052b50050e50050af01522f0052b5005103005","0x12900522f0052b500522f00557001523a0052b500523a0051e4015238005","0x150060150152b50050152040150090052b500501538001522f23a2380e5","0x1500f0052b500512900c00729501500c0052b500501502b0150152b5005","0x2b500500f00506a0150b70052b50050155760150100052b50050ba005575","0x1b501502e0052b500502100502c0150152b50050f60052910150210f6007","0x100070050af2c401502e0052b500502e0050310150b70052b50050b7005","0x150152b500501500701502f02c02b0ba5770270af0060ba2b500702e0b7","0x52b500503300500f0150152b50050151290150330052b50050270050ba","0x90150af0052b50050af00900712b0150060052b5005006005033015033","0x3400500c0150152b50050150070151b50055780340310072b5007033005","0x1c80052b50051120050100151b90052b500503100500f0151120052b5005","0x151d20052b50050150f60150152b50050150070150155790050150b7015","0x51d90050100151b90052b50051b500500f0151d90052b50051d2005021","0x152b50050150070151e300557a1e10052b50071c800502e0151c80052b5","0x52b500501557b0151e40052b50051e10050270150152b5005015006015","0x50af0150190052b50051e400502f0151e60052b50051b900502c01511a","0x52b50051e60050310150060052b50050060050330150150052b5005015","0xaf57d0150190052b50050190051b501511a0052b500511a00557c0151e6","0x57e2060052b50072020052c90152021fe1f50ba2b500501911a1e6006015","0x528001521c20f0072b500520600557f0150152b5005015007015209005","0x2b500501500701523700558121f0052b500721c0055800150152b500520f","0x50af0150500052b500504f00558301504f0052b500521f005582015015","0x52b50050af0051e40151fe0052b50051fe0050330151f50052b50051f5","0x2b50050150070150500af1fe1f51290050500052b50050500055840150af","0x1fe0050330150520052b50051f50050af0150152b50052370051e6015015","0x2090055860150152b50050150070150155850050150b70152530052b5005","0x1fe0052b50051fe0050330151f50052b50051f50050af0152590052b5005","0x1fe1f51290052590052b50052590055840150af0052b50050af0051e4015","0x2b50051e30051e60150152b50050150060150152b50050150070152590af","0x60050330150520052b50050150050af0150152b50051b900526d015015","0x1b50152640052b50050155870152620052b500501502b0152530052b5005","0x2b50050152970150540052b50052642620072950152640052b5005264005","0x152710052b500526f00558601526f0052b500505426d00729901526d005","0x50af0051e40152530052b50052530050330150520052b50050520050af","0x150070152710af2530521290052710052b50052710055840150af0052b5","0x729901527a0052b50050152970150152b50050090051fb0150152b5005","0x2b50050150050af0152910052b50052800055860152800052b500502f27a","0x58401502c0052b500502c0051e401502b0052b500502b005033015015005","0x151290052b500501509901529102c02b0151290052910052b5005291005","0x70150155890152b50071290af0075880150af0ba0072b50050ba0050ac","0x2b500500f0050ac01500f00c0090ba2b500500500558a0150152b5005015","0x2b50070b70100150ba58b0150b70ba0072b50050ba0050ac01501000f007","0x270052b50050151c10150152b500501500701500602e00758c0210f6007","0xba2130150270052b500502700509a01502b0210072b50050210050ac015","0x2660150152b500501500701503103300758d02f02c0072b500702702b0f6","0x210072b50050210050ac0150340052b50050151c10150152b500502f005","0x1501558e0152b50070341b500758801502c0052b500502c0050af0151b5","0x151120052b50050151c10150152b50050ba0052660150152b5005015007","0x758f1c81b90072b500711202102c0ba2130151120052b500511200509a","0x51c80050ac0151e10052b50050155900150152b50050150070151d91d2","0x152b50071e11e30075880151b90052b50051b90050af0151e31c80072b5","0x51c80050ac0151e40052b50050155900150152b5005015007015015591","0x2b50071e411a1b90ba2130151e40052b50051e400509a01511a1c80072b5","0x152b50050190052660150152b50050150070151fe1f50075920191e6007","0x2b500501500701521c20f2090ba5932062020072b50070071e600704f015","0x2060055580152370052b50052020050af01521f0052b5005015554015015","0x70150155940050150b70150500052b500521f00555801504f0052b5005","0x4f0052b500521c0055580152370052b50052090050af0150152b5005015","0x51c80050ac0150520052b50050155900150500052b500520f005558015","0x2b50070522532370ba2130150520052b500505200509a0152531c80072b5","0x52b50052590050af0150152b5005015007015054264007595262259007","0x1526f26d0072b50052622590075960152620052b500526200509a015259","0x2710052c10150152b500501500701527a0055982710052b500726f005597","0x152b500501500701529100559a0152b50072800055990152800052b5005","0x2b500500f0052660150152b50051c80052660150152b500500900559b015","0x50500052590150152b500500c00527a0150152b500504f005259015015","0x2950051b50152950052b500501559c0152930052b500501502b0150152b5","0x52b500526d0050af0152970052b50052952930072950152950052b5005","0x2b500501500701501559d0050150b70150580052b5005297005034015299","0x52b500529e00559f01529e05c0590ba2b500529105026d0ba59e015015","0x151c10152aa0052b500505c00559f0150600052b500504f00559f01505e","0x52b50052a900509a01506200f0072b500500f0050ac0152a90052b5005","0x50150070152a62a70075a02a80640072b50070622a90590ba2130152a9","0x1b501506a0052b50052a405e0075a20152a40052b50050155a10150152b5","0x2b50050155900152a30052b500506006a0073e501506a0052b500506a005","0x150280052b500502800509a0152a02a80072b50052a80050ac015028005","0x75a306f0710072b50070282a00640ba2130152a30052b50052a30051b5","0x2b50050155900150152b500506f0052660150152b5005015007015073070","0x760072b500729f2a80710ba21301529f0052b500529f00509a01529f005","0x150760052b50050760050af0150152b500501500701529b29c0075a429d","0x559701529829a0072b500529d07600759601529d0052b500529d00509a","0x2b50052960052c10150152b50050150070152940055a52960052b5007298","0x75a20150820052b50050155a10152900052b500529200559f015292005","0x2b50050e30051b50150e20052b500529a0050af0150e30052b5005082290","0x2b500500900559b0150152b50050150070150155a60050150b701528f005","0x52a300527a0150152b500500f0052660150152b50051c8005266015015","0x2940052550150152b500500c00527a0150152b50052aa00527a0150152b5","0x28c0052b500529a0050af0150152b500528e0050bf01528d28e0072b5005","0x152b50050150070150155a70050150b701508e0052b500528d005034015","0x2b50051c80052660150152b500500900559b0150152b500529b005266015","0x500c00527a0150152b50052a300527a0150152b500500f005266015015","0x501508f01508d0052b500501502b0150152b50052aa00527a0150152b5","0x1060052b500510708d0072950151070052b50051070051b50151070052b5","0x50150b701508e0052b500510600503401528c0052b500529c0050af015","0x50700050af0150152b50050730052660150152b50050150070150155a7","0x1020072b50052a80700075960152a80052b50052a800509a0150700052b5","0x2c10150152b50050150070152890055a828a0052b5007091005597015091","0x2b50051020050af0152830052b500528600559f0152860052b500528a005","0x1501c0052b500528f00c0075a201528f0052b50052830051b50150e2005","0x960051b50150960052b500501c2aa0073e501501c0052b500501c0051b5","0x1500701527c0055aa27f2810072b50070960e20075a90150960052b5005","0x2b500500f2a30990ba1cd0150990052b500527f0090075ab0150152b5005","0xb701527b0052b500509a0051bf01509c0052b50052810050af01509a005","0x52660150152b500500900559b0150152b50050150070150155ac005015","0x2b0150152b50052a300527a0150152b500500f0052660150152b50051c8","0x2770052b50052770051b50152770052b500501559c0152780052b5005015","0x50340152990052b500527c0050af0152760052b5005277278007295015","0x559b0150152b500501500701501559d0050150b70150580052b5005276","0x27a0150152b500500f0052660150152b50051c80052660150152b5005009","0x150152b500500c00527a0150152b50052aa00527a0150152b50052a3005","0x51020050af0150152b500509f0050bf01500d09f0072b5005289005255","0x152990052b500528c00526401508e0052b500500d00503401528c0052b5","0x150152b500501500701501559d0050150b70150580052b500508e005251","0x152b50051c80052660150152b500500900559b0150152b50052a6005266","0x2b500500c00527a0150152b500505e00527a0150152b500500f005266015","0x2b500501502b0150152b500506000527a0150152b50052aa00527a015015","0x72950150a20052b50050a20051b50150a20052b500501508f0150a0005","0x2b50052730050340152990052b50052a70050af0152730052b50050a20a0","0x2b500500900559b0150152b500501500701501559d0050150b7015058005","0x504f0052590150152b500500f0052660150152b50051c8005266015015","0x27a0052550150152b50050500052590150152b500500c00527a0150152b5","0x2990052b500526d0050af0150152b50050a40050bf0152720a40072b5005","0x152b500501500701501559d0050150b70150580052b5005272005034015","0x2b500500900559b0150152b50050500052590150152b5005054005266015","0x504f0052590150152b500500f0052660150152b50051c8005266015015","0x501508f0150a50052b500501502b0150152b500500c00527a0150152b5","0xa80052b50050a70a50072950150a70052b50050a70051b50150a70052b5","0x50152970150580052b50050a80050340152990052b50052640050af015","0xac0052b50052700055ad0152700052b50050580aa0072990150aa0052b5","0xac2990070050ac0052b50050ac0055ae0152990052b50052990050af015","0x2b50070071f500704f0150152b50051fe0052660150152b5005015007015","0x52b50050155540150152b500501500701526926a26b0ba5af2af26e007","0x55580150b10052b50052af0055580152670052b500526e0050af015268","0x50af0150152b50050150070150155b00050150b70152660052b5005268","0x52b500526a0055580150b10052b50052690055580152670052b500526b","0x9a0150b31c80072b50051c80050ac0152670052b50052670050af015266","0xb40055970150b42650072b50050b32670075960150b30052b50050b3005","0x52b50050b60052c10150152b50050150070152630055b10b60052b5007","0x559b0150152b500501500701525e0055b20152b500725f00559901525f","0x2590150152b500500f0052660150152b50051c80052660150152b5005009","0x150152b50050b10052590150152b500500c00527a0150152b5005266005","0x52b500525c0051b501525c0052b500501559c01525d0052b500501502b","0x340150b90052b50052650050af01525b0052b500525c25d00729501525c","0x59e0150152b50050150070150155b30050150b701525a0052b500525b005","0x5900150bf0052b500526600559f0152552562580ba2b500525e0b12650ba","0x2b50050c100509a0152541c80072b50051c80050ac0150c10052b5005015","0x150070150c70c50075b42512520072b50072540c12580ba2130150c1005","0x152510052b500525100509a0152520052b50052520050af0150152b5005","0x24f0055b50cb0052b50070c90055970150c92500072b5005251252007596","0x2b500524e00559f01524e0052b50050cb0052c10150152b5005015007015","0x1c101524b0052b500525500559f01524c0052b500525600559f01524d005","0x2b500524a00509a01524900f0072b500500f0050ac01524a0052b5005015","0x150070152412430075b60e12480072b500724924a2500ba21301524a005","0x2400052b50052400051b50152400052b500524d0bf0075a20150152b5005","0xe10050ac01523e0052b50050155900150e00052b500524c2400073e5015","0x52b50050e00051b501523e0052b500523e00509a01523d0e10072b5005","0x501500701523b0df0075b723c1560072b500723e23d2480ba2130150e0","0x23900509a0152390052b50050155900150152b500523c0052660150152b5","0x152380e50075b81050de0072b50072390e11560ba2130152390052b5005","0x52b500510500509a0150de0052b50050de0050af0150152b5005015007","0x5b91040052b50070e90055970150e923a0072b50051050de007596015105","0x23300559f0152330052b50051040052c10150152b5005015007015234005","0xf00052b50052312320075a20152310052b50050155a10152320052b5005","0x50150b70151030052b50050f00051b50150f20052b500523a0050af015","0x51c80052660150152b500500900559b0150152b50050150070150155ba","0xe000527a0150152b500524b00527a0150152b500500f0052660150152b5","0x1522d22f0072b50052340052550150152b500500c00527a0150152b5005","0x2b500522d00503401522c0052b500523a0050af0150152b500522f0050bf","0x2b50052380052660150152b50050150070150155bb0050150b7015229005","0x500f0052660150152b50051c80052660150152b500500900559b015015","0xe000527a0150152b500500c00527a0150152b500524b00527a0150152b5","0x51b50152270052b500501508f0152280052b500501502b0150152b5005","0x2b50050e50050af0152260052b50052272280072950152270052b5005227","0x50150070150155bb0050150b70152290052b500522600503401522c005","0x509a0150df0052b50050df0050af0150152b500523b0052660150152b5","0x71080055970151082250072b50050e10df0075960150e10052b50050e1","0x2200052b50052240052c10150152b50050150070150fe0055bc2240052b5","0x1090051b50150f20052b50052250050af0151090052b500522000559f015","0x52b500521e0051b501521e0052b500510300c0075a20151030052b5005","0x5a901510b0052b500510b0051b501510b0052b500521e0e00073e501521e","0x75ab0150152b500501500701510e0055bd10c21d0072b500710b0f2007","0x21d0050af0151100052b500500f24b21b0ba1cd01521b0052b500510c009","0x2180052b500509c00526401527b0052b50051100051bf01509c0052b5005","0x152b50050150070150155bf0050150b70152170052b500527b0055be015","0x2b500500f0052660150152b50051c80052660150152b500500900559b015","0x2b500501559c0152160052b500501502b0150152b500524b00527a015015","0x152140052b50052152160072950152150052b50052150051b5015215005","0x5b30050150b701525a0052b50052140050340150b90052b500510e0050af","0x2b50051c80052660150152b500500900559b0150152b5005015007015015","0x50e000527a0150152b500524b00527a0150152b500500f005266015015","0xbf0152122130072b50050fe0052550150152b500500c00527a0150152b5","0x52b500521200503401522c0052b50052250050af0150152b5005213005","0x150b701525a0052b50052290052510150b90052b500522c005264015229","0x900559b0150152b50052410052660150152b50050150070150155b3005","0x527a0150152b500500f0052660150152b50051c80052660150152b5005","0x27a0150152b50050bf00527a0150152b500500c00527a0150152b500524b","0x152110052b500501502b0150152b500524c00527a0150152b500524d005","0x51192110072950151190052b50051190051b50151190052b500501508f","0x1525a0052b50052100050340150b90052b50052430050af0152100052b5","0x2660150152b500500900559b0150152b50050150070150155b30050150b7","0x150152b500500c00527a0150152b500500f0052660150152b50051c8005","0x152b50052550052590150152b50052560052590150152b50050bf00527a","0x2500050af0150152b500520e0050bf01520d20e0072b500524f005255015","0x70150155b30050150b701525a0052b500520d0050340150b90052b5005","0x2660150152b500500900559b0150152b50050c70052660150152b5005015","0x150152b50052550052590150152b500500f0052660150152b50051c8005","0x152b50052560052590150152b50050bf00527a0150152b500500c00527a","0x2b500520c0051b501520c0052b500501508f01508f0052b500501502b015","0x150b90052b50050c50050af01520b0052b500520c08f00729501520c005","0x150152b50050150070150155b30050150b701525a0052b500520b005034","0x152b500500f0052660150152b50051c80052660150152b500500900559b","0x2b50050b10052590150152b500500c00527a0150152b5005266005259015","0x50af0150152b50053800050bf01511e3800072b5005263005255015015","0x20a0052b500501529701525a0052b500511e0050340150b90052b5005265","0x50af0151210052b50052080055ad0152080052b500525a20a007299015","0x150070151210b90070051210052b50051210055ae0150b90052b50050b9","0x70151281262040ba5c02052070072b50070071b900704f0150152b5005","0x152000052b50052070050af0152030052b50050155540150152b5005015","0x5c10050150b70151ff0052b500520300555801512b0052b5005205005558","0x51280055580152000052b50052040050af0150152b5005015007015015","0x1512d0052b50051ff00559f0151ff0052b500512600555801512b0052b5","0x2b500500f0050ac0151f60052b50050151c101512f0052b500512b00559f","0x72b50070451f62000ba2130151f60052b50051f600509a01504500f007","0x1522a0052b50050155900150152b50050150070151311f80075c21fc1fb","0x1fb0ba21301522a0052b500522a00509a0151331fc0072b50051fc0050ac","0x52660150152b50050150070151f91fa0075c31351fd0072b500722a133","0x151f20052b50051f200509a0151f20052b50050155900150152b5005135","0x152b500501500701513b1390075c41eb1f10072b50071f21fc1fd0ba213","0x1f10075960151eb0052b50051eb00509a0151f10052b50051f10050af015","0x150070151e50055c513f0052b500713d00559701513d1e70072b50051eb","0x151430052b500514100559f0151410052b500513f0052c10150152b5005","0x51e70050af0151e00052b50051e21430075a20151e20052b50050155a1","0x150070150155c60050150b70151de0052b50051e00051b50151df0052b5","0x52660150152b50051c80052660150152b500500900559b0150152b5005","0x27a0150152b500512d00527a0150152b500512f00527a0150152b500500f","0x2b50051dd0050bf0151dc1dd0072b50051e50052550150152b500500c005","0x150b701514b0052b50051dc0050340151db0052b50051e70050af015015","0x900559b0150152b500513b0052660150152b50050150070150155c7005","0x527a0150152b500500f0052660150152b50051c80052660150152b5005","0x2b0150152b500512d00527a0150152b500500c00527a0150152b500512f","0x1d80052b50051d80051b50151d80052b500501508f0151da0052b5005015","0x50340151db0052b50051390050af0151d70052b50051d81da007295015","0x52660150152b50050150070150155c70050150b701514b0052b50051d7","0x1fc0052b50051fc00509a0151fa0052b50051fa0050af0150152b50051f9","0x55c81520052b50071510055970151511d40072b50051fc1fa007596015","0x51d300559f0151d30052b50051520052c10150152b5005015007015154","0x151de0052b50051610051b50151df0052b50051d40050af0151610052b5","0x12d0073e50151600052b50051600051b50151600052b50051de00c0075a2","0x2b50071581df0075a90151580052b50051580051b50151580052b5005160","0x52b500515d0090075ab0150152b50050150070151550055c915d15a007","0x152180052b500515a0050af0151620052b500500f12f1570ba1cd015157","0x52660151631ce1d10ba2b500521700558a0152170052b50051620051bf","0x52b50050150f60151cb0052b50051c81ce1d10ba1cd0150152b5005163","0xaf0151c40052b50051c60055cb0151c60052b50051c71cb0075ca0151c7","0x70151c42180070051c40052b50051c40055ae0152180052b5005218005","0x2660150152b50051c80052660150152b500500900559b0150152b5005015","0x151c10052b500501502b0150152b500512f00527a0150152b500500f005","0x51cd1c10072950151cd0052b50051cd0051b50151cd0052b500501559c","0x151bb0052b50051bf0050340151bc0052b50051550050af0151bf0052b5","0x2660150152b500500900559b0150152b50050150070150155cc0050150b7","0x150152b500512f00527a0150152b500500f0052660150152b50051c8005","0x72b50051540052550150152b500500c00527a0150152b500512d00527a","0x50340151db0052b50051d40050af0150152b500516b0050bf0151b816b","0x52b500514b0052510151bc0052b50051db00526401514b0052b50051b8","0x152b50051310052660150152b50050150070150155cc0050150b70151bb","0x2b500500f0052660150152b50051c80052660150152b500500900559b015","0x512d00527a0150152b500500c00527a0150152b500512f00527a015015","0x16f0051b501516f0052b500501508f01516d0052b500501502b0150152b5","0x52b50051f80050af0151b60052b500516f16d00729501516f0052b5005","0x1720072990151720052b50050152970151bb0052b50051b60050340151bc","0x52b50051bc0050af0151b20052b50051b40055ad0151b40052b50051bb","0x150152b50050150070151b21bc0070051b20052b50051b20055ae0151bc","0x152b500500900559b0150152b500500c00527a0150152b50051d9005266","0x52b500501502b0150152b500500f0052660150152b500500700527a015","0x1b10072950151b00052b50051b00051b50151b00052b500501508f0151b1","0x52b50051af1ae0072990151ae0052b50050152970151af0052b50051b0","0x55ae0151d20052b50051d20050af01517a0052b50051850055ad015185","0x500f0052660150152b500501500701517a1d200700517a0052b500517a","0xba0050ac01517c0052b50050155900150152b50050210052660150152b5","0x717c1ac02c0ba21301517c0052b500517c00509a0151ac0ba0072b5005","0x2b50051a70052660150152b50050150070151811a50075cd1a71aa0072b5","0x1aa0ba2130151830052b500518300509a0151830052b5005015590015015","0x50af0150152b50050150070151891870075ce1a11a30072b50071830ba","0x2b50051a11a30075960151a10052b50051a100509a0151a30052b50051a3","0x152b500501500701519e0055cf1960052b500718b00559701518b1a0007","0x50155a101539d0052b500500000559f0150000052b50051960052c1015","0x3a10052b50051a00050af01539f0052b500539e39d0075a201539e0052b5","0x152b50050150070150155d00050150b70153a20052b500539f0051b5015","0x2b500500c00527a0150152b500500700527a0150152b500500900559b015","0x50af0150152b50053a40050bf0153a53a40072b500519e005255015015","0x150155d10050150b70153a70052b50053a50050340153a60052b50051a0","0x150152b500500900559b0150152b50051890052660150152b5005015007","0x3a80052b500501502b0150152b500500700527a0150152b500500c00527a","0x3a93a80072950153a90052b50053a90051b50153a90052b500501508f015","0x3a70052b50053ab0050340153a60052b50051870050af0153ab0052b5005","0x150152b50051810052660150152b50050150070150155d10050150b7015","0xba1a50075960150ba0052b50050ba00509a0151a50052b50051a50050af","0x50150070153af0055d23ae0052b50073ad0055970153ad3ac0072b5005","0xaf0153b10052b50053b000559f0153b00052b50053ae0052c10150152b5","0x53a200c0075a20153a20052b50053b10051b50153a10052b50053ac005","0x3b30052b50053b20070073e50153b20052b50053b20051b50153b20052b5","0x55d33b63b50072b50073b33a10075a90153b30052b50053b30051b5015","0x50150a20152b70052b50053b60090075ab0150152b50050150070153b7","0x3ba0052b50053b93b82b70ba1cd0153b90052b50050150990153b80052b5","0x2b80055cb0152b80052b50053bb3ba0075ca0153bb0052b50050150f6015","0x3bc0052b50053bc0055ae0153b50052b50053b50050af0153bc0052b5005","0x1502b0150152b500500900559b0150152b50050150070153bc3b5007005","0x153be0052b50053be0051b50153be0052b500501559c0153bd0052b5005","0x3bf3c00072990153c00052b50050152970153bf0052b50053be3bd007295","0x3b70052b50053b70050af0153c30052b50053c20055ad0153c20052b5005","0x59b0150152b50050150070153c33b70070053c30052b50053c30055ae015","0x150152b500500c00527a0150152b500500700527a0150152b5005009005","0x53ac0050af0150152b50053c40050bf0153c53c40072b50053af005255","0x2990153c60052b50050152970153a70052b50053c50050340153a60052b5","0x53a60050af0153c80052b50053c70055ad0153c70052b50053a73c6007","0x2b50050150070153c83a60070053c80052b50053c80055ae0153a60052b5","0x2b50050150990150152b50050210052660150152b5005031005266015015","0x150330052b50050330050af0153ca00f0072b500500f0050ac0153c9005","0x2b50050155900150152b50050150070150155d40152b50073c93ca007588","0x153cb0052b50053cb00509a0153cc0ba0072b50050ba0050ac0153cb005","0x152b50050150070153d13cf0075d53ce3cd0072b50073cb3cc0330ba213","0x2b50050ba0050ac0153d20052b50050155900150152b50053ce005266015","0x72b50073d23d33cd0ba2130153d20052b50053d200509a0153d30ba007","0x2bb0052b50052bb0050af0150152b50050150070153d63d50075d63d42bb","0x5970153d83d70072b50053d42bb0075960153d40052b50053d400509a015","0x53d90052c10150152b50050150070153da0055d73d90052b50073d8005","0x5a20153de0052b50050155a10153dc0052b50053db00559f0153db0052b5","0x53df0051b50153e00052b50053d70050af0153df0052b50053de3dc007","0x500c00527a0150152b50050150070150155d80050150b70153e10052b5","0xba0052660150152b500500700527a0150152b500500900559b0150152b5","0x153e33e20072b50053da0052550150152b500500f0052660150152b5005","0x2b50053e30050340153e40052b50053d70050af0150152b50053e20050bf","0x2b50053d60052660150152b50050150070150155d90050150b70153e5005","0x500900559b0150152b500500f0052660150152b500500c00527a015015","0x501502b0150152b50050ba0052660150152b500500700527a0150152b5","0x2950153e70052b50053e70051b50153e70052b500501508f0153e60052b5","0x53e80050340153e40052b50053d50050af0153e80052b50053e73e6007","0x53d10052660150152b50050150070150155d90050150b70153e50052b5","0x153e90ba0072b50050ba0050ac0153cf0052b50053cf0050af0150152b5","0x55970153ec3ea0072b50053e93cf0075960153e90052b50053e900509a","0x2b50053ed0052c10150152b50050150070153ee0055da3ed0052b50073ec","0x1b50153e00052b50053ea0050af0153f00052b50053ef00559f0153ef005","0x3f30075db3f23f10072b50070ba00f3e00ba58b0153e10052b50053f0005","0x3f50051b50153f50052b50053e100c0075a20150152b50050150070153f4","0x53f23f60090ba1cd0153f60052b50053f50070073e50153f50052b5005","0x153fa0052b50053f93f70075ca0153f90052b50050150f60153f70052b5","0x52be0055ae0153f10052b50053f10050af0152be0052b50053fa0055cb","0x152b50053f40052660150152b50050150070152be3f10070052be0052b5","0x2b500500900559b0150152b50053e100527a0150152b500500c00527a015","0x2b50050155dc0153fb0052b500501502b0150152b500500700527a015015","0x153fd0052b50053fc3fb0072950153fc0052b50053fc0051b50153fc005","0x53ff0055ad0153ff0052b50053fd3fe0072990153fe0052b5005015297","0x52bd0052b50052bd0055ae0153f30052b50053f30050af0152bd0052b5","0x900559b0150152b500500c00527a0150152b50050150070152bd3f3007","0x52660150152b50050ba0052660150152b500500700527a0150152b5005","0x152b50054000050bf0154014000072b50053ee0052550150152b500500f","0x50152970153e50052b50054010050340153e40052b50053ea0050af015","0x4040052b50054030055ad0154030052b50053e54020072990154020052b5","0x4043e40070054040052b50054040055ae0153e40052b50053e40050af015","0x152b500500f0052660150152b500500c00527a0150152b5005015007015","0x75ca0154050052b50050150f60152bf0052b50050ba0070090ba1cd015","0x2b50050330050af0154070052b50054060055cb0154060052b50054052bf","0x152b50050150070154070330070054070052b50054070055ae015033005","0x2b500500900559b0150152b500500c00527a0150152b5005006005266015","0x50ba0052660150152b500500f0052660150152b500500700527a015015","0x4090051b50154090052b50050155dc0154080052b500501502b0150152b5","0x40a0052b50050152970152bc0052b50054094080072950154090052b5005","0x50af01540c0052b500540b0055ad01540b0052b50052bc40a007299015","0x1500701540c02e00700540c0052b500540c0055ae01502e0052b500502e","0x150f60150152b500500700527a0150152b50050ba0052660150152b5005","0x52b500540e0055cb01540e0052b500540d0050075ca01540d0052b5005","0x1500700540f0052b500540f0055ae0150150052b50050150050af01540f","0x90075dd0af1290072b50070050150070050150152b500501500601540f","0x2b500501512901500f0052b50050070055de0150152b500501500701500c","0x55e00b70100072b500700f0055df0151290052b50051290050af015015","0x50100055e20150210052b50050b70055e10150152b50050150070150f6","0x150070150155e40050150b70150060052b50050210055e301502e0052b5","0x5e201502b0052b50050270055e50150270052b50050150f60150152b5005","0x2b50070060055e60150060052b500502b0055e301502e0052b50050f6005","0x55e80150152b50050150060150152b500501500701502f0055e702c005","0x52b50050310055ea0150310052b50050330055e90150330052b500502c","0x1aa0151b50052b50050340ba0072950150340052b50050340051b5015034","0x2b50050af0050330151290052b50051290050af0151120052b500502e005","0x1a50151b50052b50051b50050340151120052b50051120051a70150af005","0x50150070151d21c81b90ba0051d21c81b90ba2b50051b51120af129129","0x502e0051a30150152b500502f0051e60150152b50050150060150152b5","0x5ec0151e10052b50051d90ba0075eb0151d90052b50050150f60150152b5","0x2b50050af0050330151290052b50051290050af0151e30052b50051e1005","0x2b50050150070151e30af1290ba0051e30052b50051e30055ed0150af005","0x2b500501502b0150152b50050070052cb0150152b50050ba005291015015","0x729501511a0052b500511a0051b501511a0052b50050152a00151e4005","0x2b50051e60190072990150190052b50050152970151e60052b500511a1e4","0x330150090052b50050090050af0151fe0052b50051f50055ee0151f5005","0x151fe00c0090ba0051fe0052b50051fe0055ed01500c0052b500500c005","0x5f200c0055f10090055f00af0052b500f0070055ef0150152b5005015006","0x152b50050150070150210055f60f60055f50b70055f40100055f300f005","0x155f90150152b50050150070150060055f802e0052b50070af0055f7015","0x52b50050270ba0072950150270052b50050270051b50150270052b5005","0x1503302f0072b500502c0055fa01502c02e0072b500502e0052cc01502b","0x2b500503100508d0150310052b500502f0055fb0150152b50050330050e2","0x1b91120072b500502e0055fa0151b50052b500503402b007295015034005","0x51c800508d0151c80052b50051b90055fb0150152b50051120050e2015","0x1e10052b50051d90050340151d90052b50051d21b50072950151d20052b5","0x151e30052b50050155fd0150152b50050150070150155fc0050150b7015","0x60055fe0151e40052b50051e30ba0072950151e30052b50051e30051b5","0x2b50050190050e20150191e60072b500511a0055ff01511a0060072b5005","0x72950151fe0052b50051f500508d0151f50052b50051e60055fb015015","0x52060050e20152092060072b50050060055ff0152020052b50051fe1e4","0x29501521c0052b500520f00508d01520f0052b50052090055fb0150152b5","0x51e10052510151e10052b500521f00503401521f0052b500521c202007","0x150070150156000050150b701504f0052b50051290050340152370052b5","0x156020150152b50050150070150500052b50050090056010150152b5005","0x52b50050520ba0072950150520052b50050520051b50150520052b5005","0x53c30152620052b50052590056040152590052b5005050005603015253","0x2b50052530050340150540052b50052641290072950152640052b5005262","0x50150070150156000050150b701504f0052b5005054005034015237005","0x50156060150152b500501500701526d0052b500500c0056050150152b5","0x2710052b500526f0ba00729501526f0052b500526f0051b501526f0052b5","0x2952932912800af2b500527a00560801527a26d0072b500526d005607015","0x529500527a0150152b500529300527a0150152b500529100527a015297","0x508d0152990052b50052800055fb0150152b500529700527a0150152b5","0x2b500526d0056070150590052b50050581290072950150580052b5005299","0x529e0050e20152a92aa06005e29e0af2b500505c00560801505c26d007","0x2a900527a0150152b50052aa00527a0150152b500506000527a0150152b5","0x640052b50050620590072950150620052b500505e00502f0150152b5005","0x6a2a42a62a70af2b50052a80056080152a826d0072b500526d005607015","0x506a00527a0150152b50052a600527a0150152b50052a70050e20152a3","0x72950150280052b50052a400502f0150152b50052a300527a0150152b5","0x507100560801507126d0072b500526d0056070152a00052b5005028064","0x2b500507000527a0150152b500506f0050e201507629f07307006f0af2b5","0x529f00502f0150152b500507600527a0150152b500507300527a015015","0x29b0af2b500526d00560801529c0052b500529d2a000729501529d0052b5","0x27a0150152b500529a00527a0150152b500529b0050e201529429629829a","0x2920052b500529400502f0150152b500529600527a0150152b5005298005","0x50340152370052b50052710050340152900052b500529229c007295015","0x56090150152b50050150070150156000050150b701504f0052b5005290","0x28d00560e28e00560d28f00560c0e200560b0e300560a0820052b500900f","0x52b500528c0051b501528c0052b500501560f0150152b5005015007015","0x330150150052b50050150050af01508e0052b500528c0ba00729501528c","0x2b500508e0050340150820052b50050820056100150050052b5005005005","0x2b500512908e0820050150af6110151290052b500512900503401508e005","0x2b50050156120150152b500501500701510610708d0ba00510610708d0ba","0x150910052b50051020ba0072950151020052b50051020051b5015102005","0x50e30056130150050052b50050050050330150150052b50050150050af","0x151290052b50051290050340150910052b50050910050340150e30052b5","0x701528628928a0ba00528628928a0ba2b50051290910e30050150af614","0x152830052b50052830051b50152830052b50050156150150152b5005015","0x50050330150150052b50050150050af01501c0052b50052830ba007295","0x1c0052b500501c0050340150e20052b50050e20056160150050052b5005","0x960ba2b500512901c0e20050150af6170151290052b5005129005034015","0x27c0052b50050152cf0150152b500501500701527f2810960ba00527f281","0x50af0150990052b500527c0ba00729501527c0052b500527c0051b5015","0x52b500528f0056180150050052b50050050050330150150052b5005015","0xaf6190151290052b50051290050340150990052b500509900503401528f","0x501500701527b09c09a0ba00527b09c09a0ba2b500512909928f005015","0x72950152780052b50052780051b50152780052b500501561a0150152b5","0x2b50050050050330150150052b50050150050af0152770052b50052780ba","0x340152770052b500527700503401528e0052b500528e00561b015005005","0xd09f2760ba2b500512927728e0050150af61c0151290052b5005129005","0x1b50150a00052b500501561d0150152b500501500701500d09f2760ba005","0x50150050af0150a20052b50050a00ba0072950150a00052b50050a0005","0x1528d0052b500528d00561e0150050052b50050050050330150150052b5","0x50150af61f0151290052b50051290050340150a20052b50050a2005034","0x152b50050150070152720a42730ba0052720a42730ba2b50051290a228d","0x50150070150150b70056210150152b5005015007015015010005620015","0x72950150a50052b50050a50051b50150a50052b50050156220150152b5","0x50a80056240150a80f60072b50050f60056230150a70052b50050a50ba","0x150152b50050ac00527a0150152b500527000527a0150ac2700aa0ba2b5","0xf60056230152af0052b500526e12900729501526e0052b50050aa00502f","0x526a00527a01526826926a0ba2b500526b00562401526b0f60072b5005","0x72950152670052b500526900502f0150152b500526800527a0150152b5","0x26600527a0152650b32660ba2b50050f60056240150b10052b50052672af","0x2950150b40052b500526500502f0150152b50050b300527a0150152b5005","0x50b60050340152370052b50050a70050340150b60052b50050b40b1007","0x2b50050156250150152b50050150070150156000050150b701504f0052b5","0x1525f0052b50052630ba0072950152630052b50052630051b5015263005","0x25d00555a01525d25e0072b500525e00562701525e0052b5005021005626","0xb90052b500525c0056280150152b500525b00525901525b25c0072b5005","0x555a0152580052b500525a12900729501525a0052b50050b900559f015","0x52b50052550056280150152b50052560052590152552560072b500525e","0x340152540052b50050c12580072950150c10052b50050bf00559f0150bf","0x52b50050150f601504f0052b50052540050340152370052b500525f005","0x150c50052b500525100562a0152510052b500525204f2370ba629015252","0x50c500562b0150050052b50050050050330150150052b50050150050af","0x52b500501502b0150152b50050150060150c50050150ba0050c50052b5","0x8d0150b70052b500501000f0072950150100052b50050ba00508d01500f","0xc0210072950150210052b50050f60b70072950150f60052b5005129005","0x72b50050090051f20150060052b50050af02e00729501502e0052b5005","0x1ac01502c0052b500502b00521401502b0052b50050270050ba015027009","0x502f00600729501502f0052b500502f0051b501502f0052b500502c005","0x330150150052b50050150050af0150310052b50050150990150330052b5","0x2b50050330050340150090052b50050090050310150050052b5005005005","0x2b50050310330090050150af62c0150310052b500503100509a015033005","0x2b50050150070151c800562e1b90052b500711200562d0151121b50340ba","0x2660151e31e11d90ba2b50051b900562f0151d20052b5005015058015015","0x1e40072b50051d900506a0150152b50051e30051e60150152b50051e1005","0x2b50050150a20151e60052b50050150a20150152b50051e400529101511a","0x151fe0052b50051f50191e60ba0fe0151f50052b50050150a2015019005","0x51b50050330150340052b50050340050af0152020052b500511a00502c","0x151d20052b50051d20050590150070052b50050070051e30151b50052b5","0x1b50340091fd0152020052b50052020050310151fe0052b50051fe005133","0x63021f0052b500721c0051fa01521c20f2092061292b50052021fe1d2007","0x528001505004f0072b500521f0051390150152b5005015007015237005","0x2530052b50050520056320150520052b50050500056310150152b500504f","0x20f0051e30152090052b50052090050330152060052b50052060050af015","0x701525320f2092061290052530052b500525300563301520f0052b5005","0x2060052b50052060050af0152590052b50052370056340150152b5005015","0x25900563301520f0052b500520f0051e30152090052b5005209005033015","0x1c80056340150152b500501500701525920f2092061290052590052b5005","0x1b50052b50051b50050330150340052b50050340050af0152620052b5005","0x1b50341290052620052b50052620056330150070052b50050070051e3015","0x50ba00508d0150090052b500501502b0150152b5005015006015262007","0x100052b500512900508d01500f0052b500500c00900729501500c0052b5","0xba0150f60af0072b50050af0051f20150b70052b500501000f007295015","0x2b500502e0051ac01502e0052b50050210052140150210052b50050f6005","0x150270052b50050060b70072950150060052b50050060051b5015006005","0x2b50050050050330150150052b50050150050af01502b0052b5005015099","0x9a0150270052b50050270050340150af0052b50050af005031015005005","0x3302f02c0ba2b500502b0270af0050150af63501502b0052b500502b005","0x150580150152b50050150070150340056360310052b500703300562d015","0x2b50051b90052660151c81b91120ba2b500503100562f0151b50052b5005","0x52910151d91d20072b500511200506a0150152b50051c80051e6015015","0xa20151e30052b50050150a20151e10052b50050150a20150152b50051d2","0x51d900502c01511a0052b50051e41e31e10ba0fe0151e40052b5005015","0x1502f0052b500502f00503301502c0052b500502c0050af0151e60052b5","0x511a0051330151b50052b50051b50050590150070052b50050070051e3","0x1e611a1b500702f02c0091fd0151e60052b50051e600503101511a0052b5","0x70152090056372060052b50072020051fa0152021fe1f50191292b5005","0x152b500520f00528001521c20f0072b50052060051390150152b5005015","0x190050af0152370052b500521f00563201521f0052b500521c005631015","0x1fe0052b50051fe0051e30151f50052b50051f50050330150190052b5005","0x152b50050150070152371fe1f50191290052370052b5005237005633015","0x1f50050330150190052b50050190050af01504f0052b5005209005634015","0x4f0052b500504f0056330151fe0052b50051fe0051e30151f50052b5005","0x500052b50050340056340150152b500501500701504f1fe1f5019129005","0x70051e301502f0052b500502f00503301502c0052b500502c0050af015","0x601505000702f02c1290050500052b50050500056330150070052b5005","0x701500f00c0076380090af0072b50070050150070050150152b5005015","0x52b50050af0050af0150101290072b500512900523e0150152b5005015","0x527a0150152b50050150070150b70056390152b500701000527b0150af","0x52b50050f600700763a0150f60052b50050ba0052d10150152b5005129","0x50330150af0052b50050af0050af01502e0052b500502100563b015021","0x701502e0090af0ba00502e0052b500502e00563c0150090052b5005009","0x150af0052b50050af0050af0150152b50050b70052760150152b5005015","0x90af0ba63d0150070052b50050070050310150090052b5005009005033","0x701502f00563f02c0052b500702b00563e01502b0270060ba2b5005007","0x52b50070310056410150310330072b500502c0056400150152b5005015","0x151120052b50050340ba0072d00150152b50050150070151b5005642034","0x50060050af0151c80052b50051b91290071100151b90052b5005015273","0x150330052b50050330050310150270052b50050270050330150060052b5","0x270060af4240151c80052b50051c80051b50151120052b5005112005423","0x152b50050150070151e11d91d20ba0051e11d91d20ba2b50051c8112033","0x2b50051b50054290150152b50050ba0051cb0150152b500512900527a015","0x1511a0052b50051e400563b0151e40052b50051e303300763a0151e3005","0x511a00563c0150270052b50050270050330150060052b50050060050af","0x2b500512900527a0150152b500501500701511a0270060ba00511a0052b5","0x60050af0151e60052b500502f0056430150152b50050ba0051cb015015","0x1e60052b50051e600563c0150270052b50050270050330150060052b5005","0x1cb0150152b500512900527a0150152b50050150070151e60270060ba005","0x150190052b500501502b0150152b50050070052800150152b50050ba005","0x51f50190072950151f50052b50051f50051b50151f50052b50050152a0","0x152060052b50051fe2020072990152020052b50050152970151fe0052b5","0x500f00503301500c0052b500500c0050af0152090052b5005206005643","0x501500601520900f00c0ba0052090052b500520900563c01500f0052b5","0x501500701500f00c0076440090af0072b50070050150070050150152b5","0x150af0052b50050af0050af0150101290072b500512900523e0150152b5","0x512900527a0150152b50050150070150b70056450152b500701000527b","0x150210052b50050f60070076470150f60052b50050ba0056460150152b5","0x50090050330150af0052b50050af0050af01502e0052b5005021005648","0x501500701502e0090af0ba00502e0052b500502e0056490150090052b5","0x151290150060052b50050070050ba0150152b50050b70052760150152b5","0x2b500501500701502c00564a02b0270072b50070060050090150152b5005","0x520f0150330052b500502f00502f01502f0052b500502b005027015015","0x52b500503100521c0150340052b500502700500f0150310052b5005033","0x1120052b50050150f60150152b500501500701501564b0050150b70151b5","0x1b900521c0150340052b500502c00500f0151b90052b500511200521f015","0x2b50050150070151d200564c1c80052b50071b50052370151b50052b5005","0x150152b50050150070151e300564d1e11d90072b5007034005009015015","0x511a00520f01511a0052b50051e400502f0151e40052b50051e1005027","0x151f50052b50051e600521c0150190052b50051d900500f0151e60052b5","0x21f0151fe0052b50050150f60150152b500501500701501564e0050150b7","0x2b500520200521c0150190052b50051e300500f0152020052b50051fe005","0x150152b500501500701520900564f2060052b50071f50052370151f5005","0x1900500f01521c0052b500520f00565001520f0052b50052061c8007269","0x70150156520050150b70152370052b500521c00565101521f0052b5005","0x1504f0052b50052090056530150152b50051c800527a0150152b5005015","0x6520050150b70152370052b500504f00565101521f0052b500501900500f","0x503400500f0150500052b50051d20056530150152b5005015007015015","0x150520052b500521f00502c0152370052b500505000565101521f0052b5","0x50150060150152b50050150070152590056552530052b5007237005654","0x1100152640052b50050152730152620052b50052530ba0076560150152b5","0x50090050330150af0052b50050af0050af0150540052b5005264129007","0x152620052b500526200542c0150520052b50050520050310150090052b5","0x26f26d0ba2b50050542620520090af0af42d0150540052b50050540051b5","0x27a0150152b50050150060150152b500501500701527126f26d0ba005271","0x27a0052b50052590054310150152b50050ba0051630150152b5005129005","0x50af0152910052b50052800056480152800052b500527a052007647015","0x52b50052910056490150090052b50050090050330150af0052b50050af","0x150152b500512900527a0150152b50050150070152910090af0ba005291","0x2930052b500501502b0150152b50050070052800150152b50050ba005163","0x2952930072950152950052b50052950051b50152950052b50050152a0015","0x580052b50052972990072990152990052b50050152970152970052b5005","0xf00503301500c0052b500500c0050af0150590052b5005058005657015","0x1538001505900f00c0ba0050590052b500505900564901500f0052b5005","0x50156580150152b50050150060150152b50050152040150090052b5005","0x27c0150100052b500500c00508d01500f0052b500501565901500c0052b5","0x52b50050f600509a0150f60052b50050150990150b70052b500500f005","0xaf2560150100052b50050100051b50150b70052b50050b700526e0150f6","0x2b500501500701502b0270060ba65a02e0210072b50070100b70f6007005","0x2f0052ce01502f0052b500502c00565b01502c0052b50050153ba015015","0x340052b500503100565d0150152b500503300565c0150310330072b5005","0x50150990151120052b50051b500527c0151b50052b500503400527f015","0x151b90052b50051b900509a0150210052b50050210050330151b90052b5","0x1d91d21c80ba2b50071121b902e02112909c0151120052b500511200526e","0x151d90052b50051d90051b50150152b50050150070151e41e31e10ba65e","0x1d90150070e30151d20052b50051d20051e40151c80052b50051c8005033","0x52b50050153ba0150152b500501500701501900565f1e611a0072b5007","0x27c0152020052b50051fe00508d0151fe1290072b50051290053bb0151f5","0x52b500520900509a0152090052b50050150990152060052b50051f5005","0xaf25601511a0052b500511a0050af0152060052b500520600526e015209","0x2b500501500701523721f21c0ba6600af20f0072b50072022062091d21c8","0x1291e60076610150500052b500501502b01504f0052b500501502b015015","0x2590052b50052530056620152530052b50050520052cd0150520052b5005","0x11a0050af0150152b50052620050000152642620072b500525900519e015","0x2640052b500526400539d01520f0052b500520f00503301511a0052b5005","0x900712b0150500052b500505000503401504f0052b500504f005034015","0x1526f26d0540ba2b500505004f26420f11a0af39e0150af0052b50050af","0x501500701527a0056632710052b500726f00539f0150152b5005015129","0x150152b50052930051e60152932912800ba2b50052710053a10150152b5","0x529100506a0150152b50052950052910152972950072b500528000506a","0x150590052b500529700502c0150152b50052990052910150582990072b5","0x66405e29e0072b500705c0590af26d1293a201505c0052b500505800502c","0x50150f60150152b50050150060150152b50050150070152a92aa0600ba","0x2a80052b50050640056660150640052b50050620ba0076650150620052b5","0x5e0051e401529e0052b500529e0050330150540052b50050540050af015","0x70152a805e29e0541290052a80052b50052a80052d201505e0052b5005","0x152a70052b50050600050330150152b50050ba00529c0150152b5005015","0x6670050150b70152a40052b50052a90050340152a60052b50052aa0051e4","0x2b500527a0052550150152b50050ba00529c0150152b5005015007015015","0x1e40152a70052b500526d0050330150152b500506a0050bf0152a306a007","0x152b50050150060152a40052b50052a30050340152a60052b50050af005","0x2a00056680152a00052b50052a40280072990150280052b5005015297015","0x2a70052b50052a70050330150540052b50050540050af0150710052b5005","0x2a70541290050710052b50050710052d20152a60052b50052a60051e4015","0x51e60050e20150152b50050ba00529c0150152b50050150070150712a6","0x50152970150152b50050090051fb0150152b50051290050e20150152b5","0x730052b50050700056680150700052b500523706f00729901506f0052b5","0x21f0051e401521c0052b500521c00503301511a0052b500511a0050af015","0x701507321f21c11a1290050730052b50050730052d201521f0052b5005","0x1fb0150152b50051290050e20150152b50050ba00529c0150152b5005015","0x150760052b50050150cb01529f0052b500501502b0150152b5005009005","0x190050af01529d0052b500507629f0072950150760052b50050760051b5","0x29a0052b50051d20051e401529b0052b50051c800503301529c0052b5005","0x152b50050150070150156690050150b70152980052b500529d005034015","0x2b50051290050e20150152b50050ba00529c0150152b50050090051fb015","0x51e401529b0052b50051e100503301529c0052b50050150050af015015","0x2960052b50050152970152980052b50051e400503401529a0052b50051e3","0x50af0152920052b50052940056680152940052b5005298296007299015","0x52b500529a0051e401529b0052b500529b00503301529c0052b500529c","0x2b500501500701529229a29b29c1290052920052b50052920052d201529a","0x51290050e20150152b50050ba00529c0150152b50050090051fb015015","0x6680150820052b500502b2900072990152900052b50050152970150152b5","0x2b50050060050330150150052b50050150050af0150e30052b5005082005","0x1290050e30052b50050e30052d20150270052b50050270051e4015006005","0x90af0072b50070050150070050150152b50050150060150e3027006015","0x150b70100072b500500700566b0150152b500501500701500f00c00766a","0x2b50050b70050ac0150f60100072b50050100050ac0150152b5005015129","0x66c0152b50070210f60075880150af0052b50050af0050af0150210b7007","0x2b50050100050ac01502e0052b500501545c0150152b5005015007015015","0x72b500702e0060af0ba58b01502e0052b500502e00509a015006010007","0x330052b500501000566e0150152b500501500701502f02c00766d02b027","0x3300566f0150340052b500502b00509a0150310052b50050270050af015","0x50150060150152b50050150070150156700050150b70151b50052b5005","0xba0051d80150152b50050100052660150152b500502f0052660150152b5","0x1502b0150152b50050b70052660150152b50051290052910150152b5005","0x151b90052b50051b90051b50151b90052b50050155dc0151120052b5005","0x1c81d20072990151d20052b50050152970151c80052b50051b9112007295","0x2c0052b500502c0050af0151e10052b50051d90056710151d90052b5005","0x902c0ba0051e10052b50051e10056720150090052b5005009005033015","0x2b50051e30056730151e30052b50050150f60150152b50050150070151e1","0x66f0150340052b500501000509a0150310052b50050af0050af0151e4005","0x150070151e600567511a0052b50071b50056740151b50052b50051e4005","0x6770150190052b50050ba0056760150152b500511a0052660150152b5005","0x1fe0050270150152b50050150070152020056781fe1f50072b5007019005","0x20f0052b50051f50050340152090052b500520600520f0152060052b5005","0x152b50050150070150156790050150b701521c0052b500520900521c015","0x52020050340152370052b500521f00521f01521f0052b50050150f6015","0x5004f0072b500521c00567a01521c0052b500523700521c01520f0052b5","0x527a0150152b500501500701525300567b0520052b5007050005237015","0x2b500501500701526200567c2590052b500704f0052370150152b5005052","0x340074510152640052b50052591290072950150152b5005015006015015","0x52b50050310050af01526d0052b500520f00513b0150540052b50050b7","0x51e70150540052b50050540054530150090052b5005009005033015031","0x26d0540090310af4540152640052b500526400503401526d0052b500526d","0x60150152b500501500701527a27126f0ba00527a27126f0ba2b5005264","0x2910150152b50050340052660150152b50052620051e60150152b5005015","0x150152b50051290052910150152b50050b70052660150152b500520f005","0x52b50052910051b50152910052b500501559c0152800052b500501502b","0x72990152950052b50050152970152930052b5005291280007295015291","0x2b50050310050af0152990052b50052970056710152970052b5005293295","0xba0052990052b50052990056720150090052b5005009005033015031005","0x52530051e60150152b50050150060150152b5005015007015299009031","0x13b0150580052b50050b70340074510150152b500504f00567d0150152b5","0x5c12905905812967e01505c0052b50050150f60150590052b500520f005","0x310052b50050310050af01505e0052b500529e0052ca01529e0052b5005","0x90310ba00505e0052b500505e0056720150090052b5005009005033015","0x152b50051e60051e60150152b50050150060150152b500501500701505e","0x6012967e0152aa0052b50050150f60150600052b50050b7034007451015","0x50310050af0150620052b50052a90052ca0152a90052b50052aa1290ba","0x50620052b50050620056720150090052b50050090050330150310052b5","0x51d80150152b50050070054570150152b50050150070150620090310ba","0x2a00150640052b500501502b0150152b50051290052910150152b50050ba","0x2b50052a80640072950152a80052b50052a80051b50152a80052b5005015","0x6710152a40052b50052a72a60072990152a60052b50050152970152a7005","0x2b500500f00503301500c0052b500500c0050af01506a0052b50052a4005","0x2b500501541d01506a00f00c0ba00506a0052b500506a00567201500f005","0x2b50050070056760150152b50050150060150152b5005015204015129005","0x152b500501500701500f00567f00c0090072b50070af0056770150af005","0x90050340150b70052b500501000520f0150100052b500500c005027015","0x70150156800050150b70150210052b50050b700521c0150f60052b5005","0x150060052b500502e00521f01502e0052b50050150f60150152b5005015","0x70210052370150210052b500500600521c0150f60052b500500f005034","0x72b50070270150072160150152b500501500701502b0056810270052b5","0x990150310052b500501502b0150152b500501500701503300568202f02c","0x1b50340072b50050340050ac0150152b50050151290150340052b5005015","0xba2130151b50052b50051b500509a01511202f0072b500502f0050ac015","0x2660150152b50050150070151d91d20076831c81b90072b50071121b502c","0x1e10052b50051b90050af0150152b50050340052660150152b50051c8005","0x150b70151e30052b50051e300509a0151e302f0072b500502f0050ac015","0x1d20050af0150152b50051d90052660150152b5005015007015015684005","0x52b500502f1e30074510151e30052b500503400509a0151e10052b5005","0x50330151e10052b50051e10050af01511a0052b50050f600513b0151e4","0x52b500511a0051e70151e40052b50051e40054530150050052b5005005","0xba2b500503111a1e40051e10af4540150310052b500503100503401511a","0x152b50050150070152020056851fe0052b50071f50054550151f50191e6","0x1e60150152b500520600545701520f0ba2092061292b50051fe005456015","0x150152b500501512901521c0052b50052090056760150152b500520f005","0x4f00568623721f0072b500721c0056770150ba0052b50050ba129007421","0x2b500505000520f0150500052b50052370050270150152b5005015007015","0xb70152590052b500505200521c0152530052b500521f005034015052005","0x521f0152620052b50050150f60150152b5005015007015015687005015","0x52b500526400521c0152530052b500504f0050340152640052b5005262","0x2160150152b500501500701526d0056880540052b5007259005237015259","0x1502b0150152b500501500701527a00568927126f0072b50070541e6007","0x2932910072b50052910050ac0152910052b50050150990152800052b5005","0xba2130152930052b500529300509a0152952710072b50052710050ac015","0x2660150152b500501500701505905800768a2992970072b500729529326f","0x5c0052b50052970050af0150152b50052910052660150152b5005299005","0x150b701529e0052b500529e00509a01529e2710072b50052710050ac015","0x580050af0150152b50050590052660150152b500501500701501568b005","0x4510150152b500501500601529e0052b500529100509a01505c0052b5005","0x505c0050af0150600052b500525300513b01505e0052b500527129e007","0x1505e0052b500505e0054530150190052b500501900503301505c0052b5","0x1905c0af4540152800052b50052800050340150600052b50050600051e7","0x2a800568c0640052b50070620054550150622a92aa0ba2b500528006005e","0x545701506a2a42a62a71292b50050640054560150152b5005015007015","0x2a30052b50052a40ba00768d0150152b500506a0051e60150152b50052a7","0x50af0152a00052b500502800568f0150280052b50052a32a600768e015","0x52b50052a00056900152a90052b50052a90050330152aa0052b50052aa","0x150152b50050ba0052910150152b50050150070152a02a92aa0ba0052a0","0x52a90050330152aa0052b50052aa0050af0150710052b50052a8005691","0x50150070150712a92aa0ba0050710052b50050710056900152a90052b5","0x52530052910150152b50050ba0052910150152b50050150060150152b5","0x700051b50150700052b500501569201506f0052b500501502b0150152b5","0x29f0052b50050152970150730052b500507006f0072950150700052b5005","0x50af01529d0052b50050760056910150760052b500507329f007299015","0x52b500529d0056900150190052b500501900503301527a0052b500527a","0x1e60150152b50050150060150152b500501500701529d01927a0ba00529d","0x150152b50052530052910150152b50050ba0052910150152b500526d005","0x52b500529b0051b501529b0052b500501559c01529c0052b500501502b","0x72990152980052b500501529701529a0052b500529b29c00729501529b","0x2b50051e60050af0152940052b50052960056910152960052b500529a298","0xba0052940052b50052940056900150190052b50050190050330151e6005","0x2020056910150152b500512900541e0150152b50050150070152940191e6","0x190052b50050190050330151e60052b50051e60050af0152920052b5005","0x150152b50050150070152920191e60ba0052920052b5005292005690015","0x2900052b500501502b0150152b50050f60052910150152b500512900541e","0x822900072950150820052b50050820051b50150820052b5005015693015","0x28f0052b50050e30e20072990150e20052b50050152970150e30052b5005","0x50050330150330052b50050330050af01528e0052b500528f005691015","0x1500701528e0050330ba00528e0052b500528e0056900150050052b5005","0x52910150152b500512900541e0150152b500502b0051e60150152b5005","0x1b501528c0052b500501559c01528d0052b500501502b0150152b50050f6","0x2b500501529701508e0052b500528c28d00729501528c0052b500528c005","0x151060052b50051070056910151070052b500508e08d00729901508d005","0x51060056900150050052b50050050050330150150052b50050150050af","0x2b50050070056760150152b50050150060151060050150ba0051060052b5","0x152b500501500701500c0056940090af0072b5007129005677015129005","0xaf0050340150100052b500500f00520f01500f0052b5005009005027015","0x70150156950050150b70150f60052b500501000521c0150b70052b5005","0x1502e0052b500502100521f0150210052b50050150f60150152b5005015","0x70f60052370150f60052b500502e00521c0150b70052b500500c005034","0x72b50070060150072160150152b50050150070150270056960060052b5","0x990150330052b50050154220150152b500501500701502f00569702c02b","0x340310072b50050310050ac0150152b50050151290150310052b5005015","0xba2130150340052b500503400509a0151b502c0072b500502c0050ac015","0x2660150152b50050150070151d21c80076981b91120072b50071b503402b","0x1d90052b50051120050af0150152b50050310052660150152b50051b9005","0x150b70151e10052b50051e100509a0151e102c0072b500502c0050ac015","0x1c80050af0150152b50051d20052660150152b5005015007015015699005","0x4510150152b50050150060151e10052b500503100509a0151d90052b5005","0x51d90050af0151e40052b50050b700513b0151e30052b500502c1e1007","0x151e30052b50051e30054530150050052b50050050050330151d90052b5","0x50ba0051b50151e40052b50051e40051e70150330052b5005033005423","0x69b0150191e611a0ba2b50050ba1e40331e30051d900969a0150ba0052b5","0x51f500569d0150152b50050150070151fe00569c1f50052b5007019005","0x152b500520f0051e60150152b500520200545701520f2092062021292b5","0x50af01521f0052b500521c00569f01521c0052b500520620900769e015","0x52b500521f0056a00151e60052b50051e600503301511a0052b500511a","0x2370052b50051fe0056a10150152b500501500701521f1e611a0ba00521f","0x2370056a00151e60052b50051e600503301511a0052b500511a0050af015","0x50ba00527a0150152b50050150070152371e611a0ba0052370052b5005","0x50156a201504f0052b500501502b0150152b50050b70052910150152b5","0x520052b500505004f0072950150500052b50050500051b50150500052b5","0x2590056a10152590052b50050522530072990152530052b5005015297015","0x50052b500500500503301502f0052b500502f0050af0152620052b5005","0x150152b500501500701526200502f0ba0052620052b50052620056a0015","0x152b50050b70052910150152b50050ba00527a0150152b50050270051e6","0x2b50050540051b50150540052b500501559c0152640052b500501502b015","0x29901526f0052b500501529701526d0052b5005054264007295015054005","0x50150050af01527a0052b50052710056a10152710052b500526d26f007","0x527a0052b500527a0056a00150050052b50050050050330150150052b5","0x6770151290052b50050070056760150152b500501500601527a0050150ba","0x90050270150152b500501500701500c0056a30090af0072b5007129005","0xb70052b50050af0050340150100052b500500f00520f01500f0052b5005","0x152b50050150070150156a40050150b70150f60052b500501000521c015","0x500c00503401502e0052b500502100521f0150210052b50050150f6015","0x6a50060052b50070f60052370150f60052b500502e00521c0150b70052b5","0x56a602c02b0072b50070060150072160150152b5005015007015027005","0x52b50050150990150330052b500501542b0150152b500501500701502f","0x2c0050ac0150340310072b50050310050ac0150152b5005015129015031","0x71b503402b0ba2130150340052b500503400509a0151b502c0072b5005","0x2b50051b90052660150152b50050150070151d21c80076a71b91120072b5","0x2c0050ac0151d90052b50051120050af0150152b5005031005266015015","0x150156a80050150b70151e10052b50051e100509a0151e102c0072b5005","0x1d90052b50051c80050af0150152b50051d20052660150152b5005015007","0x2b50050ba0056a90150152b50050150060151e10052b500503100509a015","0x1511a0052b500502c1e10074510150152b50051e300527a0151e41e3007","0x50050050330151d90052b50051d90050af0151e60052b50050b700513b","0x151e60052b50051e60051e701511a0052b500511a0054530150050052b5","0x51d90096aa0151e40052b50051e40051b50150330052b500503300542c","0x56ac2020052b50071fe0056ab0151fe1f50190ba2b50051e40331e611a","0x45701521f21c20f2091292b50052020056ad0150152b5005015007015206","0x52b500521c20f0076ae0150152b500521f0051e60150152b5005209005","0x50330150190052b50050190050af01504f0052b50052370056af015237","0x701504f1f50190ba00504f0052b500504f0056b00151f50052b50051f5","0x190052b50050190050af0150500052b50052060056b10150152b5005015","0x1f50190ba0050500052b50050500056b00151f50052b50051f5005033015","0x2b50050b70052910150152b50050ba00527a0150152b5005015007015050","0x52530051b50152530052b50050156a20150520052b500501502b015015","0x152620052b50050152970152590052b50052530520072950152530052b5","0x2f0050af0150540052b50052640056b10152640052b5005259262007299","0x540052b50050540056b00150050052b500500500503301502f0052b5005","0x27a0150152b50050270051e60150152b500501500701505400502f0ba005","0x1526d0052b500501502b0150152b50050b70052910150152b50050ba005","0x526f26d00729501526f0052b500526f0051b501526f0052b500501559c","0x152800052b500527127a00729901527a0052b50050152970152710052b5","0x50050050330150150052b50050150050af0152910052b50052800056b1","0x50150060152910050150ba0052910052b50052910056b00150050052b5","0x501500701500f00c0076b20090af0072b50070050150070050150152b5","0x150b70052b50050100050ba0150100070072b50050070051f20150152b5","0xf600509a0150211290072b50051290050ac0150f60052b50050b7005214","0x152b50070f60210075880150af0052b50050af0050af0150f60052b5005","0x50ba01502e0070072b50050070051f20150152b50050150070150156b3","0x270060af0ba1f10150271290072b50051290050ac0150060052b500502e","0x2b500502c0051eb0150152b500501500701502f0056b402c02b0072b5007","0x1290150310052b500503300502f0150330052b500502c00502701502c005","0x72b500703102b00704f0150310052b50050310051b50150152b5005015","0x1d20052b50050155540150152b50050150070151c81b91120ba6b51b5034","0x1d20055580151e10052b50051b50055580151d90052b50050340050af015","0x1120050af0150152b50050150070150156b60050150b70151e30052b5005","0x1e30052b50051b90055580151e10052b50051c80055580151d90052b5005","0x748a0151e40052b50051e40052a80151e40052b50051e31e1007050015","0x52b50051e600509a0151e60052b500501545c01511a0052b50051e40ba","0x1f50190072b50071e61291d90ba58b01511a0052b500511a0054760151e6","0x190050af0150152b50050150060150152b50050150070152021fe0076b7","0x70052b50050070050310150090052b50050090050330150190052b5005","0x190af4780151f50052b50051f500509a01511a0052b500511a005476015","0x2b500501500701520f2092060ba00520f2092060ba2b50051f511a007009","0x2b500511a00547d0150152b50052020052660150152b5005015006015015","0x2b50050155dc01521c0052b500501502b0150152b5005007005280015015","0x152370052b500521f21c00729501521f0052b500521f0051b501521f005","0x50500056b80150500052b500523704f00729901504f0052b5005015297","0x150090052b50050090050330151fe0052b50051fe0050af0150520052b5","0x2660150152b50050150070150520091fe0ba0050520052b50050520056b9","0x150152b50050ba00547d0150152b50050070052800150152b5005129005","0x52b50052590051b50152590052b500501520e0152530052b500501502b","0x72990152640052b50050152970152620052b5005259253007295015259","0x2b500502f0050af01526d0052b50050540056b80150540052b5005262264","0xba00526d0052b500526d0056b90150090052b500500900503301502f005","0x50150f60150152b50050070052800150152b500501500701526d00902f","0x52b50052710056bb0152710052b500526f1290ba0ba6ba01526f0052b5","0x56b90150090052b50050090050330150af0052b50050af0050af01527a","0xba00547d0150152b500501500701527a0090af0ba00527a0052b500527a","0x1502b0150152b50050070052800150152b50051290052660150152b5005","0x152910052b50052910051b50152910052b50050152a00152800052b5005","0x2932950072990152950052b50050152970152930052b5005291280007295","0xc0052b500500c0050af0152990052b50052970056b80152970052b5005","0xf00c0ba0052990052b50052990056b901500f0052b500500f005033015","0x90076bc0af1290072b50070050150070050150152b5005015006015299","0x2b500501512901500f0052b50050070056bd0150152b500501500701500c","0x56be0b70100072b500700f0052d40151290052b50051290050af015015","0x50100056c00150210052b50050b70056bf0150152b50050150070150f6","0x150070150156c20050150b70150060052b50050210056c101502e0052b5","0x6c001502b0052b50050270056c30150270052b50050150f60150152b5005","0x2b500502e00547e0150060052b500502b0056c101502e0052b50050f6005","0x150152b50050150070150330056c402f0052b50070060052d501502c005","0x51290050af0150340052b50050310056c60150310052b500502f0056c5","0x150340052b50050340052a80150ba0052b50050ba0054800151290052b5","0x56c81b90052b50071120054850151121b50072b50050340ba1290ba6c7","0x72b50051b90054860150152b50050150060150152b50050150070151c8","0x50330151b50052b50051b50050af0150152b50051d90051e60151d91d2","0x52b50051d200548001502c0052b500502c00547f0150af0052b50050af","0x151e41e31e10ba0051e41e31e10ba2b50051d202c0af1b51294810151d2","0x6c90150152b500502c0054830150152b50050150060150152b5005015007","0x2b50050af0050330151b50052b50051b50050af01511a0052b50051c8005","0x2b500501500701511a0af1b50ba00511a0052b500511a0056ca0150af005","0x52b50050150f60150152b50050330051e60150152b5005015006015015","0x151f50052b50050190056cc0150190052b50051e60ba02c0ba6cb0151e6","0x51f50056ca0150af0052b50050af0050330151290052b50051290050af","0x2b50050070054830150152b50050150070151f50af1290ba0051f50052b5","0x2b50050152a00151fe0052b500501502b0150152b50050ba005488015015","0x152060052b50052021fe0072950152020052b50052020051b5015202005","0x520f0056c901520f0052b50052062090072990152090052b5005015297","0x1500c0052b500500c0050330150090052b50050090050af01521c0052b5","0x4870150152b500501500601521c00c0090ba00521c0052b500521c0056ca","0x2b50050156ce01500c0052b50050090056cd0150090af0072b5005007005","0x6d001500f0052b500500f0056cf01500c0052b500500c00509a01500f005","0x150990150152b50050b70052660150f60b70100ba2b500500f00c0150ba","0x1502e1290072b50051290050ac0150152b50050151290150210052b5005","0x150070150156d10152b500702102e0075880150100052b50050100050af","0x150271290072b50051290050ac0150060052b500501545c0150152b5005","0x2b500501545b0150152b50050150070150156d20152b5007006027007588","0x6d30152b500702b02c00758801502c1290072b50051290050ac01502b005","0x2b50051290050ac01502f0052b50050154500150152b5005015007015015","0x150152b50050150070150156d40152b500702f033007588015033129007","0x310340075880150341290072b50051290050ac0150310052b50050151f9","0xac0151b50052b500501546b0150152b50050150070150156d50152b5007","0x150070150156d60152b50071b51120075880151121290072b5005129005","0x151c81290072b50051290050ac0151b90052b500501546c0150152b5005","0x2b500501546d0150152b50050150070150156d70152b50071b91c8007588","0x60150152b50050150070150156d80152b50071d21290075880151d2005","0x4060150152b50050f60052660150152b50050af0054880150152b5005015","0x151e10052b50050156d90151d90052b500501502b0150152b50050ba005","0x50152970151e30052b50051e11d90072950151e10052b50051e10051b5","0x1e60052b500511a0056da01511a0052b50051e31e40072990151e40052b5","0x1e60056db0150050052b50050050050330150100052b50050100050af015","0x2b50050156dc0150152b50050150070151e60050100ba0051e60052b5005","0x50150070150156dd0050150b70151f50052b5005019005416015019005","0x1fe0054160151fe0052b50050156de0150152b50051290052660150152b5","0x70150156e00050150b70152020052b50051f50056df0151f50052b5005","0x4160152060052b50050156e10150152b50051290052660150152b5005015","0x156e20050150b70152090052b50052020056df0152020052b5005206005","0x20f0052b50050156e30150152b50051290052660150152b5005015007015","0x50150b701521c0052b50052090056df0152090052b500520f005416015","0x2b50050156e50150152b50051290052660150152b50050150070150156e4","0xb70152370052b500521c0056df01521c0052b500521f00541601521f005","0x156e70150152b50051290052660150152b50050150070150156e6005015","0x500052b50052370056df0152370052b500504f00541601504f0052b5005","0x150152b50051290052660150152b50050150070150156e80050150b7015","0x2b50050500054100150500052b50050520054160150520052b50050156e9","0x150152b50050150070152590056eb0152b50072530056ea015253050007","0x150152b50050f60052660150152b50050af0054880150152b5005015006","0x2620052b500501502b0150152b50050ba0054060150152b5005050005406","0x2642620072950152640052b50052640051b50152640052b500501559c015","0x26f0052b500505426d00729901526d0052b50050152970150540052b5005","0x50050330150100052b50050100050af0152710052b500526f0056da015","0x150070152710050100ba0052710052b50052710056db0150050052b5005","0x528000540601529128027a0ba2b50052590ba0100ba2d80150152b5005","0x150070152992970076ec2952930072b500729105027a0ba4130150152b5","0x150590052b50052950054160150580052b50052930050af0150152b5005","0x54060150152b50050150060150152b50050150070150156ed0050150b7","0x2b0150152b50050f60052660150152b50050af0054880150152b5005299","0x29e0052b500529e0051b501529e0052b500501541a01505c0052b5005015","0x600072990150600052b500501529701505e0052b500529e05c007295015","0x52b50052970050af0152a90052b50052aa0056da0152aa0052b500505e","0x2970ba0052a90052b50052a90056db0150050052b5005005005033015297","0x51290052660150152b50050ba0054060150152b50050150070152a9005","0x54160150580052b50050100050af0150620052b50050156ee0150152b5","0xf60072b50050f60050ac0150640052b50050155900150590052b5005062","0x150060150152b50050150070150156ef0152b50070642a80075880152a8","0x152a60052b50050155900152a70052b50050590af0076f00150152b5005","0x2a60580ba2130152a70052b50052a70054800152a60052b50052a600509a","0x2a40050af0150152b50050150070150282a30076f106a2a40072b50070f6","0x2a70052b50052a70054800150050052b50050050050330152a40052b5005","0x712a00ba2b500506a2a70052a41296f201506a0052b500506a00509a015","0x150152b50050280052660150152b500501500701506f0712a00ba00506f","0x730052b500501508f0150700052b500501502b0150152b50052a7005488","0x1529701529f0052b50050730700072950150730052b50050730051b5015","0x52b500529d0056da01529d0052b500529f0760072990150760052b5005","0x56db0150050052b50050050050330152a30052b50052a30050af01529c","0x50150060150152b500501500701529c0052a30ba00529c0052b500529c","0x29b00541601529b0052b50050156f30150152b50050f60052660150152b5","0x152942960076f429829a0072b500705929b0580ba41301529b0052b5005","0x52b50050150f60152920052b50052980af0076f00150152b5005015007","0xaf0150e30052b50050820056f60150820052b50052902920076f5015290","0x2b50050e30056db0150050052b500500500503301529a0052b500529a005","0x152b50052940054060150152b50050150070150e300529a0ba0050e3005","0x52b500501541a0150e20052b500501502b0150152b50050af005488015","0x29701528e0052b500528f0e200729501528f0052b500528f0051b501528f","0x2b500528c0056da01528c0052b500528e28d00729901528d0052b5005015","0x6db0150050052b50050050050330152960052b50052960050af01508e005","0x70050150152b500501500601508e0052960ba00508e0052b500508e005","0x523e0150152b500501500701500f00c0076f70090af0072b5007005015","0x2b500701000527b0150af0052b50050af0050af0150101290072b5005129","0x56f90150152b500512900527a0150152b50050150070150b70056f8015","0x2b50050210056fb0150210052b50050f60070076fa0150f60052b50050ba","0x6fc0150090052b50050090050330150af0052b50050af0050af01502e005","0x52760150152b500501500701502e0090af0ba00502e0052b500502e005","0x90150152b50050151290150060052b50050070050ba0150152b50050b7","0x2b0050270150152b500501500701502c0056fd02b0270072b5007006005","0x310052b500503300520f0150330052b500502f00502f01502f0052b5005","0x50150b70151b50052b500503100521c0150340052b500502700500f015","0x511200521f0151120052b50050150f60150152b50050150070150156fe","0x151b50052b50051b900521c0150340052b500502c00500f0151b90052b5","0x340050090150152b50050150070151d20056ff1c80052b50071b5005237","0x2b50051e100500c0150152b50050150070151e30057001e11d90072b5007","0xb70151e60052b50051e400501001511a0052b50051d900500f0151e4005","0x50210150190052b50050150f60150152b5005015007015015701005015","0x52b50051f500501001511a0052b50051e300500f0151f50052b5005019","0x270150152b50050150070152020057021fe0052b50071e600502e0151e6","0x2b50052090051b50152090052b500520600502f0152060052b50051fe005","0x2b500501500701521f00570321c20f0072b50072090af007216015209005","0x57040152370052b50052370050310152370052b500511a00502c015015","0x2b500705000570501520f0052b500520f0050af01505004f0072b5005237","0x52b500505221c1c80ba7070150152b5005015007015253005706052005","0x50af0152640052b500504f0050ba0152620052b5005259005708015259","0x52b500526200570901526d0052b500526400500f0150540052b500520f","0x152b50051c800527a0150152b500501500701501570a0050150b701526f","0x504f0050ba0152710052b500525300570b0150152b500521c005266015","0x1526d0052b500527a00500f0150540052b500520f0050af01527a0052b5","0x150152b500501500701501570a0050150b701526f0052b5005271005709","0x1570c0050150b70152800052b500521f0050af0150152b50051c800527a","0x152b50051c800527a0150152b50052020051e60150152b5005015007015","0x529100570b0152910052b50050150f60152800052b50050af0050af015","0x1526d0052b500511a00500f0150540052b50052800052640152930052b5","0x150152b500501500701501570a0050150b701526f0052b5005293005709","0x503400500f0150540052b50050af0050af0152950052b50051d200570b","0x152970052b500526d00502c01526f0052b500529500570901526d0052b5","0x50150060150152b500501500701505800570e2990052b500726f00570d","0x11001505c0052b50050152730150590052b50052990ba00770f0150152b5","0x50090050330150540052b50050540050af01529e0052b500505c129007","0x150590052b500505900557c0152970052b50052970050310150090052b5","0x6005e0ba2b500529e0592970090540af57d01529e0052b500529e0051b5","0x27a0150152b50050150060150152b50050150070152aa06005e0ba0052aa","0x2a90052b50050580057100150152b50050ba0051580150152b5005129005","0x50af0150640052b50050620056fb0150620052b50052a92970076fa015","0x52b50050640056fc0150090052b50050090050330150540052b5005054","0x150152b500512900527a0150152b50050150070150640090540ba005064","0x2a80052b500501502b0150152b50050070052800150152b50050ba005158","0x2a72a80072950152a70052b50052a70051b50152a70052b50050152a0015","0x6a0052b50052a62a40072990152a40052b50050152970152a60052b5005","0xf00503301500c0052b500500c0050af0152a30052b500506a005711015","0x77120152a300f00c0ba0052a30052b50052a30056fc01500f0052b5005","0xba0057140150152b50050150070151290057130ba0070072b5007005015","0x70052b50050070050af0150af0052b50050af0057150150af0052b5005","0xb700571a01000571900f00571800c0057170090052b502b0af005716015","0x572102b00572002700571f00600571e02e00571d02100571c0f600571b","0x150152b500501500701503400572503100572403300572302f00572202c","0x52b50051b50055580151b50052b50050155600150152b50050090051e6","0x152b500500c0051e60150152b50050150070150157260050150b7015112","0x7260050150b70151120052b50051b90055580151b90052b5005015727015","0x52b50050157280150152b500500f0051e60150152b5005015007015015","0x2b50050150070150157260050150b70151120052b50051c80055580151c8","0x51d20055580151d20052b50050157290150152b50050100051e6015015","0x50b70051e60150152b50050150070150157260050150b70151120052b5","0x150b70151120052b50051d90055580151d90052b500501572a0150152b5","0x501572b0150152b50050f60051e60150152b5005015007015015726005","0x150070150157260050150b70151120052b50051e10055580151e10052b5","0x55580151e30052b500501572c0150152b50050210051e60150152b5005","0x51e60150152b50050150070150157260050150b70151120052b50051e3","0x151120052b50051e40055580151e40052b500501572d0150152b500502e","0x72e0150152b50050060051e60150152b50050150070150157260050150b7","0x150157260050150b70151120052b500511a00555801511a0052b5005015","0x151e60052b500501572f0150152b50050270051e60150152b5005015007","0x150152b50050150070150157260050150b70151120052b50051e6005558","0x52b50050190055580150190052b50050157300150152b500502b0051e6","0x152b500502c0051e60150152b50050150070150157260050150b7015112","0x7260050150b70151120052b50051f50055580151f50052b5005015731015","0x52b50050157320150152b500502f0051e60150152b5005015007015015","0x2b50050150070150157260050150b70151120052b50051fe0055580151fe","0x52020055580152020052b50050157330150152b50050330051e6015015","0x50310051e60150152b50050150070150157260050150b70151120052b5","0x150b70151120052b50052060055580152060052b50050157340150152b5","0x50157350150152b50050340051e60150152b5005015007015015726005","0x1520f0052b50051120052db0151120052b50052090055580152090052b5","0x521c0057370150070052b50050070050af01521c0052b500520f005736","0x21f0052b500501502b0150152b500501500701521c00700700521c0052b5","0x23721f0072950152370052b50052370051b50152370052b5005015738015","0x520052b500504f0500072990150500052b500501529701504f0052b5005","0x2530057370151290052b50051290050af0152530052b5005052005739015","0x2b50050152040150090052b500501541d0152531290070052530052b5005","0xc00573a01500c0070072b50050070052dc0150152b5005015006015015","0xb70050e20150152b50050100050e201502e0210f60b701000f0092b5005","0x52800150152b500502100527a0150152b50050f600527a0150152b5005","0x52b50050060ba0072950150060052b500500f00502f0150152b500502e","0x3302f02c0092b500502b00573a01502b0070072b50050070052dc015027","0x527a0150152b50050330050e20150152b500502c00527a0151b5034031","0x5fb0150152b50051b50052800150152b500503400527a0150152b5005031","0x51b90270072950151b90052b500511200508d0151120052b500502f005","0x1d90092b50051d200573a0151d20070072b50050070052dc0151c80052b5","0x150152b50051e10050e20150152b50051d900527a0151e611a1e41e31e1","0x152b50051e60052800150152b500511a00527a0150152b50051e400527a","0x1c80072950151f50052b500501900508d0150190052b50051e30055fb015","0x2b50051fe00573a0151fe0070072b50050070052dc0150af0052b50051f5","0x2b50052060050e20150152b500520200527a01521f21c20f209206202009","0x521f0052800150152b500521c00527a0150152b50052090050e2015015","0x1504f0052b50052371290072950152370052b500520f00502f0150152b5","0x2642622592530520092b500505000573a0150500070072b50050070052dc","0x52590050e20150152b50052530050e20150152b500505200527a015054","0x26400502f0150152b50050540052800150152b500526200527a0150152b5","0x92b500500700573a01526f0052b500526d04f00729501526d0052b5005","0x152b500527a0050e20150152b500527100527a01529529329128027a271","0x2b500529300527a0150152b500529100527a0150152b50052800050e2015","0xba0152990052b500529700573b0152972950072b50052950051f2015015","0x2b50050590051ac0150590052b50050580052140150580052b5005299005","0x1529e0052b500505c26f00729501505c0052b500505c0051b501505c005","0x50050050330150150052b50050150050af01505e0052b500529500573b","0x1529e0052b500529e00503401505e0052b500505e0050310150050052b5","0x2aa0600ba2b500529e05e00501512973c0150af0052b50050af009007421","0x1830150152b500501500701506400573d0620052b50072a90051810152a9","0x52b50050150f60150152b50052a70051e60152a72a80072b5005062005","0x1506a0052b50052a400562a0152a40052b50052a62a80af0ba6290152a6","0x506a00562b0152aa0052b50052aa0050330150600052b50050600050af","0x2b50050af0052910150152b500501500701506a2aa0600ba00506a0052b5","0x50330150600052b50050600050af0152a30052b500506400573e015015","0x41d0152a32aa0600ba0052a30052b50052a300562b0152aa0052b50052aa","0x573f0150152b50050150060150152b50050152040150090052b5005015","0x50e20150f60b701000f1292b500500c00574001500c0070072b5005007","0x2f0150152b50050f60052800150152b50050b70050e20150152b5005010","0x500700573f01502e0052b50050210ba0072950150210052b500500f005","0x502700527a01502f02c02b0271292b50050060057400150060070072b5","0x2b0055fb0150152b500502f0052800150152b500502c0050e20150152b5","0x52b500503102e0072950150310052b500503300508d0150330052b5005","0x1c81b91121292b50051b50057400151b50070072b500500700573f015034","0x51d20052800150152b50051b90050e20150152b500511200527a0151d2","0x2950151e10052b50051d900508d0151d90052b50051c80055fb0150152b5","0x27a0151e611a1e41e31292b50050070057400150af0052b50051e1034007","0x150152b500511a0050e20150152b50051e40050e20150152b50051e3005","0x1f50050ba0151f50052b500501900573b0150191e60072b50051e60051f2","0x2060052b50052020051ac0152020052b50051fe0052140151fe0052b5005","0x573b0152090052b50052061290072950152060052b50052060051b5015","0x52b50050050050330150150052b50050150050af01520f0052b50051e6","0x74210152090052b500520900503401520f0052b500520f005031015005","0x1523721f21c0ba2b500520920f00501512973c0150af0052b50050af009","0x4f0051830150152b500501500701505000574104f0052b5007237005181","0x152590052b50050150f60150152b50052530051e60152530520072b5005","0x50af0152640052b500526200562a0152620052b50052590520af0ba629","0x52b500526400562b01521f0052b500521f00503301521c0052b500521c","0x150152b50050af0052910150152b500501500701526421f21c0ba005264","0x521f00503301521c0052b500521c0050af0150540052b500505000573e","0x501541d01505421f21c0ba0050540052b500505400562b01521f0052b5","0x50070057420150152b50050150060150152b50050152040150090052b5","0x50e201502e0210f60b701000f0092b500500c00574301500c0070072b5","0x2800150152b50050f600527a0150152b50050b70050e20150152b5005010","0x60052b500500f00502f0150152b500502e00527a0150152b5005021005","0x74301502b0070072b50050070057420150270052b50050060ba007295015","0xe20150152b500502c00527a0151b503403103302f02c0092b500502b005","0x150152b50050340052800150152b500503100527a0150152b5005033005","0x2b500511200508d0151120052b500502f0055fb0150152b50051b500527a","0x1d20070072b50050070057420151c80052b50051b90270072950151b9005","0x152b50051d900527a0151e611a1e41e31e11d90092b50051d2005743015","0x2b500511a0052800150152b50051e400527a0150152b50051e10050e2015","0x1900508d0150190052b50051e30055fb0150152b50051e600527a015015","0x72b50050070057420150af0052b50051f51c80072950151f50052b5005","0x520200527a01521f21c20f2092062020092b50051fe0057430151fe007","0x21c0052800150152b50052090050e20150152b50052060050e20150152b5","0x2950152370052b500520f00502f0150152b500521f00527a0150152b5005","0x500057430150500070072b500500700574201504f0052b5005237129007","0x2530050e20150152b500505200527a0150542642622592530520092b5005","0x527a0150152b500526200527a0150152b50052590050e20150152b5005","0x52b500526d00573b01526d2640072b50052640051f20150152b5005054","0x51ac01527a0052b50052710052140152710052b500526f0050ba01526f","0x2b500528004f0072950152800052b50052800051b50152800052b500527a","0x330150150052b50050150050af0152930052b500526400573b015291005","0x2b50052910050340152930052b50052930050310150050052b5005005005","0x2b500529129300501512973c0150af0052b50050af009007421015291005","0x2b50050150070150590057440580052b50072990051810152992972950ba","0x57430150152b500529e0051e601529e05c0072b5005058005183015015","0x50e20150152b500505e00527a0150640622a92aa06005e0092b5005007","0x2800150152b50052a900527a0150152b50052aa0050e20150152b5005060","0x2b50052a805c0072950152a80052b500506400502f0150152b5005062005","0x152a40052b50052a62a70af0ba6290152a60052b50050150f60152a7005","0x52970050330152950052b50052950050af01506a0052b50052a400562a","0x501500701506a2972950ba00506a0052b500506a00562b0152970052b5","0x5900573e0150152b50050070052dd0150152b50050af0052910150152b5","0x2970052b50052970050330152950052b50052950050af0152a30052b5005","0x90052b500501541d0152a32972950ba0052a30052b50052a300562b015","0x70072b50050070057450150152b50050150060150152b5005015204015","0x2b50050100050e201502e0210f60b701000f0092b500500c00574601500c","0x50210052800150152b50050f600527a0150152b50050b70050e2015015","0x72950150060052b500500f00502f0150152b500502e00527a0150152b5","0x502b00574601502b0070072b50050070057450150270052b50050060ba","0x50330050e20150152b500502c00527a0151b503403103302f02c0092b5","0x1b500527a0150152b50050340052800150152b500503100527a0150152b5","0x151b90052b500511200508d0151120052b500502f0055fb0150152b5005","0x57460151d20070072b50050070057450151c80052b50051b9027007295","0x50e20150152b50051d900527a0151e611a1e41e31e11d90092b50051d2","0x27a0150152b500511a0052800150152b50051e400527a0150152b50051e1","0x52b500501900508d0150190052b50051e30055fb0150152b50051e6005","0x151fe0070072b50050070057450150af0052b50051f51c80072950151f5","0x150152b500520200527a01521f21c20f2092062020092b50051fe005746","0x152b500521c0052800150152b50052090050e20150152b50052060050e2","0x2371290072950152370052b500520f00502f0150152b500521f00527a015","0x92b50050500057460150500070072b500500700574501504f0052b5005","0x152b50052530050e20150152b500505200527a015054264262259253052","0x2b500505400527a0150152b500526200527a0150152b50052590050e2015","0xba01526f0052b500526d00573b01526d2640072b50052640051f2015015","0x2b500527a0051ac01527a0052b50052710052140152710052b500526f005","0x152910052b500528004f0072950152800052b50052800051b5015280005","0x50050050330150150052b50050150050af0152930052b500526400573b","0x152910052b50052910050340152930052b50052930050310150050052b5","0x2972950ba2b500529129300501512973c0150af0052b50050af009007421","0x1830150152b50050150070150590057470580052b5007299005181015299","0x2b50050070057460150152b500529e0051e601529e05c0072b5005058005","0x2b50050600050e20150152b500505e00527a0150640622a92aa06005e009","0x50620052800150152b50052a900527a0150152b50052aa0050e2015015","0x152a70052b50052a805c0072950152a80052b500506400502f0150152b5","0x2a400562a0152a40052b50052a62a70af0ba6290152a60052b50050150f6","0x2970052b50052970050330152950052b50052950050af01506a0052b5005","0x150152b500501500701506a2972950ba00506a0052b500506a00562b015","0x52b500505900573e0150152b50050070057480150152b50050af005291","0x562b0152970052b50052970050330152950052b50052950050af0152a3","0x152040150090052b500501541d0152a32972950ba0052a30052b50052a3","0x74901500c0070072b50050070052de0150152b50050150060150152b5005","0xb70050e20150152b50050100050e20150f60b701000f1292b500500c005","0x2950150210052b500500f00502f0150152b50050f60052800150152b5005","0x60057490150060070072b50050070052de01502e0052b50050210ba007","0x2b500502c0050e20150152b500502700527a01502f02c02b0271292b5005","0x3300508d0150330052b500502b0055fb0150152b500502f005280015015","0x72b50050070052de0150340052b500503102e0072950150310052b5005","0x152b500511200527a0151d21c81b91121292b50051b50057490151b5007","0x2b50051c80055fb0150152b50051d20052800150152b50051b90050e2015","0x150af0052b50051e10340072950151e10052b50051d900508d0151d9005","0x50e20150152b50051e300527a0151e611a1e41e31292b5005007005749","0x191e60072b50051e60051f20150152b500511a0050e20150152b50051e4","0x1fe0052140151fe0052b50051f50050ba0151f50052b500501900573b015","0x2060052b50052060051b50152060052b50052020051ac0152020052b5005","0x50af01520f0052b50051e600573b0152090052b5005206129007295015","0x52b500520f0050310150050052b50050050050330150150052b5005015","0x73c0150af0052b50050af0090074210152090052b500520900503401520f","0x74a04f0052b500723700518101523721f21c0ba2b500520920f005015129","0x51e60152530520072b500504f0051830150152b5005015007015050005","0x52b50052590520af0ba6290152590052b50050150f60150152b5005253","0x503301521c0052b500521c0050af0152640052b500526200562a015262","0x701526421f21c0ba0052640052b500526400562b01521f0052b500521f","0x150540052b500505000573e0150152b50050af0052910150152b5005015","0x505400562b01521f0052b500521f00503301521c0052b500521c0050af","0x2b50050152040150090052b500501541d01505421f21c0ba0050540052b5","0xc00574c01500c0070072b500500700574b0150152b5005015006015015","0xb70050e20150152b50050100050e201502e0210f60b701000f0092b5005","0x52800150152b500502100527a0150152b50050f600527a0150152b5005","0x52b50050060ba0072950150060052b500500f00502f0150152b500502e","0x3302f02c0092b500502b00574c01502b0070072b500500700574b015027","0x527a0150152b50050330050e20150152b500502c00527a0151b5034031","0x5fb0150152b50051b50052800150152b500503400527a0150152b5005031","0x51b90270072950151b90052b500511200508d0151120052b500502f005","0x1d90092b50051d200574c0151d20070072b500500700574b0151c80052b5","0x150152b50051e10050e20150152b50051d900527a0151e611a1e41e31e1","0x152b50051e60052800150152b500511a00527a0150152b50051e400527a","0x1c80072950151f50052b500501900508d0150190052b50051e30055fb015","0x2b50051fe00574c0151fe0070072b500500700574b0150af0052b50051f5","0x2b50052060050e20150152b500520200527a01521f21c20f209206202009","0x521f0052800150152b500521c00527a0150152b50052090050e2015015","0x1504f0052b50052371290072950152370052b500520f00502f0150152b5","0x2642622592530520092b500505000574c0150500070072b500500700574b","0x52590050e20150152b50052530050e20150152b500505200527a015054","0x26400502f0150152b50050540052800150152b500526200527a0150152b5","0x92b500500700574c01526f0052b500526d04f00729501526d0052b5005","0x152b500527a0050e20150152b500527100527a01529529329128027a271","0x2b500529300527a0150152b500529100527a0150152b50052800050e2015","0xba0152990052b500529700573b0152972950072b50052950051f2015015","0x2b50050590051ac0150590052b50050580052140150580052b5005299005","0x1529e0052b500505c26f00729501505c0052b500505c0051b501505c005","0x50050050330150150052b50050150050af01505e0052b500529500573b","0x1529e0052b500529e00503401505e0052b500505e0050310150050052b5","0x2aa0600ba2b500529e05e00501512973c0150af0052b50050af009007421","0x1830150152b500501500701506400574d0620052b50072a90051810152a9","0x52b50050150f60150152b50052a70051e60152a72a80072b5005062005","0x1506a0052b50052a400562a0152a40052b50052a62a80af0ba6290152a6","0x506a00562b0152aa0052b50052aa0050330150600052b50050600050af","0x2b50050af0052910150152b500501500701506a2aa0600ba00506a0052b5","0x50330150600052b50050600050af0152a30052b500506400573e015015","0x60152a32aa0600ba0052a30052b50052a300562b0152aa0052b50052aa","0x701500f00c00774e0090af0072b50070050150070050150152b5005015","0x52b50050100050ba0150100070072b50050070051f20150152b5005015","0x9a0150211290072b50051290050ac0150f60052b50050b70052140150b7","0x70f60210075880150af0052b50050af0050af0150f60052b50050f6005","0x1500602e0072b50050070057500150152b500501500701501574f0152b5","0x51290050ac01502b0052b50050270057510150270052b50050060050ba","0x701503100575203302f0072b500702c02b0af0ba1f101502c1290072b5","0x340052b50050330050270150330052b50050330051eb0150152b5005015","0xba0072950151b50052b50051b50051b50151b50052b500503400502f015","0x1b90052b50051b900509a0151b90052b500501545c0151120052b50051b5","0x7531d21c80072b50071b912902f0ba58b0151120052b5005112005034015","0x50330151c80052b50051c80050af0150152b50050150070151e11d9007","0x52b500511200503401502e0052b500502e0050310150090052b5005009","0xba2b50051d211202e0091c80af62c0151d20052b50051d200509a015112","0x2b50051e10052660150152b500501500701511a1e41e30ba00511a1e41e3","0x2b500501502b0150152b500502e0052800150152b5005112005291015015","0x72950150190052b50050190051b50150190052b50050155dc0151e6005","0x2b50051f51fe0072990151fe0052b50050152970151f50052b50050191e6","0x330151d90052b50051d90050af0152060052b5005202005754015202005","0x152060091d90ba0052060052b50052060057550150090052b5005009005","0x150152b500502e0052800150152b50051290052660150152b5005015007","0x20f0052b500501520e0152090052b500501502b0150152b50050ba005291","0x1529701521c0052b500520f20900729501520f0052b500520f0051b5015","0x52b50052370057540152370052b500521c21f00729901521f0052b5005","0x57550150090052b50050090050330150310052b50050310050af01504f","0x70052800150152b500501500701504f0090310ba00504f0052b500504f","0x520052b50050501290ba0ba7560150500052b50050150f60150152b5005","0x90050330150af0052b50050af0050af0152530052b5005052005757015","0x150070152530090af0ba0052530052b50052530057550150090052b5005","0x52800150152b50051290052660150152b50050ba0052910150152b5005","0x1b50152620052b50050152a00152590052b500501502b0150152b5005007","0x2b50050152970152640052b50052622590072950152620052b5005262005","0x1526f0052b500526d00575401526d0052b5005264054007299015054005","0x526f00575501500f0052b500500f00503301500c0052b500500c0050af","0x70050150070050150152b500501500601526f00f00c0ba00526f0052b5","0x2b50050070051f20150152b500501500701500f00c0077580090af0072b5","0x150f60052b50050b70052140150b70052b50050100050ba015010007007","0xaf0050af0150f60052b50050f600509a0150211290072b50051290050ac","0x152b50050150070150157590152b50070f60210075880150af0052b5005","0x57510150270052b50050060050ba01500602e0072b5005007005750015","0x2c02b0af0ba1f101502c1290072b50051290050ac01502b0052b5005027","0x2b50050330051eb0150152b500501500701503100575a03302f0072b5007","0x1b50151b50052b500503400502f0150340052b5005033005027015033005","0x2b500501545c0151120052b50051b50ba0072950151b50052b50051b5005","0x58b0151120052b50051120050340151b90052b50051b900509a0151b9005","0x150152b50050150070151e11d900775b1d21c80072b50071b912902f0ba","0x502e0050310150090052b50050090050330151c80052b50051c80050af","0x151d20052b50051d200509a0151120052b500511200503401502e0052b5","0x701511a1e41e30ba00511a1e41e30ba2b50051d211202e0091c80af635","0x2800150152b50051120052910150152b50051e10052660150152b5005015","0x150190052b50050155dc0151e60052b500501502b0150152b500502e005","0x50152970151f50052b50050191e60072950150190052b50050190051b5","0x2060052b50052020057540152020052b50051f51fe0072990151fe0052b5","0x2060057550150090052b50050090050330151d90052b50051d90050af015","0x51290052660150152b50050150070152060091d90ba0052060052b5005","0x501502b0150152b50050ba0052910150152b500502e0052800150152b5","0x29501520f0052b500520f0051b501520f0052b500501520e0152090052b5","0x521c21f00729901521f0052b500501529701521c0052b500520f209007","0x150310052b50050310050af01504f0052b50052370057540152370052b5","0x4f0090310ba00504f0052b500504f0057550150090052b5005009005033","0x500052b50050150f60150152b50050070052800150152b5005015007015","0xaf0152530052b50050520057570150520052b50050501290ba0ba756015","0x2b50052530057550150090052b50050090050330150af0052b50050af005","0x152b50050ba0052910150152b50050150070152530090af0ba005253005","0x52b500501502b0150152b50050070052800150152b5005129005266015","0x2590072950152620052b50052620051b50152620052b50050152a0015259","0x52b50052640540072990150540052b50050152970152640052b5005262","0x503301500c0052b500500c0050af01526f0052b500526d00575401526d","0x75c01526f00f00c0ba00526f0052b500526f00575501500f0052b500500f","0x1500f0052b50050152050150090052b50050152050151290052b5005015","0x150100052b50050070050ba0150152b50050150060150152b5005015204","0x50270150152b500501500701502100575d0f60b70072b5007010005009","0x52b500500600520f0150060052b500502e00502f01502e0052b50050f6","0x150b701502c0052b500502700521c01502b0052b50050b700500f015027","0x2f00521f01502f0052b50050150f60150152b500501500701501575e005","0x2c0052b500503300521c01502b0052b500502100500f0150330052b5005","0x71de0150152b500501500701503100575f0af0052b500702c005237015","0x70151120057601b50340072b500702b0050090150af0052b50050af009","0x1c80052b50051b900502f0151b90052b50051b50050270150152b5005015","0x1d200521c0151d90052b500503400500f0151d20052b50051c800520f015","0x50150f60150152b50050150070150157610050150b70151e10052b5005","0x151d90052b500511200500f0151e40052b50051e300521f0151e30052b5","0x71e100523701511a0052b50051d900502c0151e10052b50051e400521c","0x11a0052b500511a0050310150152b50050150070151e600576200c0052b5","0x76401500c0052b500500c00f0071de0151f50190072b500511a005763015","0x50190050ba0150152b50050150070151fe0057650ba0052b50071f5005","0x2060072b50072020050090150ba0052b50050ba1290072da0152020052b5","0xf01521c0052b500520900500c0150152b500501500701520f005766209","0x157670050150b70152370052b500521c00501001521f0052b5005206005","0x52b500504f00502101504f0052b50050150f60150152b5005015007015","0x502e0152370052b500505000501001521f0052b500520f00500f015050","0x2b50050520050270150152b50050150070152530057680520052b5007237","0x502f0152640052b500521f00502c0152620052b500501542b015259005","0x52b50050050050330150150052b50050150050af0150540052b5005259","0x51b50152620052b500526200542c0152640052b5005264005031015005","0x1527126f26d0ba2b50050542622640050150af42d0150540052b5005054","0x27a00542f0150152b500501500701528000576927a0052b500727100542e","0x52b500526d0050af0152950052b50052910050ba0152932910072b5005","0x54300150580052b500529500500f0152990052b500526f005033015297","0x527a0150152b500501500701501576a0050150b70150590052b5005293","0x76b0150152b50050ba00567d0150152b50050af00527a0150152b500500c","0x2b500526f00503301526d0052b500526d0050af01505c0052b5005280005","0x2b500501500701505c26f26d0ba00505c0052b500505c00576c01526f005","0x529e00543101529e0052b50050150f60150152b50052530051e6015015","0x152990052b50050050050330152970052b50050150050af01505e0052b5","0x505800502c0150590052b500505e0054300150580052b500521f00500f","0x152b50050150070152a900576d2aa0052b50070590054320150600052b5","0x150640052b500506200576f0150620052b50052aa0ba00c0af12976e015","0x2970050af0152a70052b50052a80057710152a80052b5005064060007770","0x2a70052b50052a700576c0152990052b50052990050330152970052b5005","0x67d0150152b50050af00527a0150152b50050150070152a72992970ba005","0x2a60052b50052a90057720150152b500500c00527a0150152b50050ba005","0x50af01506a0052b50052a40057710152a40052b50052a6060007770015","0x52b500506a00576c0152990052b50052990050330152970052b5005297","0x150152b50050af00527a0150152b500501500701506a2992970ba00506a","0x52b50051fe0057720150152b50051290057730150152b500500c00527a","0xaf0152a00052b50050280057710150280052b50052a30190077700152a3","0x2b50052a000576c0150050052b50050050050330150150052b5005015005","0x152b50051290057730150152b50050150070152a00050150ba0052a0005","0x2b50051e60057720150152b500500f0051ff0150152b50050af00527a015","0x150700052b500506f00577101506f0052b500507111a007770015071005","0x507000576c0150050052b50050050050330150150052b50050150050af","0x2b50051290057730150152b50050150070150700050150ba0050700052b5","0x502b00502c0150152b50050090051ff0150152b500500f0051ff015015","0x760052b500529f07300777001529f0052b50050310057720150730052b5","0x50050330150150052b50050150050af01529d0052b5005076005771015","0x1547201529d0050150ba00529d0052b500529d00576c0150050052b5005","0x150070050150152b50050150060150152b500501520401500c0052b5005","0x700566b0150152b50050150070150f60b700777401000f0072b5007005","0x60210072b50050210050ac0150152b500501512901502e0210072b5005","0x758801500f0052b500500f0050af01502702e0072b500502e0050ac015","0x2b0052b500501545c0150152b50050150070150157750152b5007027006","0xba58b01502b0052b500502b00509a01502c0210072b50050210050ac015","0x66e0150152b500501500701503403100777603302f0072b500702b02c00f","0x2b500503300509a0151120052b500502f0050af0151b50052b5005021005","0x50150070150157770050150b70151b90052b50051b500566f015009005","0x50af00527a0150152b50050340052660150152b50050150060150152b5","0x2e0052660150152b500500c0057780150152b50050ba0051cb0150152b5","0x1502b0150152b50050210052660150152b50051290051d80150152b5005","0x151d20052b50051d20051b50151d20052b50050155dc0151c80052b5005","0x1d91e10072990151e10052b50050152970151d90052b50051d21c8007295","0x310052b50050310050af0151e40052b50051e30052d90151e30052b5005","0x100310ba0051e40052b50051e40057790150100052b5005010005033015","0x2b500511a00567301511a0052b50050150f60150152b50050150070151e4","0x66f0150090052b500502100509a0151120052b500500f0050af0151e6005","0x71b90056740150090052b500500900c0074730151b90052b50051e6005","0x2660150152b50050150060150152b50050150070151f500577a0190052b5","0x52b50050100050330151120052b50051120050af0150152b5005019005","0x1b50151fe0af0072b50050af00523e0151290052b50051290051e7015010","0x152092062020ba2b50051fe12901011212977b0151fe0052b50051fe005","0x20f00577e0150152b500501500701521c00577d20f0052b500720900577c","0x502e00900745101504f0052b50052370ba0072d001523721f0072b5005","0x152060052b50052060050330152020052b50052020050af0150500052b5","0x521f0051e701504f0052b500504f0054230150500052b5005050005453","0xaf21f04f05020620200969a0150af0052b50050af0051b501521f0052b5","0x527a0150152b50050150070152592530520ba0052592530520ba2b5005","0x2660150152b50050090052660150152b50050ba0051cb0150152b50050af","0x52b50052020050af0152620052b500521c0052d90150152b500502e005","0x2020ba0052620052b50052620057790152060052b5005206005033015202","0x2b50051f50051e60150152b50050150060150152b5005015007015262206","0x150f60152640052b500502e0090074510150152b50050af00527a015015","0x526d00578001526d0052b50050541290ba26412977f0150540052b5005","0x150100052b50050100050330151120052b50051120050af01526f0052b5","0x27a0150152b500501500701526f0101120ba00526f0052b500526f005779","0x150152b500500c0057780150152b50050ba0051cb0150152b50050af005","0x2710052b500501502b0150152b50051290051d80150152b5005007005457","0x27a27100729501527a0052b500527a0051b501527a0052b50050152a0015","0x2930052b50052802910072990152910052b50050152970152800052b5005","0xf60050330150b70052b50050b70050af0152950052b50052930052d9015","0x150060152950f60b70ba0052950052b50052950057790150f60052b5005","0x1500701501000f00778100c0090072b50070050150070050150152b5005","0xac0150152b50050151290150f60b70072b500500700566b0150152b5005","0x90050af01502e0f60072b50050f60050ac0150210b70072b50050b7005","0x152b50050150070150157820152b500702e0210075880150090052b5005","0x600509a0150270b70072b50050b70050ac0150060052b500501545c015","0x1503302f00778302c02b0072b50070060270090ba58b0150060052b5005","0x52b500502b0050af0150310052b50050b700566e0150152b5005015007","0x150b70151120052b500503100566f0151b50052b500502c00509a015034","0x50330052660150152b50050150060150152b5005015007015015784005","0xba0051d80150152b50050b70052660150152b50050af00527a0150152b5","0x1502b0150152b50050f60052660150152b50051290051630150152b5005","0x151c80052b50051c80051b50151c80052b50050155dc0151b90052b5005","0x1d21d90072990151d90052b50050152970151d20052b50051c81b9007295","0x2f0052b500502f0050af0151e30052b50051e10057850151e10052b5005","0xc02f0ba0051e30052b50051e300578601500c0052b500500c005033015","0x2b50051e40056730151e40052b50050150f60150152b50050150070151e3","0x66f0151b50052b50050b700509a0150340052b50050090050af01511a005","0x150070150190057871e60052b50071120056740151120052b500511a005","0x6770151f50052b50050ba0056760150152b50051e60052660150152b5005","0x2020050270150152b50050150070152060057882021fe0072b50071f5005","0x21c0052b50051fe00503401520f0052b500520900520f0152090052b5005","0x152b50050150070150157890050150b701521f0052b500520f00521c015","0x520600503401504f0052b500523700521f0152370052b50050150f6015","0x78a0500052b500721f00523701521f0052b500504f00521c01521c0052b5","0x502f0152530af0072b50050af00523e0150152b5005015007015052005","0x2b500501500701526200578b0152b500725900527b0152590052b5005253","0x2b500501500701501578c0050150b70152640052b500521c005034015015","0x26f00578d26d0540072b500721c0056770150152b5005262005276015015","0x2b500527100520f0152710052b500526d0050270150152b5005015007015","0xb70152910052b500527a00521c0152800052b500505400503401527a005","0x521f0152930052b50050150f60150152b500501500701501578e005015","0x52b500529500521c0152800052b500526f0050340152950052b5005293","0x27a0150152b500501500701529900578f2970052b5007291005237015291","0x72b50072640056770152640052b50052800050340150152b5005297005","0x1529e0052b50050590050270150152b500501500701505c005790059058","0x505e00521c0150600052b500505800503401505e0052b500529e00520f","0x2b50050150f60150152b50050150070150157910050150b70152aa0052b5","0x21c0150600052b500505c0050340150620052b50052a900521f0152a9005","0x150070152a80057920640052b50072aa0052370152aa0052b5005062005","0x7930152a70052b50050640500072690150152b50050150060150152b5005","0xf61b50074510152a60052b50052a71290076560152a70052b50052a7005","0x340052b50050340050af01506a0052b500506000513b0152a40052b5005","0x6a0051e70152a40052b50052a400545301500c0052b500500c005033015","0xaf0052b50050af0051b50152a60052b50052a600542c01506a0052b5005","0x2a00282a30ba0052a00282a30ba2b50050af2a606a2a400c0340096aa015","0x150152b50052a80051e60150152b50050150060150152b5005015007015","0x152b50050600052910150152b50051b50052660150152b50050af00527a","0x2b500505000527a0150152b50051290051630150152b50050f6005266015","0x506f0051b501506f0052b500501559c0150710052b500501502b015015","0x150730052b50050152970150700052b500506f07100729501506f0052b5","0x340050af0150760052b500529f00578501529f0052b5005070073007299","0x760052b500507600578601500c0052b500500c0050330150340052b5005","0x51e60150152b50050150060150152b500501500701507600c0340ba005","0x27a0150152b50051b50052660150152b50050af00527a0150152b5005299","0x150152b50051290051630150152b50050f60052660150152b5005050005","0x29c0052b500501559c01529d0052b500501502b0150152b5005280005291","0x1529701529b0052b500529c29d00729501529c0052b500529c0051b5015","0x52b50052980057850152980052b500529b29a00729901529a0052b5005","0x578601500c0052b500500c0050330150340052b50050340050af015296","0x50150060150152b500501500701529600c0340ba0052960052b5005296","0x1b50052660150152b50050af00527a0150152b50050520051e60150152b5","0x51630150152b50050f60052660150152b500521c0052910150152b5005","0x1b50152920052b500501559c0152940052b500501502b0150152b5005129","0x2b50050152970152900052b50052922940072950152920052b5005292005","0x150e20052b50050e30057850150e30052b5005290082007299015082005","0x50e200578601500c0052b500500c0050330150340052b50050340050af","0x152b50050150060150152b50050150070150e200c0340ba0050e20052b5","0x50f61b50074510150152b50050af00527a0150152b50050190051e6015","0x52b500528e1290ba28f12979401528e0052b50050150f601528f0052b5","0x50330150340052b50050340050af01528c0052b500528d00579501528d","0x701528c00c0340ba00528c0052b500528c00578601500c0052b500500c","0x1d80150152b50050070054570150152b50050af00527a0150152b5005015","0x1508e0052b500501502b0150152b50051290051630150152b50050ba005","0x508d08e00729501508d0052b500508d0051b501508d0052b50050152a0","0x151020052b50051071060072990151060052b50050152970151070052b5","0x501000503301500f0052b500500f0050af0150910052b5005102005785","0x700555a01509101000f0ba0050910052b50050910057860150100052b5","0xaf0052b50050af0057970150af0052b50050157960151290ba0072b5005","0x72b500700c00900779801500f00c0090ba2b50050af0ba0150ba59e015","0x210072b500700f0100077980150152b50050150070150f60057990b7010","0x150270052b500502e0050076f00150152b500501500701500600579a02e","0x502c00579701502c0052b500501579601502b0052b50050b70270076f0","0x502b00548001503103302f0ba2b500502c1290210ba59e01502c0052b5","0x501500701511200579b1b50340072b500703302f00779801502b0052b5","0x2b50050150070151d200579c1c81b90072b50070310340077980150152b5","0x151e10052b50051b51d90076f00151d90052b50051c802b0076f0015015","0x51e40056f60151e40052b50051e31e10076f50151e30052b50050150f6","0x511a0052b500511a0056db0151b90052b50051b90050af01511a0052b5","0x1b50054060150152b500502b0054880150152b500501500701511a1b9007","0x51b50150190052b500501559c0151e60052b500501502b0150152b5005","0x2b50051d20050af0151f50052b50050191e60072950150190052b5005019","0x501500701501579d0050150b70152020052b50051f50050340151fe005","0x501502b0150152b50050310052590150152b500502b0054880150152b5","0x2950152090052b50052090051b50152090052b500501559c0152060052b5","0x520f0050340151fe0052b50051120050af01520f0052b5005209206007","0x1521f0052b500520221c00729901521c0052b50050152970152020052b5","0x52370056db0151fe0052b50051fe0050af0152370052b500521f0056da","0x152b50051290052590150152b50050150070152371fe0070052370052b5","0x52b500501502b0150152b50050b70054060150152b5005005005488015","0x4f0072950150500052b50050500051b50150500052b500501559c01504f","0x52b50050520050340152530052b50050060050af0150520052b5005050","0x152b50051290052590150152b500501500701501579e0050150b7015259","0x52b500501502b0150152b500500f0052590150152b5005005005488015","0x2620072950152640052b50052640051b50152640052b500501559c015262","0x52b50050540050340152530052b50050f60050af0150540052b5005264","0x56da01526f0052b500525926d00729901526d0052b5005015297015259","0x52b50052710056db0152530052b50052530050af0152710052b500526f","0x1290072b50070050150070050150152b5005015006015271253007005271","0xac01500f0052b500501545c0150152b500501500701500c00900779f0af","0xf0100075880151290052b50051290050af0150100ba0072b50050ba005","0x4160150b70052b500501540f0150152b50050150070150157a00152b5007","0x2b500501545c0150f60052b50050b70070076f00150b70052b50050b7005","0x2130150f60052b50050f60054800150210052b500502100509a015021005","0x150152b500501500701502b0270077a100602e0072b50070210ba1290ba","0x50f60054800150af0052b50050af00503301502e0052b500502e0050af","0x2b50050060f60af02e1296f20150060052b500500600509a0150f60052b5","0x502b0052660150152b500501500701503302f02c0ba00503302f02c0ba","0x501508f0150310052b500501502b0150152b50050f60054880150152b5","0x1b50052b50050340310072950150340052b50050340051b50150340052b5","0x1b90056da0151b90052b50051b51120072990151120052b5005015297015","0xaf0052b50050af0050330150270052b50050270050af0151c80052b5005","0x150152b50050150070151c80af0270ba0051c80052b50051c80056db015","0x52b50051d20054160151d20052b50050156f30150152b50050ba005266","0x76f50151e10052b50050150f60151d90052b50051d20070076f00151d2","0x2b50051290050af0151e40052b50051e30056f60151e30052b50051e11d9","0xba0051e40052b50051e40056db0150af0052b50050af005033015129005","0xba0052660150152b50050070054880150152b50050150070151e40af129","0x51b50151e60052b50050152a001511a0052b500501502b0150152b5005","0x52b50050152970150190052b50051e611a0072950151e60052b50051e6","0xaf0152020052b50051fe0056da0151fe0052b50050191f50072990151f5","0x2b50052020056db01500c0052b500500c0050330150090052b5005009005","0x70050050090150050052b50050150050ba01520200c0090ba005202005","0x52b50050ba0050270150152b50050150070151290057a20ba0070072b5","0x500f01500c0052b500500900520f0150090052b50050af00502f0150af","0x150157a30050150b70150100052b500500c00521c01500f0052b5005007","0xf60052b50050b700521f0150b70052b50050150f60150152b5005015007","0x100052370150100052b50050f600521c01500f0052b500512900500f015","0x72b500700f0050090150152b500501500701502e0057a40210052b5007","0x1502c0052b50050270050270150152b500501500701502b0057a5027006","0x500600500f0150330052b500502f00520f01502f0052b500502c00502f","0x150070150157a60050150b70150340052b500503300521c0150310052b5","0xf0151120052b50051b500521f0151b50052b50050150f60150152b5005","0x2b50070340052370150340052b500511200521c0150310052b500502b005","0x1d91d20072b50070310050090150152b50050150070151c80057a71b9005","0x502f0151e30052b50051d90050270150152b50050150070151e10057a8","0x52b50051d200500f01511a0052b50051e400520f0151e40052b50051e3","0x2b50050150070150157a90050150b70150190052b500511a00521c0151e6","0x1e100500f0151fe0052b50051f500521f0151f50052b50050150f6015015","0x2020052b50070190052370150190052b50051fe00521c0151e60052b5005","0x57ab20f2090072b50071e60050090150152b50050150070152060057aa","0x521f00502f01521f0052b500520f0050270150152b500501500701521c","0x150500052b500520900500f01504f0052b500523700520f0152370052b5","0x150152b50050150070150157ac0050150b70150520052b500504f00521c","0x2b500521c00500f0152590052b500525300521f0152530052b50050150f6","0x2370152620052b500505000502c0150520052b500525900521c015050005","0x1b90211292d70150152b50050150070150540057ad2640052b5007052005","0x2b500526200503101526f0052b500526d0057ae01526d0052b5005264202","0x152b500501500701526f26200700526f0052b500526f0057af015262005","0x2b50051b900527a0150152b500520200527a0150152b500502100527a015","0x57af0152620052b50052620050310152710052b50050540057b0015015","0x51b900527a0150152b50050150070152712620070052710052b5005271","0x57b001527a0052b50051e600502c0150152b500502100527a0150152b5","0x52b50052800057af01527a0052b500527a0050310152800052b5005206","0x2c0150152b500502100527a0150152b500501500701528027a007005280","0x2b50052910050310152930052b50051c80057b00152910052b5005031005","0x152b50050150070152932910070052930052b50052930057af015291005","0x2950050310152970052b500502e0057b00152950052b500500f00502c015","0x50150060152972950070052970052b50052970057af0152950052b5005","0x501500701500c0090077b10af1290072b50070050150070050150152b5","0x1290050af0150152b500501512901500f0052b50050070050ba0150152b5","0x50150070150f60057b20b70100072b500700f0050090151290052b5005","0x1001502e0052b500501000500f0150210052b50050b700500c0150152b5","0xf60150152b50050150070150157b30050150b70150060052b5005021005","0x52b50050f600500f01502b0052b50050270050210150270052b5005015","0x2f0057b402c0052b500700600502e0150060052b500502b00501001502e","0x330052b500502c0050270150152b50050150060150152b5005015007015","0xba0072950150310052b50050310051b50150310052b500503300502f015","0x52b50051290050af0151b50052b500502e00502c0150340052b5005031","0x50340151b50052b50051b50050310150af0052b50050af005033015129","0xba0051c81b91120ba2b50050341b50af12912973c0150340052b5005034","0x502f0051e60150152b50050150060150152b50050150070151c81b9112","0xba0075eb0151d20052b50050150f60150152b500502e00526d0150152b5","0x52b50051290050af0151e10052b50051d90055ec0151d90052b50051d2","0x1290ba0051e10052b50051e10055ed0150af0052b50050af005033015129","0x50070052800150152b50050ba0052910150152b50050150070151e10af","0x1e40051b50151e40052b50050152a00151e30052b500501502b0150152b5","0x1e60052b500501529701511a0052b50051e41e30072950151e40052b5005","0x50af0151f50052b50050190055ee0150190052b500511a1e6007299015","0x52b50051f50055ed01500c0052b500500c0050330150090052b5005009","0x2b50070050050090150050052b50050150050ba0151f500c0090ba0051f5","0xaf0052b50050ba0050270150152b50050150070151290057b50ba007007","0x700500f01500c0052b500500900520f0150090052b50050af00502f015","0x70150157b60050150b70150100052b500500c00521c01500f0052b5005","0x150f60052b50050b700521f0150b70052b50050150f60150152b5005015","0x500f0052170150100052b50050f600521c01500f0052b500512900500f","0x60052b500701000523701502e0052b500502100502c01502100f0072b5","0x27b01502b0060072b500500600523e0150152b50050150070150270057b7","0x2b500502e0052800150152b500501500701502c0057b80152b500702b005","0x310057b903302f0072b500700f0050090150152b500500600527a015015","0x2b500503400502f0150340052b50050330050270150152b5005015007015","0x21c0151b90052b500502f00500f0151120052b50051b500520f0151b5005","0xf60150152b50050150070150157ba0050150b70151c80052b5005112005","0x52b500503100500f0151d90052b50051d200521f0151d20052b5005015","0x1e30057bb1e10052b50071c80052370151c80052b50051d900521c0151b9","0x2b50051b900500f0151e40052b50051e100520f0150152b5005015007015","0x50150070150157bc0050150b70151e60052b50051e400521c01511a005","0x310151f50052b50051e30052d60150190052b50051b900502c0150152b5","0x70151f50190070051f50052b50051f50057bd0150190052b5005019005","0x1100151fe0052b50050152730150152b500502c0052760150152b5005015","0x720200527b0152020052b50052020051b50152020052b50051fe006007","0xf60150152b500502e0052800150152b50050150070152060057be0152b5","0x52b500500f00500f01520f0052b500520900521f0152090052b5005015","0x502c01521c0052b50051e60057bf0151e60052b500520f00521c01511a","0x52b500521c0057bd01521f0052b500521f00503101521f0052b500511a","0x26d0150152b50052060052760150152b500501500701521c21f00700521c","0x4f0052b50052370052d60152370052b50050150f60150152b500500f005","0x4f02e00700504f0052b500504f0057bd01502e0052b500502e005031015","0x52b50050270052d60150152b500500f00526d0150152b5005015007015","0x2e0070050500052b50050500057bd01502e0052b500502e005031015050","0x2b500501520501500c0052b500501575c0150af0052b50050157c0015050","0x2b50050070056760150152b50050150060150152b5005015204015010005","0x152b500501500701502e0057c10210f60072b50070b70056770150b7005","0xf60050340150270052b500500600520f0150060052b5005021005027015","0x70150157c20050150b701502c0052b500502700521c01502b0052b5005","0x150330052b500502f00521f01502f0052b50050150f60150152b5005015","0x702c00523701502c0052b500503300521c01502b0052b500502e005034","0x52b500500f0100071de0150152b50050150070150310057c300f0052b5","0x150152b50050150070151120057c41b50340072b500702b00567701500f","0x50340050340151c80052b50051b900520f0151b90052b50051b5005027","0x150070150157c50050150b70151d90052b50051c800521c0151d20052b5","0x340151e30052b50051e100521f0151e10052b50050150f60150152b5005","0x2b50071d90052370151d90052b50051e300521c0151d20052b5005112005","0x704f0150152b50050151290150152b500501500701511a0057c61e4005","0x5540150152b50050150070152021fe1f50ba7c70191e60072b50071e4015","0x52b50050190055580152090052b50051e60050af0152060052b5005015","0x2b50050150070150157c80050150b701521c0052b500520600555801520f","0x555801520f0052b50052020055580152090052b50051f50050af015015","0x52b500521c20f00705001521f0052b50050152df01521c0052b50051fe","0x7c901504f2370072b50052370056270152370052b50052370052a8015237","0x152592530520501292b500521f04f2090ba7ca01521f0052b500521f005","0x2b50050152df0152620052b50052590500077cb0150152b5005253005271","0x541292b50052642372620ba7ca0152640052b50052640057c9015264005","0x1527a0052b50052710540077cb0150152b500526d00527101527126f26d","0x52800057c90152910520072b50050520056270152800052b50050152df","0x2970052710152992972952931292b500528029127a0ba7ca0152800052b5","0x150590052b50050152df0150580052b50052992930077cb0150152b5005","0x5e12929e05c1292b50050590520580ba7ca0150590052b50050590057c9","0xba00523e0150600052b500505e05c0077cb0150152b500529e005271015","0x2b50050600050af0151290052b50051290af0077cc0152aa0ba0072b5005","0x7ce0150152b50050150070152a90057cd0152b50072aa00527b015060005","0x52b50050157cf0150152b50050620052710150640620072b5005295005","0x56270150152b50052a70052710152a62a70072b50052a80057ce0152a8","0x52a30052590152a306a0072b50052a400555a0152a40640072b5005064","0x712a00072b500502800555a0150282a60072b50052a60056270150152b5","0x52a000562801506f0052b500506a0056280150152b5005071005259015","0x150152b50050150070150157d10152b500707006f0077d00150700052b5","0x150157d20050150b70150152b50050640052710150152b50052a6005271","0x2b500507300525901529f0730072b500506400555a0150152b5005015007","0x56280150152b500507600525901529d0760072b50052a600555a015015","0x2b500729b29c0077d001529b0052b500529d00562801529c0052b500529f","0x57d429829a0072b50071d20056770150152b50050150070150157d3015","0x529400520f0152940052b50052980050270150152b5005015007015296","0x150820052b500529200521c0152900052b500529a0050340152920052b5","0x21f0150e30052b50050150f60150152b50050150070150157d50050150b7","0x2b50050e200521c0152900052b50052960050340150e20052b50050e3005","0x150152b500501500701528e0057d628f0052b5007082005237015082005","0x528d00521c01528c0052b500529000503401528d0052b500528f00520f","0x2b50050150060150152b50050150070150157d70050150b701508e0052b5","0x526f0052710150152b50051290052710150152b500528e0051e6015015","0xc0057730150152b50050ba00527a0150152b500500f00527a0150152b5","0x1559c01508d0052b500501502b0150152b50052900052910150152b5005","0x52b500510708d0072950151070052b50051070051b50151070052b5005","0x57d80150910052b50051061020072990151020052b5005015297015106","0x52b50050050050330150600052b50050600050af01528a0052b5005091","0x152b500501500701528a0050600ba00528a0052b500528a0057d9015005","0x51d20050340152860052b500528900521f0152890052b50050150f6015","0x152830052b500528c00525101508e0052b500528600521c01528c0052b5","0x150152b50050150070150157db0050150b70150090052b500508e0057da","0x72b50071d20056770150152b50052950052710150152b50052a9005276","0x1527f0052b50050960050270150152b50050150070152810057dc09601c","0x527c00521c0150990052b500501c00503401527c0052b500527f00520f","0x2b50050150f60150152b50050150070150157dd0050150b701509a0052b5","0x21c0150990052b500528100503401527b0052b500509c00521f01509c005","0x150070152770057de2780052b500709a00523701509a0052b500527b005","0x7df09f2760072b50070990056770150152b500527800527a0150152b5005","0xa000520f0150a00052b500509f0050270150152b500501500701500d005","0xa40052b50050a200521c0152730052b50052760050340150a20052b5005","0x152720052b50050150f60150152b50050150070150157e00050150b7015","0x50a500521c0152730052b500500d0050340150a50052b500527200521f","0x152b50050150070150a80057e10a70052b50070a40052370150a40052b5","0xaa00521c0152830052b50052730050340150aa0052b50050a700520f015","0x26e0052b50050155540150ac2700072b500526f00555a0150090052b5005","0x150157e20152b500726e0ac0077d00150090052b500500900c0072da015","0x67d0150152b50051290052710150152b50050150060150152b5005015007","0x150152b50050ba00527a0150152b500500f00527a0150152b5005009005","0x52b50050600050af0150152b50052700052590150152b5005283005291","0x2b50072700600077e40150152b50050150070150157e30050150b70152af","0x152680052b500501542b0150152b50050150070152690057e526a26b007","0x526a0050ac0150b12670072b50052670050ac0152670052b5005015099","0x2b50072660b126b0ba2130150b10052b50050b100509a01526626a0072b5","0x152b50052650052660150152b50050150070150b60b40077e62650b3007","0x526a0050ac0152630052b50050b30050af0150152b5005267005266015","0x70150157e70050150b701525f0052b500525f00509a01525f26a0072b5","0x152630052b50050b40050af0150152b50050b60052660150152b5005015","0x25e00527a01525d25e0072b50050ba0056a901525f0052b500526700509a","0x25b0052b500528300513b01525c0052b500526a25f0074510150152b5005","0x25c0054530150050052b50050050050330152630052b50052630050af015","0x2680052b500526800542c01525b0052b500525b0051e701525c0052b5005","0xba2b500525d26825b25c0052630097e801525d0052b500525d0051b5015","0x152b50050150070152550057e92560052b50072580056ab01525825a0b9","0x50151290152540052b50050157ea0150c10bf0072b500512900555a015","0x2510c10072b50050c10055550152522540072b50052540055550150152b5","0x7eb0c70c50072b50072512520b90ba2d30152520052b5005252005558015","0x2560056ad0150152b50050c70052590150152b50050150070150c9250007","0x2b500524d0051e60150152b50050cb00545701524d24e24f0cb1292b5005","0x7d00150c50052b50050c50050af01524c0c10072b50050c1005555015015","0x157ed0050150b70150152b50050150070150157ec0152b500725424c007","0x72b50050bf00555501524b0052b50050155540150152b5005015007015","0x1d80150152b50050150070150157ee0152b500724b24a0077d001524a0bf","0x150152b500500900567d0150152b500524e0051630150152b500524f005","0x152b50050c10052590150152b50050bf0052590150152b500500f00527a","0x152b50050150070150157ef0050150b70152490052b50050c50050af015","0x2b50050bf00559f0152480052b50050c100559f0150152b5005015006015","0x1b50152410052b50052432480075a20152430052b50050155a10150e1005","0x24000f12976e0152400052b50050e12410073e50152410052b5005241005","0x523e0057f101523e0052b50050e024f0077f00150e00052b500524e009","0x1525a0052b500525a0050330150c50052b50050c50050af01523d0052b5","0x2590150152b500501500701523d25a0c50ba00523d0052b500523d0057d9","0x150152b50052540052590150152b50052560057f20150152b50050c9005","0x152b500500f00527a0150152b500500900567d0150152b50050c1005259","0x2b50050150060152490052b50052500050af0150152b50050bf005259015","0x523c0051b501523c0052b500501559c0151560052b500501502b015015","0x1523b0052b50050152970150df0052b500523c15600729501523c0052b5","0x2490050af0150de0052b50052390057d80152390052b50050df23b007299","0xde0052b50050de0057d901525a0052b500525a0050330152490052b5005","0x27a0150152b50051290052710150152b50050150070150de25a2490ba005","0x1050052b50052550057d80150152b500500900567d0150152b500500f005","0x1050057d901525a0052b500525a0050330150b90052b50050b90050af015","0x2b50050150060150152b500501500701510525a0b90ba0051050052b5005","0x500f00527a0150152b500500900567d0150152b5005129005271015015","0x2690050af0150152b50052830052910150152b50050ba00527a0150152b5","0x1b50152380052b50050157f30150e50052b500501502b0152af0052b5005","0x2b500501529701523a0052b50052380e50072950152380052b5005238005","0x152340052b50051040057d80151040052b500523a0e90072990150e9005","0x52340057d90150050052b50050050050330152af0052b50052af0050af","0x152b50050150060150152b50050150070152340052af0ba0052340052b5","0x2b500526f0052710150152b50051290052710150152b50050a80051e6015","0x500c0057730150152b50050ba00527a0150152b500500f00527a015015","0x501559c0152330052b500501502b0150152b50052730052910150152b5","0x2310052b50052322330072950152320052b50052320051b50152320052b5","0xf20057d80150f20052b50052310f00072990150f00052b5005015297015","0x50052b50050050050330150600052b50050600050af0151030052b5005","0x150152b50050150070151030050600ba0051030052b50051030057d9015","0x150152b50051290052710150152b50052770051e60150152b5005015006","0x152b50050ba00527a0150152b500500f00527a0150152b500526f005271","0x52b500501502b0150152b50050990052910150152b500500c005773015","0x22f00729501522d0052b500522d0051b501522d0052b500501559c01522f","0x52b500522c2290072990152290052b500501529701522c0052b500522d","0x50330150600052b50050600050af0152270052b50052280057d8015228","0x70152270050600ba0052270052b50052270057d90150050052b5005005","0x2910150152b500500c0057730150152b500511a0051e60150152b5005015","0x150152b500500f00527a0150152b50050af0057f40150152b50051d2005","0x2250052b500501559c0152260052b500501502b0150152b50050ba00527a","0x152970151080052b50052252260072950152250052b50052250051b5015","0x52b50050fe0057d80150fe0052b50051082240072990152240052b5005","0x57d90150050052b50050050050330150150052b50050150050af015220","0x310051e60150152b50050150070152200050150ba0052200052b5005220","0x52910150152b50050af0057f40150152b500500c0057730150152b5005","0x2b0150152b50050100051ff0150152b50050ba00527a0150152b500502b","0x21e0052b500521e0051b501521e0052b500501559c0151090052b5005015","0x21d00729901521d0052b500501529701510b0052b500521e109007295015","0x52b50050150050af01510e0052b500510c0057d801510c0052b500510b","0x150ba00510e0052b500510e0057d90150050052b5005005005033015015","0x77f500c0090072b50070050150070050150152b500501500601510e005","0x151290150f60b70072b500500700566b0150152b500501500701501000f","0xf60072b50050f60050ac0150210b70072b50050b70050ac0150152b5005","0x150157f60152b500702e0210075880150090052b50050090050af01502e","0xb70072b50050b70050ac0150060052b500501545c0150152b5005015007","0x2c02b0072b50070060270090ba58b0150060052b500500600509a015027","0xaf0150310052b50050b700566e0150152b500501500701503302f0077f7","0x2b500503100566f0151b50052b500502c00509a0150340052b500502b005","0x152b50050150060150152b50050150070150157f80050150b7015112005","0x2b50050b70052660150152b50050af00527a0150152b5005033005266015","0x50f60052660150152b50051290051630150152b50050ba0051d8015015","0x1c80051b50151c80052b50050155dc0151b90052b500501502b0150152b5","0x1d90052b50050152970151d20052b50051c81b90072950151c80052b5005","0x50af0151e30052b50051e10057850151e10052b50051d21d9007299015","0x52b50051e300578601500c0052b500500c00503301502f0052b500502f","0x151e40052b50050150f60150152b50050150070151e300c02f0ba0051e3","0x50b700509a0150340052b50050090050af01511a0052b50051e4005673","0x7f91e60052b50071120056740151120052b500511a00566f0151b50052b5","0x50ba0056760150152b50051e60052660150152b5005015007015019005","0x2b50050150070152060057fa2021fe0072b50071f50056770151f50052b5","0x503401520f0052b500520900520f0152090052b5005202005027015015","0x150157fb0050150b701521f0052b500520f00521c01521c0052b50051fe","0x4f0052b500523700521f0152370052b50050150f60150152b5005015007","0x21f00523701521f0052b500504f00521c01521c0052b5005206005034015","0x72b50050af00523e0150152b50050150070150520057fc0500052b5007","0x2620057fd0152b500725900527b0152590052b500525300502f0152530af","0x157fe0050150b70152640052b500521c0050340150152b5005015007015","0x72b500721c0056770150152b50052620052760150152b5005015007015","0x152710052b500526d0050270150152b500501500701526f0057ff26d054","0x527a00521c0152800052b500505400503401527a0052b500527100520f","0x2b50050150f60150152b50050150070150158000050150b70152910052b5","0x21c0152800052b500526f0050340152950052b500529300521f015293005","0x150070152990058012970052b50072910052370152910052b5005295005","0x6770152640052b50052800050340150152b500529700527a0150152b5005","0x590050270150152b500501500701505c0058020590580072b5007264005","0x600052b500505800503401505e0052b500529e00520f01529e0052b5005","0x152b50050150070150158030050150b70152aa0052b500505e00521c015","0x505c0050340150620052b50052a900521f0152a90052b50050150f6015","0x8040640052b50072aa0052370152aa0052b500506200521c0150600052b5","0x50640500072690150152b50050150060150152b50050150070152a8005","0x2a60052b50052a71290076560152a70052b50052a70057930152a70052b5","0x50af01506a0052b500506000513b0152a40052b50050f61b5007451015","0x52b50052a400545301500c0052b500500c0050330150340052b5005034","0x51b50152a60052b50052a600542c01506a0052b500506a0051e70152a4","0x2a00282a30ba2b50050af2a606a2a400c0340097e80150af0052b50050af","0x51e60150152b50050150060150152b50050150070152a00282a30ba005","0x2910150152b50051b50052660150152b50050af00527a0150152b50052a8","0x150152b50051290051630150152b50050f60052660150152b5005060005","0x6f0052b500501559c0150710052b500501502b0150152b500505000527a","0x152970150700052b500506f07100729501506f0052b500506f0051b5015","0x52b500529f00578501529f0052b50050700730072990150730052b5005","0x578601500c0052b500500c0050330150340052b50050340050af015076","0x50150060150152b500501500701507600c0340ba0050760052b5005076","0x1b50052660150152b50050af00527a0150152b50052990051e60150152b5","0x51630150152b50050f60052660150152b500505000527a0150152b5005","0x59c01529d0052b500501502b0150152b50052800052910150152b5005129","0x2b500529c29d00729501529c0052b500529c0051b501529c0052b5005015","0x7850152980052b500529b29a00729901529a0052b500501529701529b005","0x2b500500c0050330150340052b50050340050af0152960052b5005298005","0x2b500501500701529600c0340ba0052960052b500529600578601500c005","0x2b50050af00527a0150152b50050520051e60150152b5005015006015015","0x50f60052660150152b500521c0052910150152b50051b5005266015015","0x501559c0152940052b500501502b0150152b50051290051630150152b5","0x2900052b50052922940072950152920052b50052920051b50152920052b5","0xe30057850150e30052b50052900820072990150820052b5005015297015","0xc0052b500500c0050330150340052b50050340050af0150e20052b5005","0x150152b50050150070150e200c0340ba0050e20052b50050e2005786015","0x150152b50050af00527a0150152b50050190051e60150152b5005015006","0xba28f12979401528e0052b50050150f601528f0052b50050f61b5007451","0x2b50050340050af01528c0052b500528d00579501528d0052b500528e129","0xba00528c0052b500528c00578601500c0052b500500c005033015034005","0x70054570150152b50050af00527a0150152b500501500701528c00c034","0x1502b0150152b50051290051630150152b50050ba0051d80150152b5005","0x1508d0052b500508d0051b501508d0052b50050152a001508e0052b5005","0x1071060072990151060052b50050152970151070052b500508d08e007295","0xf0052b500500f0050af0150910052b50051020057850151020052b5005","0x1000f0ba0050910052b50050910057860150100052b5005010005033015","0xba0070050151a113519e13301513100907013519e1330151310092a4091","0x1310af3a80ba0070050151a113519e01512907013519e0151290150af129","0x19e0151310af4391290ba0070050151a113519e0151310af07013519e015","0x7013519e0151310af5dc1290ba0070050151a113519e0151310af070135","0x1310af07013519e0151310af6a21290ba0070050151a113519e0151310af","0x1a113519e01512907013519e0151298051290ba0070050151a113519e015","0x70050151a113519e0151310af07013519e0151310af8060ba007005015","0x1310098080ba0070050151a113519e01512907013519e0151298071290ba","0xaf8090af1290ba0070050151a113519e13301513100907013519e133015","0x151310af80a1290ba0070050151a113519e0151310af07013519e015131","0x19e13301513100980b1290ba0070050151a113519e0151310af07013519e","0x13301513100980c0af1290ba0070050151a113519e133015131009070135","0x1513100980d0af1290ba0070050151a113519e13301513100907013519e","0x1512980e0af1290ba0070050151a113519e13301513100907013519e133","0x1512907013519e01512980f0ba0070050151a113519e01512907013519e","0x50151a113519e01512907013519e0151298100ba0070050151a113519e","0x78121290ba0070050151a019e0150ba00602107019e0150af8110ba007","0x900c00607002117a13513313119e0150b78130050151a3015007070015","0x19e01512981401000f00c0090af1290ba0070050151ac13513313119e015","0x151290f016d13519e0150af8150ba0070050151b413519e0151290bf135","0x13119e0150af08215113513119e0150098161290ba0070050151b613519e","0x13119e0150af08215113513119e0150098170af1290ba0070050151b8135","0x19e0150af00600615113513119e01500c8180af1290ba0070050151b8135","0x150af08215113513119e0150098190090af1290ba0070050151b8135131","0x608214113513313119e01501081a0af1290ba0070050151d313513119e","0x1500f81b00f00c0090af1290ba0070050151da13513313119e015009070","0xaf1290ba0070050151e213513313119e01500907008214113513313119e","0x13513313119e01500900607000608214113513313119e0150b781c00c009","0x8214113513313119e0150b781d01000f00c0090af1290ba0070050151e2","0x1000f00c0090af1290ba0070050151e213513313119e015009006070006","0x12912613913519e0150af81f0070050151e519e0150ba07019e0150ba81e","0x12900600600608217a13519e01500f8201290ba0070050151e713519e015","0x1512912d13519e01512982100c0090af1290ba0070050151ac13519e015","0x1ff13319e0151290701eb1aa13319e0150098220ba0070050151f613519e","0xaf82400700501520319e0150ba02e19e0150ba8230af1290ba007005015","0x13519e0150af8251290ba00700501520713519e01512920507013519e015","0x1500704f0061100151298261290ba00700501520a13519e015129006208","0x8280ba00700501521d19e0150ba02121b19e0151298270ba007005015210","0x13319e01500f8291290ba00700501522019e0150ba02102121e19e0150af","0x982a00c0090af1290ba00700501523813319e015129006070006082082","0x150af82b0af1290ba00700501523813319e01512907008208213319e015","0x602c07019e0150af82c1290ba00700501524f19e0150ba00606207019e","0x13519e0151290820bf13519e0150af82d1290ba00700501525019e0150ba","0x700501525a19e0150ba02102e05e19e0150af82e1290ba007005015254","0x602e19e01512983000700501526319e0150ba02e19e0150ba82f1290ba","0x501526619e0150ba00602e19e0151298310ba00700501526519e0150ba","0x1298331290ba00700501526e19e0150ba04f0a507019e0150af8320ba007","0xba04f0540a019e0150af8340ba00700501527019e0150ba0a00a819e015","0x1527b19e0150ba00609907019e0150af8351290ba00700501527219e015","0x210210de19e0150af83700501528101500704f0150078361290ba007005","0x22019e0150ba0210210df19e0150af8381290ba00700501522019e0150ba","0xba00700501522019e0150ba0210210e019e0150af8391290ba007005015","0x150af83b1290ba00700501522019e0150ba0210210e119e0150af83a129","0x210210e319e0150af83c1290ba00700501522019e0150ba0210210e219e","0x29d19e0150ba04f02107019e0150af83d1290ba00700501522019e0150ba","0xba00700501529d19e0150ba04f02107019e0150af83e1290ba007005015","0x2e06205e19e01500984000700501529f19e0150ba07019e0150ba83f129","0xba00602c02e05e19e0150098410af1290ba0070050152a819e0150ba006","0x700501527201500700c0a00150ba8420af1290ba0070050152aa19e015","0x2590700070700058440ba00700501527219e0150ba04f0a019e015129843","0x700070700058460ba00700501521d19e0150ba02107019e015129845015","0x19e0150098480ba0070050151b519e0150ba00602e19e015129847015112","0x8490af1290ba0070050152aa19e0150ba00602c02e05e"],"sierra_program_debug_info":{"type_names":[[0,"RangeCheck"],[1,"Const"],[2,"Const"],[3,"Const, Const>"],[4,"U128MulGuarantee"],[5,"Const, Const, Const>>"],[6,"u128"],[7,"core::integer::u256"],[8,"NonZero"],[9,"Const, Const>"],[10,"Uninitialized"],[11,"Const, Const>"],[12,"Array"],[13,"core::array::ArrayIter::"],[14,"felt252"],[15,"Unit"],[16,"core::option::Option::"],[17,"Array>"],[18,"piltover::snos_output::ContractChanges"],[19,"Tuple, piltover::snos_output::ContractChanges>"],[20,"core::panics::Panic"],[21,"Tuple>"],[22,"core::panics::PanicResult::<(core::array::ArrayIter::, piltover::snos_output::ContractChanges)>"],[23,"core::option::Option::>"],[24,"Uninitialized>"],[25,"Const"],[26,"Const"],[27,"Const"],[28,"Const"],[29,"Const"],[30,"Const"],[31,"Const"],[32,"Const"],[33,"Const"],[34,"Const"],[35,"Const"],[36,"Const"],[37,"Const"],[38,"Const"],[39,"Const"],[40,"Const"],[41,"index_enum_type<16>"],[42,"BoundedInt<0, 15>"],[43,"u32"],[44,"piltover::fact_registry::VerifierConfiguration"],[45,"piltover::fact_registry::VerificationListElement"],[46,"core::option::Option::"],[47,"core::option::Option::"],[48,"Const"],[49,"Const"],[50,"u64"],[51,"NonZero"],[52,"Const"],[53,"Const"],[54,"Const"],[55,"Const"],[56,"Const"],[57,"Const"],[58,"Const"],[59,"Const"],[60,"Const, Const>"],[61,"NonZero"],[62,"Const"],[63,"Box"],[64,"core::option::Option::>"],[65,"core::ops::range::RangeIterator::"],[66,"Tuple, core::array::ArrayIter::, Array>, Unit>"],[67,"core::panics::PanicResult::<(core::ops::range::RangeIterator::, core::array::ArrayIter::, core::array::Array::<(core::felt252, core::felt252)>, ())>"],[68,"Const"],[69,"Array"],[70,"Tuple, Array, core::array::ArrayIter::, Unit>"],[71,"core::panics::PanicResult::<(core::ops::range::RangeIterator::, core::array::Array::, core::array::ArrayIter::, ())>"],[72,"Const"],[73,"Const"],[74,"core::option::Option::"],[75,"StorageBaseAddress"],[76,"core::starknet::storage::StoragePointer0Offset::>"],[77,"Tuple"],[78,"core::option::Option::<(core::felt252, core::felt252)>"],[79,"core::option::Option::"],[80,"Snapshot>"],[81,"core::array::Span::"],[82,"Tuple, core::option::Option::>"],[83,"core::panics::PanicResult::<(core::array::Span::, core::option::Option::)>"],[84,"Tuple, u32, Unit>"],[85,"core::panics::PanicResult::<(core::array::Array::, core::integer::u32, ())>"],[86,"Const"],[87,"Const"],[88,"Const"],[89,"Const"],[90,"Const"],[91,"Const"],[92,"Const"],[93,"Const"],[94,"ContractAddress"],[95,"piltover::messaging::component::messaging_cpt::MessageToAppchainSealed"],[96,"piltover::messaging::component::messaging_cpt::MessageToStarknetReceived"],[97,"Const"],[98,"Const"],[99,"Const"],[100,"Const"],[101,"openzeppelin_access::ownable::ownable::OwnableComponent::OwnershipTransferStarted"],[102,"openzeppelin_access::ownable::ownable::OwnableComponent::OwnershipTransferred"],[103,"piltover::state::component::state_cpt::Event"],[104,"openzeppelin_security::reentrancyguard::ReentrancyGuardComponent::Event"],[105,"openzeppelin_access::ownable::ownable::OwnableComponent::Event"],[106,"Box"],[107,"core::option::Option::>"],[108,"Const"],[109,"Const"],[110,"Const"],[111,"NonZero"],[112,"Tuple"],[113,"core::panics::PanicResult::<(core::integer::u128,)>"],[114,"Const"],[115,"Const"],[116,"Array"],[117,"core::option::Option::>"],[118,"Tuple, core::option::Option::>>"],[119,"core::panics::PanicResult::<(core::array::Span::, core::option::Option::>)>"],[120,"Const"],[121,"Const"],[122,"Const, Const>"],[123,"Const"],[124,"Const"],[125,"Array"],[126,"Snapshot>"],[127,"core::array::Span::"],[128,"Tuple, Unit>"],[129,"core::panics::PanicResult::<(core::array::Array::, ())>"],[130,"Array"],[131,"Snapshot>"],[132,"core::array::Span::"],[133,"Tuple, Array, Unit>"],[134,"core::panics::PanicResult::<(core::array::Span::, core::array::Array::, ())>"],[135,"Tuple, u32, Unit>"],[136,"core::panics::PanicResult::<(core::array::Array::, core::integer::u32, ())>"],[137,"Uninitialized"],[138,"Const"],[139,"Const"],[140,"Const"],[141,"Const"],[142,"Const"],[143,"Tuple, Array>>"],[144,"core::panics::PanicResult::<(core::array::ArrayIter::, core::array::Array::<(core::felt252, core::felt252)>)>"],[145,"Tuple, Array>"],[146,"core::panics::PanicResult::<(core::array::ArrayIter::, core::array::Array::)>"],[147,"Tuple, Array>"],[148,"Tuple, Tuple, Array>>"],[149,"core::panics::PanicResult::<(core::array::ArrayIter::, (core::array::Array::, core::array::Array::))>"],[150,"Const"],[151,"Const"],[152,"Const"],[153,"Const"],[154,"Const"],[155,"Tuple, core::array::ArrayIter::, Array, Unit>"],[156,"core::panics::PanicResult::<(core::ops::range::RangeIterator::, core::array::ArrayIter::, core::array::Array::, ())>"],[157,"Const"],[158,"Const"],[159,"Const"],[160,"openzeppelin_access::ownable::ownable::OwnableComponent::ComponentState::"],[161,"Tuple, Unit>"],[162,"core::panics::PanicResult::<(openzeppelin_access::ownable::ownable::OwnableComponent::ComponentState::, ())>"],[163,"Const"],[164,"Const"],[165,"core::option::Option::>"],[166,"Tuple, core::option::Option::>>"],[167,"core::panics::PanicResult::<(core::array::Span::, core::option::Option::>)>"],[168,"core::option::Option::>"],[169,"Tuple, core::option::Option::>>"],[170,"core::panics::PanicResult::<(core::array::Span::, core::option::Option::>)>"],[171,"Uninitialized>"],[172,"Uninitialized>"],[173,"Const"],[174,"Const"],[175,"Const"],[176,"Const"],[177,"Const"],[178,"piltover::messaging::component::messaging_cpt::MessageCanceled"],[179,"Const"],[180,"core::starknet::storage::StoragePointer0Offset::>"],[181,"Const"],[182,"piltover::messaging::component::messaging_cpt::MessageCancellationStarted"],[183,"core::starknet::storage::storage_base::StorageBase::>>"],[184,"Const"],[185,"core::starknet::info::BlockInfo"],[186,"Const"],[187,"piltover::messaging::component::messaging_cpt::MessageConsumed"],[188,"Const"],[189,"core::starknet::storage::storage_base::StorageBase::>>"],[190,"piltover::messaging::component::messaging_cpt::MessageSent"],[191,"piltover::messaging::component::messaging_cpt::Event"],[192,"Tuple"],[193,"core::panics::PanicResult::<(core::felt252,)>"],[194,"Uninitialized"],[195,"piltover::config::component::config_cpt::ProgramInfoChanged"],[196,"piltover::config::component::config_cpt::Event"],[197,"Const"],[198,"Const"],[199,"core::starknet::storage::StoragePointer0Offset::>"],[200,"core::starknet::storage::storage_base::StorageBase::>>"],[201,"ClassHash"],[202,"openzeppelin_upgrades::upgradeable::UpgradeableComponent::Upgraded"],[203,"openzeppelin_upgrades::upgradeable::UpgradeableComponent::Event"],[204,"Const"],[205,"Const"],[206,"Const"],[207,"Const"],[208,"Const"],[209,"Const"],[210,"Const"],[211,"Const"],[212,"piltover::appchain::appchain::LogStateUpdate"],[213,"core::starknet::storage::StoragePointer0Offset::>"],[214,"Tuple, Array, Unit>"],[215,"core::panics::PanicResult::<(core::array::Array::, core::array::Array::, ())>"],[216,"piltover::appchain::appchain::LogStateTransitionFact"],[217,"piltover::appchain::appchain::Event"],[218,"Tuple, Unit>"],[219,"core::panics::PanicResult::<(core::array::Array::, ())>"],[220,"Array"],[221,"Snapshot>"],[222,"core::array::Span::"],[223,"core::byte_array::ByteArray"],[224,"Snapshot"],[225,"Const"],[226,"Const"],[227,"Const"],[228,"Const"],[229,"Const"],[230,"Const"],[231,"Const"],[232,"Tuple"],[233,"core::panics::PanicResult::<(core::byte_array::ByteArray, ())>"],[234,"Const"],[235,"Const"],[236,"bytes31"],[237,"Const"],[238,"Box"],[239,"Snapshot>"],[240,"Tuple>"],[241,"core::panics::PanicResult::<(core::array::Array::,)>"],[242,"piltover::fact_registry::IFactRegistryDispatcher"],[243,"Tuple"],[244,"core::panics::PanicResult::<(core::integer::u256,)>"],[245,"piltover::components::onchain_data_fact_tree_encoder::DataAvailabilityFact"],[246,"core::starknet::storage::StoragePointer0Offset::>"],[247,"piltover::snos_output::StarknetOsOutput"],[248,"Tuple, piltover::snos_output::StarknetOsOutput>"],[249,"core::panics::PanicResult::<(core::array::ArrayIter::, piltover::snos_output::StarknetOsOutput)>"],[250,"Const"],[251,"Tuple, felt252>"],[252,"core::panics::PanicResult::<(core::array::Span::, core::felt252)>"],[253,"openzeppelin_security::reentrancyguard::ReentrancyGuardComponent::ComponentState::"],[254,"Tuple, Unit>"],[255,"core::panics::PanicResult::<(openzeppelin_security::reentrancyguard::ReentrancyGuardComponent::ComponentState::, ())>"],[256,"Uninitialized"],[257,"Uninitialized, piltover::snos_output::StarknetOsOutput>>"],[258,"Uninitialized, felt252>>"],[259,"Uninitialized>"],[260,"Pedersen"],[261,"Uninitialized"],[262,"Poseidon"],[263,"Uninitialized"],[264,"System"],[265,"Uninitialized"],[266,"Uninitialized"],[267,"Const"],[268,"Const"],[269,"Tuple"],[270,"piltover::state::component::state_cpt::ComponentState::"],[271,"Tuple, Unit>"],[272,"core::panics::PanicResult::<(piltover::state::component::state_cpt::ComponentState::, ())>"],[273,"core::option::Option::"],[274,"Tuple, core::option::Option::>"],[275,"core::panics::PanicResult::<(core::array::Span::, core::option::Option::)>"],[276,"piltover::messaging::component::messaging_cpt::ComponentState::"],[277,"Tuple, felt252>"],[278,"core::panics::PanicResult::<(piltover::messaging::component::messaging_cpt::ComponentState::, core::felt252)>"],[279,"piltover::messaging::types::MessageToStarknetStatus"],[280,"Const"],[281,"piltover::messaging::types::MessageToAppchainStatus"],[282,"core::starknet::storage::StoragePointer0Offset::"],[283,"core::starknet::storage::storage_base::StorageBase::>"],[284,"Const"],[285,"Tuple, Tuple>"],[286,"core::panics::PanicResult::<(piltover::messaging::component::messaging_cpt::ComponentState::, (core::felt252, core::felt252))>"],[287,"Const"],[288,"core::starknet::storage::StoragePointer0Offset::"],[289,"Const"],[290,"piltover::config::component::config_cpt::ComponentState::"],[291,"core::bool"],[292,"Tuple, core::bool>"],[293,"core::panics::PanicResult::<(piltover::config::component::config_cpt::ComponentState::, core::bool)>"],[294,"Box"],[295,"Box"],[296,"Array"],[297,"Snapshot>"],[298,"core::array::Span::"],[299,"core::starknet::info::v2::TxInfo"],[300,"core::starknet::info::v2::ResourceBounds"],[301,"core::starknet::info::v2::ExecutionInfo"],[302,"Box"],[303,"Const"],[304,"u8"],[305,"core::starknet::storage::StoragePointer0Offset::<(core::felt252, core::felt252)>"],[306,"Const"],[307,"Const"],[308,"NonZero"],[309,"Const"],[310,"StorageAddress"],[311,"core::starknet::storage::StoragePointer0Offset::"],[312,"core::starknet::storage::storage_base::StorageBase::>"],[313,"Const"],[314,"Tuple, Unit>"],[315,"core::panics::PanicResult::<(piltover::config::component::config_cpt::ComponentState::, ())>"],[316,"openzeppelin_upgrades::upgradeable::UpgradeableComponent::ComponentState::"],[317,"Tuple, Unit>"],[318,"core::panics::PanicResult::<(openzeppelin_upgrades::upgradeable::UpgradeableComponent::ComponentState::, ())>"],[319,"Tuple"],[320,"core::panics::PanicResult::<((),)>"],[321,"Const"],[322,"Const"],[323,"Const"],[324,"Const"],[325,"Const"],[326,"Tuple>"],[327,"piltover::appchain::appchain::ContractState"],[328,"Tuple"],[329,"core::panics::PanicResult::<(piltover::appchain::appchain::ContractState, ())>"],[330,"BuiltinCosts"],[331,"Const"],[332,"core::option::Option::"],[333,"core::option::Option::>"],[334,"Tuple, core::option::Option::>>"],[335,"core::panics::PanicResult::<(core::array::Span::, core::option::Option::>)>"],[336,"core::panics::PanicResult::<(core::array::Span::,)>"],[337,"core::option::Option::>"],[338,"Tuple, core::option::Option::>>"],[339,"core::panics::PanicResult::<(core::array::Span::, core::option::Option::>)>"],[340,"Box"],[341,"core::option::Option::>"],[342,"GasBuiltin"]],"libfunc_names":[[0,"revoke_ap_tracking"],[1,"withdraw_gas"],[2,"branch_align"],[3,"struct_deconstruct>"],[4,"enable_ap_tracking"],[5,"store_temp"],[6,"array_snapshot_pop_front"],[7,"enum_init>, 0>"],[8,"store_temp>>"],[9,"store_temp>>"],[10,"jump"],[11,"struct_construct"],[12,"enum_init>, 1>"],[13,"enum_match>>"],[14,"disable_ap_tracking"],[15,"unbox"],[16,"array_new"],[17,"struct_construct>"],[18,"rename"],[19,"store_temp"],[20,"store_temp>"],[21,"store_temp>"],[22,"store_temp"],[23,"function_call>"],[24,"enum_match, core::option::Option::>)>>"],[25,"struct_deconstruct, core::option::Option::>>>"],[26,"store_temp>>"],[27,"enum_init,)>, 1>"],[28,"store_temp"],[29,"store_temp"],[30,"store_temp"],[31,"store_temp,)>>"],[32,"drop"],[33,"enum_init>, 1>"],[34,"enum_match>>"],[35,"function_call"],[36,"enum_match, core::option::Option::>)>>"],[37,"struct_deconstruct, core::option::Option::>>>"],[38,"enum_match>>"],[39,"enum_init, 0>"],[40,"store_temp>"],[41,"enum_init, 1>"],[42,"enum_match>"],[43,"u128s_from_felt252"],[44,"struct_construct"],[45,"enum_init, 0>"],[46,"store_temp>"],[47,"drop"],[48,"enum_init, 1>"],[49,"rename"],[50,"enum_match>"],[51,"drop>>"],[52,"drop>"],[53,"drop"],[54,"drop"],[55,"drop>"],[56,"drop>"],[57,"const_as_immediate>"],[58,"array_append"],[59,"struct_construct"],[60,"struct_construct>>"],[61,"get_builtin_costs"],[62,"store_temp"],[63,"withdraw_gas_all"],[64,"struct_construct>"],[65,"struct_construct>"],[66,"struct_construct>"],[67,"struct_construct>"],[68,"struct_construct>"],[69,"struct_construct>"],[70,"struct_construct"],[71,"store_temp"],[72,"function_call"],[73,"enum_match>"],[74,"drop>"],[75,"snapshot_take>"],[76,"struct_construct>>"],[77,"enum_init,)>, 0>"],[78,"const_as_immediate>"],[79,"const_as_immediate>"],[80,"const_as_immediate>"],[81,"const_as_immediate>"],[82,"const_as_immediate>"],[83,"class_hash_try_from_felt252"],[84,"drop"],[85,"snapshot_take>"],[86,"drop>"],[87,"function_call::assert_only_owner>"],[88,"enum_match>"],[89,"drop>"],[90,"store_temp"],[91,"function_call::upgrade>"],[92,"enum_match, ())>>"],[93,"drop, Unit>>"],[94,"store_temp>>"],[95,"contract_address_try_from_felt252"],[96,"drop"],[97,"store_temp"],[98,"function_call::register_operator>"],[99,"enum_match, ())>>"],[100,"drop, Unit>>"],[101,"function_call::unregister_operator>"],[102,"contract_address_to_felt252"],[103,"const_as_immediate>"],[104,"struct_construct>>"],[105,"snapshot_take>>"],[106,"drop>>"],[107,"struct_deconstruct>>"],[108,"pedersen"],[109,"storage_base_address_from_felt252"],[110,"struct_construct>"],[111,"snapshot_take>"],[112,"drop>"],[113,"struct_deconstruct>"],[114,"rename"],[115,"storage_address_from_base"],[116,"const_as_immediate>"],[117,"store_temp"],[118,"storage_read_syscall"],[119,"felt252_is_zero"],[120,"enum_init"],[121,"store_temp"],[122,"drop>"],[123,"enum_init"],[124,"bool_not_impl"],[125,"enum_match"],[126,"const_as_immediate>"],[127,"const_as_immediate>"],[128,"function_call::set_program_info>"],[129,"storage_base_address_const<897951062914455787057706264758967433291621382369224120925925183454734167898>"],[130,"struct_construct>"],[131,"snapshot_take>"],[132,"drop>"],[133,"struct_deconstruct>"],[134,"dup"],[135,"dup"],[136,"store_temp"],[137,"const_as_immediate>"],[138,"store_temp"],[139,"storage_address_from_base_and_offset"],[140,"struct_construct>"],[141,"snapshot_take>"],[142,"drop>"],[143,"struct_deconstruct>"],[144,"drop"],[145,"drop"],[146,"get_execution_info_v2_syscall"],[147,"store_temp>"],[148,"unbox"],[149,"struct_deconstruct"],[150,"drop>"],[151,"drop>"],[152,"function_call::is_owner_or_operator>"],[153,"enum_match, core::bool)>>"],[154,"struct_deconstruct, core::bool>>"],[155,"drop>"],[156,"const_as_immediate>"],[157,"storage_base_address_const<1229685481650523187208926305059271985877225951187549955016730480626818673443>"],[158,"storage_write_syscall"],[159,"struct_deconstruct>>"],[160,"drop"],[161,"rename"],[162,"rename"],[163,"rename"],[164,"rename>"],[165,"struct_construct>"],[166,"snapshot_take>"],[167,"drop>"],[168,"struct_deconstruct>"],[169,"const_as_immediate>"],[170,"function_call::send_message_to_appchain>"],[171,"enum_match, (core::felt252, core::felt252))>>"],[172,"struct_deconstruct, Tuple>>"],[173,"drop>"],[174,"const_as_immediate>"],[175,"struct_construct>>"],[176,"snapshot_take>>"],[177,"drop>>"],[178,"struct_deconstruct>>"],[179,"struct_construct>"],[180,"snapshot_take>"],[181,"drop>"],[182,"struct_deconstruct>"],[183,"dup"],[184,"enum_init"],[185,"store_temp"],[186,"enum_init"],[187,"snapshot_take"],[188,"drop"],[189,"enum_match"],[190,"const_as_immediate>"],[191,"enum_init"],[192,"store_temp"],[193,"enum_init"],[194,"snapshot_take"],[195,"drop"],[196,"enum_match"],[197,"function_call::consume_message_from_appchain>"],[198,"enum_match, core::felt252)>>"],[199,"struct_deconstruct, felt252>>"],[200,"function_call::start_message_cancellation>"],[201,"function_call::cancel_message>"],[202,"function_call"],[203,"enum_match, core::option::Option::)>>"],[204,"struct_deconstruct, core::option::Option::>>"],[205,"enum_match>"],[206,"drop"],[207,"store_temp"],[208,"function_call::update>"],[209,"enum_match, ())>>"],[210,"drop, Unit>>"],[211,"storage_base_address_const<289565229787362368933081636443797405535488074065834425092593015835915391953>"],[212,"storage_base_address_const<1129664241071644691371073118594794953592340198277473102285062464307545102410>"],[213,"storage_base_address_const<1804974537427402286278400303388660593172206410421526189703894999503593972097>"],[214,"struct_construct>"],[215,"snapshot_take>"],[216,"drop>"],[217,"struct_deconstruct>"],[218,"function_call"],[219,"enum_init>, 0>"],[220,"struct_construct, core::option::Option::>>>"],[221,"enum_init, core::option::Option::>)>, 0>"],[222,"store_temp, core::option::Option::>)>>"],[223,"felt252_sub"],[224,"enum_init, core::option::Option::>)>, 1>"],[225,"dup>>"],[226,"u32_try_from_felt252"],[227,"array_slice"],[228,"array_len"],[229,"u32_overflowing_sub"],[230,"enum_init>, 0>"],[231,"struct_construct, core::option::Option::>>>"],[232,"enum_init, core::option::Option::>)>, 0>"],[233,"store_temp, core::option::Option::>)>>"],[234,"const_as_immediate>"],[235,"enum_init, core::option::Option::>)>, 1>"],[236,"const_as_immediate>"],[237,"enum_init>, 1>"],[238,"alloc_local"],[239,"alloc_local"],[240,"alloc_local"],[241,"alloc_local"],[242,"alloc_local>"],[243,"alloc_local, felt252>>"],[244,"alloc_local, piltover::snos_output::StarknetOsOutput>>"],[245,"alloc_local"],[246,"finalize_locals"],[247,"struct_deconstruct"],[248,"function_call::start>"],[249,"enum_match, ())>>"],[250,"store_local"],[251,"store_local"],[252,"drop>"],[253,"drop>>"],[254,"drop>"],[255,"drop>"],[256,"drop, Unit>>"],[257,"drop>"],[258,"drop>"],[259,"drop, piltover::snos_output::StarknetOsOutput>>>"],[260,"drop, felt252>>>"],[261,"drop>"],[262,"store_temp>"],[263,"function_call"],[264,"store_local"],[265,"enum_match, core::felt252)>>"],[266,"const_as_immediate>"],[267,"dup>"],[268,"array_get"],[269,"store_temp>"],[270,"struct_deconstruct, felt252>>"],[271,"struct_construct>"],[272,"store_temp>"],[273,"store_local, felt252>>"],[274,"function_call"],[275,"enum_match, piltover::snos_output::StarknetOsOutput)>>"],[276,"struct_construct>>"],[277,"snapshot_take>>"],[278,"drop>>"],[279,"struct_deconstruct>>"],[280,"store_local, piltover::snos_output::StarknetOsOutput>>"],[281,"store_local"],[282,"struct_construct"],[283,"store_temp"],[284,"function_call"],[285,"enum_match>"],[286,"struct_deconstruct, piltover::snos_output::StarknetOsOutput>>"],[287,"drop>"],[288,"struct_deconstruct"],[289,"store_local>"],[290,"struct_construct"],[291,"store_temp"],[292,"function_call"],[293,"enum_match,)>>"],[294,"struct_deconstruct>>"],[295,"snapshot_take>"],[296,"drop>"],[297,"array_get"],[298,"store_temp>"],[299,"unbox"],[300,"struct_deconstruct"],[301,"drop"],[302,"rename"],[303,"const_as_immediate>"],[304,"drop>>"],[305,"drop>"],[306,"drop>"],[307,"array_new"],[308,"const_as_immediate>"],[309,"const_as_immediate>"],[310,"struct_construct"],[311,"store_temp"],[312,"function_call"],[313,"enum_match>"],[314,"struct_deconstruct>"],[315,"const_as_immediate>"],[316,"const_as_immediate>"],[317,"const_as_immediate>"],[318,"const_as_immediate>"],[319,"const_as_immediate>"],[320,"const_as_immediate>"],[321,"const_as_immediate>"],[322,"store_local"],[323,"snapshot_take"],[324,"drop"],[325,"dup>"],[326,"struct_snapshot_deconstruct"],[327,"dup>>"],[328,"array_len"],[329,"u32_to_felt252"],[330,"struct_construct>"],[331,"store_temp>"],[332,"function_call, core::bytes_31::bytes31Drop>>"],[333,"enum_match, ())>>"],[334,"struct_deconstruct, Unit>>"],[335,"drop>>"],[336,"drop>"],[337,"enum_init, 1>"],[338,"store_temp>"],[339,"struct_deconstruct>"],[340,"struct_construct"],[341,"enum_init"],[342,"snapshot_take"],[343,"drop"],[344,"store_temp"],[345,"function_call"],[346,"enum_match, core::array::Array::, ())>>"],[347,"struct_deconstruct, Array, Unit>>"],[348,"emit_event_syscall"],[349,"struct_construct"],[350,"storage_base_address_const<156362789606235336197082706430724496541581765233419757414883543862011615425>"],[351,"bool_to_felt252"],[352,"struct_construct>>"],[353,"snapshot_take>>"],[354,"drop>>"],[355,"struct_deconstruct>>"],[356,"struct_construct"],[357,"enum_init"],[358,"struct_deconstruct, Unit>>"],[359,"struct_deconstruct, Unit>>"],[360,"struct_construct>"],[361,"enum_init, 0>"],[362,"drop, felt252>>"],[363,"const_as_immediate>"],[364,"drop, piltover::snos_output::StarknetOsOutput>>"],[365,"const_as_immediate>"],[366,"const_as_immediate>"],[367,"const_as_immediate>"],[368,"const_as_immediate>"],[369,"drop>"],[370,"storage_base_address_const<1239149872729906871793169171313897310809028090219849129902089947133222824240>"],[371,"dup"],[372,"const_as_immediate>"],[373,"enum_init, 1>"],[374,"store_temp>"],[375,"struct_construct>"],[376,"enum_init, 0>"],[377,"const_as_immediate>"],[378,"dup"],[379,"class_hash_to_felt252"],[380,"const_as_immediate>"],[381,"enum_init, ())>, 1>"],[382,"store_temp, ())>>"],[383,"replace_class_syscall"],[384,"struct_construct"],[385,"enum_init"],[386,"enum_init"],[387,"struct_construct, Unit>>"],[388,"enum_init, ())>, 0>"],[389,"struct_construct>>>"],[390,"snapshot_take>>>"],[391,"drop>>>"],[392,"struct_deconstruct>>>"],[393,"struct_construct>>"],[394,"snapshot_take>>"],[395,"drop>>"],[396,"struct_deconstruct>>"],[397,"struct_construct, Unit>>"],[398,"enum_init, ())>, 0>"],[399,"store_temp, ())>>"],[400,"enum_init, ())>, 1>"],[401,"const_as_immediate>"],[402,"const_as_immediate>"],[403,"struct_construct"],[404,"enum_init"],[405,"enum_init"],[406,"struct_construct, core::bool>>"],[407,"enum_init, core::bool)>, 0>"],[408,"store_temp, core::bool)>>"],[409,"enum_init, core::bool)>, 1>"],[410,"alloc_local"],[411,"storage_base_address_const<1797750322404263956506055577226893145606273830944403103130357494860989748873>"],[412,"felt252_add"],[413,"store_local"],[414,"function_call"],[415,"enum_match>"],[416,"struct_deconstruct>"],[417,"struct_construct"],[418,"enum_init"],[419,"enum_init"],[420,"struct_construct>>>"],[421,"snapshot_take>>>"],[422,"drop>>>"],[423,"struct_deconstruct>>>"],[424,"struct_construct, Tuple>>"],[425,"enum_init, (core::felt252, core::felt252))>, 0>"],[426,"store_temp, (core::felt252, core::felt252))>>"],[427,"enum_init, (core::felt252, core::felt252))>, 1>"],[428,"drop>"],[429,"function_call"],[430,"const_as_immediate>"],[431,"enum_init, core::felt252)>, 1>"],[432,"store_temp, core::felt252)>>"],[433,"struct_construct"],[434,"enum_init"],[435,"struct_construct, felt252>>"],[436,"enum_init, core::felt252)>, 0>"],[437,"const_as_immediate>"],[438,"store_temp>"],[439,"unbox"],[440,"const_as_immediate>"],[441,"struct_construct>>>"],[442,"snapshot_take>>>"],[443,"drop>>>"],[444,"struct_deconstruct>>>"],[445,"struct_deconstruct"],[446,"drop"],[447,"u64_to_felt252"],[448,"struct_construct"],[449,"enum_init"],[450,"const_as_immediate>"],[451,"struct_construct>>"],[452,"snapshot_take>>"],[453,"drop>>"],[454,"struct_deconstruct>>"],[455,"u64_try_from_felt252"],[456,"const_as_immediate>"],[457,"dup"],[458,"u64_eq"],[459,"storage_base_address_const<1001666092121413518972607095611289009321985163796988836773455972707894918717>"],[460,"u64_overflowing_add"],[461,"u64_overflowing_sub"],[462,"store_temp"],[463,"struct_construct"],[464,"enum_init"],[465,"const_as_immediate>"],[466,"const_as_immediate>"],[467,"const_as_immediate>"],[468,"const_as_immediate>"],[469,"const_as_immediate>"],[470,"alloc_local>"],[471,"alloc_local>"],[472,"drop>>"],[473,"drop>>"],[474,"enum_init, core::option::Option::)>, 1>"],[475,"store_temp, core::option::Option::)>>"],[476,"store_local>"],[477,"array_new"],[478,"store_temp>"],[479,"function_call>"],[480,"enum_match, core::option::Option::>)>>"],[481,"struct_deconstruct, core::option::Option::>>>"],[482,"store_temp>>"],[483,"enum_init>, 1>"],[484,"enum_match>>"],[485,"store_local>"],[486,"array_new>"],[487,"store_temp>>"],[488,"function_call, core::serde::SerializeTupleNext::<(@core::felt252, @core::felt252), core::metaprogramming::TupleSplitTupleSize2::<@core::felt252, @core::felt252>, core::serde::SerdeBasedSerializeTuple::, core::serde::SerializeTupleNext::<(@core::felt252,), core::metaprogramming::TupleSplitTupleSize1::<@core::felt252>, core::serde::SerdeBasedSerializeTuple::, core::serde::SerializeTupleBaseTuple, core::traits::TupleSize0Drop>, core::traits::TupleNextDrop::<(@core::felt252,), core::metaprogramming::TupleSplitTupleSize1::<@core::felt252>, core::metaprogramming::IsTupleTupleSize1::<@core::felt252>, core::traits::SnapshotDrop::, core::traits::TupleSize0Drop>>, core::serde::DeserializeTupleNext::<(core::felt252, core::felt252), core::metaprogramming::TupleSplitTupleSize2::, core::Felt252Serde, core::serde::DeserializeTupleNext::<(core::felt252,), core::metaprogramming::TupleSplitTupleSize1::, core::Felt252Serde, core::serde::DeserializeTupleBaseTuple, core::felt252Drop>, core::felt252Drop>>, core::traits::TupleNextDrop::<(core::felt252, core::felt252), core::metaprogramming::TupleSplitTupleSize2::, core::metaprogramming::IsTupleTupleSize2::, core::felt252Drop, core::traits::TupleNextDrop::<(core::felt252,), core::metaprogramming::TupleSplitTupleSize1::, core::metaprogramming::IsTupleTupleSize1::, core::felt252Drop, core::traits::TupleSize0Drop>>>>"],[489,"enum_match, core::option::Option::>)>>"],[490,"struct_deconstruct, core::option::Option::>>>"],[491,"store_temp>>"],[492,"enum_init>, 1>"],[493,"enum_match>>"],[494,"enum_init, 0>"],[495,"struct_construct, core::option::Option::>>"],[496,"enum_init, core::option::Option::)>, 0>"],[497,"enum_init, 1>"],[498,"struct_construct, Unit>>"],[499,"enum_init, ())>, 0>"],[500,"store_temp, ())>>"],[501,"enum_init, ())>, 1>"],[502,"const_as_immediate>"],[503,"const_as_immediate>"],[504,"function_call::_transfer_ownership>"],[505,"enum_match, ())>>"],[506,"const_as_immediate>"],[507,"struct_deconstruct, Unit>>"],[508,"drop, Unit>>"],[509,"drop>"],[510,"struct_construct, Unit>>"],[511,"enum_init, ())>, 0>"],[512,"store_temp, ())>>"],[513,"enum_init, ())>, 1>"],[514,"const_as_immediate>"],[515,"hades_permutation"],[516,"dup"],[517,"drop"],[518,"enum_init, core::felt252)>, 1>"],[519,"store_temp, core::felt252)>>"],[520,"struct_construct, felt252>>"],[521,"enum_init, core::felt252)>, 0>"],[522,"const_as_immediate>"],[523,"struct_construct>"],[524,"store_temp>"],[525,"function_call"],[526,"enum_match, core::array::ArrayIter::, core::array::Array::, ())>>"],[527,"struct_deconstruct, core::array::ArrayIter::, Array, Unit>>"],[528,"drop>"],[529,"const_as_immediate>"],[530,"const_as_immediate>"],[531,"const_as_immediate>"],[532,"const_as_immediate>"],[533,"const_as_immediate>"],[534,"u32_wide_mul"],[535,"downcast"],[536,"function_call"],[537,"enum_match, (core::array::Array::, core::array::Array::))>>"],[538,"struct_deconstruct, Tuple, Array>>>"],[539,"struct_deconstruct, Array>>"],[540,"function_call"],[541,"enum_match, core::array::Array::)>>"],[542,"struct_deconstruct, Array>>"],[543,"function_call"],[544,"enum_match, core::array::Array::<(core::felt252, core::felt252)>)>>"],[545,"struct_deconstruct, Array>>>"],[546,"enum_init, piltover::snos_output::StarknetOsOutput)>, 1>"],[547,"store_temp, piltover::snos_output::StarknetOsOutput)>>"],[548,"const_as_immediate>"],[549,"const_as_immediate>"],[550,"const_as_immediate>"],[551,"struct_construct, piltover::snos_output::StarknetOsOutput>>"],[552,"enum_init, piltover::snos_output::StarknetOsOutput)>, 0>"],[553,"const_as_immediate>"],[554,"const_as_immediate>"],[555,"alloc_local"],[556,"store_local"],[557,"upcast"],[558,"array_new"],[559,"store_temp>"],[560,"function_call"],[561,"enum_match, core::integer::u32, ())>>"],[562,"array_new"],[563,"struct_deconstruct, u32, Unit>>"],[564,"snapshot_take>"],[565,"drop>"],[566,"struct_construct>"],[567,"store_temp>"],[568,"store_temp>"],[569,"function_call"],[570,"enum_match, core::array::Array::, ())>>"],[571,"struct_deconstruct, Array, Unit>>"],[572,"drop>"],[573,"function_call"],[574,"enum_match, ())>>"],[575,"struct_deconstruct, Unit>>"],[576,"snapshot_take>"],[577,"drop>"],[578,"struct_construct>"],[579,"keccak_syscall"],[580,"array_append"],[581,"const_as_immediate>"],[582,"dup"],[583,"struct_deconstruct"],[584,"store_temp"],[585,"struct_deconstruct"],[586,"u128_overflowing_add"],[587,"const_as_immediate>"],[588,"drop"],[589,"const_as_immediate, Const>>"],[590,"struct_construct>"],[591,"enum_init, 0>"],[592,"store_temp>"],[593,"const_as_immediate>"],[594,"enum_init, 1>"],[595,"drop"],[596,"struct_deconstruct"],[597,"const_as_immediate>"],[598,"call_contract_syscall"],[599,"array_new"],[600,"store_temp>"],[601,"function_call>"],[602,"enum_match, core::option::Option::>)>>"],[603,"struct_deconstruct, core::option::Option::>>>"],[604,"enum_match>>"],[605,"struct_construct>>"],[606,"enum_init,)>, 0>"],[607,"store_temp,)>>"],[608,"enum_init,)>, 1>"],[609,"const_as_immediate>"],[610,"u32_eq"],[611,"struct_deconstruct"],[612,"u32_overflowing_add"],[613,"const_as_immediate>"],[614,"function_call"],[615,"enum_match>"],[616,"struct_deconstruct>"],[617,"u128_is_zero"],[618,"drop>"],[619,"const_as_immediate>"],[620,"u128_safe_divmod"],[621,"u128_to_felt252"],[622,"const_as_immediate>"],[623,"felt252_mul"],[624,"bytes31_try_from_felt252"],[625,"array_append"],[626,"enum_init, 1>"],[627,"store_temp>"],[628,"rename"],[629,"struct_construct>"],[630,"enum_init, 0>"],[631,"const_as_immediate>"],[632,"struct_deconstruct>"],[633,"array_snapshot_pop_front"],[634,"enum_init>, 0>"],[635,"store_temp>>"],[636,"store_temp>>"],[637,"enum_init>, 1>"],[638,"enum_match>>"],[639,"unbox"],[640,"rename"],[641,"bytes31_to_felt252"],[642,"struct_construct, Unit>>"],[643,"enum_init, ())>, 0>"],[644,"store_temp, ())>>"],[645,"drop>"],[646,"enum_init, ())>, 1>"],[647,"enum_match"],[648,"enum_match"],[649,"const_as_immediate>"],[650,"dup"],[651,"struct_deconstruct"],[652,"rename"],[653,"const_as_immediate>"],[654,"dup"],[655,"struct_deconstruct"],[656,"enum_match"],[657,"const_as_immediate>"],[658,"struct_deconstruct"],[659,"rename"],[660,"enum_match"],[661,"const_as_immediate>"],[662,"dup"],[663,"struct_deconstruct"],[664,"enum_match"],[665,"const_as_immediate>"],[666,"store_temp"],[667,"function_call"],[668,"const_as_immediate>"],[669,"store_temp"],[670,"function_call"],[671,"const_as_immediate>"],[672,"store_temp"],[673,"function_call"],[674,"const_as_immediate>"],[675,"store_temp"],[676,"function_call"],[677,"const_as_immediate>"],[678,"store_temp"],[679,"function_call"],[680,"const_as_immediate>"],[681,"store_temp"],[682,"function_call"],[683,"enum_match"],[684,"enum_match"],[685,"const_as_immediate>"],[686,"dup"],[687,"struct_deconstruct"],[688,"const_as_immediate>"],[689,"struct_deconstruct"],[690,"dup"],[691,"rename"],[692,"struct_construct, Array, Unit>>"],[693,"enum_init, core::array::Array::, ())>, 0>"],[694,"store_temp, core::array::Array::, ())>>"],[695,"function_call"],[696,"enum_match, core::integer::u32, ())>>"],[697,"struct_deconstruct, u32, Unit>>"],[698,"struct_construct>"],[699,"enum_init, 0>"],[700,"store_temp>"],[701,"enum_init, 1>"],[702,"function_call"],[703,"enum_init>, 0>"],[704,"struct_construct, core::option::Option::>>>"],[705,"enum_init, core::option::Option::>)>, 0>"],[706,"store_temp, core::option::Option::>)>>"],[707,"function_call"],[708,"enum_match, core::option::Option::)>>"],[709,"struct_deconstruct, core::option::Option::>>"],[710,"enum_match>"],[711,"array_append"],[712,"enum_init, core::option::Option::>)>, 1>"],[713,"enum_init>, 0>"],[714,"struct_construct, core::option::Option::>>>"],[715,"enum_init, core::option::Option::>)>, 0>"],[716,"store_temp, core::option::Option::>)>>"],[717,"enum_init, 0>"],[718,"store_temp>"],[719,"enum_init, 1>"],[720,"enum_match>"],[721,"array_append>"],[722,"enum_init, core::option::Option::>)>, 1>"],[723,"contract_address_const<0>"],[724,"storage_base_address_const<1014900818724271728842439076957192829503821588950108430443072887165436450886>"],[725,"struct_construct>>"],[726,"snapshot_take>>"],[727,"drop>>"],[728,"struct_deconstruct>>"],[729,"struct_construct"],[730,"enum_init"],[731,"enum_init"],[732,"struct_construct, Unit>>"],[733,"enum_init, ())>, 0>"],[734,"store_temp, ())>>"],[735,"enum_init, ())>, 1>"],[736,"struct_deconstruct>"],[737,"enum_init, 0>"],[738,"store_temp>"],[739,"enum_init, core::array::ArrayIter::, core::array::Array::, ())>, 1>"],[740,"store_temp, core::array::ArrayIter::, core::array::Array::, ())>>"],[741,"enum_init, 1>"],[742,"enum_match>"],[743,"struct_deconstruct>"],[744,"array_pop_front"],[745,"snapshot_take>"],[746,"drop>"],[747,"struct_construct, core::array::ArrayIter::, Array, Unit>>"],[748,"enum_init, core::array::ArrayIter::, core::array::Array::, ())>, 0>"],[749,"struct_construct, Array>>"],[750,"struct_construct, Tuple, Array>>>"],[751,"enum_init, (core::array::Array::, core::array::Array::))>, 0>"],[752,"store_temp, (core::array::Array::, core::array::Array::))>>"],[753,"enum_init, (core::array::Array::, core::array::Array::))>, 1>"],[754,"const_as_immediate>"],[755,"const_as_immediate>"],[756,"function_call"],[757,"enum_match, core::array::Array::, core::array::ArrayIter::, ())>>"],[758,"struct_deconstruct, Array, core::array::ArrayIter::, Unit>>"],[759,"struct_construct, Array>>"],[760,"enum_init, core::array::Array::)>, 0>"],[761,"store_temp, core::array::Array::)>>"],[762,"enum_init, core::array::Array::)>, 1>"],[763,"const_as_immediate>"],[764,"snapshot_take"],[765,"function_call"],[766,"enum_match, core::array::ArrayIter::, core::array::Array::<(core::felt252, core::felt252)>, ())>>"],[767,"struct_deconstruct, core::array::ArrayIter::, Array>, Unit>>"],[768,"struct_construct, Array>>>"],[769,"enum_init, core::array::Array::<(core::felt252, core::felt252)>)>, 0>"],[770,"store_temp, core::array::Array::<(core::felt252, core::felt252)>)>>"],[771,"enum_init, core::array::Array::<(core::felt252, core::felt252)>)>, 1>"],[772,"enum_init, core::integer::u32, ())>, 1>"],[773,"store_temp, core::integer::u32, ())>>"],[774,"struct_construct, u32, Unit>>"],[775,"enum_init, core::integer::u32, ())>, 0>"],[776,"struct_deconstruct>"],[777,"array_snapshot_pop_front"],[778,"enum_init>, 0>"],[779,"store_temp>>"],[780,"store_temp>>"],[781,"enum_init>, 1>"],[782,"enum_match>>"],[783,"unbox"],[784,"rename"],[785,"function_call"],[786,"enum_init, core::array::Array::, ())>, 1>"],[787,"store_temp, core::array::Array::, ())>>"],[788,"struct_construct, Array, Unit>>"],[789,"enum_init, core::array::Array::, ())>, 0>"],[790,"array_len"],[791,"const_as_immediate, Const>>"],[792,"store_temp>"],[793,"u32_safe_divmod"],[794,"const_as_immediate>"],[795,"enum_init, ())>, 1>"],[796,"store_temp, ())>>"],[797,"const_as_immediate>"],[798,"const_as_immediate>"],[799,"rename"],[800,"const_as_immediate>"],[801,"const_as_immediate>"],[802,"const_as_immediate>"],[803,"const_as_immediate>"],[804,"const_as_immediate>"],[805,"u64_is_zero"],[806,"u64_safe_divmod"],[807,"const_as_immediate>"],[808,"array_append"],[809,"function_call"],[810,"const_as_immediate>"],[811,"struct_construct, Unit>>"],[812,"enum_init, ())>, 0>"],[813,"enum_init>, 0>"],[814,"struct_construct, core::option::Option::>>>"],[815,"enum_init, core::option::Option::>)>, 0>"],[816,"store_temp, core::option::Option::>)>>"],[817,"function_call"],[818,"enum_match>"],[819,"struct_construct"],[820,"enum_init, 0>"],[821,"store_temp>"],[822,"enum_init, 1>"],[823,"enum_match>"],[824,"array_append"],[825,"enum_init>, 1>"],[826,"enum_init, core::option::Option::>)>, 1>"],[827,"downcast>"],[828,"enum_from_bounded_int>"],[829,"store_temp>"],[830,"enum_match>"],[831,"const_as_immediate>"],[832,"const_as_immediate>"],[833,"const_as_immediate>"],[834,"const_as_immediate>"],[835,"const_as_immediate>"],[836,"const_as_immediate>"],[837,"const_as_immediate>"],[838,"const_as_immediate>"],[839,"const_as_immediate>"],[840,"const_as_immediate>"],[841,"const_as_immediate>"],[842,"const_as_immediate>"],[843,"const_as_immediate>"],[844,"const_as_immediate>"],[845,"const_as_immediate>"],[846,"struct_construct>"],[847,"enum_init, 0>"],[848,"store_temp>"],[849,"const_as_immediate>"],[850,"enum_init, 1>"],[851,"dup"],[852,"struct_deconstruct"],[853,"rename>"],[854,"function_call>"],[855,"enum_init, core::array::Array::, ())>, 1>"],[856,"dup"],[857,"struct_deconstruct"],[858,"dup"],[859,"struct_deconstruct"],[860,"drop"],[861,"dup"],[862,"struct_deconstruct"],[863,"drop"],[864,"dup"],[865,"struct_deconstruct"],[866,"dup"],[867,"struct_deconstruct"],[868,"snapshot_take>"],[869,"rename>>"],[870,"enum_init, core::integer::u32, ())>, 1>"],[871,"store_temp, core::integer::u32, ())>>"],[872,"struct_construct, u32, Unit>>"],[873,"enum_init, core::integer::u32, ())>, 0>"],[874,"alloc_local>"],[875,"function_call>::deserialize>"],[876,"enum_match>>"],[877,"store_local>"],[878,"enum_init, core::option::Option::)>, 1>"],[879,"store_temp, core::option::Option::)>>"],[880,"struct_construct"],[881,"enum_init, 0>"],[882,"struct_construct, core::option::Option::>>"],[883,"enum_init, core::option::Option::)>, 0>"],[884,"enum_init, 1>"],[885,"drop>>"],[886,"drop>"],[887,"enum_init, core::array::Array::, core::array::ArrayIter::, ())>, 1>"],[888,"store_temp, core::array::Array::, core::array::ArrayIter::, ())>>"],[889,"function_call"],[890,"enum_match, piltover::snos_output::ContractChanges)>>"],[891,"struct_deconstruct, piltover::snos_output::ContractChanges>>"],[892,"struct_construct, Array, core::array::ArrayIter::, Unit>>"],[893,"enum_init, core::array::Array::, core::array::ArrayIter::, ())>, 0>"],[894,"enum_init, core::array::ArrayIter::, core::array::Array::<(core::felt252, core::felt252)>, ())>, 1>"],[895,"store_temp, core::array::ArrayIter::, core::array::Array::<(core::felt252, core::felt252)>, ())>>"],[896,"store_temp>"],[897,"struct_construct, core::array::ArrayIter::, Array>, Unit>>"],[898,"enum_init, core::array::ArrayIter::, core::array::Array::<(core::felt252, core::felt252)>, ())>, 0>"],[899,"const_as_immediate, Const>>"],[900,"store_temp>"],[901,"downcast"],[902,"struct_construct"],[903,"enum_init, 0>"],[904,"store_temp>"],[905,"enum_init, 1>"],[906,"enum_init>, 1>"],[907,"store_temp>>"],[908,"enum_init>, 0>"],[909,"alloc_local"],[910,"const_as_immediate, Const, Const>>>"],[911,"store_temp>"],[912,"u256_safe_divmod"],[913,"u128_mul_guarantee_verify"],[914,"store_local"],[915,"snapshot_take"],[916,"const_as_immediate, Const>>"],[917,"u128_eq"],[918,"enum_init, piltover::snos_output::ContractChanges)>, 1>"],[919,"store_temp, piltover::snos_output::ContractChanges)>>"],[920,"rename>"],[921,"downcast"],[922,"function_call"],[923,"const_as_immediate>"],[924,"u128_overflowing_sub"],[925,"struct_construct, piltover::snos_output::ContractChanges>>"],[926,"enum_init, piltover::snos_output::ContractChanges)>, 0>"],[927,"drop, core::array::ArrayIter::, Array>, Unit>>"],[928,"const_as_immediate>"],[929,"drop>"]],"user_func_names":[[0,"piltover::appchain::appchain::__wrapper__Appchain__update_state"],[1,"piltover::appchain::appchain::__wrapper__UpgradeableImpl__upgrade"],[2,"piltover::config::component::config_cpt::__wrapper__ConfigImpl__register_operator::"],[3,"piltover::config::component::config_cpt::__wrapper__ConfigImpl__unregister_operator::"],[4,"piltover::config::component::config_cpt::__wrapper__ConfigImpl__is_operator::"],[5,"piltover::config::component::config_cpt::__wrapper__ConfigImpl__set_program_info::"],[6,"piltover::config::component::config_cpt::__wrapper__ConfigImpl__get_program_info::"],[7,"piltover::config::component::config_cpt::__wrapper__ConfigImpl__set_facts_registry::"],[8,"piltover::config::component::config_cpt::__wrapper__ConfigImpl__get_facts_registry::"],[9,"piltover::messaging::component::messaging_cpt::__wrapper__MessagingImpl__send_message_to_appchain::"],[10,"piltover::messaging::component::messaging_cpt::__wrapper__MessagingImpl__sn_to_appchain_messages::"],[11,"piltover::messaging::component::messaging_cpt::__wrapper__MessagingImpl__appchain_to_sn_messages::"],[12,"piltover::messaging::component::messaging_cpt::__wrapper__MessagingImpl__consume_message_from_appchain::"],[13,"piltover::messaging::component::messaging_cpt::__wrapper__MessagingImpl__start_message_cancellation::"],[14,"piltover::messaging::component::messaging_cpt::__wrapper__MessagingImpl__cancel_message::"],[15,"piltover::state::component::state_cpt::__wrapper__StateImpl__update::"],[16,"piltover::state::component::state_cpt::__wrapper__StateImpl__get_state::"],[17,"piltover::appchain::appchain::__wrapper__constructor"],[18,"core::array::deserialize_array_helper::"],[19,"core::array::SpanFelt252Serde::deserialize"],[20,"piltover::appchain::appchain::Appchain::update_state"],[21,"openzeppelin_access::ownable::ownable::OwnableComponent::InternalImpl::::assert_only_owner"],[22,"openzeppelin_upgrades::upgradeable::UpgradeableComponent::InternalImpl::::upgrade"],[23,"piltover::config::component::config_cpt::Config::::register_operator"],[24,"piltover::config::component::config_cpt::Config::::unregister_operator"],[25,"piltover::config::component::config_cpt::Config::::set_program_info"],[26,"piltover::config::component::config_cpt::InternalImpl::::is_owner_or_operator"],[27,"piltover::messaging::component::messaging_cpt::Messaging::::send_message_to_appchain"],[28,"piltover::messaging::component::messaging_cpt::Messaging::::consume_message_from_appchain"],[29,"piltover::messaging::component::messaging_cpt::Messaging::::start_message_cancellation"],[30,"piltover::messaging::component::messaging_cpt::Messaging::::cancel_message"],[31,"piltover::snos_output::StarknetOsOutputSerde::deserialize"],[32,"piltover::state::component::state_cpt::State::::update"],[33,"piltover::appchain::appchain::constructor"],[34,"openzeppelin_security::reentrancyguard::ReentrancyGuardComponent::InternalImpl::::start"],[35,"core::poseidon::_poseidon_hash_span_inner"],[36,"piltover::snos_output::deserialize_os_output"],[37,"piltover::components::onchain_data_fact_tree_encoder::encode_fact_with_onchain_data"],[38,"piltover::fact_registry::IFactRegistryDispatcherImpl::get_all_verifications_for_fact_hash"],[39,"core::byte_array::ByteArrayImpl::append_word"],[40,"core::array::serialize_array_helper::, core::bytes_31::bytes31Drop>"],[41,"piltover::appchain::appchain::EventIsEvent::append_keys_and_data"],[42,"piltover::messaging::hash::compute_message_hash_sn_to_appc"],[43,"piltover::messaging::hash::compute_message_hash_appc_to_sn"],[44,"core::array::deserialize_array_helper::"],[45,"core::array::deserialize_array_helper::<(core::felt252, core::felt252), core::serde::SerdeTuple::<(core::felt252, core::felt252), core::metaprogramming::TupleSnapForwardTupleSize2::, core::serde::SerializeTupleNext::<(@core::felt252, @core::felt252), core::metaprogramming::TupleSplitTupleSize2::<@core::felt252, @core::felt252>, core::serde::SerdeBasedSerializeTuple::, core::serde::SerializeTupleNext::<(@core::felt252,), core::metaprogramming::TupleSplitTupleSize1::<@core::felt252>, core::serde::SerdeBasedSerializeTuple::, core::serde::SerializeTupleBaseTuple, core::traits::TupleSize0Drop>, core::traits::TupleNextDrop::<(@core::felt252,), core::metaprogramming::TupleSplitTupleSize1::<@core::felt252>, core::metaprogramming::IsTupleTupleSize1::<@core::felt252>, core::traits::SnapshotDrop::, core::traits::TupleSize0Drop>>, core::serde::DeserializeTupleNext::<(core::felt252, core::felt252), core::metaprogramming::TupleSplitTupleSize2::, core::Felt252Serde, core::serde::DeserializeTupleNext::<(core::felt252,), core::metaprogramming::TupleSplitTupleSize1::, core::Felt252Serde, core::serde::DeserializeTupleBaseTuple, core::felt252Drop>, core::felt252Drop>>, core::traits::TupleNextDrop::<(core::felt252, core::felt252), core::metaprogramming::TupleSplitTupleSize2::, core::metaprogramming::IsTupleTupleSize2::, core::felt252Drop, core::traits::TupleNextDrop::<(core::felt252,), core::metaprogramming::TupleSplitTupleSize1::, core::metaprogramming::IsTupleTupleSize1::, core::felt252Drop, core::traits::TupleSize0Drop>>>"],[46,"openzeppelin_access::ownable::ownable::OwnableComponent::InternalImpl::::_transfer_ownership"],[47,"piltover::snos_output::read_segment[expr20]"],[48,"piltover::snos_output::deserialize_messages"],[49,"piltover::snos_output::deserialize_contract_state"],[50,"piltover::snos_output::deserialize_contract_class_da_changes"],[51,"piltover::components::onchain_data_fact_tree_encoder::hash_main_public_input[expr21]"],[52,"core::keccak::keccak_u256s_le_inputs[expr12]"],[53,"core::keccak::add_padding"],[54,"core::array::deserialize_array_helper::"],[55,"core::bytes_31::one_shift_left_bytes_u128"],[56,"piltover::messaging::component::messaging_cpt::MessageSentIsEvent::append_keys_and_data"],[57,"piltover::messaging::component::messaging_cpt::MessageConsumedIsEvent::append_keys_and_data"],[58,"piltover::messaging::component::messaging_cpt::MessageCancellationStartedIsEvent::append_keys_and_data"],[59,"piltover::messaging::component::messaging_cpt::MessageCanceledIsEvent::append_keys_and_data"],[60,"piltover::messaging::component::messaging_cpt::MessageToStarknetReceivedIsEvent::append_keys_and_data"],[61,"piltover::messaging::component::messaging_cpt::MessageToAppchainSealedIsEvent::append_keys_and_data"],[62,"piltover::messaging::hash::compute_message_hash_sn_to_appc[expr43]"],[63,"piltover::messaging::hash::compute_message_hash_appc_to_sn[expr37]"],[64,"piltover::snos_output::ContractChangesSerde::deserialize"],[65,"piltover::snos_output::deserialize_contract_state[expr20]"],[66,"piltover::snos_output::deserialize_contract_class_da_changes[expr34]"],[67,"core::keccak::keccak_add_u256_le"],[68,"core::keccak::finalize_padding"],[69,"piltover::fact_registry::VerifierConfigurationSerde::deserialize"],[70,"core::array::serialize_array_helper::"],[71,"core::option::OptionSerde::>::deserialize"],[72,"piltover::snos_output::deserialize_contract_state_inner"],[73,"piltover::snos_output::deserialize_da_changes[expr28]"]]},"contract_class_version":"0.1.0","entry_points_by_type":{"EXTERNAL":[{"selector":"0x2549ff25a175de1943bdc22c5dbb78a525c80609acee4de9a04e7492863f3c","function_idx":7},{"selector":"0x2663aeb45b150dac35764aa08d6f63f22d2b4de34ee48444bc273dcbf1cd67","function_idx":8},{"selector":"0x409b12db665f7299411a305bc58eea6f517a0553d0647770676955301d7534","function_idx":5},{"selector":"0xc6e11b003f40319c50fc40fbb855706de530051f40b8e7a655f8d153450e24","function_idx":9},{"selector":"0xe89119be80b4c44200d6d1ce55e034a3df6f520732ecd72a4f9de66315944a","function_idx":3},{"selector":"0xee6a55b4abdb140c29238ac4fab5a7bffe017ea803d34c252ab577e0ac574f","function_idx":13},{"selector":"0xf2f7c15cbe06c8d94597cd91fd7f3369eae842359235712def5584f8d270cd","function_idx":1},{"selector":"0x1225b6991f74ebc0ae2a3d9a901862d834f58cf35b47c6377b0deb13f11f101","function_idx":11},{"selector":"0x13f487a4369c7172d6956afb2cf6f64608acbb24acc5f3d5aa90bd5816286d3","function_idx":4},{"selector":"0x196a5f000a6df3bde4c611e5561aa002ac6cc04b4149c1f02f473b2ef445c76","function_idx":0},{"selector":"0x208c00df249878f8454e5528363b3d27190a6646050e118c95896081e79b625","function_idx":2},{"selector":"0x283750cfd3a499d9b1d0474cd10389c83d2e0119075e6a83ecb4bf2a6d46fe4","function_idx":14},{"selector":"0x2f8d21b3c3919d0cb2b4728880495e379f8c1817d7867ff6b1360f2321f9598","function_idx":15},{"selector":"0x3593f537ca0121e22c58378d40e0f5e2ba89b1c6d92a6990ab3066b68088f9c","function_idx":16},{"selector":"0x37a1c5f4fd1421fad75205fd22017e4bebdf839206943366bea9db7c5aa78b7","function_idx":6},{"selector":"0x3a331f510f425bd13c385fe39217545457f909476a12fdd52d2c5ee6bd3e5f9","function_idx":10},{"selector":"0x3f96d0c5ada364a9c9a865acbe185cc58f06889894eb77b9cdc8e3554a82463","function_idx":12}],"L1_HANDLER":[],"CONSTRUCTOR":[{"selector":"0x28ffe4ff0f226a9107253e17a904099aa4f63a02a5621de0576e5aa71bc5194","function_idx":17}]},"abi":[{"type":"impl","name":"Appchain","interface_name":"piltover::interface::IAppchain"},{"type":"struct","name":"core::array::Span::","members":[{"name":"snapshot","type":"@core::array::Array::"}]},{"type":"struct","name":"core::integer::u256","members":[{"name":"low","type":"core::integer::u128"},{"name":"high","type":"core::integer::u128"}]},{"type":"interface","name":"piltover::interface::IAppchain","items":[{"type":"function","name":"update_state","inputs":[{"name":"snos_output","type":"core::array::Array::"},{"name":"program_output","type":"core::array::Span::"},{"name":"onchain_data_hash","type":"core::felt252"},{"name":"onchain_data_size","type":"core::integer::u256"}],"outputs":[],"state_mutability":"external"}]},{"type":"impl","name":"UpgradeableImpl","interface_name":"openzeppelin_upgrades::interface::IUpgradeable"},{"type":"interface","name":"openzeppelin_upgrades::interface::IUpgradeable","items":[{"type":"function","name":"upgrade","inputs":[{"name":"new_class_hash","type":"core::starknet::class_hash::ClassHash"}],"outputs":[],"state_mutability":"external"}]},{"type":"impl","name":"ConfigImpl","interface_name":"piltover::config::interface::IConfig"},{"type":"enum","name":"core::bool","variants":[{"name":"False","type":"()"},{"name":"True","type":"()"}]},{"type":"interface","name":"piltover::config::interface::IConfig","items":[{"type":"function","name":"register_operator","inputs":[{"name":"address","type":"core::starknet::contract_address::ContractAddress"}],"outputs":[],"state_mutability":"external"},{"type":"function","name":"unregister_operator","inputs":[{"name":"address","type":"core::starknet::contract_address::ContractAddress"}],"outputs":[],"state_mutability":"external"},{"type":"function","name":"is_operator","inputs":[{"name":"address","type":"core::starknet::contract_address::ContractAddress"}],"outputs":[{"type":"core::bool"}],"state_mutability":"view"},{"type":"function","name":"set_program_info","inputs":[{"name":"program_hash","type":"core::felt252"},{"name":"config_hash","type":"core::felt252"}],"outputs":[],"state_mutability":"external"},{"type":"function","name":"get_program_info","inputs":[],"outputs":[{"type":"(core::felt252, core::felt252)"}],"state_mutability":"view"},{"type":"function","name":"set_facts_registry","inputs":[{"name":"address","type":"core::starknet::contract_address::ContractAddress"}],"outputs":[],"state_mutability":"external"},{"type":"function","name":"get_facts_registry","inputs":[],"outputs":[{"type":"core::starknet::contract_address::ContractAddress"}],"state_mutability":"view"}]},{"type":"impl","name":"MessagingImpl","interface_name":"piltover::messaging::interface::IMessaging"},{"type":"enum","name":"piltover::messaging::types::MessageToAppchainStatus","variants":[{"name":"SealedOrNotSent","type":"()"},{"name":"Pending","type":"core::felt252"}]},{"type":"enum","name":"piltover::messaging::types::MessageToStarknetStatus","variants":[{"name":"NothingToConsume","type":"()"},{"name":"ReadyToConsume","type":"core::felt252"}]},{"type":"interface","name":"piltover::messaging::interface::IMessaging","items":[{"type":"function","name":"send_message_to_appchain","inputs":[{"name":"to_address","type":"core::starknet::contract_address::ContractAddress"},{"name":"selector","type":"core::felt252"},{"name":"payload","type":"core::array::Span::"}],"outputs":[{"type":"(core::felt252, core::felt252)"}],"state_mutability":"external"},{"type":"function","name":"consume_message_from_appchain","inputs":[{"name":"from_address","type":"core::starknet::contract_address::ContractAddress"},{"name":"payload","type":"core::array::Span::"}],"outputs":[{"type":"core::felt252"}],"state_mutability":"external"},{"type":"function","name":"sn_to_appchain_messages","inputs":[{"name":"message_hash","type":"core::felt252"}],"outputs":[{"type":"piltover::messaging::types::MessageToAppchainStatus"}],"state_mutability":"view"},{"type":"function","name":"appchain_to_sn_messages","inputs":[{"name":"message_hash","type":"core::felt252"}],"outputs":[{"type":"piltover::messaging::types::MessageToStarknetStatus"}],"state_mutability":"view"},{"type":"function","name":"start_message_cancellation","inputs":[{"name":"to_address","type":"core::starknet::contract_address::ContractAddress"},{"name":"selector","type":"core::felt252"},{"name":"payload","type":"core::array::Span::"},{"name":"nonce","type":"core::felt252"}],"outputs":[{"type":"core::felt252"}],"state_mutability":"external"},{"type":"function","name":"cancel_message","inputs":[{"name":"to_address","type":"core::starknet::contract_address::ContractAddress"},{"name":"selector","type":"core::felt252"},{"name":"payload","type":"core::array::Span::"},{"name":"nonce","type":"core::felt252"}],"outputs":[{"type":"core::felt252"}],"state_mutability":"external"}]},{"type":"impl","name":"StateImpl","interface_name":"piltover::state::interface::IState"},{"type":"enum","name":"core::option::Option::","variants":[{"name":"Some","type":"core::felt252"},{"name":"None","type":"()"}]},{"type":"struct","name":"piltover::snos_output::ContractChanges","members":[{"name":"addr","type":"core::felt252"},{"name":"nonce","type":"core::felt252"},{"name":"class_hash","type":"core::option::Option::"},{"name":"storage_changes","type":"core::array::Array::<(core::felt252, core::felt252)>"}]},{"type":"struct","name":"piltover::snos_output::StarknetOsOutput","members":[{"name":"initial_root","type":"core::felt252"},{"name":"final_root","type":"core::felt252"},{"name":"prev_block_number","type":"core::felt252"},{"name":"new_block_number","type":"core::felt252"},{"name":"prev_block_hash","type":"core::felt252"},{"name":"new_block_hash","type":"core::felt252"},{"name":"os_program_hash","type":"core::felt252"},{"name":"starknet_os_config_hash","type":"core::felt252"},{"name":"use_kzg_da","type":"core::felt252"},{"name":"full_output","type":"core::felt252"},{"name":"messages_to_l1","type":"core::array::Array::"},{"name":"messages_to_l2","type":"core::array::Array::"},{"name":"contracts","type":"core::array::Array::"},{"name":"classes","type":"core::array::Array::<(core::felt252, core::felt252)>"}]},{"type":"interface","name":"piltover::state::interface::IState","items":[{"type":"function","name":"update","inputs":[{"name":"program_output","type":"piltover::snos_output::StarknetOsOutput"}],"outputs":[],"state_mutability":"external"},{"type":"function","name":"get_state","inputs":[],"outputs":[{"type":"(core::felt252, core::felt252, core::felt252)"}],"state_mutability":"view"}]},{"type":"constructor","name":"constructor","inputs":[{"name":"owner","type":"core::starknet::contract_address::ContractAddress"},{"name":"state_root","type":"core::felt252"},{"name":"block_number","type":"core::felt252"},{"name":"block_hash","type":"core::felt252"}]},{"type":"event","name":"openzeppelin_access::ownable::ownable::OwnableComponent::OwnershipTransferred","kind":"struct","members":[{"name":"previous_owner","type":"core::starknet::contract_address::ContractAddress","kind":"key"},{"name":"new_owner","type":"core::starknet::contract_address::ContractAddress","kind":"key"}]},{"type":"event","name":"openzeppelin_access::ownable::ownable::OwnableComponent::OwnershipTransferStarted","kind":"struct","members":[{"name":"previous_owner","type":"core::starknet::contract_address::ContractAddress","kind":"key"},{"name":"new_owner","type":"core::starknet::contract_address::ContractAddress","kind":"key"}]},{"type":"event","name":"openzeppelin_access::ownable::ownable::OwnableComponent::Event","kind":"enum","variants":[{"name":"OwnershipTransferred","type":"openzeppelin_access::ownable::ownable::OwnableComponent::OwnershipTransferred","kind":"nested"},{"name":"OwnershipTransferStarted","type":"openzeppelin_access::ownable::ownable::OwnableComponent::OwnershipTransferStarted","kind":"nested"}]},{"type":"event","name":"openzeppelin_upgrades::upgradeable::UpgradeableComponent::Upgraded","kind":"struct","members":[{"name":"class_hash","type":"core::starknet::class_hash::ClassHash","kind":"data"}]},{"type":"event","name":"openzeppelin_upgrades::upgradeable::UpgradeableComponent::Event","kind":"enum","variants":[{"name":"Upgraded","type":"openzeppelin_upgrades::upgradeable::UpgradeableComponent::Upgraded","kind":"nested"}]},{"type":"event","name":"piltover::config::component::config_cpt::ProgramInfoChanged","kind":"struct","members":[{"name":"changed_by","type":"core::starknet::contract_address::ContractAddress","kind":"data"},{"name":"old_program_hash","type":"core::felt252","kind":"data"},{"name":"new_program_hash","type":"core::felt252","kind":"data"},{"name":"old_config_hash","type":"core::felt252","kind":"data"},{"name":"new_config_hash","type":"core::felt252","kind":"data"}]},{"type":"event","name":"piltover::config::component::config_cpt::Event","kind":"enum","variants":[{"name":"ProgramInfoChanged","type":"piltover::config::component::config_cpt::ProgramInfoChanged","kind":"nested"}]},{"type":"event","name":"piltover::messaging::component::messaging_cpt::MessageSent","kind":"struct","members":[{"name":"message_hash","type":"core::felt252","kind":"key"},{"name":"from","type":"core::starknet::contract_address::ContractAddress","kind":"key"},{"name":"to","type":"core::starknet::contract_address::ContractAddress","kind":"key"},{"name":"selector","type":"core::felt252","kind":"data"},{"name":"nonce","type":"core::felt252","kind":"data"},{"name":"payload","type":"core::array::Span::","kind":"data"}]},{"type":"event","name":"piltover::messaging::component::messaging_cpt::MessageConsumed","kind":"struct","members":[{"name":"message_hash","type":"core::felt252","kind":"key"},{"name":"from","type":"core::starknet::contract_address::ContractAddress","kind":"key"},{"name":"to","type":"core::starknet::contract_address::ContractAddress","kind":"key"},{"name":"payload","type":"core::array::Span::","kind":"data"}]},{"type":"event","name":"piltover::messaging::component::messaging_cpt::MessageCancellationStarted","kind":"struct","members":[{"name":"message_hash","type":"core::felt252","kind":"key"},{"name":"from","type":"core::starknet::contract_address::ContractAddress","kind":"key"},{"name":"to","type":"core::starknet::contract_address::ContractAddress","kind":"key"},{"name":"selector","type":"core::felt252","kind":"data"},{"name":"payload","type":"core::array::Span::","kind":"data"},{"name":"nonce","type":"core::felt252","kind":"data"}]},{"type":"event","name":"piltover::messaging::component::messaging_cpt::MessageCanceled","kind":"struct","members":[{"name":"message_hash","type":"core::felt252","kind":"key"},{"name":"from","type":"core::starknet::contract_address::ContractAddress","kind":"key"},{"name":"to","type":"core::starknet::contract_address::ContractAddress","kind":"key"},{"name":"selector","type":"core::felt252","kind":"data"},{"name":"payload","type":"core::array::Span::","kind":"data"},{"name":"nonce","type":"core::felt252","kind":"data"}]},{"type":"event","name":"piltover::messaging::component::messaging_cpt::MessageToStarknetReceived","kind":"struct","members":[{"name":"message_hash","type":"core::felt252","kind":"key"},{"name":"from","type":"core::starknet::contract_address::ContractAddress","kind":"key"},{"name":"to","type":"core::starknet::contract_address::ContractAddress","kind":"key"},{"name":"payload","type":"core::array::Span::","kind":"data"}]},{"type":"event","name":"piltover::messaging::component::messaging_cpt::MessageToAppchainSealed","kind":"struct","members":[{"name":"message_hash","type":"core::felt252","kind":"key"},{"name":"from","type":"core::starknet::contract_address::ContractAddress","kind":"key"},{"name":"to","type":"core::starknet::contract_address::ContractAddress","kind":"key"},{"name":"selector","type":"core::felt252","kind":"data"},{"name":"nonce","type":"core::felt252","kind":"data"},{"name":"payload","type":"core::array::Span::","kind":"data"}]},{"type":"event","name":"piltover::messaging::component::messaging_cpt::Event","kind":"enum","variants":[{"name":"MessageSent","type":"piltover::messaging::component::messaging_cpt::MessageSent","kind":"nested"},{"name":"MessageConsumed","type":"piltover::messaging::component::messaging_cpt::MessageConsumed","kind":"nested"},{"name":"MessageCancellationStarted","type":"piltover::messaging::component::messaging_cpt::MessageCancellationStarted","kind":"nested"},{"name":"MessageCanceled","type":"piltover::messaging::component::messaging_cpt::MessageCanceled","kind":"nested"},{"name":"MessageToStarknetReceived","type":"piltover::messaging::component::messaging_cpt::MessageToStarknetReceived","kind":"nested"},{"name":"MessageToAppchainSealed","type":"piltover::messaging::component::messaging_cpt::MessageToAppchainSealed","kind":"nested"}]},{"type":"event","name":"openzeppelin_security::reentrancyguard::ReentrancyGuardComponent::Event","kind":"enum","variants":[]},{"type":"event","name":"piltover::state::component::state_cpt::Event","kind":"enum","variants":[]},{"type":"event","name":"piltover::appchain::appchain::LogStateUpdate","kind":"struct","members":[{"name":"state_root","type":"core::felt252","kind":"data"},{"name":"block_number","type":"core::felt252","kind":"data"},{"name":"block_hash","type":"core::felt252","kind":"data"}]},{"type":"event","name":"piltover::appchain::appchain::LogStateTransitionFact","kind":"struct","members":[{"name":"state_transition_fact","type":"core::integer::u256","kind":"data"}]},{"type":"event","name":"piltover::appchain::appchain::Event","kind":"enum","variants":[{"name":"OwnableEvent","type":"openzeppelin_access::ownable::ownable::OwnableComponent::Event","kind":"flat"},{"name":"UpgradeableEvent","type":"openzeppelin_upgrades::upgradeable::UpgradeableComponent::Event","kind":"flat"},{"name":"ConfigEvent","type":"piltover::config::component::config_cpt::Event","kind":"flat"},{"name":"MessagingEvent","type":"piltover::messaging::component::messaging_cpt::Event","kind":"flat"},{"name":"ReentrancyGuardEvent","type":"openzeppelin_security::reentrancyguard::ReentrancyGuardComponent::Event","kind":"flat"},{"name":"StateEvent","type":"piltover::state::component::state_cpt::Event","kind":"flat"},{"name":"LogStateUpdate","type":"piltover::appchain::appchain::LogStateUpdate","kind":"nested"},{"name":"LogStateTransitionFact","type":"piltover::appchain::appchain::LogStateTransitionFact","kind":"nested"}]}]} \ No newline at end of file diff --git a/crates/katana/contracts/messaging/cairo/src/appchain_messaging.cairo b/crates/katana/contracts/messaging/cairo/src/appchain_messaging.cairo index 0d8897f5a5..41bd5651a4 100644 --- a/crates/katana/contracts/messaging/cairo/src/appchain_messaging.cairo +++ b/crates/katana/contracts/messaging/cairo/src/appchain_messaging.cairo @@ -6,17 +6,6 @@ //! listen for those events. When an event with a message is gathered //! by katana, a L1 handler transaction is then created and added to the pool. //! -//! For the appchain to send a message to starknet, the process can be done in two -//! fashions: -//! -//! 1. The appchain register messages hashes exactly as starknet does. And then -//! a transaction on starknet must be issued to consume the message. -//! -//! 2. The sequencer (katana in that case) has also the capability of directly send -//! send a transaction to "execute" the content of the message. In the appchain -//! context this is a very effective manner to have a more dynamic and real-time -//! messaging than manual consuming of a message. -//! /// Trait for Appchain messaging. For now, the messaging only whitelist one /// appchain. @@ -46,17 +35,6 @@ trait IAppchainMessaging { fn consume_message_from_appchain( ref self: T, from_address: starknet::ContractAddress, payload: Span, ) -> felt252; - - /// Executes a message sent from the appchain. A message to execute - /// does not need to be registered as consumable. It is automatically - /// consumed while executed. - fn execute_message_from_appchain( - ref self: T, - from_address: starknet::ContractAddress, - to_address: starknet::ContractAddress, - selector: felt252, - payload: Span, - ); } #[starknet::interface] @@ -76,7 +54,7 @@ mod appchain_messaging { // Owner of this contract. owner: ContractAddress, // The account on Starknet (or the chain where this contract is deployed) - // used by the appchain sequencer to register messages hashes / execute messages. + // used by the appchain sequencer to register messages hashes. appchain_account: ContractAddress, // The nonce for messages sent from Starknet. sn_to_appc_nonce: felt252, @@ -93,7 +71,6 @@ mod appchain_messaging { MessageSentToAppchain: MessageSentToAppchain, MessagesRegisteredFromAppchain: MessagesRegisteredFromAppchain, MessageConsumed: MessageConsumed, - MessageExecuted: MessageExecuted, Upgraded: Upgraded, } @@ -126,17 +103,6 @@ mod appchain_messaging { payload: Span, } - #[derive(Drop, starknet::Event)] - struct MessageExecuted { - #[key] - from_address: ContractAddress, - #[key] - to_address: ContractAddress, - #[key] - selector: felt252, - payload: Span, - } - #[derive(Drop, starknet::Event)] struct Upgraded { class_hash: ClassHash, @@ -171,8 +137,19 @@ mod appchain_messaging { hash.try_into().expect('starknet keccak overflow') } - /// Computes message hash to consume messages from appchain. - /// starknet_keccak(from_address, to_address, payload_len, payload). + /// Computes the hash of a message that is sent from the Appchain to Starknet. + /// + /// + /// + /// # Arguments + /// + /// * `from_address` - Contract address of the message sender on the Appchain. + /// * `to_address` - Contract address to send the message to on the Appchain. + /// * `payload` - The message payload. + /// + /// # Returns + /// + /// The hash of the message from the Appchain to Starknet. fn compute_hash_appc_to_sn( from_address: ContractAddress, to_address: ContractAddress, payload: Span ) -> felt252 { @@ -192,12 +169,32 @@ mod appchain_messaging { starknet_keccak(hash_data.span()) } - /// Computes message hash to send messages to appchain. - /// starknet_keccak(nonce, to_address, selector, payload). + /// Computes the hash of a message that is sent from Starknet to the Appchain. + /// + /// + /// + /// # Arguments + /// + /// * `from_address` - Contract address of the message sender on the Appchain. + /// * `to_address` - Contract address to send the message to on the Appchain. + /// * `selector` - The `l1_handler` function selector of the contract on the Appchain + /// to execute. + /// * `payload` - The message payload. + /// * `nonce` - Nonce of the message. + /// + /// # Returns + /// + /// The hash of the message from Starknet to the Appchain. fn compute_hash_sn_to_appc( - nonce: felt252, to_address: ContractAddress, selector: felt252, payload: Span + from_address: ContractAddress, + to_address: ContractAddress, + selector: felt252, + payload: Span, + nonce: felt252 ) -> felt252 { - let mut hash_data = array![nonce, to_address.into(), selector,]; + let mut hash_data = array![ + from_address.into(), to_address.into(), nonce, selector, payload.len().into(), + ]; let mut i = 0_usize; loop { @@ -208,7 +205,7 @@ mod appchain_messaging { i += 1; }; - starknet_keccak(hash_data.span()) + core::poseidon::poseidon_hash_span(hash_data.span()) } #[abi(embed_v0)] @@ -308,26 +305,5 @@ mod appchain_messaging { msg_hash } - - fn execute_message_from_appchain( - ref self: ContractState, - from_address: ContractAddress, - to_address: ContractAddress, - selector: felt252, - payload: Span, - ) { - assert( - self.appchain_account.read() == starknet::get_caller_address(), - 'Unauthorized executor', - ); - - match starknet::call_contract_syscall(to_address, selector, payload) { - Result::Ok(_) => self - .emit(MessageExecuted { from_address, to_address, selector, payload, }), - Result::Err(e) => { - panic(e) - } - } - } } } diff --git a/crates/katana/contracts/messaging/cairo/src/contract_msg_starknet.cairo b/crates/katana/contracts/messaging/cairo/src/contract_msg_starknet.cairo index eacffbb26c..4cc6e681bb 100644 --- a/crates/katana/contracts/messaging/cairo/src/contract_msg_starknet.cairo +++ b/crates/katana/contracts/messaging/cairo/src/contract_msg_starknet.cairo @@ -4,10 +4,9 @@ //! This contract can sends messages using the send message to l1 //! syscall as we normally do for messaging. //! -//! If the message contains a `to_address` that is not zero, the message -//! hash will be sent to starknet to be registered. -//! If the `to_address` is zero, then the message will then fire a transaction -//! on the starknet to directly execute the message content. +//! However, the `to_address` is set to the `MSG` magic value since +//! this field is restricted to a valid Ethereum address, too small to +//! be a valid Starknet address. use starknet::ContractAddress; #[starknet::interface] @@ -21,20 +20,6 @@ trait IContractAppchain { /// * `to_address` - Contract address on Starknet. /// * `value` - Value to be sent in the payload. fn send_message(ref self: T, to_address: ContractAddress, value: felt252); - - /// Executes a message on Starknet. When the Katana will see this message - /// with `to_address` set to 0, an invoke transaction will be fired. - /// So basically this function can invoke any contract on Starknet, the fees on starknet - /// being paid by the sequencer. We can here imagine several scenarios. :) - /// The invoke though is not directly done to the destination contract, but the - /// app messaging contract that will forward the execution. - /// - /// # Arguments - /// - /// * `to_address` - Contract address on Starknet. - /// * `selector` - Selector. - /// * `value` - Value to be sent as argument to the contract being executed on starknet. - fn execute_message(ref self: T, to_address: ContractAddress, selector: felt252, value: felt252); } #[starknet::contract] @@ -67,12 +52,5 @@ mod contract_msg_starknet { let buf: Array = array![to_address.into(), value]; starknet::send_message_to_l1_syscall('MSG', buf.span()).unwrap_syscall(); } - - fn execute_message( - ref self: ContractState, to_address: ContractAddress, selector: felt252, value: felt252, - ) { - let buf: Array = array![to_address.into(), selector, value]; - starknet::send_message_to_l1_syscall('EXE', buf.span()).unwrap_syscall(); - } } } diff --git a/crates/katana/contracts/piltover b/crates/katana/contracts/piltover new file mode 160000 index 0000000000..d373a844c3 --- /dev/null +++ b/crates/katana/contracts/piltover @@ -0,0 +1 @@ +Subproject commit d373a844c3428383a48518adf468bf83249dec3a diff --git a/crates/katana/core/Cargo.toml b/crates/katana/core/Cargo.toml index 2d166c6795..9f1f8ecd58 100644 --- a/crates/katana/core/Cargo.toml +++ b/crates/katana/core/Cargo.toml @@ -36,10 +36,11 @@ url.workspace = true alloy-primitives = { workspace = true, features = [ "serde" ] } alloy-sol-types = { workspace = true, default-features = false, features = [ "json" ] } +starknet-crypto = { workspace = true, optional = true } alloy-contract = { workspace = true, default-features = false } alloy-network = { workspace = true, default-features = false } -alloy-provider = { workspace = true, default-features = false, features = [ "reqwest" ] } +alloy-provider = { workspace = true, default-features = false, features = [ "reqwest", "reqwest-rustls-tls" ] } alloy-rpc-types-eth = { workspace = true, default-features = false } alloy-transport = { workspace = true, default-features = false } @@ -49,4 +50,4 @@ hex.workspace = true tempfile.workspace = true [features] -starknet-messaging = [ ] +starknet-messaging = [ "dep:starknet-crypto" ] diff --git a/crates/katana/core/src/backend/contract.rs b/crates/katana/core/src/backend/contract.rs index 1cfe9f57cf..8a75f0eeef 100644 --- a/crates/katana/core/src/backend/contract.rs +++ b/crates/katana/core/src/backend/contract.rs @@ -1,8 +1,8 @@ -use katana_primitives::class::DeprecatedCompiledClass; +use katana_primitives::class::LegacyContractClass; use starknet::core::types::FlattenedSierraClass; #[derive(Debug)] pub enum StarknetContract { - Legacy(DeprecatedCompiledClass), + Legacy(LegacyContractClass), Sierra(FlattenedSierraClass), } diff --git a/crates/katana/core/src/backend/gas_oracle.rs b/crates/katana/core/src/backend/gas_oracle.rs index 1822612380..381b110655 100644 --- a/crates/katana/core/src/backend/gas_oracle.rs +++ b/crates/katana/core/src/backend/gas_oracle.rs @@ -1,25 +1,339 @@ +use std::collections::VecDeque; +use std::fmt::Debug; +use std::sync::Arc; + +use alloy_provider::{Provider, ProviderBuilder}; +use alloy_rpc_types_eth::{BlockNumberOrTag, FeeHistory}; +use anyhow::{Context, Ok}; use katana_primitives::block::GasPrices; +use katana_tasks::TaskSpawner; +use parking_lot::Mutex; +use tokio::time::Duration; +use tracing::error; +use url::Url; + +const BUFFER_SIZE: usize = 60; +const INTERVAL: Duration = Duration::from_secs(60); +const ONE_GWEI: u128 = 1_000_000_000; // TODO: implement a proper gas oracle function - sample the l1 gas and data gas prices // currently this just return the hardcoded value set from the cli or if not set, the default value. #[derive(Debug)] -pub struct L1GasOracle { +pub enum L1GasOracle { + Fixed(FixedL1GasOracle), + Sampled(SampledL1GasOracle), +} + +#[derive(Debug)] +pub struct FixedL1GasOracle { + gas_prices: GasPrices, + data_gas_prices: GasPrices, +} + +#[derive(Debug, Clone)] +pub struct SampledL1GasOracle { + prices: Arc>, + l1_provider: Url, +} + +#[derive(Debug, Default)] +pub struct SampledPrices { gas_prices: GasPrices, data_gas_prices: GasPrices, } +#[derive(Debug, Clone)] +pub struct GasOracleWorker { + pub prices: Arc>, + pub l1_provider_url: Url, + pub gas_price_buffer: GasPriceBuffer, + pub data_gas_price_buffer: GasPriceBuffer, +} + impl L1GasOracle { pub fn fixed(gas_prices: GasPrices, data_gas_prices: GasPrices) -> Self { - Self { gas_prices, data_gas_prices } + L1GasOracle::Fixed(FixedL1GasOracle { gas_prices, data_gas_prices }) + } + + pub fn sampled(l1_provider: Url) -> Self { + let prices: Arc> = Arc::new(Mutex::new(SampledPrices::default())); + L1GasOracle::Sampled(SampledL1GasOracle { prices, l1_provider }) } /// Returns the current gas prices. pub fn current_gas_prices(&self) -> GasPrices { - self.gas_prices.clone() + match self { + L1GasOracle::Fixed(fixed) => fixed.current_gas_prices(), + L1GasOracle::Sampled(sampled) => sampled.prices.lock().gas_prices.clone(), + } } /// Returns the current data gas prices. + pub fn current_data_gas_prices(&self) -> GasPrices { + match self { + L1GasOracle::Fixed(fixed) => fixed.current_data_gas_prices(), + L1GasOracle::Sampled(sampled) => sampled.prices.lock().data_gas_prices.clone(), + } + } + + pub fn run_worker(&self, task_spawner: TaskSpawner) { + match self { + Self::Fixed(..) => {} + Self::Sampled(oracle) => { + let prices = oracle.prices.clone(); + let l1_provider = oracle.l1_provider.clone(); + + task_spawner.build_task().critical().name("L1 Gas Oracle worker").spawn( + async move { + let mut worker = GasOracleWorker::new(prices, l1_provider); + worker + .run() + .await + .inspect_err(|error| error!(target: "gas_oracle", %error, "Gas oracle worker failed.")) + }, + ); + } + } + } +} + +impl SampledL1GasOracle { + pub fn current_data_gas_prices(&self) -> GasPrices { + self.prices.lock().data_gas_prices.clone() + } + + pub fn current_gas_prices(&self) -> GasPrices { + self.prices.lock().gas_prices.clone() + } +} + +impl FixedL1GasOracle { pub fn current_data_gas_prices(&self) -> GasPrices { self.data_gas_prices.clone() } + + pub fn current_gas_prices(&self) -> GasPrices { + self.gas_prices.clone() + } +} + +pub fn update_gas_price( + l1_oracle: &mut SampledPrices, + gas_price_buffer: &mut GasPriceBuffer, + data_gas_price_buffer: &mut GasPriceBuffer, + fee_history: FeeHistory, +) -> anyhow::Result<()> { + let latest_gas_price = fee_history.base_fee_per_gas.last().context("Getting eth gas price")?; + + gas_price_buffer.add_sample(*latest_gas_price); + + let blob_fee_history = fee_history.base_fee_per_blob_gas; + let avg_blob_base_fee = blob_fee_history.iter().last().context("Getting blob gas price")?; + + data_gas_price_buffer.add_sample(*avg_blob_base_fee); + // The price of gas on Starknet is set to the average of the last 60 gas price samples, plus 1 + // gwei. + let avg_gas_price = GasPrices { + eth: gas_price_buffer.average() + ONE_GWEI, + strk: gas_price_buffer.average() + ONE_GWEI, + }; + // The price of data gas on Starknet is set to the average of the last 60 data gas price + // samples. + let avg_blob_price = + GasPrices { eth: data_gas_price_buffer.average(), strk: data_gas_price_buffer.average() }; + + l1_oracle.gas_prices = avg_gas_price; + l1_oracle.data_gas_prices = avg_blob_price; + Ok(()) +} + +impl GasOracleWorker { + pub fn new(prices: Arc>, l1_provider_url: Url) -> Self { + Self { + prices, + l1_provider_url, + gas_price_buffer: GasPriceBuffer::new(), + data_gas_price_buffer: GasPriceBuffer::new(), + } + } + + pub async fn run(&mut self) -> anyhow::Result<()> { + let provider = ProviderBuilder::new().on_http(self.l1_provider_url.clone()); + // every 60 seconds, Starknet samples the base price of gas and data gas on L1 + let mut interval = tokio::time::interval(INTERVAL); + interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); + + loop { + // Wait for the interval to tick + interval.tick().await; + + { + // Attempt to get the gas price from L1 + let last_block_number = provider.get_block_number().await?; + + let fee_history = provider + .get_fee_history(1, BlockNumberOrTag::Number(last_block_number), &[]) + .await?; + + let mut prices = self.prices.lock(); + + if let Err(error) = update_gas_price( + &mut prices, + &mut self.gas_price_buffer, + &mut self.data_gas_price_buffer, + fee_history, + ) { + error!(target: "gas_oracle", %error, "Error updating gas prices."); + } + } + } + } + + pub async fn update_once(&mut self) -> anyhow::Result<()> { + let provider = ProviderBuilder::new().on_http(self.l1_provider_url.clone()); + + // Attempt to get the gas price from L1 + let last_block_number = provider.get_block_number().await?; + + let fee_history = + provider.get_fee_history(1, BlockNumberOrTag::Number(last_block_number), &[]).await?; + + let mut prices = self.prices.lock(); + + update_gas_price( + &mut prices, + &mut self.gas_price_buffer, + &mut self.data_gas_price_buffer, + fee_history, + ) + } +} + +// Buffer to store the last 60 gas price samples +#[derive(Debug, Clone)] +pub struct GasPriceBuffer { + buffer: VecDeque, +} + +impl GasPriceBuffer { + fn new() -> Self { + Self { buffer: VecDeque::with_capacity(BUFFER_SIZE) } // 60 size + } + + fn add_sample(&mut self, sample: u128) { + if self.buffer.len() == BUFFER_SIZE { + // remove oldest sample if buffer is full + self.buffer.pop_front(); + } + self.buffer.push_back(sample); + } + + fn average(&self) -> u128 { + if self.buffer.is_empty() { + return 0; + } + let sum: u128 = self.buffer.iter().sum(); + sum / self.buffer.len() as u128 + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + // Test the buffer functionality separately + #[test] + fn test_buffer_size_limit() { + let mut buffer = GasPriceBuffer::new(); + + // Add more samples than the buffer size + for i in 0..BUFFER_SIZE + 10 { + buffer.add_sample(i as u128); + } + + // Check if buffer size is maintained + assert_eq!(buffer.buffer.len(), BUFFER_SIZE); + + // Check if oldest values were removed (should start from 10) + assert_eq!(*buffer.buffer.front().unwrap(), 10); + } + + #[test] + fn test_empty_buffer_average() { + let buffer = GasPriceBuffer::new(); + assert_eq!(buffer.average(), 0); + } + + #[test] + fn test_buffer_single_sample_average() { + let mut buffer = GasPriceBuffer::new(); + buffer.add_sample(100); + assert_eq!(buffer.average(), 100); + } + + #[test] + fn test_bufffer_multiple_samples_average() { + let mut buffer = GasPriceBuffer::new(); + // Add some test values + let test_values = [100, 200, 300]; + for value in test_values.iter() { + buffer.add_sample(*value); + } + + let expected_avg = 200; // (100 + 200 + 300) / 3 + assert_eq!(buffer.average(), expected_avg); + } + + #[tokio::test] + #[ignore = "Requires external assumption"] + async fn test_gas_oracle() { + let url = Url::parse("https://eth.merkle.io/").expect("Invalid URL"); + let oracle = L1GasOracle::sampled(url.clone()); + + let shared_prices = match &oracle { + L1GasOracle::Sampled(sampled) => sampled.prices.clone(), + _ => panic!("Expected sampled oracle"), + }; + + let mut worker = GasOracleWorker::new(shared_prices.clone(), url); + + for i in 0..3 { + let initial_gas_prices = oracle.current_gas_prices(); + + // Verify initial state for first iteration + if i == 0 { + assert_eq!( + initial_gas_prices, + GasPrices { eth: 0, strk: 0 }, + "First iteration should start with zero prices" + ); + } + + worker.update_once().await.expect("Failed to update prices"); + + let updated_gas_prices = oracle.current_gas_prices(); + let updated_data_gas_prices = oracle.current_data_gas_prices(); + + // Verify gas prices + assert!(updated_gas_prices.eth > 0, "ETH gas price should be non-zero"); + + assert!(updated_data_gas_prices.eth > 0, "ETH data gas price should be non-zero"); + + // For iterations after the first, verify that prices have been updated + if i > 0 { + // Give some flexibility for price changes + if initial_gas_prices.eth != 0 { + assert!( + initial_gas_prices.eth != updated_gas_prices.eth + || initial_gas_prices.strk != updated_gas_prices.strk, + "Prices should potentially change between updates" + ); + } + } + + // ETH current avg blocktime is ~12 secs so we need a delay to wait for block creation + tokio::time::sleep(std::time::Duration::from_secs(9)).await; + } + } } diff --git a/crates/katana/core/src/backend/mod.rs b/crates/katana/core/src/backend/mod.rs index 7d8d74a142..f2a6d5f906 100644 --- a/crates/katana/core/src/backend/mod.rs +++ b/crates/katana/core/src/backend/mod.rs @@ -3,7 +3,8 @@ use std::sync::Arc; use gas_oracle::L1GasOracle; use katana_executor::{ExecutionOutput, ExecutionResult, ExecutorFactory}; use katana_primitives::block::{ - FinalityStatus, Header, PartialHeader, SealedBlock, SealedBlockWithStatus, + BlockHash, BlockNumber, FinalityStatus, Header, PartialHeader, SealedBlock, + SealedBlockWithStatus, }; use katana_primitives::chain_spec::ChainSpec; use katana_primitives::da::L1DataAvailabilityMode; @@ -11,9 +12,9 @@ use katana_primitives::env::BlockEnv; use katana_primitives::receipt::{Event, ReceiptWithTxHash}; use katana_primitives::state::{compute_state_diff_hash, StateUpdates}; use katana_primitives::transaction::{TxHash, TxWithHash}; -use katana_primitives::Felt; +use katana_primitives::{address, ContractAddress, Felt}; use katana_provider::traits::block::{BlockHashProvider, BlockWriter}; -use katana_provider::traits::trie::{ClassTrieWriter, ContractTrieWriter}; +use katana_provider::traits::trie::TrieWriter; use katana_trie::compute_merkle_root; use parking_lot::RwLock; use starknet::macros::short_string; @@ -33,7 +34,7 @@ pub(crate) const LOG_TARGET: &str = "katana::core::backend"; #[derive(Debug)] pub struct Backend { - pub chain_spec: ChainSpec, + pub chain_spec: Arc, /// stores all block related data in memory pub blockchain: Blockchain, /// The block context generator. @@ -49,7 +50,7 @@ impl Backend { pub fn do_mine_block( &self, block_env: &BlockEnv, - execution_output: ExecutionOutput, + mut execution_output: ExecutionOutput, ) -> Result { // we optimistically allocate the maximum amount possible let mut txs = Vec::with_capacity(execution_output.transactions.len()); @@ -68,6 +69,12 @@ impl Backend { let tx_count = txs.len() as u32; let tx_hashes = txs.iter().map(|tx| tx.hash).collect::>(); + // Update special contract address 0x1 + self.update_block_hash_registry_contract( + &mut execution_output.states.state_updates, + block_env.number, + )?; + // create a new block and compute its commitment let block = self.commit_block( block_env.clone(), @@ -93,6 +100,35 @@ impl Backend { Ok(MinedBlockOutcome { block_number, txs: tx_hashes, stats: execution_output.stats }) } + // TODO: create a dedicated struct for this contract. + // https://docs.starknet.io/architecture-and-concepts/network-architecture/starknet-state/#address_0x1 + fn update_block_hash_registry_contract( + &self, + state_updates: &mut StateUpdates, + block_number: BlockNumber, + ) -> Result<(), BlockProductionError> { + const STORED_BLOCK_HASH_BUFFER: u64 = 10; + + if block_number >= STORED_BLOCK_HASH_BUFFER { + let block_number = block_number - STORED_BLOCK_HASH_BUFFER; + let block_hash = self.blockchain.provider().block_hash_by_num(block_number)?; + + // When in forked mode, we might not have the older block hash in the database. This + // could be the case where the `block_number - STORED_BLOCK_HASH_BUFFER` is + // earlier than the forked block, which right now, Katana doesn't + // yet have the ability to fetch older blocks on the database level. So, we default to + // `BlockHash::ZERO` in this case. + // + // TODO: Fix quick! + let block_hash = block_hash.unwrap_or(BlockHash::ZERO); + + let storages = state_updates.storage_updates.entry(address!("0x1")).or_default(); + storages.insert(block_number.into(), block_hash); + } + + Ok(()) + } + pub fn update_block_env(&self, block_env: &mut BlockEnv) { let mut context_gen = self.block_context_generator.write(); let current_timestamp_secs = get_current_timestamp().as_secs() as i64; @@ -158,21 +194,15 @@ impl Backend { } #[derive(Debug, Clone)] -pub struct UncommittedBlock<'a, P> -where - P: ClassTrieWriter + ContractTrieWriter, -{ +pub struct UncommittedBlock<'a, P: TrieWriter> { header: PartialHeader, transactions: Vec, receipts: &'a [ReceiptWithTxHash], state_updates: &'a StateUpdates, - trie_provider: P, + provider: P, } -impl<'a, P> UncommittedBlock<'a, P> -where - P: ClassTrieWriter + ContractTrieWriter, -{ +impl<'a, P: TrieWriter> UncommittedBlock<'a, P> { pub fn new( header: PartialHeader, transactions: Vec, @@ -180,7 +210,7 @@ where state_updates: &'a StateUpdates, trie_provider: P, ) -> Self { - Self { header, transactions, receipts, state_updates, trie_provider } + Self { header, transactions, receipts, state_updates, provider: trie_provider } } pub fn commit(self) -> SealedBlock { @@ -258,19 +288,15 @@ where // state_commitment = hPos("STARKNET_STATE_V0", contract_trie_root, class_trie_root) fn compute_new_state_root(&self) -> Felt { - let class_trie_root = ClassTrieWriter::insert_updates( - &self.trie_provider, - self.header.number, - &self.state_updates.declared_classes, - ) - .unwrap(); - - let contract_trie_root = ContractTrieWriter::insert_updates( - &self.trie_provider, - self.header.number, - self.state_updates, - ) - .unwrap(); + let class_trie_root = self + .provider + .trie_insert_declared_classes(self.header.number, &self.state_updates.declared_classes) + .expect("failed to update class trie"); + + let contract_trie_root = self + .provider + .trie_insert_contract_updates(self.header.number, self.state_updates) + .expect("failed to update contract trie"); hash::Poseidon::hash_array(&[ short_string!("STARKNET_STATE_V0"), diff --git a/crates/katana/core/src/backend/storage.rs b/crates/katana/core/src/backend/storage.rs index a8f64e7b49..ceca70d17d 100644 --- a/crates/katana/core/src/backend/storage.rs +++ b/crates/katana/core/src/backend/storage.rs @@ -7,24 +7,27 @@ use katana_primitives::block::{ }; use katana_primitives::chain_spec::ChainSpec; use katana_primitives::da::L1DataAvailabilityMode; -use katana_primitives::state::StateUpdatesWithDeclaredClasses; +use katana_primitives::hash::{self, StarkHash}; +use katana_primitives::state::StateUpdatesWithClasses; use katana_primitives::version::ProtocolVersion; use katana_provider::providers::db::DbProvider; use katana_provider::providers::fork::ForkedProvider; use katana_provider::traits::block::{BlockProvider, BlockWriter}; -use katana_provider::traits::contract::ContractClassWriter; +use katana_provider::traits::contract::{ContractClassWriter, ContractClassWriterExt}; use katana_provider::traits::env::BlockEnvProvider; -use katana_provider::traits::state::{StateFactoryProvider, StateRootProvider, StateWriter}; +use katana_provider::traits::stage::StageCheckpointProvider; +use katana_provider::traits::state::{StateFactoryProvider, StateWriter}; use katana_provider::traits::state_update::StateUpdateProvider; use katana_provider::traits::transaction::{ ReceiptProvider, TransactionProvider, TransactionStatusProvider, TransactionTraceProvider, TransactionsProviderExt, }; -use katana_provider::traits::trie::{ClassTrieWriter, ContractTrieWriter}; +use katana_provider::traits::trie::TrieWriter; use katana_provider::BlockchainProvider; use num_traits::ToPrimitive; use starknet::core::types::{BlockStatus, MaybePendingBlockWithTxHashes}; use starknet::core::utils::parse_cairo_short_string; +use starknet::macros::short_string; use starknet::providers::jsonrpc::HttpTransport; use starknet::providers::{JsonRpcClient, Provider}; use tracing::info; @@ -39,13 +42,13 @@ pub trait Database: + TransactionsProviderExt + ReceiptProvider + StateUpdateProvider - + StateRootProvider + StateWriter + ContractClassWriter + + ContractClassWriterExt + StateFactoryProvider + BlockEnvProvider - + ClassTrieWriter - + ContractTrieWriter + + TrieWriter + + StageCheckpointProvider + 'static + Send + Sync @@ -62,13 +65,13 @@ impl Database for T where + TransactionsProviderExt + ReceiptProvider + StateUpdateProvider - + StateRootProvider + StateWriter + ContractClassWriter + + ContractClassWriterExt + StateFactoryProvider + BlockEnvProvider - + ClassTrieWriter - + ContractTrieWriter + + TrieWriter + + StageCheckpointProvider + 'static + Send + Sync @@ -76,7 +79,7 @@ impl Database for T where { } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Blockchain { inner: BlockchainProvider>, } @@ -213,15 +216,28 @@ impl Blockchain { fn new_with_genesis_block_and_state( provider: impl Database, block: SealedBlockWithStatus, - states: StateUpdatesWithDeclaredClasses, + states: StateUpdatesWithClasses, ) -> Result { - BlockWriter::insert_block_with_states_and_receipts( - &provider, - block, - states, - vec![], - vec![], - )?; + let mut block = block; + let block_number = block.block.header.number; + + let class_trie_root = provider + .trie_insert_declared_classes(block_number, &states.state_updates.declared_classes) + .context("failed to update class trie")?; + + let contract_trie_root = provider + .trie_insert_contract_updates(block_number, &states.state_updates) + .context("failed to update contract trie")?; + + let genesis_state_root = hash::Poseidon::hash_array(&[ + short_string!("STARKNET_STATE_V0"), + contract_trie_root, + class_trie_root, + ]); + + block.block.header.state_root = genesis_state_root; + provider.insert_block_with_states_and_receipts(block, states, vec![], vec![])?; + Ok(Self::new(provider)) } } @@ -234,11 +250,11 @@ mod tests { use katana_primitives::da::L1DataAvailabilityMode; use katana_primitives::fee::{PriceUnit, TxFeeInfo}; use katana_primitives::genesis::constant::{ - DEFAULT_ETH_FEE_TOKEN_ADDRESS, DEFAULT_LEGACY_ERC20_CASM, DEFAULT_LEGACY_ERC20_CLASS_HASH, - DEFAULT_LEGACY_UDC_CASM, DEFAULT_LEGACY_UDC_CLASS_HASH, DEFAULT_UDC_ADDRESS, + DEFAULT_ETH_FEE_TOKEN_ADDRESS, DEFAULT_LEGACY_ERC20_CLASS, DEFAULT_LEGACY_ERC20_CLASS_HASH, + DEFAULT_LEGACY_UDC_CLASS, DEFAULT_LEGACY_UDC_CLASS_HASH, DEFAULT_UDC_ADDRESS, }; use katana_primitives::receipt::{InvokeTxReceipt, Receipt}; - use katana_primitives::state::StateUpdatesWithDeclaredClasses; + use katana_primitives::state::StateUpdatesWithClasses; use katana_primitives::trace::TxExecInfo; use katana_primitives::transaction::{InvokeTx, Tx, TxWithHash}; use katana_primitives::{chain_spec, Felt}; @@ -305,7 +321,7 @@ mod tests { .provider() .insert_block_with_states_and_receipts( dummy_block.clone(), - StateUpdatesWithDeclaredClasses::default(), + StateUpdatesWithClasses::default(), vec![Receipt::Invoke(InvokeTxReceipt { revert_error: None, events: Vec::new(), @@ -335,10 +351,10 @@ mod tests { let actual_fee_token_class = state.class(actual_fee_token_class_hash).unwrap().unwrap(); assert_eq!(actual_udc_class_hash, DEFAULT_LEGACY_UDC_CLASS_HASH); - assert_eq!(actual_udc_class, DEFAULT_LEGACY_UDC_CASM.clone()); + assert_eq!(actual_udc_class, DEFAULT_LEGACY_UDC_CLASS.clone()); assert_eq!(actual_fee_token_class_hash, DEFAULT_LEGACY_ERC20_CLASS_HASH); - assert_eq!(actual_fee_token_class, DEFAULT_LEGACY_ERC20_CASM.clone()); + assert_eq!(actual_fee_token_class, DEFAULT_LEGACY_ERC20_CLASS.clone()); } // re open the db and assert the state is the same and not overwritten @@ -361,10 +377,10 @@ mod tests { let actual_fee_token_class = state.class(actual_fee_token_class_hash).unwrap().unwrap(); assert_eq!(actual_udc_class_hash, DEFAULT_LEGACY_UDC_CLASS_HASH); - assert_eq!(actual_udc_class, DEFAULT_LEGACY_UDC_CASM.clone()); + assert_eq!(actual_udc_class, DEFAULT_LEGACY_UDC_CLASS.clone()); assert_eq!(actual_fee_token_class_hash, DEFAULT_LEGACY_ERC20_CLASS_HASH); - assert_eq!(actual_fee_token_class, DEFAULT_LEGACY_ERC20_CASM.clone()); + assert_eq!(actual_fee_token_class, DEFAULT_LEGACY_ERC20_CLASS.clone()); let block_number = blockchain.provider().latest_number().unwrap(); let block_hash = blockchain.provider().latest_hash().unwrap(); diff --git a/crates/katana/core/src/service/messaging/mod.rs b/crates/katana/core/src/service/messaging/mod.rs index 2626947ad5..55174777f5 100644 --- a/crates/katana/core/src/service/messaging/mod.rs +++ b/crates/katana/core/src/service/messaging/mod.rs @@ -52,7 +52,7 @@ use katana_executor::ExecutorFactory; use katana_primitives::chain::ChainId; use katana_primitives::receipt::MessageToL1; use serde::{Deserialize, Serialize}; -use tracing::{error, info}; +use tracing::{error, info, trace}; pub use self::service::{MessagingOutcome, MessagingService}; #[cfg(feature = "starknet-messaging")] @@ -229,11 +229,19 @@ impl Future for MessagingTask { while let Poll::Ready(Some(outcome)) = this.messaging.poll_next_unpin(cx) { match outcome { MessagingOutcome::Gather { msg_count, .. } => { - info!(target: LOG_TARGET, %msg_count, "Collected messages from settlement chain."); + if msg_count > 0 { + info!(target: LOG_TARGET, %msg_count, "Collected messages from settlement chain."); + } + + trace!(target: LOG_TARGET, %msg_count, "Collected messages from settlement chain."); } MessagingOutcome::Send { msg_count, .. } => { - info!(target: LOG_TARGET, %msg_count, "Sent messages to the settlement chain."); + if msg_count > 0 { + info!(target: LOG_TARGET, %msg_count, "Sent messages to the settlement chain."); + } + + trace!(target: LOG_TARGET, %msg_count, "Sent messages to the settlement chain."); } } } diff --git a/crates/katana/core/src/service/messaging/service.rs b/crates/katana/core/src/service/messaging/service.rs index 46d2379ce0..d290b611b5 100644 --- a/crates/katana/core/src/service/messaging/service.rs +++ b/crates/katana/core/src/service/messaging/service.rs @@ -265,33 +265,11 @@ fn interval_from_seconds(secs: u64) -> Interval { fn trace_msg_to_l1_sent(messages: &[MessageToL1], hashes: &[String]) { assert_eq!(messages.len(), hashes.len()); - #[cfg(feature = "starknet-messaging")] - let hash_exec_str = format!("{:#064x}", super::starknet::HASH_EXEC); - for (i, m) in messages.iter().enumerate() { let payload_str: Vec = m.payload.iter().map(|f| format!("{:#x}", *f)).collect(); let hash = &hashes[i]; - #[cfg(feature = "starknet-messaging")] - if hash == &hash_exec_str { - let to_address = &payload_str[0]; - let selector = &payload_str[1]; - let payload_str = &payload_str[2..]; - - #[rustfmt::skip] - info!( - target: LOG_TARGET, - from_address = %m.from_address, - to_address = %to_address, - selector = %selector, - payload = %payload_str.join(", "), - "Message executed on settlement layer.", - ); - - continue; - } - // We check for magic value 'MSG' used only when we are doing L3-L2 messaging. let (to_address, payload_str) = if format!("{}", m.to_address) == "0x4d5347" { (payload_str[0].clone(), &payload_str[1..]) diff --git a/crates/katana/core/src/service/messaging/starknet.rs b/crates/katana/core/src/service/messaging/starknet.rs index 7c3f2bb3db..5b379940f5 100644 --- a/crates/katana/core/src/service/messaging/starknet.rs +++ b/crates/katana/core/src/service/messaging/starknet.rs @@ -1,14 +1,13 @@ use std::sync::Arc; +use alloy_primitives::B256; use anyhow::Result; use async_trait::async_trait; use katana_primitives::chain::ChainId; use katana_primitives::receipt::MessageToL1; use katana_primitives::transaction::L1HandlerTx; -use katana_primitives::utils::transaction::compute_l2_to_l1_message_hash; use starknet::accounts::{Account, ExecutionEncoding, SingleOwnerAccount}; use starknet::core::types::{BlockId, BlockTag, Call, EmittedEvent, EventFilter, Felt}; -use starknet::core::utils::starknet_keccak; use starknet::macros::{felt, selector}; use starknet::providers::jsonrpc::HttpTransport; use starknet::providers::{AnyProvider, JsonRpcClient, Provider}; @@ -19,14 +18,14 @@ use url::Url; use super::{Error, MessagingConfig, Messenger, MessengerResult, LOG_TARGET}; /// As messaging in starknet is only possible with EthAddress in the `to_address` -/// field, we have to set magic value to understand what the user want to do. -/// In the case of execution -> the felt 'EXE' will be passed. -/// And for normal messages, the felt 'MSG' is used. -/// Those values are very not likely a valid account address on starknet. +/// field, in teh current design we set the `to_address` to the `MSG` magic value. +/// +/// Blockifier is the one responsible for this out of range error. +/// const MSG_MAGIC: Felt = felt!("0x4d5347"); -const EXE_MAGIC: Felt = felt!("0x455845"); -pub const HASH_EXEC: Felt = felt!("0xee"); +/// TODO: This may come from the configuration. +pub const MESSAGE_SENT_EVENT_KEY: Felt = selector!("MessageSent"); #[derive(Debug)] pub struct StarknetMessaging { @@ -73,11 +72,10 @@ impl StarknetMessaging { from_block: Some(from_block), to_block: Some(to_block), address: Some(self.messaging_contract_address), - // TODO: this might come from the configuration actually. - keys: None, + keys: Some(vec![vec![MESSAGE_SENT_EVENT_KEY]]), }; - // TODO: this chunk_size may also come from configuration? + // TODO: This chunk_size may also come from configuration? let chunk_size = 200; let mut continuation_token: Option = None; @@ -127,9 +125,7 @@ impl StarknetMessaging { } /// Sends messages hashes to settlement layer by sending a transaction. - async fn send_hashes(&self, mut hashes: Vec) -> MessengerResult { - hashes.retain(|&x| x != HASH_EXEC); - + async fn send_hashes(&self, hashes: Vec) -> MessengerResult { if hashes.is_empty() { return Ok(Felt::ZERO); } @@ -221,94 +217,54 @@ impl Messenger for StarknetMessaging { return Ok(vec![]); } - let (hashes, calls) = parse_messages(messages)?; - - if !calls.is_empty() { - match self.send_invoke_tx(calls).await { - Ok(tx_hash) => { - trace!(target: LOG_TARGET, tx_hash = %format!("{:#064x}", tx_hash), "Invoke transaction hash."); - } - Err(e) => { - error!(target: LOG_TARGET, error = %e, "Sending invoke tx on Starknet."); - return Err(Error::SendError); - } - }; - } - + let hashes = parse_messages(messages)?; self.send_hashes(hashes.clone()).await?; Ok(hashes) } } -/// Parses messages sent by cairo contracts to compute their hashes. -/// -/// Messages can also be labelled as EXE, which in this case generate a `Call` -/// additionally to the hash. -fn parse_messages(messages: &[MessageToL1]) -> MessengerResult<(Vec, Vec)> { +/// Parses messages sent by cairo contracts on the appchain to compute their hashes. +fn parse_messages(messages: &[MessageToL1]) -> MessengerResult> { let mut hashes: Vec = vec![]; - let mut calls: Vec = vec![]; for m in messages { // Field `to_address` is restricted to eth addresses space. So the - // `to_address` is set to 'EXE'/'MSG' to indicate that the message - // has to be executed or sent normally. + // `to_address` is set to 'MSG' to indicate that the message + // has to be sent to the L2 messaging contract. + // + // Blockifier is the one responsible for this out of range error. + // let magic = m.to_address; - if magic == EXE_MAGIC { - if m.payload.len() < 2 { - error!( - target: LOG_TARGET, - "Message execution is expecting a payload of at least length \ - 2. With [0] being the contract address, and [1] the selector.", - ); - } + if magic != MSG_MAGIC { + warn!(target: LOG_TARGET, magic = %magic, "Skipping message with non-MSG magic."); + continue; + } - let to = m.payload[0]; - let selector = m.payload[1]; + // In the case or regular message, we compute the message's hash + // which will then be sent in a transaction to be registered as being + // ready for consumption by the L2 messaging contract. - let mut calldata = vec![]; - // We must exclude the `to_address` and `selector` from the actual payload. - if m.payload.len() >= 3 { - calldata.extend(m.payload[2..].to_vec()); - } + // As to_address is used by the magic, the `to_address` we want + // is the first element of the payload. + let to_address = m.payload[0]; - calls.push(Call { to, selector, calldata }); - hashes.push(HASH_EXEC); - } else if magic == MSG_MAGIC { - // In the case or regular message, we compute the message's hash - // which will then be sent in a transaction to be registered. - - // As to_address is used by the magic, the `to_address` we want - // is the first element of the payload. - let to_address = m.payload[0]; - - // Then, the payload must be changed to only keep the rest of the - // data, without the first element that was the `to_address`. - let payload = &m.payload[1..]; - - let mut buf: Vec = vec![]; - buf.extend(m.from_address.to_bytes_be()); - buf.extend(to_address.to_bytes_be()); - buf.extend(Felt::from(payload.len()).to_bytes_be()); - for p in payload { - buf.extend(p.to_bytes_be()); - } + // Then, the payload must be changed to only keep the rest of the + // data, without the first element that was the `to_address`. + let payload = &m.payload[1..]; - hashes.push(starknet_keccak(&buf)); - } else { - // Skip the message if no valid magic number found. - warn!(target: LOG_TARGET, magic = ?magic, "Invalid message to_address magic value."); - continue; - } + let message_hash = + compute_appchain_to_starknet_message_hash(m.from_address.into(), to_address, payload); + hashes.push(message_hash); } - Ok((hashes, calls)) + Ok(hashes) } fn l1_handler_tx_from_event(event: &EmittedEvent, chain_id: ChainId) -> Result { - if event.keys[0] != selector!("MessageSentToAppchain") { - debug!( + if event.keys[0] != MESSAGE_SENT_EVENT_KEY { + error!( target: LOG_TARGET, event_key = ?event.keys[0], "Event can't be converted into L1HandlerTx." @@ -331,8 +287,15 @@ fn l1_handler_tx_from_event(event: &EmittedEvent, chain_id: ChainId) -> Result l2 hash computation instead. - let message_hash = compute_l2_to_l1_message_hash(from_address, to_address, &calldata); + let message_hash = compute_starknet_to_appchain_message_hash( + from_address, + to_address, + nonce, + entry_point_selector, + &calldata, + ); + + let message_hash = B256::from_slice(message_hash.to_bytes_be().as_slice()); Ok(L1HandlerTx { nonce, @@ -340,6 +303,7 @@ fn l1_handler_tx_from_event(event: &EmittedEvent, chain_id: ChainId) -> Result Result +fn compute_starknet_to_appchain_message_hash( + from_address: Felt, + to_address: Felt, + nonce: Felt, + entry_point_selector: Felt, + payload: &[Felt], +) -> Felt { + let mut buf: Vec = + vec![from_address, to_address, nonce, entry_point_selector, Felt::from(payload.len())]; + for p in payload { + buf.push(*p); + } + + starknet_crypto::poseidon_hash_many(&buf) +} + +/// Computes the hash of a L3 to L2 message. +/// +/// Piltover uses poseidon hash for all hashes computation. +/// +fn compute_appchain_to_starknet_message_hash( + from_address: Felt, + to_address: Felt, + payload: &[Felt], +) -> Felt { + let mut buf: Vec = vec![from_address, to_address, Felt::from(payload.len())]; + for p in payload { + buf.push(*p); + } + + starknet_crypto::poseidon_hash_many(&buf) +} + #[cfg(test)] mod tests { @@ -359,41 +360,25 @@ mod tests { fn parse_messages_msg() { let from_address = selector!("from_address"); let to_address = selector!("to_address"); - let selector = selector!("selector"); + let _selector = selector!("selector"); let payload_msg = vec![to_address, Felt::ONE, Felt::TWO]; - let payload_exe = vec![to_address, selector, Felt::ONE, Felt::TWO]; - - let messages = vec![ - MessageToL1 { - from_address: from_address.into(), - to_address: MSG_MAGIC, - payload: payload_msg, - }, - MessageToL1 { - from_address: from_address.into(), - to_address: EXE_MAGIC, - payload: payload_exe.clone(), - }, - ]; - - let (hashes, calls) = parse_messages(&messages).unwrap(); - - assert_eq!(hashes.len(), 2); + + let messages = vec![MessageToL1 { + from_address: from_address.into(), + to_address: MSG_MAGIC, + payload: payload_msg, + }]; + + let hashes = parse_messages(&messages).unwrap(); + + assert_eq!(hashes.len(), 1); assert_eq!( hashes, vec![ - Felt::from_hex( - "0x03a1d2e131360f15e26dd4f6ff10550685611cc25f75e7950b704adb04b36162" - ) - .unwrap(), - HASH_EXEC, + Felt::from_hex("0x5063bd24379be4da83d607725d1a9f7e5571cb1be30784b4a7a22996f59ff22") + .unwrap(), ] ); - - assert_eq!(calls.len(), 1); - assert_eq!(calls[0].to, to_address); - assert_eq!(calls[0].selector, selector); - assert_eq!(calls[0].calldata, payload_exe[2..].to_vec()); } #[test] @@ -411,21 +396,6 @@ mod tests { parse_messages(&messages).unwrap(); } - #[test] - #[should_panic] - fn parse_messages_exe_bad_payload() { - let from_address = selector!("from_address"); - let payload_exe = vec![Felt::ONE]; - - let messages = vec![MessageToL1 { - from_address: from_address.into(), - to_address: EXE_MAGIC, - payload: payload_exe, - }]; - - parse_messages(&messages).unwrap(); - } - #[test] fn l1_handler_tx_from_event_parse_ok() { let from_address = selector!("from_address"); @@ -448,25 +418,26 @@ mod tests { from_address: felt!( "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7" ), - keys: vec![ - selector!("MessageSentToAppchain"), - selector!("random_hash"), - from_address, - to_address, - ], + keys: vec![MESSAGE_SENT_EVENT_KEY, selector!("random_hash"), from_address, to_address], data: vec![selector, nonce, Felt::from(calldata.len() as u128), Felt::THREE], block_hash: Some(selector!("block_hash")), block_number: Some(0), transaction_hash, }; - let message_hash = compute_l2_to_l1_message_hash(from_address, to_address, &calldata); + let message_hash = compute_starknet_to_appchain_message_hash( + from_address, + to_address, + nonce, + selector, + &calldata, + ); let expected = L1HandlerTx { nonce, calldata, chain_id, - message_hash, + message_hash: B256::from_slice(message_hash.to_bytes_be().as_slice()), paid_fee_on_l1: 30000_u128, version: Felt::ZERO, entry_point_selector: selector, diff --git a/crates/katana/docs/pipeline.md b/crates/katana/docs/pipeline.md new file mode 100644 index 0000000000..0c79474646 --- /dev/null +++ b/crates/katana/docs/pipeline.md @@ -0,0 +1,35 @@ +# Syncing pipeline + +```mermaid +flowchart TD + A[Start Pipeline Run] --> B[Initialize chunk_tip] + + B --> D{Process Blocks in Chunks} + D --> E[run_once_until] + + %% run_once_until subflow + E --> S1[For each Stage] + S1 --> S2[Get Stage Checkpoint] + S2 --> S3{Checkpoint >= Target?} + S3 -->|Yes| S4[Skip Stage] + S3 -->|No| S5[Execute Stage
from checkpoint+1 to target] + S5 --> S6[Update Stage Checkpoint] + S6 --> S1 + S4 --> S1 + + S1 -->|All Stages Complete| F{Reached Target Tip?} + F -->|No| G[Increment chunk_tip by
chunk_size] + G --> D + + F -->|Yes| H[Wait for New Tip] + H -->|New Tip Received| D + H -->|Channel Closed| I[Pipeline Complete] + + style A fill:#f9f,stroke:#333 + style I fill:#f96,stroke:#333 + +%% Example annotations + classDef note fill:#fff,stroke:#333,stroke-dasharray: 5 5 + N1[For example: Tip=1000
chunk_size=100
Processes: 0-100, 100-200, etc]:::note + N1 -.-> D +``` diff --git a/crates/katana/executor/Cargo.toml b/crates/katana/executor/Cargo.toml index 0939be3dea..785d18d3d4 100644 --- a/crates/katana/executor/Cargo.toml +++ b/crates/katana/executor/Cargo.toml @@ -9,6 +9,7 @@ version.workspace = true [dependencies] katana-primitives.workspace = true katana-provider.workspace = true +katana-trie.workspace = true parking_lot = { workspace = true, optional = true } starknet = { workspace = true, optional = true } diff --git a/crates/katana/executor/src/abstraction/mod.rs b/crates/katana/executor/src/abstraction/mod.rs index 49518f32cf..d9a0e99af4 100644 --- a/crates/katana/executor/src/abstraction/mod.rs +++ b/crates/katana/executor/src/abstraction/mod.rs @@ -3,16 +3,17 @@ mod executor; pub use error::*; pub use executor::*; -use katana_primitives::class::{ClassHash, CompiledClass, CompiledClassHash, FlattenedSierraClass}; +use katana_primitives::class::{ClassHash, CompiledClass, CompiledClassHash, ContractClass}; use katana_primitives::contract::{ContractAddress, Nonce, StorageKey, StorageValue}; use katana_primitives::receipt::Receipt; -use katana_primitives::state::{StateUpdates, StateUpdatesWithDeclaredClasses}; +use katana_primitives::state::{StateUpdates, StateUpdatesWithClasses}; use katana_primitives::trace::TxExecInfo; use katana_primitives::transaction::TxWithHash; use katana_primitives::Felt; use katana_provider::traits::contract::ContractClassProvider; -use katana_provider::traits::state::StateProvider; +use katana_provider::traits::state::{StateProofProvider, StateProvider, StateRootProvider}; use katana_provider::ProviderResult; +use katana_trie::MultiProof; pub type ExecutorResult = Result; @@ -90,7 +91,7 @@ pub struct ExecutionOutput { /// Statistics throughout the executions process. pub stats: ExecutionStats, /// The state updates produced by the executions. - pub states: StateUpdatesWithDeclaredClasses, + pub states: StateUpdatesWithClasses, /// The transactions that have been executed. pub transactions: Vec<(TxWithHash, ExecutionResult)>, } @@ -168,20 +169,20 @@ impl<'a> StateProviderDb<'a> { } impl<'a> ContractClassProvider for StateProviderDb<'a> { - fn class(&self, hash: ClassHash) -> ProviderResult> { + fn class(&self, hash: ClassHash) -> ProviderResult> { self.0.class(hash) } + fn compiled_class(&self, hash: ClassHash) -> ProviderResult> { + self.0.compiled_class(hash) + } + fn compiled_class_hash_of_class_hash( &self, hash: ClassHash, ) -> ProviderResult> { self.0.compiled_class_hash_of_class_hash(hash) } - - fn sierra_class(&self, hash: ClassHash) -> ProviderResult> { - self.0.sierra_class(hash) - } } impl<'a> StateProvider for StateProviderDb<'a> { @@ -204,3 +205,39 @@ impl<'a> StateProvider for StateProviderDb<'a> { self.0.storage(address, storage_key) } } + +impl<'a> StateProofProvider for StateProviderDb<'a> { + fn class_multiproof(&self, classes: Vec) -> ProviderResult { + self.0.class_multiproof(classes) + } + + fn contract_multiproof(&self, addresses: Vec) -> ProviderResult { + self.0.contract_multiproof(addresses) + } + + fn storage_multiproof( + &self, + address: ContractAddress, + key: Vec, + ) -> ProviderResult { + self.0.storage_multiproof(address, key) + } +} + +impl<'a> StateRootProvider for StateProviderDb<'a> { + fn classes_root(&self) -> ProviderResult { + self.0.classes_root() + } + + fn contracts_root(&self) -> ProviderResult { + self.0.contracts_root() + } + + fn storage_root(&self, contract: ContractAddress) -> ProviderResult> { + self.0.storage_root(contract) + } + + fn state_root(&self) -> ProviderResult { + self.0.state_root() + } +} diff --git a/crates/katana/executor/src/implementation/blockifier/mod.rs b/crates/katana/executor/src/implementation/blockifier/mod.rs index a1948db15f..d515beb5a9 100644 --- a/crates/katana/executor/src/implementation/blockifier/mod.rs +++ b/crates/katana/executor/src/implementation/blockifier/mod.rs @@ -175,7 +175,7 @@ impl<'a> BlockExecutor<'a> for StarknetVMProcessor<'a> { // Collect class artifacts if its a declare tx let class_decl_artifacts = if let ExecutableTx::Declare(tx) = exec_tx.as_ref() { let class_hash = tx.class_hash(); - Some((class_hash, tx.compiled_class.clone(), tx.sierra_class.clone())) + Some((class_hash, tx.class.clone())) } else { None }; @@ -194,8 +194,8 @@ impl<'a> BlockExecutor<'a> for StarknetVMProcessor<'a> { info!(target: LOG_TARGET, hash = format!("{hash:#x}"), %reason, "Transaction reverted."); } - if let Some((class_hash, compiled, sierra)) = class_decl_artifacts { - state.declared_classes.insert(class_hash, (compiled, sierra)); + if let Some((class_hash, class)) = class_decl_artifacts { + state.declared_classes.insert(class_hash, class.as_ref().clone()); } crate::utils::log_resources(&trace.actual_resources); diff --git a/crates/katana/executor/src/implementation/blockifier/state.rs b/crates/katana/executor/src/implementation/blockifier/state.rs index 413889755b..86aba56190 100644 --- a/crates/katana/executor/src/implementation/blockifier/state.rs +++ b/crates/katana/executor/src/implementation/blockifier/state.rs @@ -6,11 +6,11 @@ use blockifier::state::errors::StateError; use blockifier::state::state_api::{StateReader, StateResult}; use katana_cairo::starknet_api::core::{ClassHash, CompiledClassHash, Nonce}; use katana_cairo::starknet_api::state::StorageKey; -use katana_primitives::class::{CompiledClass, FlattenedSierraClass}; +use katana_primitives::class::{self, CompiledClass, ContractClass}; use katana_primitives::Felt; use katana_provider::error::ProviderError; use katana_provider::traits::contract::ContractClassProvider; -use katana_provider::traits::state::StateProvider; +use katana_provider::traits::state::{StateProofProvider, StateProvider, StateRootProvider}; use katana_provider::ProviderResult; use parking_lot::Mutex; @@ -50,8 +50,9 @@ impl<'a> StateReader for StateProviderDb<'a> { &self, class_hash: ClassHash, ) -> StateResult { - if let Some(class) = - self.class(class_hash.0).map_err(|e| StateError::StateReadError(e.to_string()))? + if let Some(class) = self + .compiled_class(class_hash.0) + .map_err(|e| StateError::StateReadError(e.to_string()))? { let class = utils::to_class(class).map_err(|e| StateError::StateReadError(e.to_string()))?; @@ -91,19 +92,23 @@ impl Clone for CachedState { } } -type DeclaredClass = (CompiledClass, Option); +// type DeclaredClass = (CompiledClass, ContractClass); #[derive(Debug)] pub(super) struct CachedStateInner { pub(super) inner: cached_state::CachedState, - pub(super) declared_classes: HashMap, + pub(super) declared_classes: HashMap, + pub(super) compiled_classes: HashMap, } impl CachedState { pub(super) fn new(state: S) -> Self { let declared_classes = HashMap::new(); + let compiled_classes = HashMap::new(); + let cached_state = cached_state::CachedState::new(state); - let inner = CachedStateInner { inner: cached_state, declared_classes }; + let inner = CachedStateInner { inner: cached_state, declared_classes, compiled_classes }; + Self(Arc::new(Mutex::new(inner))) } } @@ -112,15 +117,32 @@ impl ContractClassProvider for CachedState { fn class( &self, hash: katana_primitives::class::ClassHash, - ) -> ProviderResult> { + ) -> ProviderResult> { let state = self.0.lock(); - if let Some((class, _)) = state.declared_classes.get(&hash) { + if let Some(class) = state.declared_classes.get(&hash) { Ok(Some(class.clone())) } else { state.inner.state.class(hash) } } + fn compiled_class( + &self, + hash: katana_primitives::class::ClassHash, + ) -> ProviderResult> { + let mut state = self.0.lock(); + + if let class @ Some(..) = state.compiled_classes.get(&hash) { + Ok(class.cloned()) + } else if let Some(class) = state.declared_classes.get(&hash) { + let compiled = class.clone().compile()?; + state.compiled_classes.insert(hash, compiled.clone()); + Ok(Some(compiled)) + } else { + state.inner.state.compiled_class(hash) + } + } + fn compiled_class_hash_of_class_hash( &self, hash: katana_primitives::class::ClassHash, @@ -131,17 +153,6 @@ impl ContractClassProvider for CachedState { if hash.0 == Felt::ZERO { Ok(None) } else { Ok(Some(hash.0)) } } - fn sierra_class( - &self, - hash: katana_primitives::class::ClassHash, - ) -> ProviderResult> { - let state = self.0.lock(); - if let Some((_, sierra)) = state.declared_classes.get(&hash) { - Ok(sierra.clone()) - } else { - state.inner.state.sierra_class(hash) - } - } } impl StateProvider for CachedState { @@ -227,31 +238,73 @@ impl StateReader for CachedState { } } +impl StateProofProvider for CachedState { + fn class_multiproof( + &self, + classes: Vec, + ) -> ProviderResult { + let _ = classes; + unimplemented!("not supported in executor's state") + } + + fn contract_multiproof( + &self, + addresses: Vec, + ) -> ProviderResult { + let _ = addresses; + unimplemented!("not supported in executor's state") + } + + fn storage_multiproof( + &self, + address: katana_primitives::ContractAddress, + key: Vec, + ) -> ProviderResult { + let _ = address; + let _ = key; + unimplemented!("not supported in executor's state") + } +} + +impl StateRootProvider for CachedState { + fn classes_root(&self) -> ProviderResult { + unimplemented!("not supported in executor's state") + } + + fn contracts_root(&self) -> ProviderResult { + unimplemented!("not supported in executor's state") + } + + fn storage_root(&self, _: katana_primitives::ContractAddress) -> ProviderResult> { + unimplemented!("not supported in executor's state") + } +} + #[cfg(test)] mod tests { use blockifier::state::state_api::{State, StateReader}; - use katana_primitives::class::{CompiledClass, FlattenedSierraClass}; + use katana_primitives::class::CompiledClass; use katana_primitives::contract::ContractAddress; use katana_primitives::genesis::constant::{ - DEFAULT_ACCOUNT_CLASS, DEFAULT_ACCOUNT_CLASS_CASM, DEFAULT_LEGACY_ERC20_CASM, - DEFAULT_LEGACY_UDC_CASM, + DEFAULT_ACCOUNT_CLASS, DEFAULT_ACCOUNT_CLASS_CASM, DEFAULT_LEGACY_ERC20_CLASS, + DEFAULT_LEGACY_UDC_CLASS, }; use katana_primitives::utils::class::{parse_compiled_class, parse_sierra_class}; use katana_primitives::{address, Felt}; use katana_provider::providers::db::DbProvider; - use katana_provider::traits::contract::ContractClassWriter; + use katana_provider::traits::contract::{ContractClassWriter, ContractClassWriterExt}; use katana_provider::traits::state::{StateFactoryProvider, StateProvider, StateWriter}; use starknet::macros::felt; use super::{CachedState, *}; use crate::StateProviderDb; - fn new_sierra_class() -> (FlattenedSierraClass, CompiledClass) { + fn new_sierra_class() -> (ContractClass, CompiledClass) { let json = include_str!("../../../../contracts/build/cairo1_contract.json"); let artifact = serde_json::from_str(json).unwrap(); let compiled_class = parse_compiled_class(artifact).unwrap(); - let sierra_class = parse_sierra_class(json).unwrap().flatten().unwrap(); + let sierra_class = parse_sierra_class(json).unwrap(); (sierra_class, compiled_class) } @@ -262,10 +315,10 @@ mod tests { let storage_value = felt!("0x2"); let class_hash = felt!("0x123"); let compiled_hash = felt!("0x456"); - let sierra_class = DEFAULT_ACCOUNT_CLASS.clone().flatten().unwrap(); - let class = DEFAULT_ACCOUNT_CLASS_CASM.clone(); - let legacy_class_hash = felt!("0x111"); - let legacy_class = DEFAULT_LEGACY_ERC20_CASM.clone(); + let class = DEFAULT_ACCOUNT_CLASS.clone(); + let compiled_class = DEFAULT_ACCOUNT_CLASS_CASM.clone(); + let legacy_class_hash = felt!("0x445"); + let legacy_class = DEFAULT_LEGACY_ERC20_CLASS.clone(); let provider = katana_provider::test_utils::test_provider(); provider.set_nonce(address, nonce).unwrap(); @@ -273,8 +326,9 @@ mod tests { provider.set_storage(address, storage_key, storage_value).unwrap(); provider.set_compiled_class_hash_of_class_hash(class_hash, compiled_hash).unwrap(); provider.set_class(class_hash, class).unwrap(); - provider.set_sierra_class(class_hash, sierra_class).unwrap(); - provider.set_class(legacy_class_hash, legacy_class).unwrap(); + provider.set_compiled_class(class_hash, compiled_class).unwrap(); + provider.set_class(legacy_class_hash, legacy_class.clone()).unwrap(); + provider.set_compiled_class(legacy_class_hash, legacy_class.compile().unwrap()).unwrap(); provider.latest().unwrap() } @@ -285,7 +339,7 @@ mod tests { let cached_state = CachedState::new(StateProviderDb::new(state)); let address = address!("0x67"); - let legacy_class_hash = felt!("0x111"); + let legacy_class_hash = felt!("0x445"); let storage_key = felt!("0x1"); let api_address = utils::to_blk_address(address); @@ -308,7 +362,9 @@ mod tests { ); assert_eq!( actual_legacy_class, - utils::to_class(DEFAULT_LEGACY_ERC20_CASM.clone()).unwrap().contract_class() + utils::to_class(DEFAULT_LEGACY_ERC20_CLASS.clone().compile().expect("can compile")) + .unwrap() + .contract_class() ); Ok(()) @@ -323,7 +379,7 @@ mod tests { let new_storage_key = felt!("0xf00"); let new_storage_value = felt!("0xba"); let new_legacy_class_hash = felt!("0x1234"); - let new_legacy_class = DEFAULT_LEGACY_UDC_CASM.clone(); + let new_legacy_class = DEFAULT_LEGACY_UDC_CLASS.clone(); let new_legacy_compiled_hash = felt!("0x5678"); let new_class_hash = felt!("0x777"); let (new_sierra_class, new_compiled_sierra_class) = new_sierra_class(); @@ -336,7 +392,7 @@ mod tests { let actual_new_storage_value = sp.storage(new_address, new_storage_key)?; let actual_new_legacy_class = sp.class(new_legacy_class_hash)?; let actual_new_legacy_sierra_class = sp.class(new_legacy_class_hash)?; - let actual_new_sierra_class = sp.sierra_class(new_class_hash)?; + // let actual_new_sierra_class = sp.sierra_class(new_class_hash)?; let actual_new_class = sp.class(new_class_hash)?; let actual_new_compiled_class_hash = sp.compiled_class_hash_of_class_hash(new_class_hash)?; @@ -348,7 +404,7 @@ mod tests { assert_eq!(actual_new_storage_value, None, "data shouldn't exist"); assert_eq!(actual_new_legacy_class, None, "data should'nt exist"); assert_eq!(actual_new_legacy_sierra_class, None, "data shouldn't exist"); - assert_eq!(actual_new_sierra_class, None, "data shouldn't exist"); + // assert_eq!(actual_new_sierra_class, None, "data shouldn't exist"); assert_eq!(actual_new_class, None, "data shouldn't exist"); assert_eq!(actual_new_compiled_class_hash, None, "data shouldn't exist"); assert_eq!(actual_new_legacy_compiled_hash, None, "data shouldn't exist"); @@ -369,7 +425,9 @@ mod tests { let compiled_hash = CompiledClassHash(new_compiled_hash); let legacy_class_hash = ClassHash(new_legacy_class_hash); let legacy_class = - utils::to_class(DEFAULT_LEGACY_UDC_CASM.clone()).unwrap().contract_class(); + utils::to_class(DEFAULT_LEGACY_UDC_CLASS.clone().compile().expect("can compile")) + .unwrap() + .contract_class(); let legacy_compiled_hash = CompiledClassHash(new_legacy_compiled_hash); blk_state.increment_nonce(address)?; @@ -381,11 +439,8 @@ mod tests { blk_state.set_compiled_class_hash(legacy_class_hash, legacy_compiled_hash)?; let declared_classes = &mut lock.declared_classes; - declared_classes.insert(new_legacy_class_hash, (new_legacy_class.clone(), None)); - declared_classes.insert( - new_class_hash, - (new_compiled_sierra_class.clone(), Some(new_sierra_class.clone())), - ); + declared_classes.insert(new_legacy_class_hash, new_legacy_class.clone()); + declared_classes.insert(new_class_hash, new_sierra_class.clone()); } // assert that can fetch data from the underlyign state provider @@ -393,13 +448,13 @@ mod tests { let address = address!("0x67"); let class_hash = felt!("0x123"); - let legacy_class_hash = felt!("0x111"); + let legacy_class_hash = felt!("0x445"); let actual_class_hash = sp.class_hash_of_contract(address)?; let actual_nonce = sp.nonce(address)?; let actual_storage_value = sp.storage(address, felt!("0x1"))?; let actual_class = sp.class(class_hash)?; - let actual_sierra_class = sp.sierra_class(class_hash)?; + let actual_compiled_class = sp.compiled_class(class_hash)?; let actual_compiled_hash = sp.compiled_class_hash_of_class_hash(class_hash)?; let actual_legacy_class = sp.class(legacy_class_hash)?; @@ -407,9 +462,9 @@ mod tests { assert_eq!(actual_class_hash, Some(class_hash)); assert_eq!(actual_storage_value, Some(felt!("0x2"))); assert_eq!(actual_compiled_hash, Some(felt!("0x456"))); - assert_eq!(actual_class, Some(DEFAULT_ACCOUNT_CLASS_CASM.clone())); - assert_eq!(actual_sierra_class, Some(DEFAULT_ACCOUNT_CLASS.clone().flatten()?)); - assert_eq!(actual_legacy_class, Some(DEFAULT_LEGACY_ERC20_CASM.clone())); + assert_eq!(actual_compiled_class, Some(DEFAULT_ACCOUNT_CLASS_CASM.clone())); + assert_eq!(actual_class, Some(DEFAULT_ACCOUNT_CLASS.clone())); + assert_eq!(actual_legacy_class, Some(DEFAULT_LEGACY_ERC20_CLASS.clone())); // assert that can fetch data native to the cached state from the state provider @@ -417,10 +472,10 @@ mod tests { let actual_new_nonce = sp.nonce(new_address)?; let actual_new_storage_value = sp.storage(new_address, new_storage_key)?; let actual_new_class = sp.class(new_class_hash)?; - let actual_new_sierra = sp.sierra_class(new_class_hash)?; + let actual_new_compiled_class = sp.compiled_class(new_class_hash)?; let actual_new_compiled_hash = sp.compiled_class_hash_of_class_hash(new_class_hash)?; let actual_legacy_class = sp.class(new_legacy_class_hash)?; - let actual_legacy_sierra = sp.sierra_class(new_legacy_class_hash)?; + let actual_legacy_compiled_class = sp.compiled_class(new_legacy_class_hash)?; let actual_new_legacy_compiled_hash = sp.compiled_class_hash_of_class_hash(new_legacy_class_hash)?; @@ -435,11 +490,14 @@ mod tests { Some(new_storage_value), "data should be in cached state" ); - assert_eq!(actual_new_class, Some(new_compiled_sierra_class)); - assert_eq!(actual_new_sierra, Some(new_sierra_class)); + assert_eq!(actual_new_compiled_class, Some(new_compiled_sierra_class)); + assert_eq!(actual_new_class, Some(new_sierra_class)); assert_eq!(actual_new_compiled_hash, Some(new_compiled_hash)); - assert_eq!(actual_legacy_class, Some(new_legacy_class)); - assert_eq!(actual_legacy_sierra, None, "legacy class should not have sierra class"); + assert_eq!(actual_legacy_class, Some(new_legacy_class.clone())); + assert_eq!( + actual_legacy_compiled_class, + Some(new_legacy_class.compile().expect("can compile")) + ); assert_eq!( actual_new_legacy_compiled_hash, Some(new_legacy_compiled_hash), diff --git a/crates/katana/executor/src/implementation/blockifier/utils.rs b/crates/katana/executor/src/implementation/blockifier/utils.rs index 2b528a796e..abae3566e6 100644 --- a/crates/katana/executor/src/implementation/blockifier/utils.rs +++ b/crates/katana/executor/src/implementation/blockifier/utils.rs @@ -1,4 +1,4 @@ -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use std::num::NonZeroU128; use std::sync::Arc; @@ -31,7 +31,7 @@ use katana_cairo::cairo_vm::types::errors::program_errors::ProgramError; use katana_cairo::cairo_vm::vm::runners::cairo_runner::ExecutionResources; use katana_cairo::starknet_api::block::{BlockNumber, BlockTimestamp}; use katana_cairo::starknet_api::core::{ - self, ChainId, ClassHash, CompiledClassHash, ContractAddress, Nonce, + self, ChainId, ClassHash, CompiledClassHash, ContractAddress, EntryPointSelector, Nonce, }; use katana_cairo::starknet_api::data_availability::DataAvailabilityMode; use katana_cairo::starknet_api::deprecated_contract_class::EntryPointType; @@ -46,7 +46,7 @@ use katana_cairo::starknet_api::transaction::{ use katana_primitives::chain::NamedChainId; use katana_primitives::env::{BlockEnv, CfgEnv}; use katana_primitives::fee::{PriceUnit, TxFeeInfo}; -use katana_primitives::state::{StateUpdates, StateUpdatesWithDeclaredClasses}; +use katana_primitives::state::{StateUpdates, StateUpdatesWithClasses}; use katana_primitives::trace::{L1Gas, TxExecInfo, TxResources}; use katana_primitives::transaction::{ DeclareTx, DeployAccountTx, ExecutableTx, ExecutableTxWithHash, InvokeTx, TxType, @@ -184,6 +184,25 @@ pub fn to_executor_tx(tx: ExecutableTxWithHash) -> Transaction { match tx.transaction { ExecutableTx::Invoke(tx) => match tx { + InvokeTx::V0(tx) => { + let calldata = tx.calldata; + let signature = tx.signature; + + Transaction::AccountTransaction(AccountTransaction::Invoke(InvokeTransaction { + tx: ApiInvokeTransaction::V0( + katana_cairo::starknet_api::transaction::InvokeTransactionV0 { + entry_point_selector: EntryPointSelector(tx.entry_point_selector), + contract_address: to_blk_address(tx.contract_address), + signature: TransactionSignature(signature), + calldata: Calldata(Arc::new(calldata)), + max_fee: Fee(tx.max_fee), + }, + ), + tx_hash: TransactionHash(hash), + only_query: false, + })) + } + InvokeTx::V1(tx) => { let calldata = tx.calldata; let signature = tx.signature; @@ -288,9 +307,17 @@ pub fn to_executor_tx(tx: ExecutableTxWithHash) -> Transaction { }, ExecutableTx::Declare(tx) => { - let contract_class = tx.compiled_class; + let compiled = tx.class.as_ref().clone().compile().expect("failed to compile"); let tx = match tx.transaction { + DeclareTx::V0(tx) => ApiDeclareTransaction::V0(DeclareTransactionV0V1 { + max_fee: Fee(tx.max_fee), + nonce: Nonce::default(), + sender_address: to_blk_address(tx.sender_address), + signature: TransactionSignature(tx.signature), + class_hash: ClassHash(tx.class_hash), + }), + DeclareTx::V1(tx) => ApiDeclareTransaction::V1(DeclareTransactionV0V1 { max_fee: Fee(tx.max_fee), nonce: Nonce(tx.nonce), @@ -338,7 +365,7 @@ pub fn to_executor_tx(tx: ExecutableTxWithHash) -> Transaction { }; let hash = TransactionHash(hash); - let class = to_class(contract_class).unwrap(); + let class = to_class(compiled).unwrap(); let tx = DeclareTransaction::new(tx, hash, class).expect("class mismatch"); Transaction::AccountTransaction(AccountTransaction::Declare(tx)) } @@ -397,31 +424,30 @@ pub fn block_context_from_envs(block_env: &BlockEnv, cfg_env: &CfgEnv) -> BlockC pub(super) fn state_update_from_cached_state( state: &CachedState, -) -> StateUpdatesWithDeclaredClasses { - use katana_primitives::class::{CompiledClass, FlattenedSierraClass}; - +) -> StateUpdatesWithClasses { let state_diff = state.0.lock().inner.to_state_diff().unwrap(); - let mut declared_compiled_classes: BTreeMap< + let mut declared_contract_classes: BTreeMap< katana_primitives::class::ClassHash, - CompiledClass, + katana_primitives::class::ContractClass, > = BTreeMap::new(); - let mut declared_sierra_classes: BTreeMap< - katana_primitives::class::ClassHash, - FlattenedSierraClass, - > = BTreeMap::new(); + let mut declared_classes = BTreeMap::new(); + let mut deprecated_declared_classes = BTreeSet::new(); - for class_hash in state_diff.compiled_class_hashes.keys() { + // TODO: Legacy class shouldn't have a compiled class hash. This is a hack we added + // in our fork of `blockifier. Check if it's possible to remove it now. + for (class_hash, compiled_hash) in state_diff.compiled_class_hashes { let hash = class_hash.0; let class = state.class(hash).unwrap().expect("must exist if declared"); - if let CompiledClass::Class(_) = class { - let sierra = state.sierra_class(hash).unwrap().expect("must exist if declared"); - declared_sierra_classes.insert(hash, sierra); + if class.is_legacy() { + deprecated_declared_classes.insert(hash); + } else { + declared_classes.insert(hash, compiled_hash.0); } - declared_compiled_classes.insert(hash, class); + declared_contract_classes.insert(hash, class); } let nonce_updates = @@ -456,25 +482,15 @@ pub(super) fn state_update_from_cached_state( katana_primitives::class::ClassHash, >>(); - let declared_classes = - state_diff - .compiled_class_hashes - .into_iter() - .map(|(key, value)| (key.0, value.0)) - .collect::>(); - - StateUpdatesWithDeclaredClasses { - declared_sierra_classes, - declared_compiled_classes, + StateUpdatesWithClasses { + classes: declared_contract_classes, state_updates: StateUpdates { nonce_updates, storage_updates, - deployed_contracts, declared_classes, - ..Default::default() + deployed_contracts, + deprecated_declared_classes, + replaced_classes: BTreeMap::default(), }, } } @@ -535,17 +551,17 @@ pub fn to_class(class: class::CompiledClass) -> Result // TODO: @kariy not sure of the variant that must be used in this case. Should we change the // return type to include this case of error for contract class conversions? match class { - class::CompiledClass::Deprecated(class) => { + class::CompiledClass::Legacy(class) => { // For cairo 0, the sierra_program_length must be 0. Ok(ClassInfo::new(&ContractClass::V0(ContractClassV0::try_from(class)?), 0, 0) .map_err(|e| ProgramError::ConstWithoutValue(format!("{e}")))?) } - class::CompiledClass::Class(class) => { - let sierra_program_len = class.sierra.program.statements.len(); + class::CompiledClass::Class(casm) => { + let sierra_program_len = casm.bytecode.len(); // TODO: @kariy not sure from where the ABI length can be grasped. Ok(ClassInfo::new( - &ContractClass::V1(ContractClassV1::try_from(class.casm)?), + &ContractClass::V1(ContractClassV1::try_from(casm)?), sierra_program_len, 0, ) diff --git a/crates/katana/executor/src/implementation/noop.rs b/crates/katana/executor/src/implementation/noop.rs index 33847e686f..0392e633eb 100644 --- a/crates/katana/executor/src/implementation/noop.rs +++ b/crates/katana/executor/src/implementation/noop.rs @@ -1,12 +1,12 @@ use katana_primitives::block::ExecutableBlock; -use katana_primitives::class::{ClassHash, CompiledClass, CompiledClassHash, FlattenedSierraClass}; +use katana_primitives::class::{ClassHash, CompiledClass, CompiledClassHash, ContractClass}; use katana_primitives::contract::{ContractAddress, Nonce, StorageKey, StorageValue}; use katana_primitives::env::{BlockEnv, CfgEnv}; use katana_primitives::fee::TxFeeInfo; use katana_primitives::transaction::{ExecutableTxWithHash, TxWithHash}; use katana_primitives::Felt; use katana_provider::traits::contract::ContractClassProvider; -use katana_provider::traits::state::StateProvider; +use katana_provider::traits::state::{StateProofProvider, StateProvider, StateRootProvider}; use katana_provider::ProviderResult; use crate::abstraction::{ @@ -127,20 +127,20 @@ impl<'a> BlockExecutor<'a> for NoopExecutor { struct NoopStateProvider; impl ContractClassProvider for NoopStateProvider { - fn class(&self, hash: ClassHash) -> ProviderResult> { + fn class(&self, hash: ClassHash) -> ProviderResult> { let _ = hash; Ok(None) } - fn compiled_class_hash_of_class_hash( - &self, - hash: ClassHash, - ) -> ProviderResult> { + fn compiled_class(&self, hash: ClassHash) -> ProviderResult> { let _ = hash; Ok(None) } - fn sierra_class(&self, hash: ClassHash) -> ProviderResult> { + fn compiled_class_hash_of_class_hash( + &self, + hash: ClassHash, + ) -> ProviderResult> { let _ = hash; Ok(None) } @@ -170,3 +170,42 @@ impl StateProvider for NoopStateProvider { Ok(None) } } + +impl StateProofProvider for NoopStateProvider { + fn class_multiproof(&self, classes: Vec) -> ProviderResult { + let _ = classes; + Ok(katana_trie::MultiProof(Default::default())) + } + + fn contract_multiproof( + &self, + addresses: Vec, + ) -> ProviderResult { + let _ = addresses; + Ok(katana_trie::MultiProof(Default::default())) + } + + fn storage_multiproof( + &self, + address: ContractAddress, + key: Vec, + ) -> ProviderResult { + let _ = address; + let _ = key; + Ok(katana_trie::MultiProof(Default::default())) + } +} + +impl StateRootProvider for NoopStateProvider { + fn classes_root(&self) -> ProviderResult { + Ok(Felt::ZERO) + } + + fn contracts_root(&self) -> ProviderResult { + Ok(Felt::ZERO) + } + + fn storage_root(&self, _: ContractAddress) -> ProviderResult> { + Ok(Some(Felt::ZERO)) + } +} diff --git a/crates/katana/executor/tests/executor.rs b/crates/katana/executor/tests/executor.rs index 61ab3713f6..83175f18c4 100644 --- a/crates/katana/executor/tests/executor.rs +++ b/crates/katana/executor/tests/executor.rs @@ -98,12 +98,12 @@ fn test_executor_with_valid_blocks_impl( // assert that the sierra class is declared let expected_class_hash = felt!("0x420"); - let (casm, sierra) = fixtures::contract_class(); - let actual_casm = state_provider.class(expected_class_hash).unwrap(); - let actual_sierra = state_provider.sierra_class(expected_class_hash).unwrap(); + let (casm, class) = fixtures::contract_class(); + let actual_class = state_provider.class(expected_class_hash).unwrap(); + let actual_compiled_class = state_provider.compiled_class(expected_class_hash).unwrap(); - assert_eq!(actual_casm, Some(casm), "casm class should be declared"); - assert_eq!(actual_sierra, Some(sierra), "sierra class should be declared"); + assert_eq!(actual_class, Some(class), "casm class should be declared"); + assert_eq!(actual_compiled_class, Some(casm), "sierra class should be declared"); let expected_compiled_class_hash = felt!("0x016c6081eb34ad1e0c5513234ed0c025b3c7f305902d291bad534cd6474c85bc"); diff --git a/crates/katana/executor/tests/fixtures/mod.rs b/crates/katana/executor/tests/fixtures/mod.rs index 564228c141..2442f961e1 100644 --- a/crates/katana/executor/tests/fixtures/mod.rs +++ b/crates/katana/executor/tests/fixtures/mod.rs @@ -8,7 +8,7 @@ use katana_primitives::block::{ }; use katana_primitives::chain::ChainId; use katana_primitives::chain_spec::{self, ChainSpec}; -use katana_primitives::class::{CompiledClass, FlattenedSierraClass}; +use katana_primitives::class::{CompiledClass, ContractClass}; use katana_primitives::contract::ContractAddress; use katana_primitives::da::L1DataAvailabilityMode; use katana_primitives::env::{CfgEnv, FeeTokenAddressses}; @@ -37,14 +37,14 @@ pub fn legacy_contract_class() -> CompiledClass { parse_compiled_class(artifact).unwrap() } -pub fn contract_class() -> (CompiledClass, FlattenedSierraClass) { +pub fn contract_class() -> (CompiledClass, ContractClass) { let json = include_str!("contract.json"); let artifact = serde_json::from_str(json).unwrap(); - let sierra = parse_sierra_class(json).unwrap().flatten().unwrap(); + let class = parse_sierra_class(json).unwrap(); let compiled = parse_compiled_class(artifact).unwrap(); - (compiled, sierra) + (compiled, class) } #[rstest::fixture] @@ -132,10 +132,9 @@ pub fn valid_blocks() -> [ExecutableBlock; 3] { }))), // declare contract ExecutableTxWithHash::new(ExecutableTx::Declare({ - let (compiled_class, sierra) = contract_class(); + let (.., class) = contract_class(); DeclareTxWithClass { - compiled_class, - sierra_class: Some(sierra), + class: class.into(), transaction: DeclareTx::V2(DeclareTxV2 { nonce: Felt::ONE, max_fee: 27092100000000000, diff --git a/crates/katana/executor/tests/simulate.rs b/crates/katana/executor/tests/simulate.rs index 8decf607b1..09d1b25627 100644 --- a/crates/katana/executor/tests/simulate.rs +++ b/crates/katana/executor/tests/simulate.rs @@ -74,8 +74,7 @@ fn test_simulate_tx_impl( assert!(states.state_updates.deployed_contracts.is_empty(), "no state updates"); assert!(states.state_updates.declared_classes.is_empty(), "no state updates"); - assert!(states.declared_sierra_classes.is_empty(), "no new classes should be declared"); - assert!(states.declared_compiled_classes.is_empty(), "no new classes should be declared"); + assert!(states.classes.is_empty(), "no new classes should be declared"); } #[cfg(feature = "blockifier")] diff --git a/crates/katana/feeder-gateway/Cargo.toml b/crates/katana/feeder-gateway/Cargo.toml new file mode 100644 index 0000000000..9a3b6d5eaa --- /dev/null +++ b/crates/katana/feeder-gateway/Cargo.toml @@ -0,0 +1,20 @@ +[package] +edition.workspace = true +license.workspace = true +name = "katana-feeder-gateway" +repository.workspace = true +version.workspace = true + +[dependencies] +katana-primitives.workspace = true +katana-rpc-types.workspace = true + +reqwest.workspace = true +serde.workspace = true +starknet.workspace = true +thiserror.workspace = true +tracing.workspace = true +url.workspace = true + +[dev-dependencies] +tokio.workspace = true diff --git a/crates/katana/feeder-gateway/src/client.rs b/crates/katana/feeder-gateway/src/client.rs new file mode 100644 index 0000000000..84edbb3aec --- /dev/null +++ b/crates/katana/feeder-gateway/src/client.rs @@ -0,0 +1,293 @@ +use katana_primitives::block::{BlockIdOrTag, BlockTag}; +use katana_primitives::class::CasmContractClass; +use katana_primitives::Felt; +use reqwest::header::{HeaderMap, HeaderValue}; +use reqwest::{Client, StatusCode}; +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; +use tracing::error; +use url::Url; + +use crate::types::{Block, ContractClass, StateUpdate, StateUpdateWithBlock}; + +/// HTTP request header for the feeder gateway API key. This allow bypassing the rate limiting. +const X_THROTTLING_BYPASS: &str = "X-Throttling-Bypass"; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error(transparent)] + Network(#[from] reqwest::Error), + + #[error(transparent)] + Sequencer(SequencerError), + + #[error("failed to parse header value '{value}'")] + InvalidHeaderValue { value: String }, + + #[error("request rate limited")] + RateLimited, +} + +impl Error { + /// Returns `true` if the error is due to rate limiting. + pub fn is_rate_limited(&self) -> bool { + matches!(self, Self::RateLimited) + } +} + +/// Client for interacting with the Starknet's feeder gateway. +#[derive(Debug, Clone)] +pub struct SequencerGateway { + /// The feeder gateway base URL. + base_url: Url, + /// The HTTP client used to send the requests. + http_client: Client, + /// The API key used to bypass the rate limiting of the feeder gateway. + api_key: Option, +} + +impl SequencerGateway { + /// Creates a new gateway client to Starknet mainnet. + /// + /// https://docs.starknet.io/tools/important-addresses/#sequencer_base_url + pub fn sn_mainnet() -> Self { + Self::new(Url::parse("https://alpha-mainnet.starknet.io/").unwrap()) + } + + /// Creates a new gateway client to Starknet sepolia. + /// + /// https://docs.starknet.io/tools/important-addresses/#sequencer_base_url + pub fn sn_sepolia() -> Self { + Self::new(Url::parse("https://alpha-sepolia.starknet.io/").unwrap()) + } + + /// Creates a new gateway client at the given base URL. + pub fn new(base_url: Url) -> Self { + let api_key = None; + let client = Client::new(); + Self { http_client: client, base_url, api_key } + } + + /// Sets the API key. + pub fn with_api_key(mut self, api_key: String) -> Self { + self.api_key = Some(api_key); + self + } + + pub async fn get_block(&self, block_id: BlockIdOrTag) -> Result { + self.feeder_gateway("get_block").with_block_id(block_id).send().await + } + + pub async fn get_state_update(&self, block_id: BlockIdOrTag) -> Result { + self.feeder_gateway("get_state_update").with_block_id(block_id).send().await + } + + pub async fn get_state_update_with_block( + &self, + block_id: BlockIdOrTag, + ) -> Result { + self.feeder_gateway("get_state_update") + .add_query_param("includeBlock", "true") + .with_block_id(block_id) + .send() + .await + } + + pub async fn get_class( + &self, + hash: Felt, + block_id: BlockIdOrTag, + ) -> Result { + self.feeder_gateway("get_class_by_hash") + .add_query_param("classHash", &format!("{hash:#x}")) + .with_block_id(block_id) + .send() + .await + } + + pub async fn get_compiled_class( + &self, + hash: Felt, + block_id: BlockIdOrTag, + ) -> Result { + self.feeder_gateway("get_compiled_class_by_class_hash") + .add_query_param("classHash", &format!("{hash:#x}")) + .with_block_id(block_id) + .send() + .await + } + + fn feeder_gateway(&self, method: &str) -> RequestBuilder<'_> { + let mut url = self.base_url.clone(); + url.path_segments_mut().expect("invalid base url").extend(["feeder_gateway", method]); + RequestBuilder { gateway_client: self, url } + } +} + +#[derive(Debug, Deserialize)] +#[serde(untagged)] +enum Response { + Data(T), + Error(SequencerError), +} + +#[derive(Debug, Clone)] +struct RequestBuilder<'a> { + gateway_client: &'a SequencerGateway, + url: Url, +} + +impl<'a> RequestBuilder<'a> { + fn with_block_id(self, block_id: BlockIdOrTag) -> Self { + match block_id { + // latest block is implied, if no block id specified + BlockIdOrTag::Tag(BlockTag::Latest) => self, + BlockIdOrTag::Tag(BlockTag::Pending) => self.add_query_param("blockNumber", "pending"), + BlockIdOrTag::Hash(hash) => self.add_query_param("blockHash", &format!("{hash:#x}")), + BlockIdOrTag::Number(num) => self.add_query_param("blockNumber", &num.to_string()), + } + } + + fn add_query_param(mut self, key: &str, value: &str) -> Self { + self.url.query_pairs_mut().append_pair(key, value); + self + } + + async fn send(self) -> Result { + let mut headers = HeaderMap::new(); + + if let Some(key) = self.gateway_client.api_key.as_ref() { + let value = HeaderValue::from_str(key) + .map_err(|_| Error::InvalidHeaderValue { value: key.to_string() })?; + headers.insert(X_THROTTLING_BYPASS, value); + } + + let response = + self.gateway_client.http_client.get(self.url).headers(headers).send().await?; + + if response.status() == StatusCode::TOO_MANY_REQUESTS { + Err(Error::RateLimited) + } else { + match response.json::>().await? { + Response::Data(data) => Ok(data), + Response::Error(error) => Err(Error::Sequencer(error)), + } + } + } +} + +#[derive(Debug, thiserror::Error, Deserialize)] +#[error("{message} ({code:?})")] +pub struct SequencerError { + pub code: ErrorCode, + pub message: String, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)] +pub enum ErrorCode { + #[serde(rename = "StarknetErrorCode.BLOCK_NOT_FOUND")] + BlockNotFound, + #[serde(rename = "StarknetErrorCode.ENTRY_POINT_NOT_FOUND_IN_CONTRACT")] + EntryPointNotFoundInContract, + #[serde(rename = "StarknetErrorCode.INVALID_PROGRAM")] + InvalidProgram, + #[serde(rename = "StarknetErrorCode.TRANSACTION_FAILED")] + TransactionFailed, + #[serde(rename = "StarknetErrorCode.TRANSACTION_NOT_FOUND")] + TransactionNotFound, + #[serde(rename = "StarknetErrorCode.UNINITIALIZED_CONTRACT")] + UninitializedContract, + #[serde(rename = "StarkErrorCode.MALFORMED_REQUEST")] + MalformedRequest, + #[serde(rename = "StarknetErrorCode.UNDECLARED_CLASS")] + UndeclaredClass, + #[serde(rename = "StarknetErrorCode.INVALID_TRANSACTION_NONCE")] + InvalidTransactionNonce, + #[serde(rename = "StarknetErrorCode.VALIDATE_FAILURE")] + ValidateFailure, + #[serde(rename = "StarknetErrorCode.CLASS_ALREADY_DECLARED")] + ClassAlreadyDeclared, + #[serde(rename = "StarknetErrorCode.COMPILATION_FAILED")] + CompilationFailed, + #[serde(rename = "StarknetErrorCode.INVALID_COMPILED_CLASS_HASH")] + InvalidCompiledClassHash, + #[serde(rename = "StarknetErrorCode.DUPLICATED_TRANSACTION")] + DuplicatedTransaction, + #[serde(rename = "StarknetErrorCode.INVALID_CONTRACT_CLASS")] + InvalidContractClass, + #[serde(rename = "StarknetErrorCode.DEPRECATED_ENDPOINT")] + DeprecatedEndpoint, +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn request_block_id() { + let base_url = Url::parse("https://example.com/").unwrap(); + let client = SequencerGateway::new(base_url); + let req = client.feeder_gateway("test"); + + // Test pending block + let pending_url = req.clone().with_block_id(BlockIdOrTag::Tag(BlockTag::Pending)).url; + assert_eq!(pending_url.query(), Some("blockNumber=pending")); + + // Test block hash + let hash = Felt::from(123); + let hash_url = req.clone().with_block_id(BlockIdOrTag::Hash(hash)).url; + assert_eq!(hash_url.query(), Some("blockHash=0x7b")); + + // Test block number + let num_url = req.clone().with_block_id(BlockIdOrTag::Number(42)).url; + assert_eq!(num_url.query(), Some("blockNumber=42")); + + // Test latest block (should have no query params) + let latest_url = req.with_block_id(BlockIdOrTag::Tag(BlockTag::Latest)).url; + assert_eq!(latest_url.query(), None); + } + + #[test] + fn multiple_query_params() { + let base_url = Url::parse("https://example.com/").unwrap(); + let client = SequencerGateway::new(base_url); + let req = client.feeder_gateway("test"); + + let url = req + .add_query_param("param1", "value1") + .add_query_param("param2", "value2") + .add_query_param("param3", "value3") + .url; + + let query = url.query().unwrap(); + assert!(query.contains("param1=value1")); + assert!(query.contains("param2=value2")); + assert!(query.contains("param3=value3")); + } + + #[test] + #[ignore] + fn request_block_id_overwrite() { + let base_url = Url::parse("https://example.com/").unwrap(); + let client = SequencerGateway::new(base_url); + let req = client.feeder_gateway("test"); + + let url = req + .clone() + .with_block_id(BlockIdOrTag::Tag(BlockTag::Pending)) + .with_block_id(BlockIdOrTag::Number(42)) + .url; + + assert_eq!(url.query(), Some("blockNumber=42")); + + let hash = Felt::from(123); + let url = req + .clone() + .with_block_id(BlockIdOrTag::Hash(hash)) + .with_block_id(BlockIdOrTag::Tag(BlockTag::Pending)) + .url; + + assert_eq!(url.query(), Some("blockNumber=pending")); + } +} diff --git a/crates/katana/feeder-gateway/src/lib.rs b/crates/katana/feeder-gateway/src/lib.rs new file mode 100644 index 0000000000..3048e22ef1 --- /dev/null +++ b/crates/katana/feeder-gateway/src/lib.rs @@ -0,0 +1,4 @@ +#![cfg_attr(not(test), warn(unused_crate_dependencies))] + +pub mod client; +pub mod types; diff --git a/crates/katana/feeder-gateway/src/types/mod.rs b/crates/katana/feeder-gateway/src/types/mod.rs new file mode 100644 index 0000000000..24e5cb18f4 --- /dev/null +++ b/crates/katana/feeder-gateway/src/types/mod.rs @@ -0,0 +1,160 @@ +use std::collections::{BTreeMap, BTreeSet}; + +use katana_primitives::block::{BlockHash, BlockNumber}; +pub use katana_primitives::class::CasmContractClass; +use katana_primitives::class::{ + ClassHash, CompiledClassHash, LegacyContractClass, SierraContractClass, +}; +use katana_primitives::contract::{Nonce, StorageKey, StorageValue}; +use katana_primitives::da::L1DataAvailabilityMode; +use katana_primitives::version::ProtocolVersion; +use katana_primitives::{ContractAddress, Felt}; +use katana_rpc_types::class::ConversionError; +pub use katana_rpc_types::class::RpcSierraContractClass; +use serde::Deserialize; +use starknet::core::types::ResourcePrice; +use starknet::providers::sequencer::models::BlockStatus; + +mod receipt; +mod serde_utils; +mod transaction; + +pub use receipt::*; +pub use transaction::*; + +/// The contract class type returns by `/get_class_by_hash` endpoint. +#[derive(Debug, Deserialize)] +#[serde(untagged)] +pub enum ContractClass { + Class(RpcSierraContractClass), + Legacy(LegacyContractClass), +} + +/// The state update type returns by `/get_state_update` endpoint. +#[derive(Debug, Deserialize)] +pub struct StateUpdate { + pub block_hash: Option, + pub new_root: Option, + pub old_root: Felt, + pub state_diff: StateDiff, +} + +#[derive(Debug, Deserialize)] +pub struct StateDiff { + pub storage_diffs: BTreeMap>, + pub deployed_contracts: Vec, + pub old_declared_contracts: Vec, + pub declared_classes: Vec, + pub nonces: BTreeMap, + pub replaced_classes: Vec, +} + +#[derive(Debug, Deserialize)] +pub struct StorageDiff { + pub key: StorageKey, + pub value: StorageValue, +} + +#[derive(Debug, Deserialize)] +pub struct DeployedContract { + pub address: ContractAddress, + pub class_hash: Felt, +} + +#[derive(Debug, Deserialize)] +pub struct DeclaredContract { + pub class_hash: ClassHash, + pub compiled_class_hash: CompiledClassHash, +} + +/// The state update type returns by `/get_state_update` endpoint, with `includeBlock=true`. +#[derive(Debug, Deserialize)] +pub struct StateUpdateWithBlock { + pub state_update: StateUpdate, + pub block: Block, +} + +// The reason why we're not using the GasPrices from the `katana_primitives` crate is because +// the serde impl is different. So for now, lets just use starknet-rs types. The type isn't +// that complex anyway so the conversion is simple. But if we can use the primitive types, we +// should. +#[derive(Debug, Deserialize)] +pub struct Block { + #[serde(default)] + pub block_hash: Option, + #[serde(default)] + pub block_number: Option, + pub parent_block_hash: BlockHash, + pub timestamp: u64, + pub sequencer_address: Option, + #[serde(default)] + pub state_root: Option, + #[serde(default)] + pub transaction_commitment: Option, + #[serde(default)] + pub event_commitment: Option, + pub status: BlockStatus, + pub l1_da_mode: L1DataAvailabilityMode, + pub l1_gas_price: ResourcePrice, + pub l1_data_gas_price: ResourcePrice, + pub transactions: Vec, + pub transaction_receipts: Vec, + #[serde(default)] + pub starknet_version: Option, +} + +// -- Conversion to Katana primitive types. + +impl TryFrom for katana_primitives::class::ContractClass { + type Error = ConversionError; + + fn try_from(value: ContractClass) -> Result { + match value { + ContractClass::Legacy(class) => Ok(Self::Legacy(class)), + ContractClass::Class(class) => { + let class = SierraContractClass::try_from(class)?; + Ok(Self::Class(class)) + } + } + } +} + +impl From for katana_primitives::state::StateUpdates { + fn from(value: StateDiff) -> Self { + let storage_updates = value + .storage_diffs + .into_iter() + .map(|(addr, diffs)| { + let storage_map = diffs.into_iter().map(|diff| (diff.key, diff.value)).collect(); + (addr, storage_map) + }) + .collect(); + + let deployed_contracts = value + .deployed_contracts + .into_iter() + .map(|contract| (contract.address, contract.class_hash)) + .collect(); + + let declared_classes = value + .declared_classes + .into_iter() + .map(|contract| (contract.class_hash, contract.compiled_class_hash)) + .collect(); + + let replaced_classes = value + .replaced_classes + .into_iter() + .map(|contract| (contract.address, contract.class_hash)) + .collect(); + + Self { + storage_updates, + declared_classes, + replaced_classes, + deployed_contracts, + nonce_updates: value.nonces, + deprecated_declared_classes: BTreeSet::from_iter(value.old_declared_contracts), + } + } +} diff --git a/crates/katana/feeder-gateway/src/types/receipt.rs b/crates/katana/feeder-gateway/src/types/receipt.rs new file mode 100644 index 0000000000..3969b85cf7 --- /dev/null +++ b/crates/katana/feeder-gateway/src/types/receipt.rs @@ -0,0 +1,22 @@ +use katana_primitives::receipt::{Event, MessageToL1}; +use katana_primitives::Felt; +use serde::Deserialize; +use starknet::providers::sequencer::models::{ + ExecutionResources, L1ToL2Message, TransactionExecutionStatus, +}; + +#[derive(Debug, Deserialize)] +pub struct ConfirmedReceipt { + pub transaction_hash: Felt, + pub transaction_index: u64, + #[serde(default)] + pub execution_status: Option, + #[serde(default)] + pub revert_error: Option, + #[serde(default)] + pub execution_resources: Option, + pub l1_to_l2_consumed_message: Option, + pub l2_to_l1_messages: Vec, + pub events: Vec, + pub actual_fee: Felt, +} diff --git a/crates/katana/feeder-gateway/src/types/serde_utils.rs b/crates/katana/feeder-gateway/src/types/serde_utils.rs new file mode 100644 index 0000000000..5741f64992 --- /dev/null +++ b/crates/katana/feeder-gateway/src/types/serde_utils.rs @@ -0,0 +1,106 @@ +use serde::de::Visitor; +use serde::Deserialize; + +pub fn deserialize_u64<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + struct U64HexVisitor; + + impl<'de> Visitor<'de> for U64HexVisitor { + type Value = u64; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "0x-prefix hex string or decimal number") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + if let Some(hex) = v.strip_prefix("0x") { + u64::from_str_radix(hex, 16).map_err(serde::de::Error::custom) + } else { + v.parse::().map_err(serde::de::Error::custom) + } + } + } + + deserializer.deserialize_any(U64HexVisitor) +} + +pub fn deserialize_u128<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + struct U128HexVisitor; + + impl<'de> Visitor<'de> for U128HexVisitor { + type Value = u128; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "0x-prefix hex string or decimal number") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + if let Some(hex) = v.strip_prefix("0x") { + u128::from_str_radix(hex, 16).map_err(serde::de::Error::custom) + } else { + v.parse::().map_err(serde::de::Error::custom) + } + } + } + + deserializer.deserialize_any(U128HexVisitor) +} + +pub fn deserialize_optional_u64<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + #[derive(Deserialize)] + #[serde(untagged)] + enum StringOrNum { + String(String), + Number(u64), + } + + match Option::::deserialize(deserializer)? { + None => Ok(None), + Some(StringOrNum::Number(n)) => Ok(Some(n)), + Some(StringOrNum::String(s)) => { + if let Some(hex) = s.strip_prefix("0x") { + u64::from_str_radix(hex, 16).map(Some).map_err(serde::de::Error::custom) + } else { + s.parse().map(Some).map_err(serde::de::Error::custom) + } + } + } +} + +pub fn deserialize_optional_u128<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + #[derive(Deserialize)] + #[serde(untagged)] + enum StringOrNum { + String(String), + Number(u128), + } + + match Option::::deserialize(deserializer)? { + None => Ok(None), + Some(StringOrNum::Number(n)) => Ok(Some(n)), + Some(StringOrNum::String(s)) => { + if let Some(hex) = s.strip_prefix("0x") { + u128::from_str_radix(hex, 16).map(Some).map_err(serde::de::Error::custom) + } else { + s.parse().map(Some).map_err(serde::de::Error::custom) + } + } + } +} diff --git a/crates/katana/feeder-gateway/src/types/transaction.rs b/crates/katana/feeder-gateway/src/types/transaction.rs new file mode 100644 index 0000000000..bc4f32b15f --- /dev/null +++ b/crates/katana/feeder-gateway/src/types/transaction.rs @@ -0,0 +1,445 @@ +use katana_primitives::class::{ClassHash, CompiledClassHash}; +use katana_primitives::contract::Nonce; +use katana_primitives::transaction::{ + DeclareTx, DeclareTxV0, DeclareTxV1, DeclareTxV2, DeclareTxV3, DeployAccountTx, + DeployAccountTxV1, DeployAccountTxV3, DeployTx, InvokeTx, InvokeTxV0, InvokeTxV1, InvokeTxV3, + L1HandlerTx, Tx, TxHash, TxType, TxWithHash, +}; +use katana_primitives::{ContractAddress, Felt}; +use serde::Deserialize; + +use super::serde_utils::{ + deserialize_optional_u128, deserialize_optional_u64, deserialize_u128, deserialize_u64, +}; + +#[derive(Debug, Deserialize)] +pub struct ConfirmedTransaction { + #[serde(rename = "transaction_hash")] + pub hash: TxHash, + #[serde(flatten)] + pub tx: TypedTransaction, +} + +#[derive(Debug, Deserialize)] +#[serde(tag = "type", rename_all = "SCREAMING_SNAKE_CASE")] +pub enum TypedTransaction { + Deploy(DeployTx), + Declare(RawDeclareTx), + L1Handler(RawL1HandlerTx), + InvokeFunction(RawInvokeTx), + DeployAccount(RawDeployAccountTx), +} + +// We redundantly define the `DataAvailabilityMode` enum here because the serde implementation is +// different from the one in the `katana_primitives` crate. And changing the serde implementation in +// the `katana_primitives` crate would break the database format. So, we have to define the type +// again. But see if we can remove it once we're okay with breaking the database format. +#[derive(Debug)] +pub enum DataAvailabilityMode { + L1, + L2, +} + +impl<'de> Deserialize<'de> for DataAvailabilityMode { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let value = u8::deserialize(deserializer)?; + match value { + 0 => Ok(DataAvailabilityMode::L1), + 1 => Ok(DataAvailabilityMode::L2), + _ => Err(serde::de::Error::custom(format!( + "Invalid data availability mode; expected 0 or 1 but got {value}" + ))), + } + } +} + +// Same reason as `DataAvailabilityMode` above, this struct is also defined because the serde +// implementation of its primitive counterpart is different. +#[derive(Debug, Deserialize)] +pub struct ResourceBounds { + #[serde(deserialize_with = "deserialize_u64")] + pub max_amount: u64, + #[serde(deserialize_with = "deserialize_u128")] + pub max_price_per_unit: u128, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub struct ResourceBoundsMapping { + pub l1_gas: ResourceBounds, + pub l2_gas: ResourceBounds, +} + +#[derive(Debug, Deserialize)] +pub struct RawL1HandlerTx { + /// The L1 to L2 message nonce. + pub nonce: Option, + /// Transaction version. + pub version: Felt, + /// The input to the L1 handler function. + pub calldata: Vec, + /// Contract address of the L1 handler. + pub contract_address: ContractAddress, + /// The L1 handler function selector. + pub entry_point_selector: Felt, +} + +#[derive(Debug, Deserialize)] +pub struct RawInvokeTx { + // Alias for v0 transaction + #[serde(alias = "contract_address")] + pub sender_address: ContractAddress, + // v0 doesn't include nonce + #[serde(default)] + pub nonce: Option, + #[serde(default)] + pub entry_point_selector: Option, + pub calldata: Vec, + pub signature: Vec, + #[serde(default)] + #[serde(deserialize_with = "deserialize_optional_u128")] + pub max_fee: Option, + #[serde(default)] + pub resource_bounds: Option, + #[serde(default)] + #[serde(deserialize_with = "deserialize_optional_u64")] + pub tip: Option, + #[serde(default)] + pub paymaster_data: Option>, + #[serde(default)] + pub account_deployment_data: Option>, + #[serde(default)] + pub nonce_data_availability_mode: Option, + #[serde(default)] + pub fee_data_availability_mode: Option, + pub version: Felt, +} + +#[derive(Debug, Deserialize)] +pub struct RawDeclareTx { + pub sender_address: ContractAddress, + pub nonce: Felt, + pub signature: Vec, + pub class_hash: ClassHash, + pub compiled_class_hash: Option, + #[serde(default)] + #[serde(deserialize_with = "deserialize_optional_u128")] + pub max_fee: Option, + #[serde(default)] + pub resource_bounds: Option, + #[serde(default)] + #[serde(deserialize_with = "deserialize_optional_u64")] + pub tip: Option, + #[serde(default)] + pub paymaster_data: Option>, + #[serde(default)] + pub account_deployment_data: Option>, + #[serde(default)] + pub nonce_data_availability_mode: Option, + #[serde(default)] + pub fee_data_availability_mode: Option, + pub version: Felt, +} + +#[derive(Debug, Deserialize)] +pub struct RawDeployAccountTx { + pub nonce: Nonce, + pub signature: Vec, + pub class_hash: ClassHash, + pub contract_address: Option, + pub contract_address_salt: Felt, + pub constructor_calldata: Vec, + #[serde(default)] + #[serde(deserialize_with = "deserialize_optional_u128")] + pub max_fee: Option, + #[serde(default)] + pub resource_bounds: Option, + #[serde(default)] + #[serde(deserialize_with = "deserialize_optional_u64")] + pub tip: Option, + #[serde(default)] + pub paymaster_data: Option>, + #[serde(default)] + pub nonce_data_availability_mode: Option, + #[serde(default)] + pub fee_data_availability_mode: Option, + pub version: Felt, +} + +#[derive(Debug, thiserror::Error)] +pub enum TxTryFromError { + #[error("unsupported transaction version; type: {r#type:?}, version: {version:#x}")] + UnsupportedVersion { r#type: TxType, version: Felt }, + + #[error("missing `tip`")] + MissingTip, + + #[error("missing `paymaster_data`")] + MissingPaymasterData, + + #[error("missing `entry_point_selector`")] + MissingEntryPointSelector, + + #[error("missing `nonce`")] + MissingNonce, + + #[error("missing `max_fee`")] + MissingMaxFee, + + #[error("missing `resource_bounds`")] + MissingResourceBounds, + + #[error("missing `account_deployment_data`")] + MissingAccountDeploymentData, + + #[error("missing nonce `data_availability_mode`")] + MissingNonceDA, + + #[error("missing fee `data_availability_mode`")] + MissingFeeDA, + + #[error("missing `compiled_class_hash`")] + MissingCompiledClassHash, +} + +// -- Conversion to Katana primitive types. + +impl TryFrom for TxWithHash { + type Error = TxTryFromError; + + fn try_from(tx: ConfirmedTransaction) -> Result { + let transaction = match tx.tx { + TypedTransaction::Deploy(tx) => Tx::Deploy(tx), + TypedTransaction::Declare(tx) => Tx::Declare(DeclareTx::try_from(tx)?), + TypedTransaction::L1Handler(tx) => Tx::L1Handler(L1HandlerTx::from(tx)), + TypedTransaction::InvokeFunction(tx) => Tx::Invoke(InvokeTx::try_from(tx)?), + TypedTransaction::DeployAccount(tx) => { + Tx::DeployAccount(DeployAccountTx::try_from(tx)?) + } + }; + + Ok(TxWithHash { hash: tx.hash, transaction }) + } +} + +impl TryFrom for InvokeTx { + type Error = TxTryFromError; + + fn try_from(value: RawInvokeTx) -> Result { + if Felt::ZERO == value.version { + Ok(InvokeTx::V0(InvokeTxV0 { + calldata: value.calldata, + signature: value.signature, + contract_address: value.sender_address, + max_fee: value.max_fee.ok_or(TxTryFromError::MissingMaxFee)?, + entry_point_selector: value + .entry_point_selector + .ok_or(TxTryFromError::MissingEntryPointSelector)?, + })) + } else if Felt::ONE == value.version { + Ok(InvokeTx::V1(InvokeTxV1 { + chain_id: Default::default(), + nonce: value.nonce.ok_or(TxTryFromError::MissingNonce)?, + calldata: value.calldata, + signature: value.signature, + max_fee: value.max_fee.ok_or(TxTryFromError::MissingMaxFee)?, + sender_address: value.sender_address, + })) + } else if Felt::THREE == value.version { + let tip = value.tip.ok_or(TxTryFromError::MissingTip)?; + let paymaster_data = + value.paymaster_data.ok_or(TxTryFromError::MissingPaymasterData)?; + let resource_bounds = + value.resource_bounds.ok_or(TxTryFromError::MissingResourceBounds)?; + let account_deployment_data = value + .account_deployment_data + .ok_or(TxTryFromError::MissingAccountDeploymentData)?; + let nonce_data_availability_mode = + value.nonce_data_availability_mode.ok_or(TxTryFromError::MissingNonceDA)?; + let fee_data_availability_mode = + value.fee_data_availability_mode.ok_or(TxTryFromError::MissingFeeDA)?; + + Ok(InvokeTx::V3(InvokeTxV3 { + tip, + paymaster_data, + chain_id: Default::default(), + nonce: value.nonce.ok_or(TxTryFromError::MissingNonce)?, + calldata: value.calldata, + signature: value.signature, + sender_address: value.sender_address, + resource_bounds: resource_bounds.into(), + account_deployment_data, + fee_data_availability_mode: fee_data_availability_mode.into(), + nonce_data_availability_mode: nonce_data_availability_mode.into(), + })) + } else { + Err(TxTryFromError::UnsupportedVersion { + r#type: TxType::Invoke, + version: value.version, + }) + } + } +} + +impl TryFrom for DeclareTx { + type Error = TxTryFromError; + + fn try_from(value: RawDeclareTx) -> Result { + if Felt::ZERO == value.version { + Ok(DeclareTx::V0(DeclareTxV0 { + signature: value.signature, + chain_id: Default::default(), + class_hash: value.class_hash, + sender_address: value.sender_address, + max_fee: value.max_fee.ok_or(TxTryFromError::MissingMaxFee)?, + })) + } else if Felt::ONE == value.version { + Ok(DeclareTx::V1(DeclareTxV1 { + chain_id: Default::default(), + sender_address: value.sender_address, + nonce: value.nonce, + signature: value.signature, + class_hash: value.class_hash, + max_fee: value.max_fee.ok_or(TxTryFromError::MissingMaxFee)?, + })) + } else if Felt::TWO == value.version { + Ok(DeclareTx::V2(DeclareTxV2 { + chain_id: Default::default(), + sender_address: value.sender_address, + nonce: value.nonce, + signature: value.signature, + class_hash: value.class_hash, + compiled_class_hash: value + .compiled_class_hash + .ok_or(TxTryFromError::MissingCompiledClassHash)?, + max_fee: value.max_fee.ok_or(TxTryFromError::MissingMaxFee)?, + })) + } else if Felt::THREE == value.version { + let resource_bounds = + value.resource_bounds.ok_or(TxTryFromError::MissingResourceBounds)?; + let tip = value.tip.ok_or(TxTryFromError::MissingTip)?; + let paymaster_data = + value.paymaster_data.ok_or(TxTryFromError::MissingPaymasterData)?; + let account_deployment_data = value + .account_deployment_data + .ok_or(TxTryFromError::MissingAccountDeploymentData)?; + let nonce_data_availability_mode = + value.nonce_data_availability_mode.ok_or(TxTryFromError::MissingNonceDA)?; + let fee_data_availability_mode = + value.fee_data_availability_mode.ok_or(TxTryFromError::MissingFeeDA)?; + let compiled_class_hash = + value.compiled_class_hash.ok_or(TxTryFromError::MissingCompiledClassHash)?; + + Ok(DeclareTx::V3(DeclareTxV3 { + chain_id: Default::default(), + sender_address: value.sender_address, + nonce: value.nonce, + signature: value.signature, + class_hash: value.class_hash, + compiled_class_hash, + resource_bounds: resource_bounds.into(), + tip, + paymaster_data, + account_deployment_data, + nonce_data_availability_mode: nonce_data_availability_mode.into(), + fee_data_availability_mode: fee_data_availability_mode.into(), + })) + } else { + Err(TxTryFromError::UnsupportedVersion { + r#type: TxType::Declare, + version: value.version, + }) + } + } +} + +impl TryFrom for DeployAccountTx { + type Error = TxTryFromError; + + fn try_from(value: RawDeployAccountTx) -> Result { + if Felt::ONE == value.version { + Ok(DeployAccountTx::V1(DeployAccountTxV1 { + chain_id: Default::default(), + nonce: value.nonce, + signature: value.signature, + class_hash: value.class_hash, + contract_address: value.contract_address.unwrap_or_default(), + contract_address_salt: value.contract_address_salt, + constructor_calldata: value.constructor_calldata, + max_fee: value.max_fee.ok_or(TxTryFromError::MissingMaxFee)?, + })) + } else if Felt::THREE == value.version { + let resource_bounds = + value.resource_bounds.ok_or(TxTryFromError::MissingResourceBounds)?; + let tip = value.tip.ok_or(TxTryFromError::MissingTip)?; + let paymaster_data = + value.paymaster_data.ok_or(TxTryFromError::MissingPaymasterData)?; + let nonce_data_availability_mode = + value.nonce_data_availability_mode.ok_or(TxTryFromError::MissingNonceDA)?; + let fee_data_availability_mode = + value.fee_data_availability_mode.ok_or(TxTryFromError::MissingFeeDA)?; + + Ok(DeployAccountTx::V3(DeployAccountTxV3 { + chain_id: Default::default(), + nonce: value.nonce, + signature: value.signature, + class_hash: value.class_hash, + contract_address: value.contract_address.unwrap_or_default(), + contract_address_salt: value.contract_address_salt, + constructor_calldata: value.constructor_calldata, + resource_bounds: resource_bounds.into(), + tip, + paymaster_data, + nonce_data_availability_mode: nonce_data_availability_mode.into(), + fee_data_availability_mode: fee_data_availability_mode.into(), + })) + } else { + Err(TxTryFromError::UnsupportedVersion { + r#type: TxType::DeployAccount, + version: value.version, + }) + } + } +} + +impl From for L1HandlerTx { + fn from(value: RawL1HandlerTx) -> Self { + Self { + version: value.version, + calldata: value.calldata, + chain_id: Default::default(), + message_hash: Default::default(), + paid_fee_on_l1: Default::default(), + nonce: value.nonce.unwrap_or_default(), + contract_address: value.contract_address, + entry_point_selector: value.entry_point_selector, + } + } +} + +impl From for katana_primitives::da::DataAvailabilityMode { + fn from(mode: DataAvailabilityMode) -> Self { + match mode { + DataAvailabilityMode::L1 => Self::L1, + DataAvailabilityMode::L2 => Self::L2, + } + } +} + +impl From for katana_primitives::fee::ResourceBoundsMapping { + fn from(bounds: ResourceBoundsMapping) -> Self { + Self { + l1_gas: katana_primitives::fee::ResourceBounds { + max_amount: bounds.l1_gas.max_amount, + max_price_per_unit: bounds.l1_gas.max_price_per_unit, + }, + l2_gas: katana_primitives::fee::ResourceBounds { + max_amount: bounds.l2_gas.max_amount, + max_price_per_unit: bounds.l2_gas.max_price_per_unit, + }, + } + } +} diff --git a/crates/katana/node/Cargo.toml b/crates/katana/node/Cargo.toml index fd55d0f9c7..aa47c61777 100644 --- a/crates/katana/node/Cargo.toml +++ b/crates/katana/node/Cargo.toml @@ -14,6 +14,7 @@ katana-pool.workspace = true katana-primitives.workspace = true katana-rpc.workspace = true katana-rpc-api.workspace = true +katana-stage.workspace = true katana-tasks.workspace = true anyhow.workspace = true @@ -21,15 +22,26 @@ dojo-metrics.workspace = true futures.workspace = true hyper.workspace = true jsonrpsee.workspace = true +serde.workspace = true serde_json.workspace = true starknet.workspace = true +thiserror.workspace = true tower = { workspace = true, features = [ "full" ] } tower-http = { workspace = true, features = [ "full" ] } tracing.workspace = true +url.workspace = true +const_format = "0.2.33" strum.workspace = true strum_macros.workspace = true -const_format = "0.2.33" + +clap = { workspace = true, optional = true } +dojo-utils = { workspace = true, optional = true } +katana-feeder-gateway = { workspace = true, optional = true } +katana-provider = { workspace = true, optional = true } +tokio = { workspace = true, features = [ "time" ], optional = true } +tracing-log = { workspace = true, optional = true } +tracing-subscriber = { workspace = true, optional = true } [build-dependencies] vergen = { version = "9.0.0", features = [ "build", "cargo", "emit_and_set" ] } @@ -37,3 +49,16 @@ vergen-gitcl = { version = "1.0.0", features = [ "build", "cargo", "rustc", "si" [features] starknet-messaging = [ "katana-core/starknet-messaging" ] +# experimental feature to test katana full node mode +full-node = [ "dep:katana-feeder-gateway", "dep:katana-provider", "dep:tokio" ] + +[[bin]] +name = "full-node" +path = "src/full/node.rs" +required-features = [ + "clap", + "dojo-utils", + "full-node", + "tracing-log", + "tracing-subscriber", +] diff --git a/crates/katana/node/src/config/mod.rs b/crates/katana/node/src/config/mod.rs index b79ae25a97..045e30bdee 100644 --- a/crates/katana/node/src/config/mod.rs +++ b/crates/katana/node/src/config/mod.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + pub mod db; pub mod dev; pub mod execution; @@ -13,6 +15,7 @@ use katana_core::service::messaging::MessagingConfig; use katana_primitives::chain_spec::ChainSpec; use metrics::MetricsConfig; use rpc::RpcConfig; +use url::Url; /// Node configurations. /// @@ -20,7 +23,7 @@ use rpc::RpcConfig; #[derive(Debug, Clone, Default)] pub struct Config { /// The chain specification. - pub chain: ChainSpec, + pub chain: Arc, /// Database options. pub db: DbConfig, @@ -45,6 +48,9 @@ pub struct Config { /// Development options. pub dev: DevConfig, + + /// Provider url for gas price oracle + pub l1_provider_url: Option, } /// Configurations related to block production. diff --git a/crates/katana/node/src/config/rpc.rs b/crates/katana/node/src/config/rpc.rs index bda2b90d8b..9093c8ad0f 100644 --- a/crates/katana/node/src/config/rpc.rs +++ b/crates/katana/node/src/config/rpc.rs @@ -1,6 +1,9 @@ use std::collections::HashSet; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use katana_rpc::cors::HeaderValue; +use serde::{Deserialize, Serialize}; + /// The default maximum number of concurrent RPC connections. pub const DEFAULT_RPC_MAX_CONNECTIONS: u32 = 100; pub const DEFAULT_RPC_ADDR: IpAddr = IpAddr::V4(Ipv4Addr::LOCALHOST); @@ -8,16 +11,28 @@ pub const DEFAULT_RPC_PORT: u16 = 5050; /// Default maximmum page size for the `starknet_getEvents` RPC method. pub const DEFAULT_RPC_MAX_EVENT_PAGE_SIZE: u64 = 1024; +/// Default maximmum number of keys for the `starknet_getStorageProof` RPC method. +pub const DEFAULT_RPC_MAX_PROOF_KEYS: u64 = 100; -/// List of APIs supported by Katana. +/// List of RPC modules supported by Katana. #[derive( - Debug, Copy, Clone, PartialEq, Eq, Hash, strum_macros::EnumString, strum_macros::Display, + Debug, + Copy, + Clone, + PartialEq, + Eq, + Hash, + strum_macros::EnumString, + strum_macros::Display, + Serialize, + Deserialize, )] -pub enum ApiKind { +#[strum(ascii_case_insensitive)] +pub enum RpcModuleKind { Starknet, Torii, - Dev, Saya, + Dev, } /// Configuration for the RPC server. @@ -26,9 +41,10 @@ pub struct RpcConfig { pub addr: IpAddr, pub port: u16, pub max_connections: u32, - pub apis: HashSet, + pub apis: RpcModulesList, + pub cors_origins: Vec, pub max_event_page_size: Option, - pub cors_origins: Option>, + pub max_proof_keys: Option, } impl RpcConfig { @@ -41,12 +57,129 @@ impl RpcConfig { impl Default for RpcConfig { fn default() -> Self { Self { - cors_origins: None, + cors_origins: Vec::new(), addr: DEFAULT_RPC_ADDR, port: DEFAULT_RPC_PORT, + apis: RpcModulesList::default(), max_connections: DEFAULT_RPC_MAX_CONNECTIONS, - apis: HashSet::from([ApiKind::Starknet]), max_event_page_size: Some(DEFAULT_RPC_MAX_EVENT_PAGE_SIZE), + max_proof_keys: Some(DEFAULT_RPC_MAX_PROOF_KEYS), + } + } +} + +#[derive(Debug, thiserror::Error)] +#[error("invalid module: {0}")] +pub struct InvalidRpcModuleError(String); + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] +#[serde(transparent)] +pub struct RpcModulesList(HashSet); + +impl RpcModulesList { + /// Creates an empty modules list. + pub fn new() -> Self { + Self(HashSet::new()) + } + + /// Creates a list with all the possible modules. + pub fn all() -> Self { + Self(HashSet::from([ + RpcModuleKind::Starknet, + RpcModuleKind::Torii, + RpcModuleKind::Saya, + RpcModuleKind::Dev, + ])) + } + + /// Adds a `module` to the list. + pub fn add(&mut self, module: RpcModuleKind) { + self.0.insert(module); + } + + /// Returns `true` if the list contains the specified `module`. + pub fn contains(&self, module: &RpcModuleKind) -> bool { + self.0.contains(module) + } + + /// Returns the number of modules in the list. + pub fn len(&self) -> usize { + self.0.len() + } + + /// Returns `true` if the list contains no modules. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Used as the value parser for `clap`. + pub fn parse(value: &str) -> Result { + if value.is_empty() { + return Ok(Self::new()); } + + let mut modules = HashSet::new(); + for module_str in value.split(',') { + let module: RpcModuleKind = module_str + .trim() + .parse() + .map_err(|_| InvalidRpcModuleError(module_str.to_string()))?; + + modules.insert(module); + } + + Ok(Self(modules)) + } +} + +impl Default for RpcModulesList { + fn default() -> Self { + Self(HashSet::from([RpcModuleKind::Starknet])) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_empty() { + let list = RpcModulesList::parse("").unwrap(); + assert_eq!(list, RpcModulesList::new()); + } + + #[test] + fn test_parse_single() { + let list = RpcModulesList::parse("dev").unwrap(); + assert!(list.contains(&RpcModuleKind::Dev)); + } + + #[test] + fn test_parse_multiple() { + let list = RpcModulesList::parse("dev,torii,saya").unwrap(); + assert!(list.contains(&RpcModuleKind::Dev)); + assert!(list.contains(&RpcModuleKind::Torii)); + assert!(list.contains(&RpcModuleKind::Saya)); + } + + #[test] + fn test_parse_with_spaces() { + let list = RpcModulesList::parse(" dev , torii ").unwrap(); + assert!(list.contains(&RpcModuleKind::Dev)); + assert!(list.contains(&RpcModuleKind::Torii)); + } + + #[test] + fn test_parse_duplicates() { + let list = RpcModulesList::parse("dev,dev,torii").unwrap(); + let mut expected = RpcModulesList::new(); + expected.add(RpcModuleKind::Dev); + expected.add(RpcModuleKind::Torii); + assert_eq!(list, expected); + } + + #[test] + fn test_parse_invalid() { + assert!(RpcModulesList::parse("invalid").is_err()); } } diff --git a/crates/katana/node/src/exit.rs b/crates/katana/node/src/exit.rs index de1e42c9f3..ec87e010ca 100644 --- a/crates/katana/node/src/exit.rs +++ b/crates/katana/node/src/exit.rs @@ -18,7 +18,7 @@ impl<'a> NodeStoppedFuture<'a> { pub(crate) fn new(handle: &'a LaunchedNode) -> Self { let fut = Box::pin(async { handle.node.task_manager.wait_for_shutdown().await; - handle.stop().await?; + handle.rpc.stop()?; Ok(()) }); Self { fut } diff --git a/crates/katana/node/src/full/exit.rs b/crates/katana/node/src/full/exit.rs new file mode 100644 index 0000000000..2ec7936c47 --- /dev/null +++ b/crates/katana/node/src/full/exit.rs @@ -0,0 +1,41 @@ +use std::future::Future; +use std::pin::Pin; +use std::task::{Context, Poll}; + +use anyhow::Result; +use futures::future::BoxFuture; +use futures::FutureExt; + +use crate::full::LaunchedNode; + +/// A Future that is resolved once the node has been stopped including all of its running tasks. +#[must_use = "futures do nothing unless polled"] +pub struct NodeStoppedFuture<'a> { + fut: BoxFuture<'a, Result<()>>, +} + +impl<'a> NodeStoppedFuture<'a> { + pub(crate) fn new(handle: &'a LaunchedNode) -> Self { + let fut = Box::pin(async { + handle.task_manager.wait_for_shutdown().await; + handle.stop().await?; + Ok(()) + }); + Self { fut } + } +} + +impl<'a> Future for NodeStoppedFuture<'a> { + type Output = Result<()>; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.get_mut(); + this.fut.poll_unpin(cx) + } +} + +impl<'a> core::fmt::Debug for NodeStoppedFuture<'a> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("NodeStoppedFuture").field("fut", &"...").finish() + } +} diff --git a/crates/katana/node/src/full/mod.rs b/crates/katana/node/src/full/mod.rs new file mode 100644 index 0000000000..a6782abc17 --- /dev/null +++ b/crates/katana/node/src/full/mod.rs @@ -0,0 +1,153 @@ +//! Experimental full node implementation. + +mod exit; +mod tip_watcher; + +use std::future::IntoFuture; +use std::sync::Arc; + +use anyhow::Result; +use dojo_metrics::exporters::prometheus::PrometheusRecorder; +use dojo_metrics::{Report, Server as MetricsServer}; +use exit::NodeStoppedFuture; +use katana_db::mdbx::DbEnv; +use katana_feeder_gateway::client::SequencerGateway; +use katana_pipeline::{Pipeline, PipelineHandle}; +use katana_pool::ordering::FiFo; +use katana_pool::pool::Pool; +use katana_pool::validation::NoopValidator; +use katana_primitives::transaction::ExecutableTxWithHash; +use katana_provider::providers::db::DbProvider; +use katana_stage::{Blocks, Classes}; +use katana_tasks::TaskManager; +use tip_watcher::ChainTipWatcher; +use tracing::info; + +use crate::config::db::DbConfig; +use crate::config::metrics::MetricsConfig; + +type TxPool = + Pool, FiFo>; + +#[derive(Debug)] +pub struct Config { + pub db: DbConfig, + pub metrics: Option, + pub gateway_api_key: Option, +} + +#[derive(Debug)] +pub struct Node { + pub db: DbEnv, + pub pool: TxPool, + pub config: Arc, + pub task_manager: TaskManager, + pub pipeline: Pipeline, +} + +impl Node { + pub fn build(config: Config) -> Result { + if config.metrics.is_some() { + // Metrics recorder must be initialized before calling any of the metrics macros, in + // order for it to be registered. + let _ = PrometheusRecorder::install("katana")?; + } + + // -- build task manager + + let task_manager = TaskManager::current(); + + // -- build db and storage provider + + let path = config.db.dir.clone().expect("database path must exist"); + + info!(target: "node", path = %path.display(), "Initializing database."); + let db = katana_db::init_db(path)?; + + let provider = DbProvider::new(db.clone()); + + // --- build transaction pool + + let pool = TxPool::new(NoopValidator::new(), FiFo::new()); + + // --- build pipeline + + let fgw = if let Some(ref key) = config.gateway_api_key { + SequencerGateway::sn_sepolia().with_api_key(key.clone()) + } else { + SequencerGateway::sn_sepolia() + }; + + let (mut pipeline, _) = Pipeline::new(provider.clone(), 64); + pipeline.add_stage(Blocks::new(provider.clone(), fgw.clone(), 3)); + pipeline.add_stage(Classes::new(provider, fgw.clone(), 3)); + + let node = Node { pool, config: Arc::new(config), task_manager, pipeline, db }; + + Ok(node) + } + + pub fn launch(self) -> Result { + if let Some(ref cfg) = self.config.metrics { + let reports: Vec> = vec![Box::new(self.db.clone()) as Box]; + let exporter = PrometheusRecorder::current().expect("qed; should exist at this point"); + + let addr = cfg.socket_addr(); + let server = MetricsServer::new(exporter).with_process_metrics().with_reports(reports); + self.task_manager.task_spawner().build_task().spawn(server.start(addr)); + + info!(%addr, "Metrics server started."); + } + + let fgw = if let Some(key) = self.config.gateway_api_key.as_ref() { + SequencerGateway::sn_sepolia().with_api_key(key.clone()) + } else { + SequencerGateway::sn_sepolia() + }; + + let pipeline_handle = self.pipeline.handle(); + let tip_watcher = ChainTipWatcher::new(fgw, pipeline_handle.clone()); + + self.task_manager + .task_spawner() + .build_task() + .critical() + .name("Chain tip watcher") + .spawn(tip_watcher.into_future()); + + self.task_manager + .task_spawner() + .build_task() + .critical() + .name("Pipeline") + .spawn(self.pipeline.into_future()); + + Ok(LaunchedNode { + db: self.db, + pipeline_handle, + pool: self.pool, + config: self.config, + task_manager: self.task_manager, + }) + } +} + +#[derive(Debug)] +pub struct LaunchedNode { + pub db: DbEnv, + pub pool: TxPool, + pub task_manager: TaskManager, + pub config: Arc, + pub pipeline_handle: PipelineHandle, +} + +impl LaunchedNode { + pub async fn stop(&self) -> Result<()> { + self.task_manager.shutdown().await; + Ok(()) + } + + pub fn stopped(&self) -> NodeStoppedFuture<'_> { + NodeStoppedFuture::new(self) + } +} diff --git a/crates/katana/node/src/full/node.rs b/crates/katana/node/src/full/node.rs new file mode 100644 index 0000000000..f0e000996c --- /dev/null +++ b/crates/katana/node/src/full/node.rs @@ -0,0 +1,89 @@ +use std::net::IpAddr; +use std::path::PathBuf; + +use anyhow::Result; +use clap::{Args, Parser}; +use katana_node::config::db::DbConfig; +use katana_node::config::metrics::{DEFAULT_METRICS_ADDR, DEFAULT_METRICS_PORT}; +use katana_node::full::{Config, Node}; + +#[derive(Debug, Args, Clone, PartialEq)] +#[command(next_help_heading = "Metrics options")] +pub struct MetricsOptions { + /// Enable metrics. + /// + /// For now, metrics will still be collected even if this flag is not set. This only + /// controls whether the metrics server is started or not. + #[arg(long)] + pub metrics: bool, + + /// The metrics will be served at the given address. + #[arg(requires = "metrics")] + #[arg(long = "metrics.addr", value_name = "ADDRESS")] + #[arg(default_value_t = DEFAULT_METRICS_ADDR)] + pub metrics_addr: IpAddr, + + /// The metrics will be served at the given port. + #[arg(requires = "metrics")] + #[arg(long = "metrics.port", value_name = "PORT")] + #[arg(default_value_t = DEFAULT_METRICS_PORT)] + pub metrics_port: u16, +} + +#[derive(Debug, Parser)] +pub struct Cli { + #[arg(long)] + #[arg(value_name = "PATH")] + db_dir: PathBuf, + + #[arg(long)] + #[arg(value_name = "API_KEY")] + gateway_api_key: Option, + + #[command(flatten)] + metrics: MetricsOptions, +} + +fn init_logging() -> Result<()> { + use tracing::subscriber::set_global_default; + use tracing_log::LogTracer; + use tracing_subscriber::{fmt, EnvFilter}; + + const DEFAULT_LOG_FILTER: &str = "pipeline=debug,stage=debug,info,tasks=debug,executor=trace,\ + forking::backend=trace,blockifier=off,jsonrpsee_server=off,\ + hyper=off,messaging=debug"; + + LogTracer::init()?; + + let filter = EnvFilter::try_from_default_env().or(EnvFilter::try_new(DEFAULT_LOG_FILTER))?; + let subscriber = fmt::Subscriber::builder().with_env_filter(filter).finish(); + set_global_default(subscriber)?; + + Ok(()) +} + +#[tokio::main] +async fn main() -> Result<()> { + init_logging()?; + + let cli = Cli::parse(); + + let config = Config { + metrics: None, + gateway_api_key: cli.gateway_api_key, + db: DbConfig { dir: Some(cli.db_dir) }, + }; + + let node = Node::build(config)?.launch()?; + + tokio::select! { + _ = dojo_utils::signal::wait_signals() => { + // Gracefully shutdown the node before exiting + node.stop().await?; + }, + + _ = node.stopped() => { } + } + + Ok(()) +} diff --git a/crates/katana/node/src/full/tip_watcher.rs b/crates/katana/node/src/full/tip_watcher.rs new file mode 100644 index 0000000000..0c265df27b --- /dev/null +++ b/crates/katana/node/src/full/tip_watcher.rs @@ -0,0 +1,61 @@ +use std::future::IntoFuture; +use std::time::Duration; + +use anyhow::Result; +use futures::future::BoxFuture; +use katana_feeder_gateway::client::SequencerGateway; +use katana_pipeline::PipelineHandle; +use katana_primitives::block::{BlockIdOrTag, BlockTag}; +use tracing::{error, info, trace}; + +type TipWatcherFut = BoxFuture<'static, Result<()>>; + +#[derive(Debug)] +pub struct ChainTipWatcher { + /// The feeder gateway client for fetching the latest block. + client: SequencerGateway, + /// The pipeline handle for setting the tip. + pipeline_handle: PipelineHandle, + /// Interval for checking the new tip. + watch_interval: Duration, +} + +impl ChainTipWatcher { + pub fn new(client: SequencerGateway, pipeline_handle: PipelineHandle) -> Self { + let watch_interval = Duration::from_secs(30); + Self { client, pipeline_handle, watch_interval } + } + + pub async fn run(&self) -> Result<()> { + let interval_in_secs = self.watch_interval.as_secs(); + info!(target: "node", interval = %interval_in_secs, "Chain tip watcher started."); + + let mut prev_tip = 0; + + loop { + let block = self.client.get_block(BlockIdOrTag::Tag(BlockTag::Latest)).await?; + let block_number = block.block_number.expect("must exist for latest block"); + + if prev_tip != block_number { + trace!(target: "node", block = %block_number, "New tip received"); + self.pipeline_handle.set_tip(block_number); + prev_tip = block_number; + } + + tokio::time::sleep(self.watch_interval).await; + } + } +} + +impl IntoFuture for ChainTipWatcher { + type Output = Result<()>; + type IntoFuture = TipWatcherFut; + + fn into_future(self) -> Self::IntoFuture { + Box::pin(async move { + self.run().await.inspect_err(|error| { + error!(target: "pipeline", %error, "Tip watcher failed."); + }) + }) + } +} diff --git a/crates/katana/node/src/lib.rs b/crates/katana/node/src/lib.rs index e2037730a2..bebddacae0 100644 --- a/crates/katana/node/src/lib.rs +++ b/crates/katana/node/src/lib.rs @@ -1,23 +1,21 @@ -#![cfg_attr(not(test), warn(unused_crate_dependencies))] +// #![cfg_attr(not(test), warn(unused_crate_dependencies))] + +#[cfg(feature = "full-node")] +pub mod full; pub mod config; pub mod exit; pub mod version; use std::future::IntoFuture; -use std::net::SocketAddr; use std::sync::Arc; -use std::time::Duration; use anyhow::Result; -use config::metrics::MetricsConfig; -use config::rpc::{ApiKind, RpcConfig}; -use config::{Config, SequencingConfig}; +use config::rpc::RpcModuleKind; +use config::Config; use dojo_metrics::exporters::prometheus::PrometheusRecorder; use dojo_metrics::{Report, Server as MetricsServer}; -use hyper::{Method, Uri}; -use jsonrpsee::server::middleware::proxy_get_request::ProxyGetRequestLayer; -use jsonrpsee::server::{AllowHosts, ServerBuilder, ServerHandle}; +use hyper::Method; use jsonrpsee::RpcModule; use katana_core::backend::gas_oracle::L1GasOracle; use katana_core::backend::storage::Blockchain; @@ -28,28 +26,26 @@ use katana_core::constants::{ }; use katana_core::env::BlockContextGenerator; use katana_core::service::block_producer::BlockProducer; -use katana_core::service::messaging::MessagingConfig; use katana_db::mdbx::DbEnv; use katana_executor::implementation::blockifier::BlockifierFactory; -use katana_executor::{ExecutionFlags, ExecutorFactory}; -use katana_pipeline::{stage, Pipeline}; +use katana_executor::ExecutionFlags; use katana_pool::ordering::FiFo; -use katana_pool::validation::stateful::TxValidator; use katana_pool::TxPool; use katana_primitives::block::GasPrices; use katana_primitives::env::{CfgEnv, FeeTokenAddressses}; +use katana_rpc::cors::Cors; use katana_rpc::dev::DevApi; -use katana_rpc::metrics::RpcServerMetrics; use katana_rpc::saya::SayaApi; use katana_rpc::starknet::forking::ForkedClient; use katana_rpc::starknet::{StarknetApi, StarknetApiConfig}; use katana_rpc::torii::ToriiApi; +use katana_rpc::{RpcServer, RpcServerHandle}; use katana_rpc_api::dev::DevApiServer; use katana_rpc_api::saya::SayaApiServer; use katana_rpc_api::starknet::{StarknetApiServer, StarknetTraceApiServer, StarknetWriteApiServer}; use katana_rpc_api::torii::ToriiApiServer; +use katana_stage::Sequencing; use katana_tasks::TaskManager; -use tower_http::cors::{AllowOrigin, CorsLayer}; use tracing::info; use crate::exit::NodeStoppedFuture; @@ -59,7 +55,7 @@ use crate::exit::NodeStoppedFuture; pub struct LaunchedNode { pub node: Node, /// Handle to the rpc server. - pub rpc: RpcServer, + pub rpc: RpcServerHandle, } impl LaunchedNode { @@ -68,7 +64,7 @@ impl LaunchedNode { /// This will instruct the node to stop and wait until it has actually stop. pub async fn stop(&self) -> Result<()> { // TODO: wait for the rpc server to stop instead of just stopping it. - self.rpc.handle.stop()?; + self.rpc.stop()?; self.node.task_manager.shutdown().await; Ok(()) } @@ -87,26 +83,23 @@ impl LaunchedNode { pub struct Node { pub pool: TxPool, pub db: Option, + pub rpc_server: RpcServer, pub task_manager: TaskManager, pub backend: Arc>, pub block_producer: BlockProducer, - pub rpc_config: RpcConfig, - pub metrics_config: Option, - pub sequencing_config: SequencingConfig, - pub messaging_config: Option, - forked_client: Option, + pub config: Arc, } impl Node { /// Start the node. /// /// This method will start all the node process, running them until the node is stopped. - pub async fn launch(mut self) -> Result { + pub async fn launch(self) -> Result { let chain = self.backend.chain_spec.id; info!(%chain, "Starting node."); // TODO: maybe move this to the build stage - if let Some(ref cfg) = self.metrics_config { + if let Some(ref cfg) = self.config.metrics { let addr = cfg.socket_addr(); let mut reports: Vec> = Vec::new(); @@ -124,34 +117,36 @@ impl Node { let pool = self.pool.clone(); let backend = self.backend.clone(); let block_producer = self.block_producer.clone(); - let validator = self.block_producer.validator().clone(); - // --- build sequencing stage + // --- build and run sequencing task - let sequencing = stage::Sequencing::new( + let sequencing = Sequencing::new( pool.clone(), backend.clone(), self.task_manager.task_spawner(), block_producer.clone(), - self.messaging_config.clone(), + self.config.messaging.clone(), ); - // --- build and start the pipeline - - let mut pipeline = Pipeline::new(); - pipeline.add_stage(Box::new(sequencing)); - self.task_manager .task_spawner() .build_task() .critical() - .name("Pipeline") - .spawn(pipeline.into_future()); + .name("Sequencing") + .spawn(sequencing.into_future()); + + // --- start the rpc server - let node_components = (pool, backend, block_producer, validator, self.forked_client.take()); - let rpc = spawn(node_components, self.rpc_config.clone()).await?; + let rpc_handle = self.rpc_server.start(self.config.rpc.socket_addr()).await?; + + // --- start the gas oracle worker task + + if let Some(ref url) = self.config.l1_provider_url { + self.backend.gas_oracle.run_worker(self.task_manager.task_spawner()); + info!(%url, "Gas Price Oracle started."); + }; - Ok(LaunchedNode { node: self, rpc }) + Ok(LaunchedNode { node: self, rpc: rpc_handle }) } } @@ -188,8 +183,9 @@ pub async fn build(mut config: Config) -> Result { // --- build backend let (blockchain, db, forked_client) = if let Some(cfg) = &config.forking { + let chain_spec = Arc::get_mut(&mut config.chain).expect("get mut Arc"); let (bc, block_num) = - Blockchain::new_from_forked(cfg.url.clone(), cfg.block, &mut config.chain).await?; + Blockchain::new_from_forked(cfg.url.clone(), cfg.block, chain_spec).await?; // TODO: it'd bee nice if the client can be shared on both the rpc and forked backend side let forked_client = ForkedClient::new_http(cfg.url.clone(), block_num); @@ -206,12 +202,14 @@ pub async fn build(mut config: Config) -> Result { // --- build l1 gas oracle // Check if the user specify a fixed gas price in the dev config. - let gas_oracle = if let Some(fixed_prices) = config.dev.fixed_gas_prices { - L1GasOracle::fixed(fixed_prices.gas_price, fixed_prices.data_gas_price) - } - // TODO: for now we just use the default gas prices, but this should be a proper oracle in the - // future that can perform actual sampling. - else { + let gas_oracle = if let Some(fixed_prices) = &config.dev.fixed_gas_prices { + // Use fixed gas prices if provided in the configuration + L1GasOracle::fixed(fixed_prices.gas_price.clone(), fixed_prices.data_gas_price.clone()) + } else if let Some(url) = &config.l1_provider_url { + // Default to a sampled gas oracle using the given provider + L1GasOracle::sampled(url.clone()) + } else { + // Use default fixed gas prices if no url and if no fixed prices are provided L1GasOracle::fixed( GasPrices { eth: DEFAULT_ETH_L1_GAS_PRICE, strk: DEFAULT_STRK_L1_GAS_PRICE }, GasPrices { eth: DEFAULT_ETH_L1_DATA_GAS_PRICE, strk: DEFAULT_STRK_L1_DATA_GAS_PRICE }, @@ -224,7 +222,7 @@ pub async fn build(mut config: Config) -> Result { blockchain, executor_factory, block_context_generator, - chain_spec: config.chain, + chain_spec: config.chain.clone(), }); // --- build block producer @@ -244,116 +242,63 @@ pub async fn build(mut config: Config) -> Result { let validator = block_producer.validator(); let pool = TxPool::new(validator.clone(), FiFo::new()); - let node = Node { - db, - pool, - backend, - forked_client, - block_producer, - rpc_config: config.rpc, - metrics_config: config.metrics, - messaging_config: config.messaging, - sequencing_config: config.sequencing, - task_manager: TaskManager::current(), - }; + // --- build rpc server - Ok(node) -} + let mut rpc_modules = RpcModule::new(()); + + let cors = Cors::new() + .allow_origins(config.rpc.cors_origins.clone()) + // Allow `POST` when accessing the resource + .allow_methods([Method::POST, Method::GET]) + .allow_headers([hyper::header::CONTENT_TYPE, "argent-client".parse().unwrap(), "argent-version".parse().unwrap()]); + + if config.rpc.apis.contains(&RpcModuleKind::Starknet) { + let cfg = StarknetApiConfig { + max_event_page_size: config.rpc.max_event_page_size, + max_proof_keys: config.rpc.max_proof_keys, + }; -// Moved from `katana_rpc` crate -pub async fn spawn( - node_components: ( - TxPool, - Arc>, - BlockProducer, - TxValidator, - Option, - ), - config: RpcConfig, -) -> Result { - let (pool, backend, block_producer, validator, forked_client) = node_components; - - let mut methods = RpcModule::new(()); - methods.register_method("health", |_, _| Ok(serde_json::json!({ "health": true })))?; - - if config.apis.contains(&ApiKind::Starknet) { - let cfg = StarknetApiConfig { max_event_page_size: config.max_event_page_size }; - - let server = if let Some(client) = forked_client { + let api = if let Some(client) = forked_client { StarknetApi::new_forked( backend.clone(), pool.clone(), block_producer.clone(), - validator, client, cfg, ) } else { - StarknetApi::new(backend.clone(), pool.clone(), block_producer.clone(), validator, cfg) + StarknetApi::new(backend.clone(), pool.clone(), Some(block_producer.clone()), cfg) }; - methods.merge(StarknetApiServer::into_rpc(server.clone()))?; - methods.merge(StarknetWriteApiServer::into_rpc(server.clone()))?; - methods.merge(StarknetTraceApiServer::into_rpc(server))?; + rpc_modules.merge(StarknetApiServer::into_rpc(api.clone()))?; + rpc_modules.merge(StarknetWriteApiServer::into_rpc(api.clone()))?; + rpc_modules.merge(StarknetTraceApiServer::into_rpc(api))?; } - if config.apis.contains(&ApiKind::Dev) { - methods.merge(DevApi::new(backend.clone(), block_producer.clone()).into_rpc())?; + if config.rpc.apis.contains(&RpcModuleKind::Dev) { + let api = DevApi::new(backend.clone(), block_producer.clone()); + rpc_modules.merge(api.into_rpc())?; } - if config.apis.contains(&ApiKind::Torii) { - methods.merge( - ToriiApi::new(backend.clone(), pool.clone(), block_producer.clone()).into_rpc(), - )?; + if config.rpc.apis.contains(&RpcModuleKind::Torii) { + let api = ToriiApi::new(backend.clone(), pool.clone(), block_producer.clone()); + rpc_modules.merge(api.into_rpc())?; } - if config.apis.contains(&ApiKind::Saya) { - methods.merge(SayaApi::new(backend.clone(), block_producer.clone()).into_rpc())?; + if config.rpc.apis.contains(&RpcModuleKind::Saya) { + let api = SayaApi::new(backend.clone(), block_producer.clone()); + rpc_modules.merge(api.into_rpc())?; } - let cors = CorsLayer::new() - // Allow `POST` when accessing the resource - .allow_methods([Method::POST, Method::GET]) - .allow_headers([hyper::header::CONTENT_TYPE, "argent-client".parse().unwrap(), "argent-version".parse().unwrap()]); - - let cors = - config.cors_origins.clone().map(|allowed_origins| match allowed_origins.as_slice() { - [origin] if origin == "*" => cors.allow_origin(AllowOrigin::mirror_request()), - origins => cors.allow_origin( - origins - .iter() - .map(|o| { - let _ = o.parse::().expect("Invalid URI"); - - o.parse().expect("Invalid origin") - }) - .collect::>(), - ), - }); - - let middleware = tower::ServiceBuilder::new() - .option_layer(cors) - .layer(ProxyGetRequestLayer::new("/", "health")?) - .timeout(Duration::from_secs(20)); - - let server = ServerBuilder::new() - .set_logger(RpcServerMetrics::new(&methods)) - .set_host_filtering(AllowHosts::Any) - .set_middleware(middleware) - .max_connections(config.max_connections) - .build(config.socket_addr()) - .await?; - - let addr = server.local_addr()?; - let handle = server.start(methods)?; - - info!(target: "rpc", %addr, "RPC server started."); - - Ok(RpcServer { handle, addr }) -} + let rpc_server = RpcServer::new().metrics().health_check().cors(cors).module(rpc_modules); -#[derive(Debug)] -pub struct RpcServer { - pub addr: SocketAddr, - pub handle: ServerHandle, + Ok(Node { + db, + pool, + backend, + rpc_server, + block_producer, + config: Arc::new(config), + task_manager: TaskManager::current(), + }) } diff --git a/crates/katana/pipeline/src/lib.rs b/crates/katana/pipeline/src/lib.rs deleted file mode 100644 index 7850fe4974..0000000000 --- a/crates/katana/pipeline/src/lib.rs +++ /dev/null @@ -1,82 +0,0 @@ -#![cfg_attr(not(test), warn(unused_crate_dependencies))] - -pub mod stage; - -use core::future::IntoFuture; - -use futures::future::BoxFuture; -use stage::Stage; -use tracing::{error, info}; - -/// The result of a pipeline execution. -pub type PipelineResult = Result<(), Error>; - -/// The future type for [Pipeline]'s implementation of [IntoFuture]. -pub type PipelineFut = BoxFuture<'static, PipelineResult>; - -#[derive(Debug, thiserror::Error)] -pub enum Error { - #[error(transparent)] - Stage(#[from] stage::Error), -} - -/// Manages the execution of stages. -/// -/// The pipeline drives the execution of stages, running each stage to completion in the order they -/// were added. -/// -/// Inspired by [`reth`]'s staged sync pipeline. -/// -/// [`reth`]: https://github.com/paradigmxyz/reth/blob/c7aebff0b6bc19cd0b73e295497d3c5150d40ed8/crates/stages/api/src/pipeline/mod.rs#L66 -pub struct Pipeline { - stages: Vec>, -} - -impl Pipeline { - /// Create a new empty pipeline. - pub fn new() -> Self { - Self { stages: Vec::new() } - } - - /// Insert a new stage into the pipeline. - pub fn add_stage(&mut self, stage: Box) { - self.stages.push(stage); - } - - /// Start the pipeline. - pub async fn run(&mut self) -> PipelineResult { - for stage in &mut self.stages { - info!(target: "pipeline", id = %stage.id(), "Executing stage."); - stage.execute().await?; - } - info!(target: "pipeline", "Pipeline finished."); - Ok(()) - } -} - -impl IntoFuture for Pipeline { - type Output = PipelineResult; - type IntoFuture = PipelineFut; - - fn into_future(mut self) -> Self::IntoFuture { - Box::pin(async move { - self.run().await.inspect_err(|error| { - error!(target: "pipeline", %error, "Pipeline failed."); - }) - }) - } -} - -impl core::default::Default for Pipeline { - fn default() -> Self { - Self::new() - } -} - -impl core::fmt::Debug for Pipeline { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - f.debug_struct("Pipeline") - .field("stages", &self.stages.iter().map(|s| s.id()).collect::>()) - .finish() - } -} diff --git a/crates/katana/pipeline/src/stage/mod.rs b/crates/katana/pipeline/src/stage/mod.rs deleted file mode 100644 index 6d50761ffc..0000000000 --- a/crates/katana/pipeline/src/stage/mod.rs +++ /dev/null @@ -1,34 +0,0 @@ -mod sequencing; - -pub use sequencing::Sequencing; - -/// The result type of a stage execution. See [Stage::execute]. -pub type StageResult = Result<(), Error>; - -#[derive(Debug, Clone, Copy)] -pub enum StageId { - Sequencing, -} - -impl core::fmt::Display for StageId { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - StageId::Sequencing => write!(f, "Sequencing"), - } - } -} - -#[derive(Debug, thiserror::Error)] -pub enum Error { - #[error(transparent)] - Other(#[from] anyhow::Error), -} - -#[async_trait::async_trait] -pub trait Stage: Send + Sync { - /// Returns the id which uniquely identifies the stage. - fn id(&self) -> StageId; - - /// Executes the stage. - async fn execute(&mut self) -> StageResult; -} diff --git a/crates/katana/pool/src/tx.rs b/crates/katana/pool/src/tx.rs index ca7117d292..aae6e4f9a3 100644 --- a/crates/katana/pool/src/tx.rs +++ b/crates/katana/pool/src/tx.rs @@ -116,11 +116,13 @@ impl PoolTransaction for ExecutableTxWithHash { fn nonce(&self) -> Nonce { match &self.transaction { ExecutableTx::Invoke(tx) => match tx { + InvokeTx::V0(..) => unimplemented!("v0 transaction not supported"), InvokeTx::V1(v1) => v1.nonce, InvokeTx::V3(v3) => v3.nonce, }, ExecutableTx::L1Handler(tx) => tx.nonce, ExecutableTx::Declare(tx) => match &tx.transaction { + DeclareTx::V0(..) => unimplemented!("v0 transaction not supported"), DeclareTx::V1(v1) => v1.nonce, DeclareTx::V2(v2) => v2.nonce, DeclareTx::V3(v3) => v3.nonce, @@ -135,11 +137,13 @@ impl PoolTransaction for ExecutableTxWithHash { fn sender(&self) -> ContractAddress { match &self.transaction { ExecutableTx::Invoke(tx) => match tx { + InvokeTx::V0(v0) => v0.contract_address, InvokeTx::V1(v1) => v1.sender_address, InvokeTx::V3(v3) => v3.sender_address, }, ExecutableTx::L1Handler(tx) => tx.contract_address, ExecutableTx::Declare(tx) => match &tx.transaction { + DeclareTx::V0(v0) => v0.sender_address, DeclareTx::V1(v1) => v1.sender_address, DeclareTx::V2(v2) => v2.sender_address, DeclareTx::V3(v3) => v3.sender_address, @@ -151,11 +155,13 @@ impl PoolTransaction for ExecutableTxWithHash { fn max_fee(&self) -> u128 { match &self.transaction { ExecutableTx::Invoke(tx) => match tx { + InvokeTx::V0(v0) => v0.max_fee, InvokeTx::V1(v1) => v1.max_fee, InvokeTx::V3(_) => 0, // V3 doesn't have max_fee }, ExecutableTx::L1Handler(tx) => tx.paid_fee_on_l1, ExecutableTx::Declare(tx) => match &tx.transaction { + DeclareTx::V0(v0) => v0.max_fee, DeclareTx::V1(v1) => v1.max_fee, DeclareTx::V2(v2) => v2.max_fee, DeclareTx::V3(_) => 0, // V3 doesn't have max_fee @@ -170,17 +176,17 @@ impl PoolTransaction for ExecutableTxWithHash { fn tip(&self) -> u64 { match &self.transaction { ExecutableTx::Invoke(tx) => match tx { - InvokeTx::V1(_) => 0, // V1 doesn't have tip InvokeTx::V3(v3) => v3.tip, + _ => 0, }, - ExecutableTx::L1Handler(_) => 0, // L1Handler doesn't have tip + ExecutableTx::L1Handler(_) => 0, ExecutableTx::Declare(tx) => match &tx.transaction { - DeclareTx::V1(_) | DeclareTx::V2(_) => 0, // V1 and V2 don't have tip DeclareTx::V3(v3) => v3.tip, + _ => 0, }, ExecutableTx::DeployAccount(tx) => match tx { - DeployAccountTx::V1(_) => 0, // V1 doesn't have tip DeployAccountTx::V3(v3) => v3.tip, + _ => 0, }, } } diff --git a/crates/katana/primitives/Cargo.toml b/crates/katana/primitives/Cargo.toml index 0c692e4d9b..9478c45cb4 100644 --- a/crates/katana/primitives/Cargo.toml +++ b/crates/katana/primitives/Cargo.toml @@ -25,8 +25,11 @@ starknet-types-core.workspace = true thiserror.workspace = true alloy-primitives = { workspace = true, features = [ "arbitrary" ] } -flate2 = { workspace = true, optional = true } +flate2.workspace = true num-bigint = "0.4.6" +serde_json_pythonic = "0.1.2" +strum.workspace = true +strum_macros.workspace = true [dev-dependencies] assert_matches.workspace = true @@ -44,6 +47,5 @@ arbitrary = [ "katana-cairo/cairo-vm-test-utils", ] controller = [ ] -rpc = [ "dep:flate2" ] serde = [ "alloy-primitives/serde" ] slot = [ "controller" ] diff --git a/crates/katana/primitives/src/block.rs b/crates/katana/primitives/src/block.rs index f78982eaa6..12f47594cc 100644 --- a/crates/katana/primitives/src/block.rs +++ b/crates/katana/primitives/src/block.rs @@ -54,7 +54,6 @@ pub struct PartialHeader { pub protocol_version: ProtocolVersion, } -// TODO: change names to wei and fri /// The L1 gas prices. #[derive(Debug, Default, Clone, PartialEq, Eq)] #[cfg_attr(feature = "arbitrary", derive(::arbitrary::Arbitrary))] diff --git a/crates/katana/primitives/src/chain_spec.rs b/crates/katana/primitives/src/chain_spec.rs index 0303433ccd..0c2a4ef861 100644 --- a/crates/katana/primitives/src/chain_spec.rs +++ b/crates/katana/primitives/src/chain_spec.rs @@ -13,14 +13,13 @@ use crate::da::L1DataAvailabilityMode; use crate::genesis::allocation::{DevAllocationsGenerator, GenesisAllocation}; use crate::genesis::constant::{ get_fee_token_balance_base_storage_address, DEFAULT_ACCOUNT_CLASS_PUBKEY_STORAGE_SLOT, - DEFAULT_ETH_FEE_TOKEN_ADDRESS, DEFAULT_LEGACY_ERC20_CASM, DEFAULT_LEGACY_ERC20_CLASS_HASH, - DEFAULT_LEGACY_UDC_CASM, DEFAULT_LEGACY_UDC_CLASS_HASH, DEFAULT_LEGACY_UDC_COMPILED_CLASS_HASH, - DEFAULT_PREFUNDED_ACCOUNT_BALANCE, DEFAULT_STRK_FEE_TOKEN_ADDRESS, DEFAULT_UDC_ADDRESS, - ERC20_DECIMAL_STORAGE_SLOT, ERC20_NAME_STORAGE_SLOT, ERC20_SYMBOL_STORAGE_SLOT, - ERC20_TOTAL_SUPPLY_STORAGE_SLOT, + DEFAULT_ETH_FEE_TOKEN_ADDRESS, DEFAULT_LEGACY_ERC20_CLASS, DEFAULT_LEGACY_ERC20_CLASS_HASH, + DEFAULT_LEGACY_UDC_CLASS, DEFAULT_LEGACY_UDC_CLASS_HASH, DEFAULT_PREFUNDED_ACCOUNT_BALANCE, + DEFAULT_STRK_FEE_TOKEN_ADDRESS, DEFAULT_UDC_ADDRESS, ERC20_DECIMAL_STORAGE_SLOT, + ERC20_NAME_STORAGE_SLOT, ERC20_SYMBOL_STORAGE_SLOT, ERC20_TOTAL_SUPPLY_STORAGE_SLOT, }; use crate::genesis::Genesis; -use crate::state::StateUpdatesWithDeclaredClasses; +use crate::state::StateUpdatesWithClasses; use crate::utils::split_u256; use crate::version::{ProtocolVersion, CURRENT_STARKNET_VERSION}; @@ -73,19 +72,20 @@ impl ChainSpec { Block { header, body: Vec::new() } } - // this method will include the the ETH and STRK fee tokens, and the UDC - pub fn state_updates(&self) -> StateUpdatesWithDeclaredClasses { - let mut states = StateUpdatesWithDeclaredClasses::default(); + // this method will include the ETH and STRK fee tokens, and the UDC + pub fn state_updates(&self) -> StateUpdatesWithClasses { + let mut states = StateUpdatesWithClasses::default(); for (class_hash, class) in &self.genesis.classes { let class_hash = *class_hash; - states.state_updates.declared_classes.insert(class_hash, class.compiled_class_hash); - states.declared_compiled_classes.insert(class_hash, class.casm.as_ref().clone()); - - if let Some(sierra) = &class.sierra { - states.declared_sierra_classes.insert(class_hash, sierra.as_ref().clone()); + if class.class.is_legacy() { + states.state_updates.deprecated_declared_classes.insert(class_hash); + } else { + states.state_updates.declared_classes.insert(class_hash, class.compiled_class_hash); } + + states.classes.insert(class_hash, class.class.as_ref().clone()); } for (address, alloc) in &self.genesis.allocations { @@ -146,12 +146,12 @@ lazy_static! { }; } -fn add_default_fee_tokens(states: &mut StateUpdatesWithDeclaredClasses, genesis: &Genesis) { +fn add_default_fee_tokens(states: &mut StateUpdatesWithClasses, genesis: &Genesis) { // declare erc20 token contract states - .declared_compiled_classes + .classes .entry(DEFAULT_LEGACY_ERC20_CLASS_HASH) - .or_insert_with(|| DEFAULT_LEGACY_ERC20_CASM.clone()); + .or_insert_with(|| DEFAULT_LEGACY_ERC20_CLASS.clone()); // -- ETH add_fee_token( @@ -177,7 +177,7 @@ fn add_default_fee_tokens(states: &mut StateUpdatesWithDeclaredClasses, genesis: } fn add_fee_token( - states: &mut StateUpdatesWithDeclaredClasses, + states: &mut StateUpdatesWithClasses, name: &str, symbol: &str, decimals: u8, @@ -225,17 +225,14 @@ fn add_fee_token( states.state_updates.storage_updates.insert(address, storage); } -fn add_default_udc(states: &mut StateUpdatesWithDeclaredClasses) { +fn add_default_udc(states: &mut StateUpdatesWithClasses) { // declare UDC class states - .declared_compiled_classes + .classes .entry(DEFAULT_LEGACY_UDC_CLASS_HASH) - .or_insert_with(|| DEFAULT_LEGACY_UDC_CASM.clone()); - states - .state_updates - .declared_classes - .entry(DEFAULT_LEGACY_UDC_CLASS_HASH) - .or_insert(DEFAULT_LEGACY_UDC_COMPILED_CLASS_HASH); + .or_insert_with(|| DEFAULT_LEGACY_UDC_CLASS.clone()); + + states.state_updates.deprecated_declared_classes.insert(DEFAULT_LEGACY_UDC_CLASS_HASH); // deploy UDC contract states @@ -259,14 +256,12 @@ mod tests { use crate::da::L1DataAvailabilityMode; use crate::genesis::allocation::{GenesisAccount, GenesisAccountAlloc, GenesisContractAlloc}; #[cfg(feature = "slot")] + use crate::genesis::constant::{CONTROLLER_ACCOUNT_CLASS, CONTROLLER_CLASS_HASH}; use crate::genesis::constant::{ - CONTROLLER_ACCOUNT_CLASS, CONTROLLER_ACCOUNT_CLASS_CASM, CONTROLLER_CLASS_HASH, - }; - use crate::genesis::constant::{ - DEFAULT_ACCOUNT_CLASS, DEFAULT_ACCOUNT_CLASS_CASM, DEFAULT_ACCOUNT_CLASS_HASH, + DEFAULT_ACCOUNT_CLASS, DEFAULT_ACCOUNT_CLASS_HASH, DEFAULT_ACCOUNT_CLASS_PUBKEY_STORAGE_SLOT, DEFAULT_ACCOUNT_COMPILED_CLASS_HASH, - DEFAULT_LEGACY_ERC20_CASM, DEFAULT_LEGACY_ERC20_COMPILED_CLASS_HASH, - DEFAULT_LEGACY_UDC_CASM, DEFAULT_LEGACY_UDC_COMPILED_CLASS_HASH, + DEFAULT_LEGACY_ERC20_CLASS, DEFAULT_LEGACY_ERC20_COMPILED_CLASS_HASH, + DEFAULT_LEGACY_UDC_CLASS, DEFAULT_LEGACY_UDC_COMPILED_CLASS_HASH, }; use crate::genesis::GenesisClass; use crate::version::CURRENT_STARKNET_VERSION; @@ -279,16 +274,14 @@ mod tests { ( DEFAULT_LEGACY_UDC_CLASS_HASH, GenesisClass { - sierra: None, - casm: DEFAULT_LEGACY_UDC_CASM.clone().into(), + class: DEFAULT_LEGACY_UDC_CLASS.clone().into(), compiled_class_hash: DEFAULT_LEGACY_UDC_COMPILED_CLASS_HASH, }, ), ( DEFAULT_LEGACY_ERC20_CLASS_HASH, GenesisClass { - sierra: None, - casm: DEFAULT_LEGACY_ERC20_CASM.clone().into(), + class: DEFAULT_LEGACY_ERC20_CLASS.clone().into(), compiled_class_hash: DEFAULT_LEGACY_ERC20_COMPILED_CLASS_HASH, }, ), @@ -296,17 +289,15 @@ mod tests { DEFAULT_ACCOUNT_CLASS_HASH, GenesisClass { compiled_class_hash: DEFAULT_ACCOUNT_COMPILED_CLASS_HASH, - casm: DEFAULT_ACCOUNT_CLASS_CASM.clone().into(), - sierra: Some(DEFAULT_ACCOUNT_CLASS.clone().flatten().unwrap().into()), + class: DEFAULT_ACCOUNT_CLASS.clone().into(), }, ), #[cfg(feature = "slot")] ( CONTROLLER_CLASS_HASH, GenesisClass { - casm: CONTROLLER_ACCOUNT_CLASS_CASM.clone().into(), compiled_class_hash: CONTROLLER_CLASS_HASH, - sierra: Some(CONTROLLER_ACCOUNT_CLASS.clone().flatten().unwrap().into()), + class: CONTROLLER_ACCOUNT_CLASS.clone().into(), }, ), ]); @@ -413,47 +404,26 @@ mod tests { assert_eq!(actual_block.body, expected_block.body); if cfg!(feature = "slot") { - assert!( - actual_state_updates.declared_compiled_classes.len() == 4, - "should be 4 casm classes: udc, erc20, oz account, controller account" - ); - - assert!( - actual_state_updates.declared_sierra_classes.len() == 2, - "should be 2 sierra classes: oz account, controller account" - ); + assert!(actual_state_updates.classes.len() == 4); } else { - assert!( - actual_state_updates.declared_compiled_classes.len() == 3, - "should be 3 casm classes: udc, erc20, oz account" - ); - - assert!( - actual_state_updates.declared_sierra_classes.len() == 1, - "should be only 1 sierra class: oz account" - ); + assert!(actual_state_updates.classes.len() == 3); } assert_eq!( actual_state_updates .state_updates - .declared_classes + .deprecated_declared_classes .get(&DEFAULT_LEGACY_ERC20_CLASS_HASH), Some(&DEFAULT_LEGACY_ERC20_COMPILED_CLASS_HASH), - "The default fee token class should be declared" ); - assert_eq!( - actual_state_updates.declared_compiled_classes.get(&DEFAULT_LEGACY_ERC20_CLASS_HASH), - Some(&DEFAULT_LEGACY_ERC20_CASM.clone()), - "The default fee token casm class should be declared" + actual_state_updates.classes.get(&DEFAULT_LEGACY_ERC20_CLASS_HASH), + Some(&DEFAULT_LEGACY_ERC20_CLASS.clone()) ); - assert!( - !actual_state_updates - .declared_sierra_classes - .contains_key(&DEFAULT_LEGACY_ERC20_CLASS_HASH), - "The default fee token class doesnt have a sierra class" + assert_eq!( + actual_state_updates.classes.get(&DEFAULT_LEGACY_ERC20_CLASS_HASH), + Some(&*DEFAULT_LEGACY_ERC20_CLASS), ); assert_eq!( @@ -474,22 +444,22 @@ mod tests { ); assert_eq!( - actual_state_updates.state_updates.declared_classes.get(&DEFAULT_LEGACY_UDC_CLASS_HASH), + actual_state_updates + .state_updates + .deprecated_declared_classes + .get(&DEFAULT_LEGACY_UDC_CLASS_HASH), Some(&DEFAULT_LEGACY_UDC_COMPILED_CLASS_HASH), "The default universal deployer class should be declared" ); assert_eq!( - actual_state_updates.declared_compiled_classes.get(&DEFAULT_LEGACY_UDC_CLASS_HASH), - Some(&DEFAULT_LEGACY_UDC_CASM.clone()), + actual_state_updates.classes.get(&DEFAULT_LEGACY_UDC_CLASS_HASH), + Some(&*DEFAULT_LEGACY_UDC_CLASS), "The default universal deployer casm class should be declared" ); - - assert!( - !actual_state_updates - .declared_sierra_classes - .contains_key(&DEFAULT_LEGACY_UDC_CLASS_HASH), - "The default universal deployer class doesnt have a sierra class" + assert_eq!( + actual_state_updates.classes.get(&DEFAULT_LEGACY_UDC_CLASS_HASH), + Some(&DEFAULT_LEGACY_UDC_CLASS.clone()) ); assert_eq!( @@ -505,17 +475,8 @@ mod tests { ); assert_eq!( - actual_state_updates - .declared_compiled_classes - .get(&DEFAULT_ACCOUNT_CLASS_HASH) - .unwrap(), - &DEFAULT_ACCOUNT_CLASS_CASM.clone(), - "The default oz account contract casm class should be declared" - ); - - assert_eq!( - actual_state_updates.declared_sierra_classes.get(&DEFAULT_ACCOUNT_CLASS_HASH), - Some(&DEFAULT_ACCOUNT_CLASS.clone().flatten().unwrap()), + actual_state_updates.classes.get(&DEFAULT_ACCOUNT_CLASS_HASH), + Some(&*DEFAULT_ACCOUNT_CLASS), "The default oz account contract sierra class should be declared" ); @@ -528,14 +489,8 @@ mod tests { ); assert_eq!( - actual_state_updates.declared_compiled_classes.get(&CONTROLLER_CLASS_HASH), - Some(&CONTROLLER_ACCOUNT_CLASS_CASM.clone()), - "The controller account contract casm class should be declared" - ); - - assert_eq!( - actual_state_updates.declared_sierra_classes.get(&CONTROLLER_CLASS_HASH), - Some(&CONTROLLER_ACCOUNT_CLASS.clone().flatten().unwrap()), + actual_state_updates.classes.get(&CONTROLLER_CLASS_HASH), + Some(&*CONTROLLER_ACCOUNT_CLASS), "The controller account contract sierra class should be declared" ); } diff --git a/crates/katana/primitives/src/class.rs b/crates/katana/primitives/src/class.rs index 570266c45a..e227e12c08 100644 --- a/crates/katana/primitives/src/class.rs +++ b/crates/katana/primitives/src/class.rs @@ -1,6 +1,10 @@ -use katana_cairo::lang::sierra::program::Program; -use katana_cairo::lang::starknet_classes::casm_contract_class::CasmContractClass; -use katana_cairo::lang::starknet_classes::contract_class::ContractEntryPoints; +use katana_cairo::lang::starknet_classes::abi; +use katana_cairo::lang::starknet_classes::casm_contract_class::StarknetSierraCompilationError; +use katana_cairo::lang::starknet_classes::contract_class::ContractEntryPoint; +use serde_json_pythonic::to_string_pythonic; +use starknet::core::utils::{normalize_address, starknet_keccak}; +use starknet::macros::short_string; +use starknet_crypto::poseidon_hash_many; use crate::Felt; @@ -9,33 +13,182 @@ pub type ClassHash = Felt; /// The hash of a compiled contract class. pub type CompiledClassHash = Felt; -pub type SierraClass = starknet::core::types::contract::SierraClass; -pub type FlattenedSierraClass = starknet::core::types::FlattenedSierraClass; +/// The canonical contract class (Sierra) type. +pub type SierraContractClass = katana_cairo::lang::starknet_classes::contract_class::ContractClass; +/// The canonical legacy class (Cairo 0) type. +pub type LegacyContractClass = katana_cairo::starknet_api::deprecated_contract_class::ContractClass; -/// Deprecated legacy (Cairo 0) CASM class -pub type DeprecatedCompiledClass = - ::katana_cairo::starknet_api::deprecated_contract_class::ContractClass; +/// The canonical compiled Sierra contract class type. +pub type CasmContractClass = + katana_cairo::lang::starknet_classes::casm_contract_class::CasmContractClass; -/// Represents an executable Sierra program. -#[derive(Debug, Clone, Eq, PartialEq)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct SierraProgram { - pub program: Program, - pub entry_points_by_type: ContractEntryPoints, +#[derive(Debug, thiserror::Error)] +pub enum ContractClassCompilationError { + #[error(transparent)] + SierraCompilation(#[from] StarknetSierraCompilationError), } +#[allow(clippy::large_enum_variant)] #[derive(Debug, Clone, Eq, PartialEq)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct SierraCompiledClass { - pub casm: CasmContractClass, - pub sierra: SierraProgram, +#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] +pub enum ContractClass { + Class(SierraContractClass), + Legacy(LegacyContractClass), +} + +impl ContractClass { + /// Computes the hash of the class. + pub fn class_hash(&self) -> Result { + match self { + Self::Class(class) => compute_sierra_class_hash(class), + Self::Legacy(class) => compute_legacy_class_hash(class), + } + } + + /// Compiles the contract class. + pub fn compile(self) -> Result { + match self { + Self::Legacy(class) => Ok(CompiledClass::Legacy(class)), + Self::Class(class) => { + let casm = CasmContractClass::from_contract_class(class, true, usize::MAX)?; + let casm = CompiledClass::Class(casm); + Ok(casm) + } + } + } + + /// Checks if this contract class is a Cairo 0 legacy class. + /// + /// Returns `true` if the contract class is a legacy class, `false` otherwise. + pub fn is_legacy(&self) -> bool { + matches!(self, Self::Legacy(_)) + } } -/// Executable contract class +/// Compiled version of [`ContractClass`]. +/// +/// This is the CASM format that can be used for execution. TO learn more about CASM, check out the +/// [Starknet docs]. +/// +/// [Starknet docs]: https://docs.starknet.io/architecture-and-concepts/smart-contracts/cairo-and-sierra/#why_do_we_need_casm #[allow(clippy::large_enum_variant)] #[derive(Debug, Clone, Eq, PartialEq, derive_more::From)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] pub enum CompiledClass { - Class(SierraCompiledClass), - Deprecated(DeprecatedCompiledClass), + /// The compiled Sierra contract class ie CASM. + Class(CasmContractClass), + + /// The compiled legacy contract class. + /// + /// This is the same as the uncompiled legacy class because prior to Sierra, + /// the classes were already in CASM format. + Legacy(LegacyContractClass), +} + +impl CompiledClass { + /// Computes the hash of the compiled class. + pub fn class_hash(&self) -> Result { + match self { + Self::Class(class) => Ok(class.compiled_class_hash()), + Self::Legacy(class) => Ok(compute_legacy_class_hash(class)?), + } + } +} + +#[derive(Debug, thiserror::Error)] +pub enum ComputeClassHashError { + #[error(transparent)] + AbiConversion(#[from] serde_json_pythonic::Error), +} + +// Taken from starknet-rs +fn compute_sierra_class_hash(class: &SierraContractClass) -> Result { + // Technically we don't have to use the Pythonic JSON style here. Doing this just to align + // with the official `cairo-lang` CLI. + // + // TODO: add an `AbiFormatter` trait and let users choose which one to use. + let abi = class.abi.as_ref(); + let abi_str = to_string_pythonic(abi.unwrap_or(&abi::Contract::default())).unwrap(); + + let mut hasher = starknet_crypto::PoseidonHasher::new(); + hasher.update(short_string!("CONTRACT_CLASS_V0.1.0")); + + // Hashes entry points + hasher.update(entrypoints_hash(&class.entry_points_by_type.external)); + hasher.update(entrypoints_hash(&class.entry_points_by_type.l1_handler)); + hasher.update(entrypoints_hash(&class.entry_points_by_type.constructor)); + + // Hashes ABI + hasher.update(starknet_keccak(abi_str.as_bytes())); + + // Hashes Sierra program + let prog = class.sierra_program.iter().map(|f| f.value.clone().into()).collect::>(); + hasher.update(poseidon_hash_many(&prog)); + + Ok(normalize_address(hasher.finalize())) +} + +fn entrypoints_hash(entrypoints: &[ContractEntryPoint]) -> Felt { + let mut hasher = starknet_crypto::PoseidonHasher::new(); + + for entry in entrypoints { + hasher.update(entry.selector.clone().into()); + hasher.update(entry.function_idx.into()); + } + + hasher.finalize() +} + +/// Computes the hash of a legacy contract class. +/// +/// This function delegates the computation to the `starknet-rs` library. Don't really care about +/// performance here because it's only for legacy classes, but we should definitely find to improve +/// this without introducing too much complexity. +fn compute_legacy_class_hash(class: &LegacyContractClass) -> Result { + pub use starknet::core::types::contract::legacy::LegacyContractClass as StarknetRsLegacyContractClass; + + let value = serde_json::to_value(class).unwrap(); + let class = serde_json::from_value::(value).unwrap(); + let hash = class.class_hash().unwrap(); + + Ok(hash) +} + +#[cfg(test)] +mod tests { + + use starknet::core::types::contract::legacy::LegacyContractClass as StarknetRsLegacyContractClass; + use starknet::core::types::contract::SierraClass as StarknetRsSierraContractClass; + + use super::{ContractClass, LegacyContractClass, SierraContractClass}; + + #[test] + fn compute_class_hash() { + let artifact = include_str!("../../contracts/build/default_account.json"); + + let class = serde_json::from_str::(artifact).unwrap(); + let actual_hash = ContractClass::Class(class).class_hash().unwrap(); + + // Compare it against the hash computed using `starknet-rs` types + + let class = serde_json::from_str::(artifact).unwrap(); + let expected_hash = class.class_hash().unwrap(); + + assert_eq!(actual_hash, expected_hash); + } + + #[test] + fn compute_legacy_class_hash() { + let artifact = include_str!("../../contracts/build/erc20.json"); + + let class = serde_json::from_str::(artifact).unwrap(); + let actual_hash = ContractClass::Legacy(class).class_hash().unwrap(); + + // Compare it against the hash computed using `starknet-rs` types + + let class = serde_json::from_str::(artifact).unwrap(); + let expected_hash = class.class_hash().unwrap(); + + assert_eq!(actual_hash, expected_hash); + } } diff --git a/crates/katana/primitives/src/contract.rs b/crates/katana/primitives/src/contract.rs index 4d355287f3..71cdabc5de 100644 --- a/crates/katana/primitives/src/contract.rs +++ b/crates/katana/primitives/src/contract.rs @@ -1,4 +1,5 @@ use std::fmt; +use std::str::FromStr; use num_bigint::BigUint; use starknet::core::utils::normalize_address; @@ -40,12 +41,26 @@ impl fmt::Display for ContractAddress { } } +impl FromStr for ContractAddress { + type Err = ::Err; + + fn from_str(s: &str) -> Result { + Felt::from_str(s).map(ContractAddress::new) + } +} + impl From for ContractAddress { fn from(value: Felt) -> Self { ContractAddress::new(value) } } +impl AsRef for ContractAddress { + fn as_ref(&self) -> &Felt { + &self.0 + } +} + impl From for Felt { fn from(value: ContractAddress) -> Self { value.0 diff --git a/crates/katana/primitives/src/conversion/mod.rs b/crates/katana/primitives/src/conversion/mod.rs index 0818b474ae..06a3fd023e 100644 --- a/crates/katana/primitives/src/conversion/mod.rs +++ b/crates/katana/primitives/src/conversion/mod.rs @@ -1,2 +1 @@ -#[cfg(feature = "rpc")] pub mod rpc; diff --git a/crates/katana/primitives/src/conversion/rpc.rs b/crates/katana/primitives/src/conversion/rpc.rs index 257b37ea1b..2fc10396cf 100644 --- a/crates/katana/primitives/src/conversion/rpc.rs +++ b/crates/katana/primitives/src/conversion/rpc.rs @@ -11,28 +11,27 @@ use serde::Deserialize; use serde_json::json; use serde_with::serde_as; use starknet::core::serde::unsigned_field_element::UfeHex; -pub use starknet::core::types::contract::legacy::{LegacyContractClass, LegacyProgram}; +pub use starknet::core::types::contract::legacy::{ + LegacyContractClass as StarknetRsLegacyContractClass, LegacyProgram, +}; use starknet::core::types::contract::legacy::{ LegacyDebugInfo, LegacyFlowTrackingData, LegacyHint, LegacyIdentifier, LegacyReferenceManager, }; pub use starknet::core::types::contract::CompiledClass; use starknet::core::types::{ - CompressedLegacyContractClass, ContractClass, FunctionStateMutability, LegacyContractAbiEntry, - LegacyContractEntryPoint, LegacyEntryPointsByType, LegacyEventAbiEntry, LegacyEventAbiType, - LegacyFunctionAbiEntry, LegacyFunctionAbiType, LegacyStructAbiEntry, LegacyStructAbiType, - LegacyStructMember, LegacyTypedParameter, + CompressedLegacyContractClass, ContractClass, FlattenedSierraClass, FunctionStateMutability, + LegacyContractAbiEntry, LegacyContractEntryPoint, LegacyEntryPointsByType, LegacyEventAbiEntry, + LegacyEventAbiType, LegacyFunctionAbiEntry, LegacyFunctionAbiType, LegacyStructAbiEntry, + LegacyStructAbiType, LegacyStructMember, LegacyTypedParameter, }; -use crate::class::{ - ClassHash, CompiledClassHash, DeprecatedCompiledClass, FlattenedSierraClass, - SierraCompiledClass, SierraProgram, -}; +use crate::class::{ClassHash, CompiledClassHash, LegacyContractClass}; use crate::Felt; /// Converts the legacy inner compiled class type [DeprecatedCompiledClass] into its RPC equivalent /// [`ContractClass`]. pub fn legacy_inner_to_rpc_class( - legacy_contract_class: DeprecatedCompiledClass, + legacy_contract_class: LegacyContractClass, ) -> Result { fn to_rpc_entry_points( entries: &HashMap>, @@ -144,14 +143,10 @@ pub fn flattened_sierra_to_compiled_class( let class = rpc_to_cairo_contract_class(contract_class)?; - let program = class.extract_sierra_program()?; - let entry_points_by_type = class.entry_points_by_type.clone(); - let sierra = SierraProgram { program, entry_points_by_type }; - let casm = CasmContractClass::from_contract_class(class, true, usize::MAX)?; let compiled_hash = casm.compiled_class_hash(); - let class = crate::class::CompiledClass::Class(SierraCompiledClass { casm, sierra }); + let class = crate::class::CompiledClass::Class(casm); Ok((class_hash, compiled_hash, class)) } @@ -167,24 +162,25 @@ pub fn compiled_class_hash_from_flattened_sierra_class( /// Converts a legacy RPC compiled contract class [CompressedLegacyContractClass] type to the inner /// compiled class type [CompiledClass](crate::class::CompiledClass) along with its class hash. -pub fn legacy_rpc_to_compiled_class( +pub fn legacy_rpc_to_class( compressed_legacy_contract: &CompressedLegacyContractClass, -) -> Result<(ClassHash, crate::class::CompiledClass)> { +) -> Result<(ClassHash, crate::class::ContractClass)> { let class_json = json!({ "abi": compressed_legacy_contract.abi.clone().unwrap_or_default(), "entry_points_by_type": compressed_legacy_contract.entry_points_by_type, "program": decompress_legacy_program_data(&compressed_legacy_contract.program)?, }); - let deprecated_class: DeprecatedCompiledClass = serde_json::from_value(class_json.clone())?; - let class_hash = serde_json::from_value::(class_json)?.class_hash()?; + let deprecated_class: LegacyContractClass = serde_json::from_value(class_json.clone())?; + let class_hash = + serde_json::from_value::(class_json)?.class_hash()?; - Ok((class_hash, crate::class::CompiledClass::Deprecated(deprecated_class))) + Ok((class_hash, crate::class::ContractClass::Legacy(deprecated_class))) } /// Converts `starknet-rs` RPC [FlattenedSierraClass] type to Cairo's /// [ContractClass](cairo_lang_starknet::contract_class::ContractClass) type. -fn rpc_to_cairo_contract_class( +pub fn rpc_to_cairo_contract_class( contract_class: &FlattenedSierraClass, ) -> Result { let value = serde_json::to_value(contract_class)?; @@ -201,7 +197,7 @@ fn rpc_to_cairo_contract_class( }) } -fn compress_legacy_program_data( +pub fn compress_legacy_program_data( legacy_program: katana_cairo::starknet_api::deprecated_contract_class::Program, ) -> Result, io::Error> { let bytes = serde_json::to_vec(&legacy_program)?; @@ -271,37 +267,30 @@ fn decompress_legacy_program_data(data: &[u8]) -> Result CompiledClass { parse_compiled_class(value).unwrap() } +fn read_legacy_class_artifact(artifact: &str) -> ContractClass { + let value = serde_json::from_str(artifact).unwrap(); + let class = parse_deprecated_compiled_class(value).unwrap(); + ContractClass::Legacy(class) +} + #[cfg(test)] mod tests { diff --git a/crates/katana/primitives/src/genesis/json.rs b/crates/katana/primitives/src/genesis/json.rs index ee79cbe602..1a294495e6 100644 --- a/crates/katana/primitives/src/genesis/json.rs +++ b/crates/katana/primitives/src/genesis/json.rs @@ -25,16 +25,13 @@ use super::allocation::{ DevGenesisAccount, GenesisAccount, GenesisAccountAlloc, GenesisContractAlloc, }; #[cfg(feature = "slot")] +use super::constant::{CONTROLLER_ACCOUNT_CLASS, CONTROLLER_CLASS_HASH}; use super::constant::{ - CONTROLLER_ACCOUNT_CLASS, CONTROLLER_ACCOUNT_CLASS_CASM, CONTROLLER_CLASS_HASH, -}; -use super::constant::{ - DEFAULT_ACCOUNT_CLASS, DEFAULT_ACCOUNT_CLASS_CASM, DEFAULT_ACCOUNT_CLASS_HASH, - DEFAULT_ACCOUNT_COMPILED_CLASS_HASH, + DEFAULT_ACCOUNT_CLASS, DEFAULT_ACCOUNT_CLASS_HASH, DEFAULT_ACCOUNT_COMPILED_CLASS_HASH, }; use super::{Genesis, GenesisAllocation}; use crate::block::{BlockHash, BlockNumber, GasPrices}; -use crate::class::{ClassHash, CompiledClass, SierraClass}; +use crate::class::{ClassHash, ContractClass, SierraContractClass}; use crate::contract::{ContractAddress, StorageKey, StorageValue}; use crate::genesis::GenesisClass; use crate::utils::class::{parse_compiled_class_v1, parse_deprecated_compiled_class}; @@ -295,9 +292,8 @@ impl TryFrom for Genesis { classes.insert( CONTROLLER_CLASS_HASH, GenesisClass { - casm: Arc::new(CONTROLLER_ACCOUNT_CLASS_CASM.clone()), + class: CONTROLLER_ACCOUNT_CLASS.clone().into(), compiled_class_hash: CONTROLLER_CLASS_HASH, - sierra: Some(Arc::new(CONTROLLER_ACCOUNT_CLASS.clone().flatten()?)), }, ); @@ -313,23 +309,20 @@ impl TryFrom for Genesis { } }; - let sierra = serde_json::from_value::(artifact.clone()); + let sierra = serde_json::from_value::(artifact.clone()); - let (class_hash, compiled_class_hash, sierra, casm) = match sierra { + let (class_hash, compiled_class_hash, class) = match sierra { Ok(sierra) => { - let class = parse_compiled_class_v1(artifact)?; + let casm = parse_compiled_class_v1(artifact)?; // check if the class hash is provided, otherwise compute it from the // artifacts - let class_hash = class_hash.unwrap_or(sierra.class_hash()?); - let compiled_hash = class.casm.compiled_class_hash(); + let class = ContractClass::Class(sierra); + let class_hash = class_hash + .unwrap_or_else(|| class.class_hash().expect("failed to compute hash")); + let compiled_hash = casm.compiled_class_hash(); - ( - class_hash, - compiled_hash, - Some(Arc::new(sierra.flatten()?)), - Arc::new(CompiledClass::Class(class)), - ) + (class_hash, compiled_hash, Arc::new(class)) } // if the artifact is not a sierra contract, we check if it's a legacy contract @@ -343,7 +336,7 @@ impl TryFrom for Genesis { casm.class_hash()? }; - (class_hash, class_hash, None, Arc::new(CompiledClass::Deprecated(casm))) + (class_hash, class_hash, Arc::new(ContractClass::Legacy(casm))) } }; @@ -362,7 +355,7 @@ impl TryFrom for Genesis { } } - classes.insert(class_hash, GenesisClass { compiled_class_hash, sierra, casm }); + classes.insert(class_hash, GenesisClass { compiled_class_hash, class }); } let mut allocations: BTreeMap = BTreeMap::new(); @@ -394,8 +387,7 @@ impl TryFrom for Genesis { if let btree_map::Entry::Vacant(e) = classes.entry(DEFAULT_ACCOUNT_CLASS_HASH) { // insert default account class to the classes map e.insert(GenesisClass { - casm: Arc::new(DEFAULT_ACCOUNT_CLASS_CASM.clone()), - sierra: Some(Arc::new(DEFAULT_ACCOUNT_CLASS.clone().flatten()?)), + class: DEFAULT_ACCOUNT_CLASS.clone().into(), compiled_class_hash: DEFAULT_ACCOUNT_COMPILED_CLASS_HASH, }); } @@ -544,7 +536,7 @@ mod tests { use super::*; use crate::address; - use crate::genesis::constant::{DEFAULT_LEGACY_ERC20_CASM, DEFAULT_LEGACY_UDC_CASM}; + use crate::genesis::constant::{DEFAULT_LEGACY_ERC20_CLASS, DEFAULT_LEGACY_UDC_CLASS}; #[test] fn deserialize_from_json() { @@ -716,33 +708,29 @@ mod tests { felt!("0x8"), GenesisClass { compiled_class_hash: felt!("0x8"), - casm: DEFAULT_LEGACY_ERC20_CASM.clone().into(), - sierra: None, + class: DEFAULT_LEGACY_ERC20_CLASS.clone().into(), }, ), ( felt!("0x80085"), GenesisClass { compiled_class_hash: felt!("0x80085"), - casm: DEFAULT_LEGACY_UDC_CASM.clone().into(), - sierra: None, + class: DEFAULT_LEGACY_UDC_CLASS.clone().into(), }, ), ( DEFAULT_ACCOUNT_CLASS_HASH, GenesisClass { compiled_class_hash: DEFAULT_ACCOUNT_COMPILED_CLASS_HASH, - casm: DEFAULT_ACCOUNT_CLASS_CASM.clone().into(), - sierra: Some(DEFAULT_ACCOUNT_CLASS.clone().flatten().unwrap().into()), + class: DEFAULT_ACCOUNT_CLASS.clone().into(), }, ), #[cfg(feature = "slot")] ( CONTROLLER_CLASS_HASH, GenesisClass { - casm: Arc::new(CONTROLLER_ACCOUNT_CLASS_CASM.clone()), compiled_class_hash: CONTROLLER_CLASS_HASH, - sierra: Some(Arc::new(CONTROLLER_ACCOUNT_CLASS.clone().flatten().unwrap())), + class: CONTROLLER_ACCOUNT_CLASS.clone().into(), }, ), ]); @@ -866,8 +854,7 @@ mod tests { for class in actual_genesis.classes { let expected_class = expected_genesis.classes.get(&class.0).unwrap(); assert_eq!(class.1.compiled_class_hash, expected_class.compiled_class_hash); - assert_eq!(class.1.casm, expected_class.casm); - assert_eq!(class.1.sierra, expected_class.sierra.clone()); + assert_eq!(class.1.class, expected_class.class); } } @@ -909,17 +896,15 @@ mod tests { DEFAULT_ACCOUNT_CLASS_HASH, GenesisClass { compiled_class_hash: DEFAULT_ACCOUNT_COMPILED_CLASS_HASH, - casm: DEFAULT_ACCOUNT_CLASS_CASM.clone().into(), - sierra: Some(DEFAULT_ACCOUNT_CLASS.clone().flatten().unwrap().into()), + class: DEFAULT_ACCOUNT_CLASS.clone().into(), }, ), #[cfg(feature = "slot")] ( CONTROLLER_CLASS_HASH, GenesisClass { - casm: Arc::new(CONTROLLER_ACCOUNT_CLASS_CASM.clone()), + class: CONTROLLER_ACCOUNT_CLASS.clone().into(), compiled_class_hash: CONTROLLER_CLASS_HASH, - sierra: Some(Arc::new(CONTROLLER_ACCOUNT_CLASS.clone().flatten().unwrap())), }, ), ]); @@ -960,8 +945,7 @@ mod tests { let expected_class = expected_genesis.classes.get(&hash).unwrap(); assert_eq!(class.compiled_class_hash, expected_class.compiled_class_hash); - assert_eq!(class.casm, expected_class.casm); - assert_eq!(class.sierra, expected_class.sierra.clone()); + assert_eq!(class.class, expected_class.class); } } diff --git a/crates/katana/primitives/src/genesis/mod.rs b/crates/katana/primitives/src/genesis/mod.rs index ca391bfa5a..2d21de936e 100644 --- a/crates/katana/primitives/src/genesis/mod.rs +++ b/crates/katana/primitives/src/genesis/mod.rs @@ -8,18 +8,18 @@ use std::sync::Arc; use constant::DEFAULT_ACCOUNT_CLASS; #[cfg(feature = "slot")] -use constant::{CONTROLLER_ACCOUNT_CLASS, CONTROLLER_ACCOUNT_CLASS_CASM, CONTROLLER_CLASS_HASH}; +use constant::{CONTROLLER_ACCOUNT_CLASS, CONTROLLER_CLASS_HASH}; use serde::{Deserialize, Serialize}; use self::allocation::{GenesisAccountAlloc, GenesisAllocation, GenesisContractAlloc}; use self::constant::{ - DEFAULT_ACCOUNT_CLASS_CASM, DEFAULT_ACCOUNT_CLASS_HASH, DEFAULT_ACCOUNT_COMPILED_CLASS_HASH, - DEFAULT_LEGACY_ERC20_CASM, DEFAULT_LEGACY_ERC20_CLASS_HASH, - DEFAULT_LEGACY_ERC20_COMPILED_CLASS_HASH, DEFAULT_LEGACY_UDC_CASM, - DEFAULT_LEGACY_UDC_CLASS_HASH, DEFAULT_LEGACY_UDC_COMPILED_CLASS_HASH, + DEFAULT_ACCOUNT_CLASS_HASH, DEFAULT_ACCOUNT_COMPILED_CLASS_HASH, DEFAULT_LEGACY_ERC20_CLASS, + DEFAULT_LEGACY_ERC20_CLASS_HASH, DEFAULT_LEGACY_ERC20_COMPILED_CLASS_HASH, + DEFAULT_LEGACY_UDC_CLASS, DEFAULT_LEGACY_UDC_CLASS_HASH, + DEFAULT_LEGACY_UDC_COMPILED_CLASS_HASH, }; use crate::block::{BlockHash, BlockNumber, GasPrices}; -use crate::class::{ClassHash, CompiledClass, CompiledClassHash, FlattenedSierraClass}; +use crate::class::{ClassHash, CompiledClassHash, ContractClass}; use crate::contract::ContractAddress; use crate::Felt; @@ -27,20 +27,14 @@ use crate::Felt; pub struct GenesisClass { /// The compiled class hash of the contract class. pub compiled_class_hash: CompiledClassHash, - /// The casm class definition. - #[serde(skip_serializing)] - pub casm: Arc, - /// The sierra class definition. - #[serde(skip_serializing)] - pub sierra: Option>, + pub class: Arc, } impl core::fmt::Debug for GenesisClass { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("GenesisClass") .field("compiled_class_hash", &self.compiled_class_hash) - .field("casm", &"...") - .field("sierra", &"...") + .field("class", &"...") .finish() } } @@ -110,8 +104,7 @@ impl Default for Genesis { ( DEFAULT_LEGACY_ERC20_CLASS_HASH, GenesisClass { - sierra: None, - casm: DEFAULT_LEGACY_ERC20_CASM.clone().into(), + class: DEFAULT_LEGACY_ERC20_CLASS.clone().into(), compiled_class_hash: DEFAULT_LEGACY_ERC20_COMPILED_CLASS_HASH, }, ), @@ -119,8 +112,7 @@ impl Default for Genesis { ( DEFAULT_LEGACY_UDC_CLASS_HASH, GenesisClass { - sierra: None, - casm: DEFAULT_LEGACY_UDC_CASM.clone().into(), + class: DEFAULT_LEGACY_UDC_CLASS.clone().into(), compiled_class_hash: DEFAULT_LEGACY_UDC_COMPILED_CLASS_HASH, }, ), @@ -128,18 +120,16 @@ impl Default for Genesis { ( DEFAULT_ACCOUNT_CLASS_HASH, GenesisClass { - sierra: Some(DEFAULT_ACCOUNT_CLASS.clone().flatten().unwrap().into()), - casm: DEFAULT_ACCOUNT_CLASS_CASM.clone().into(), compiled_class_hash: DEFAULT_ACCOUNT_COMPILED_CLASS_HASH, + class: DEFAULT_ACCOUNT_CLASS.clone().into(), }, ), #[cfg(feature = "slot")] ( CONTROLLER_CLASS_HASH, GenesisClass { - casm: CONTROLLER_ACCOUNT_CLASS_CASM.clone().into(), compiled_class_hash: CONTROLLER_CLASS_HASH, - sierra: Some(CONTROLLER_ACCOUNT_CLASS.clone().flatten().unwrap().into()), + class: CONTROLLER_ACCOUNT_CLASS.clone().into(), }, ), ]); diff --git a/crates/katana/primitives/src/lib.rs b/crates/katana/primitives/src/lib.rs index be6b106c39..23fddf3707 100644 --- a/crates/katana/primitives/src/lib.rs +++ b/crates/katana/primitives/src/lib.rs @@ -24,3 +24,4 @@ pub mod utils; pub use contract::ContractAddress; pub use starknet::macros::felt; pub use starknet_types_core::felt::{Felt, FromStrError}; +pub use starknet_types_core::hash; diff --git a/crates/katana/primitives/src/state.rs b/crates/katana/primitives/src/state.rs index 3ec9e65985..7488d28a79 100644 --- a/crates/katana/primitives/src/state.rs +++ b/crates/katana/primitives/src/state.rs @@ -4,7 +4,7 @@ use std::iter; use starknet::macros::short_string; use starknet_types_core::hash::{self, StarkHash}; -use crate::class::{ClassHash, CompiledClass, CompiledClassHash, FlattenedSierraClass}; +use crate::class::{ClassHash, CompiledClassHash, ContractClass}; use crate::contract::{ContractAddress, Nonce, StorageKey, StorageValue}; use crate::Felt; @@ -48,15 +48,13 @@ impl StateUpdates { } } -/// State update with declared classes definition. +/// State update with declared classes artifacts. #[derive(Debug, Default, Clone)] -pub struct StateUpdatesWithDeclaredClasses { +pub struct StateUpdatesWithClasses { /// State updates. pub state_updates: StateUpdates, /// A mapping of class hashes to their sierra classes definition. - pub declared_sierra_classes: BTreeMap, - /// A mapping of class hashes to their compiled classes definition. - pub declared_compiled_classes: BTreeMap, + pub classes: BTreeMap, } pub fn compute_state_diff_hash(states: StateUpdates) -> Felt { diff --git a/crates/katana/primitives/src/transaction.rs b/crates/katana/primitives/src/transaction.rs index 6bcd78e831..918742e710 100644 --- a/crates/katana/primitives/src/transaction.rs +++ b/crates/katana/primitives/src/transaction.rs @@ -1,8 +1,10 @@ +use std::sync::Arc; + use alloy_primitives::B256; use derive_more::{AsRef, Deref, From}; use crate::chain::ChainId; -use crate::class::{ClassHash, CompiledClass, CompiledClassHash, FlattenedSierraClass}; +use crate::class::{ClassHash, CompiledClassHash, ContractClass}; use crate::contract::{ContractAddress, Nonce}; use crate::da::DataAvailabilityMode; use crate::fee::ResourceBoundsMapping; @@ -21,7 +23,9 @@ pub type TxNumber = u64; /// The transaction types as defined by the [Starknet API]. /// /// [Starknet API]: https://github.com/starkware-libs/starknet-specs/blob/b5c43955b1868b8e19af6d1736178e02ec84e678/api/starknet_api_openrpc.json -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +#[derive( + Debug, Clone, Copy, PartialEq, Eq, Default, strum_macros::EnumString, strum_macros::Display, +)] #[cfg_attr(feature = "arbitrary", derive(::arbitrary::Arbitrary))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum TxType { @@ -40,6 +44,9 @@ pub enum TxType { /// It is only used internally for handling messages sent from L1. Therefore, it is not a /// transaction that can be broadcasted like the other transaction types. L1Handler, + + /// Leagcy transaction type for deploying new contracts. + Deploy, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -50,6 +57,32 @@ pub enum Tx { Declare(DeclareTx), L1Handler(L1HandlerTx), DeployAccount(DeployAccountTx), + Deploy(DeployTx), +} + +impl Tx { + /// Get the transaction version. + pub fn version(&self) -> Felt { + match self { + Tx::Invoke(tx) => match tx { + InvokeTx::V0(_) => Felt::ZERO, + InvokeTx::V1(_) => Felt::ONE, + InvokeTx::V3(_) => Felt::THREE, + }, + Tx::Declare(tx) => match tx { + DeclareTx::V0(_) => Felt::ZERO, + DeclareTx::V1(_) => Felt::ONE, + DeclareTx::V2(_) => Felt::TWO, + DeclareTx::V3(_) => Felt::THREE, + }, + Tx::L1Handler(tx) => tx.version, + Tx::DeployAccount(tx) => match tx { + DeployAccountTx::V1(_) => Felt::ONE, + DeployAccountTx::V3(_) => Felt::THREE, + }, + Tx::Deploy(tx) => tx.version, + } + } } #[derive(Debug)] @@ -134,10 +167,8 @@ impl ExecutableTxWithHash { #[derive(Debug, Clone, AsRef, Deref)] pub struct DeclareTxWithClass { - /// The Sierra class, if any. - pub sierra_class: Option, - /// The compiled contract class. - pub compiled_class: CompiledClass, + /// The contract class. + pub class: Arc, /// The raw transaction. #[deref] #[as_ref] @@ -145,12 +176,9 @@ pub struct DeclareTxWithClass { } impl DeclareTxWithClass { - pub fn new_with_classes( - transaction: DeclareTx, - sierra_class: FlattenedSierraClass, - compiled_class: CompiledClass, - ) -> Self { - Self { sierra_class: Some(sierra_class), compiled_class, transaction } + pub fn new(transaction: DeclareTx, class: ContractClass) -> Self { + let class = Arc::new(class); + Self { class, transaction } } } @@ -158,10 +186,27 @@ impl DeclareTxWithClass { #[cfg_attr(feature = "arbitrary", derive(::arbitrary::Arbitrary))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum InvokeTx { + V0(InvokeTxV0), V1(InvokeTxV1), V3(InvokeTxV3), } +#[derive(Debug, Clone, Default, PartialEq, Eq)] +#[cfg_attr(feature = "arbitrary", derive(::arbitrary::Arbitrary))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct InvokeTxV0 { + /// The account address which the transaction is initiated from. + pub contract_address: ContractAddress, + /// Entry point selector + pub entry_point_selector: Felt, + /// The data used as the input to the execute entry point of sender account contract. + pub calldata: Vec, + /// The transaction signature associated with the sender address. + pub signature: Vec, + /// The max fee that the sender is willing to pay for the transaction. + pub max_fee: u128, +} + #[derive(Debug, Clone, Default, PartialEq, Eq)] #[cfg_attr(feature = "arbitrary", derive(::arbitrary::Arbitrary))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] @@ -169,6 +214,7 @@ pub struct InvokeTxV1 { /// The chain id of the chain on which the transaction is initiated. /// /// Used as a simple replay attack protection. + #[serde(default)] pub chain_id: ChainId, /// The account address which the transaction is initiated from. pub sender_address: ContractAddress, @@ -190,6 +236,7 @@ pub struct InvokeTxV3 { /// The chain id of the chain on which the transaction is initiated. /// /// Used as a simple replay attack protection. + #[serde(default)] pub chain_id: ChainId, /// The account address which the transaction is initiated from. pub sender_address: ContractAddress, @@ -223,6 +270,10 @@ impl InvokeTx { /// Compute the hash of the transaction. pub fn calculate_hash(&self, is_query: bool) -> TxHash { match self { + InvokeTx::V0(..) => { + todo!() + } + InvokeTx::V1(tx) => compute_invoke_v1_tx_hash( Felt::from(tx.sender_address), &tx.calldata, @@ -257,11 +308,13 @@ pub enum DeclareTx { V1(DeclareTxV1), V2(DeclareTxV2), V3(DeclareTxV3), + V0(DeclareTxV0), } impl DeclareTx { pub fn class_hash(&self) -> ClassHash { match self { + DeclareTx::V0(tx) => tx.class_hash, DeclareTx::V1(tx) => tx.class_hash, DeclareTx::V2(tx) => tx.class_hash, DeclareTx::V3(tx) => tx.class_hash, @@ -269,6 +322,26 @@ impl DeclareTx { } } +/// Represents a legacy v0 declare transaction type. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +#[cfg_attr(feature = "arbitrary", derive(::arbitrary::Arbitrary))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct DeclareTxV0 { + /// The chain id of the chain on which the transaction is initiated. + /// + /// Used as a simple replay attack protection. + #[serde(default)] + pub chain_id: ChainId, + /// The account address which the transaction is initiated from. + pub sender_address: ContractAddress, + /// The transaction signature associated with the sender address. + pub signature: Vec, + /// The class hash of the contract class to be declared. + pub class_hash: ClassHash, + /// The max fee that the sender is willing to pay for the transaction. + pub max_fee: u128, +} + /// Represents a declare transaction type. #[derive(Debug, Clone, Default, PartialEq, Eq)] #[cfg_attr(feature = "arbitrary", derive(::arbitrary::Arbitrary))] @@ -277,6 +350,7 @@ pub struct DeclareTxV1 { /// The chain id of the chain on which the transaction is initiated. /// /// Used as a simple replay attack protection. + #[serde(default)] pub chain_id: ChainId, /// The account address which the transaction is initiated from. pub sender_address: ContractAddress, @@ -299,6 +373,7 @@ pub struct DeclareTxV2 { /// The chain id of the chain on which the transaction is initiated. /// /// Used as a simple replay attack protection. + #[serde(default)] pub chain_id: ChainId, /// The account address which the transaction is initiated from. pub sender_address: ContractAddress, @@ -323,6 +398,7 @@ pub struct DeclareTxV3 { /// The chain id of the chain on which the transaction is initiated. /// /// Used as a simple replay attack protection. + #[serde(default)] pub chain_id: ChainId, /// The account address which the transaction is initiated from. pub sender_address: ContractAddress, @@ -353,6 +429,8 @@ impl DeclareTx { /// Compute the hash of the transaction. pub fn calculate_hash(&self, is_query: bool) -> TxHash { match self { + DeclareTx::V0(..) => todo!(), + DeclareTx::V1(tx) => compute_declare_v1_tx_hash( Felt::from(tx.sender_address), tx.class_hash, @@ -399,6 +477,7 @@ pub struct L1HandlerTx { /// The L1 to L2 message nonce. pub nonce: Nonce, /// The chain id. + #[serde(default)] pub chain_id: ChainId, /// Amount of fee paid on L1. pub paid_fee_on_l1: u128, @@ -452,6 +531,7 @@ pub struct DeployAccountTxV1 { /// The chain id of the chain on which the transaction is initiated. /// /// Used as a simple replay attack protection. + #[serde(default)] pub chain_id: ChainId, /// The nonce value of the account. Corresponds to the number of transactions initiated by /// sender. @@ -477,6 +557,7 @@ pub struct DeployAccountTxV3 { /// The chain id of the chain on which the transaction is initiated. /// /// Used as a simple replay attack protection. + #[serde(default)] pub chain_id: ChainId, /// The nonce value of the account. Corresponds to the number of transactions initiated by /// sender. @@ -537,6 +618,23 @@ impl DeployAccountTx { } } +/// Legacy Deploy transacation type. +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "arbitrary", derive(::arbitrary::Arbitrary))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct DeployTx { + /// The contract address of the account contract that will be deployed. + pub contract_address: Felt, + /// The salt used to generate the contract address. + pub contract_address_salt: Felt, + /// The input data to the constructor function of the contract class. + pub constructor_calldata: Vec, + /// The hash of the contract class from which the account contract will be deployed from. + pub class_hash: Felt, + /// Transaction version. + pub version: Felt, +} + #[derive(Debug, Clone, AsRef, Deref, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct TxWithHash { diff --git a/crates/katana/primitives/src/utils/class.rs b/crates/katana/primitives/src/utils/class.rs index ed3602872c..974cbf0d00 100644 --- a/crates/katana/primitives/src/utils/class.rs +++ b/crates/katana/primitives/src/utils/class.rs @@ -1,11 +1,8 @@ use anyhow::Result; use katana_cairo::lang::starknet_classes::casm_contract_class::CasmContractClass; -use katana_cairo::lang::starknet_classes::contract_class::ContractClass; use serde_json::Value; -use crate::class::{ - CompiledClass, DeprecatedCompiledClass, SierraClass, SierraCompiledClass, SierraProgram, -}; +use crate::class::{CompiledClass, ContractClass, LegacyContractClass, SierraContractClass}; // TODO: this was taken from the current network limit // https://docs.starknet.io/documentation/tools/limits_and_triggers/. @@ -15,32 +12,27 @@ use crate::class::{ // pub const MAX_BYTECODE_SIZE: usize = 81_290; pub fn parse_compiled_class(artifact: Value) -> Result { - if let Ok(class) = parse_compiled_class_v1(artifact.clone()) { - Ok(CompiledClass::Class(class)) + if let Ok(casm) = parse_compiled_class_v1(artifact.clone()) { + Ok(CompiledClass::Class(casm)) } else { - Ok(CompiledClass::Deprecated(parse_deprecated_compiled_class(artifact)?)) + Ok(CompiledClass::Legacy(parse_deprecated_compiled_class(artifact)?)) } } -pub fn parse_compiled_class_v1(class: Value) -> Result { +pub fn parse_compiled_class_v1(class: Value) -> Result { + use katana_cairo::lang::starknet_classes::contract_class::ContractClass; let class: ContractClass = serde_json::from_value(class)?; - - let program = class.extract_sierra_program()?; - let entry_points_by_type = class.entry_points_by_type.clone(); - let sierra = SierraProgram { program, entry_points_by_type }; - - let casm = CasmContractClass::from_contract_class(class, true, usize::MAX)?; - - Ok(SierraCompiledClass { casm, sierra }) + Ok(CasmContractClass::from_contract_class(class, true, usize::MAX)?) } /// Parse a [`str`] into a [`SierraClass`]. -pub fn parse_sierra_class(class: &str) -> Result { - serde_json::from_str(class) +pub fn parse_sierra_class(class: &str) -> Result { + let sierra = serde_json::from_str::(class)?; + Ok(ContractClass::Class(sierra)) } pub fn parse_deprecated_compiled_class( class: Value, -) -> Result { +) -> Result { serde_json::from_value(class) } diff --git a/crates/katana/rpc/rpc-api/src/starknet.rs b/crates/katana/rpc/rpc-api/src/starknet.rs index 85aaebc8fc..42a25e8581 100644 --- a/crates/katana/rpc/rpc-api/src/starknet.rs +++ b/crates/katana/rpc/rpc-api/src/starknet.rs @@ -3,12 +3,14 @@ use jsonrpsee::core::RpcResult; use jsonrpsee::proc_macros::rpc; use katana_primitives::block::{BlockIdOrTag, BlockNumber}; +use katana_primitives::class::ClassHash; use katana_primitives::transaction::TxHash; -use katana_primitives::Felt; +use katana_primitives::{ContractAddress, Felt}; use katana_rpc_types::block::{ BlockHashAndNumber, BlockTxCount, MaybePendingBlockWithReceipts, MaybePendingBlockWithTxHashes, MaybePendingBlockWithTxs, }; +use katana_rpc_types::class::RpcContractClass; use katana_rpc_types::event::{EventFilterWithPage, EventsPage}; use katana_rpc_types::message::MsgFromL1; use katana_rpc_types::receipt::TxReceiptWithBlockInfo; @@ -17,9 +19,10 @@ use katana_rpc_types::transaction::{ BroadcastedDeclareTx, BroadcastedDeployAccountTx, BroadcastedInvokeTx, BroadcastedTx, DeclareTxResult, DeployAccountTxResult, InvokeTxResult, Tx, }; +use katana_rpc_types::trie::{ContractStorageKeys, GetStorageProofResponse}; use katana_rpc_types::{ - ContractClass, FeeEstimate, FeltAsHex, FunctionCall, SimulationFlag, - SimulationFlagForEstimateFee, SyncingStatus, + FeeEstimate, FeltAsHex, FunctionCall, SimulationFlag, SimulationFlagForEstimateFee, + SyncingStatus, }; use starknet::core::types::{ SimulatedTransaction, TransactionStatus, TransactionTrace, TransactionTraceWithHash, @@ -101,8 +104,11 @@ pub trait StarknetApi { /// Get the contract class definition in the given block associated with the given hash. #[method(name = "getClass")] - async fn get_class(&self, block_id: BlockIdOrTag, class_hash: Felt) - -> RpcResult; + async fn get_class( + &self, + block_id: BlockIdOrTag, + class_hash: Felt, + ) -> RpcResult; /// Get the contract class hash in the given block for the contract deployed at the given /// address. @@ -119,7 +125,7 @@ pub trait StarknetApi { &self, block_id: BlockIdOrTag, contract_address: Felt, - ) -> RpcResult; + ) -> RpcResult; /// Get the number of transactions in a block given a block id. #[method(name = "getBlockTransactionCount")] @@ -179,6 +185,18 @@ pub trait StarknetApi { block_id: BlockIdOrTag, contract_address: Felt, ) -> RpcResult; + + /// Get merkle paths in one of the state tries: global state, classes, individual contract. A + /// single request can query for any mix of the three types of storage proofs (classes, + /// contracts, and storage). + #[method(name = "getStorageProof")] + async fn get_storage_proof( + &self, + block_id: BlockIdOrTag, + class_hashes: Option>, + contract_addresses: Option>, + contracts_storage_keys: Option>, + ) -> RpcResult; } /// Write API. diff --git a/crates/katana/rpc/rpc-types-builder/src/state_update.rs b/crates/katana/rpc/rpc-types-builder/src/state_update.rs index 8d898485d0..ba9fa920fb 100644 --- a/crates/katana/rpc/rpc-types-builder/src/state_update.rs +++ b/crates/katana/rpc/rpc-types-builder/src/state_update.rs @@ -1,7 +1,7 @@ use katana_primitives::block::BlockHashOrNumber; use katana_primitives::Felt; use katana_provider::traits::block::{BlockHashProvider, BlockNumberProvider}; -use katana_provider::traits::state::StateRootProvider; +use katana_provider::traits::state::{StateFactoryProvider, StateRootProvider}; use katana_provider::traits::state_update::StateUpdateProvider; use katana_provider::ProviderResult; use katana_rpc_types::state_update::{StateDiff, StateUpdate}; @@ -21,7 +21,7 @@ impl

StateUpdateBuilder

{ impl

StateUpdateBuilder

where - P: BlockHashProvider + BlockNumberProvider + StateRootProvider + StateUpdateProvider, + P: BlockHashProvider + BlockNumberProvider + StateFactoryProvider + StateUpdateProvider, { /// Builds a state update for the given block. pub fn build(self) -> ProviderResult> { @@ -30,16 +30,23 @@ where return Ok(None); }; - let new_root = StateRootProvider::state_root(&self.provider, self.block_id)? - .expect("should exist if block exists"); + let new_root = self + .provider + .historical(self.block_id)? + .expect("should exist if block exists") + .state_root()?; + let old_root = { let block_num = BlockNumberProvider::block_number_by_hash(&self.provider, block_hash)? .expect("should exist if block exists"); match block_num { 0 => Felt::ZERO, - _ => StateRootProvider::state_root(&self.provider, (block_num - 1).into())? - .expect("should exist if not genesis"), + _ => self + .provider + .historical((block_num - 1).into())? + .expect("should exist if block exists") + .state_root()?, } }; diff --git a/crates/katana/rpc/rpc-types/Cargo.toml b/crates/katana/rpc/rpc-types/Cargo.toml index 52cb37c664..8cdfe76dc3 100644 --- a/crates/katana/rpc/rpc-types/Cargo.toml +++ b/crates/katana/rpc/rpc-types/Cargo.toml @@ -9,10 +9,11 @@ version.workspace = true [dependencies] katana-cairo.workspace = true katana-core.workspace = true -katana-pool.workspace = true katana-executor.workspace = true +katana-pool.workspace = true katana-primitives.workspace = true katana-provider.workspace = true +katana-trie.workspace = true anyhow.workspace = true derive_more.workspace = true @@ -26,6 +27,9 @@ starknet.workspace = true thiserror.workspace = true alloy-primitives.workspace = true +flate2.workspace = true +serde_json_pythonic = "0.1.2" +similar-asserts.workspace = true [dev-dependencies] rstest.workspace = true diff --git a/crates/katana/rpc/rpc-types/src/class.rs b/crates/katana/rpc/rpc-types/src/class.rs new file mode 100644 index 0000000000..f9a3521097 --- /dev/null +++ b/crates/katana/rpc/rpc-types/src/class.rs @@ -0,0 +1,304 @@ +//! The types used are intentionally chosen so that they can be easily converted from RPC to the +//! internal types without having to rely on intermediary representation. + +use std::collections::HashMap; +use std::io::{self, Write}; + +use katana_cairo::lang::starknet_classes::contract_class::ContractEntryPoints; +use katana_cairo::lang::utils::bigint::BigUintAsHex; +use katana_cairo::starknet_api::deprecated_contract_class::{ + ContractClassAbiEntry, EntryPoint, EntryPointType, Program as LegacyProgram, +}; +use katana_cairo::starknet_api::serde_utils::deserialize_optional_contract_class_abi_entry_vector; +use katana_primitives::class::{ContractClass, LegacyContractClass, SierraContractClass}; +use katana_primitives::{ + Felt, {self}, +}; +use serde::{Deserialize, Serialize}; +use serde_json_pythonic::to_string_pythonic; +use starknet::core::serde::byte_array::base64; +use starknet::core::types::{CompressedLegacyContractClass, FlattenedSierraClass}; + +/// RPC representation of the contract class. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum RpcContractClass { + Class(RpcSierraContractClass), + Legacy(RpcLegacyContractClass), +} + +#[derive(Debug, thiserror::Error)] +pub enum ConversionError { + #[error(transparent)] + Io(#[from] io::Error), + + #[error(transparent)] + Json(#[from] serde_json::Error), + + #[error(transparent)] + AbiPythonic(#[from] serde_json_pythonic::Error), +} + +impl TryFrom for RpcContractClass { + type Error = ConversionError; + + fn try_from(value: ContractClass) -> Result { + match value { + ContractClass::Class(class) => { + Ok(Self::Class(RpcSierraContractClass::try_from(class)?)) + } + ContractClass::Legacy(class) => { + Ok(Self::Legacy(RpcLegacyContractClass::try_from(class)?)) + } + } + } +} + +impl TryFrom for ContractClass { + type Error = ConversionError; + + fn try_from(value: RpcContractClass) -> Result { + match value { + RpcContractClass::Class(class) => { + Ok(Self::Class(SierraContractClass::try_from(class)?)) + } + RpcContractClass::Legacy(class) => { + Ok(Self::Legacy(LegacyContractClass::try_from(class)?)) + } + } + } +} + +// -- SIERRA CLASS + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct RpcSierraContractClass { + pub sierra_program: Vec, + pub contract_class_version: String, + pub entry_points_by_type: ContractEntryPoints, + pub abi: String, +} + +impl TryFrom for RpcSierraContractClass { + type Error = ConversionError; + + fn try_from(value: SierraContractClass) -> Result { + let abi = to_string_pythonic(&value.abi.unwrap_or_default())?; + let program = value.sierra_program.into_iter().map(|f| f.value.into()).collect::>(); + + Ok(Self { + abi, + sierra_program: program, + entry_points_by_type: value.entry_points_by_type, + contract_class_version: value.contract_class_version, + }) + } +} + +impl TryFrom for SierraContractClass { + type Error = ConversionError; + + fn try_from(value: RpcSierraContractClass) -> Result { + use katana_cairo::lang::starknet_classes::abi; + + let abi = serde_json::from_str::>(&value.abi)?; + let program = value + .sierra_program + .into_iter() + .map(|f| BigUintAsHex { value: f.to_biguint() }) + .collect::>(); + + Ok(Self { + abi, + sierra_program: program, + sierra_program_debug_info: None, + entry_points_by_type: value.entry_points_by_type, + contract_class_version: value.contract_class_version, + }) + } +} + +// -- LEGACY CLASS + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RpcLegacyContractClass { + /// A base64 representation of the compressed program code + #[serde(with = "base64")] + pub program: Vec, + /// The selector of each entry point is a unique identifier in the program. + pub entry_points_by_type: HashMap>, + // Starknet does not verify the abi. If we can't parse it, we set it to None. + #[serde(default, deserialize_with = "deserialize_optional_contract_class_abi_entry_vector")] + pub abi: Option>, +} + +impl TryFrom for RpcLegacyContractClass { + type Error = ConversionError; + + fn try_from(value: LegacyContractClass) -> Result { + let program = compress_legacy_program(value.program)?; + Ok(Self { program, abi: value.abi, entry_points_by_type: value.entry_points_by_type }) + } +} + +impl TryFrom for LegacyContractClass { + type Error = ConversionError; + + fn try_from(value: RpcLegacyContractClass) -> Result { + let program = decompress_legacy_program(&value.program)?; + Ok(Self { program, abi: value.abi, entry_points_by_type: value.entry_points_by_type }) + } +} + +fn compress_legacy_program(mut program: LegacyProgram) -> Result, ConversionError> { + // We don't need the debug info in the compressed program. + program.debug_info = serde_json::to_value::>(None)?; + + let bytes = serde_json::to_vec(&program)?; + let mut gzip_encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::fast()); + Write::write_all(&mut gzip_encoder, &bytes)?; + Ok(gzip_encoder.finish()?) +} + +fn decompress_legacy_program(compressed_data: &[u8]) -> Result { + let mut decoder = flate2::read::GzDecoder::new(compressed_data); + let mut decompressed = Vec::new(); + std::io::Read::read_to_end(&mut decoder, &mut decompressed)?; + Ok(serde_json::from_slice::(&decompressed)?) +} + +// Conversion from `starknet-rs` types for convenience. +// +// These are not the most efficient way to convert the types, but they are the most convenient. +// Considering we are not using `starknet-rs` types for the contract class definitions in Katana and +// mainly for utility purposes, these conversions should be avoided from being used in a program +// hot path. + +impl TryFrom for RpcSierraContractClass { + type Error = ConversionError; + + fn try_from(value: FlattenedSierraClass) -> Result { + let value = serde_json::to_value(value)?; + let class = serde_json::from_value::(value)?; + Ok(class) + } +} + +impl TryFrom for RpcLegacyContractClass { + type Error = ConversionError; + + fn try_from(value: CompressedLegacyContractClass) -> Result { + let value = serde_json::to_value(value)?; + let class = serde_json::from_value::(value)?; + Ok(class) + } +} + +#[cfg(test)] +mod tests { + use katana_primitives::class::{ContractClass, LegacyContractClass, SierraContractClass}; + use starknet::core::types::contract::legacy::LegacyContractClass as StarknetRsLegacyContractClass; + use starknet::core::types::contract::SierraClass; + + use super::RpcLegacyContractClass; + use crate::class::RpcSierraContractClass; + + #[test] + fn rt() { + let json = include_str!("../../../contracts/build/default_account.json"); + let class = serde_json::from_str::(json).unwrap(); + + let rpc = RpcSierraContractClass::try_from(class.clone()).unwrap(); + let rt = SierraContractClass::try_from(rpc).unwrap(); + + assert_eq!(class.abi, rt.abi); + assert_eq!(class.sierra_program, rt.sierra_program); + assert_eq!(class.entry_points_by_type, rt.entry_points_by_type); + assert_eq!(class.contract_class_version, rt.contract_class_version); + } + + #[test] + fn legacy_rt() { + let json = include_str!("../../../contracts/build/account.json"); + let class = serde_json::from_str::(json).unwrap(); + + let rpc = RpcLegacyContractClass::try_from(class.clone()).unwrap(); + let rt = LegacyContractClass::try_from(rpc).unwrap(); + + assert_eq!(class.abi, rt.abi); + assert_eq!(class.entry_points_by_type, rt.entry_points_by_type); + assert_eq!(class.program.builtins, rt.program.builtins); + assert_eq!(class.program.compiler_version, rt.program.compiler_version); + assert_eq!(class.program.data, rt.program.data); + assert_eq!(class.program.hints, rt.program.hints); + assert_eq!(class.program.identifiers, rt.program.identifiers); + assert_eq!(class.program.main_scope, rt.program.main_scope); + assert_eq!(class.program.prime, rt.program.prime); + assert_eq!(class.program.reference_manager, rt.program.reference_manager); + // The debug info is stripped when converting to RPC format. + assert_eq!(serde_json::to_value::>(None).unwrap(), rt.program.debug_info); + } + + #[test] + fn rt_with_starknet_rs() { + let json = include_str!("../../../contracts/build/default_account.json"); + let expected_class = serde_json::from_str::(json).unwrap(); + + // -- starknet-rs + + let starknet_rs_class = serde_json::from_str::(json).unwrap(); + let starknet_rs_hash = starknet_rs_class.class_hash().unwrap(); + let rpc = starknet_rs_class.flatten().unwrap(); + + let json = serde_json::to_string(&rpc).unwrap(); + + // -- katana + + let rpc = serde_json::from_str::(&json).unwrap(); + let class = SierraContractClass::try_from(rpc).unwrap(); + let hash = ContractClass::Class(class.clone()).class_hash().unwrap(); + + assert_eq!(starknet_rs_hash, hash); + assert_eq!(expected_class.abi, class.abi); + assert_eq!(expected_class.sierra_program, class.sierra_program); + assert_eq!(expected_class.entry_points_by_type, class.entry_points_by_type); + assert_eq!(expected_class.contract_class_version, class.contract_class_version); + } + + #[test] + fn legacy_rt_with_starknet_rs() { + use similar_asserts::assert_eq; + + let json = include_str!("../../../contracts/build/erc20.json"); + let expected_class = serde_json::from_str::(json).unwrap(); + + // -- starknet-rs + + let starknet_rs_class = + serde_json::from_str::(json).unwrap(); + let starknet_rs_hash = starknet_rs_class.class_hash().unwrap(); + let starknet_rs_rpc = starknet_rs_class.compress().unwrap(); + + let json = serde_json::to_string(&starknet_rs_rpc).unwrap(); + + // -- katana + + let rpc = serde_json::from_str::(&json).unwrap(); + let class = LegacyContractClass::try_from(rpc).unwrap(); + let hash = ContractClass::Legacy(class.clone()).class_hash().unwrap(); + + assert_eq!(starknet_rs_hash, hash); + assert_eq!(expected_class.abi, class.abi); + assert_eq!(expected_class.entry_points_by_type, class.entry_points_by_type); + assert_eq!(expected_class.program.builtins, class.program.builtins); + assert_eq!(expected_class.program.compiler_version, class.program.compiler_version); + assert_eq!(expected_class.program.data, class.program.data); + assert_eq!(expected_class.program.hints, class.program.hints); + assert_eq!(expected_class.program.identifiers, class.program.identifiers); + assert_eq!(expected_class.program.main_scope, class.program.main_scope); + assert_eq!(expected_class.program.prime, class.program.prime); + assert_eq!(expected_class.program.reference_manager, class.program.reference_manager); + // The debug info is stripped when converting to RPC format. + assert_eq!(serde_json::to_value::>(None).unwrap(), class.program.debug_info); + } +} diff --git a/crates/katana/rpc/rpc-types/src/error/starknet.rs b/crates/katana/rpc/rpc-types/src/error/starknet.rs index 9d945b360a..5c2db876b0 100644 --- a/crates/katana/rpc/rpc-types/src/error/starknet.rs +++ b/crates/katana/rpc/rpc-types/src/error/starknet.rs @@ -3,6 +3,7 @@ use jsonrpsee::types::error::CallError; use jsonrpsee::types::ErrorObject; use katana_pool::validation::error::InvalidTransactionError; use katana_pool::PoolError; +use katana_primitives::block::BlockNumber; use katana_primitives::event::ContinuationTokenError; use katana_provider::error::ProviderError; use serde::Serialize; @@ -77,12 +78,24 @@ pub enum StarknetApiError { UnsupportedContractClassVersion, #[error("An unexpected error occured")] UnexpectedError { reason: String }, - #[error("Too many storage keys requested")] - ProofLimitExceeded, #[error("Too many keys provided in a filter")] TooManyKeysInFilter, #[error("Failed to fetch pending transactions")] FailedToFetchPendingTransactions, + #[error("The node doesn't support storage proofs for blocks that are too far in the past")] + StorageProofNotSupported { + /// The oldest block whose storage proof can be obtained. + oldest_block: BlockNumber, + /// The block of the storage proof that is being requested. + requested_block: BlockNumber, + }, + #[error("Proof limit exceeded")] + ProofLimitExceeded { + /// The limit for the total number of keys that can be specified in a single request. + limit: u64, + /// The total number of keys that is being requested. + total: u64, + }, } impl StarknetApiError { @@ -103,6 +116,7 @@ impl StarknetApiError { StarknetApiError::FailedToFetchPendingTransactions => 38, StarknetApiError::ContractError { .. } => 40, StarknetApiError::TransactionExecutionError { .. } => 41, + StarknetApiError::StorageProofNotSupported { .. } => 42, StarknetApiError::InvalidContractClass => 50, StarknetApiError::ClassAlreadyDeclared => 51, StarknetApiError::InvalidTransactionNonce { .. } => 52, @@ -117,7 +131,7 @@ impl StarknetApiError { StarknetApiError::UnsupportedTransactionVersion => 61, StarknetApiError::UnsupportedContractClassVersion => 62, StarknetApiError::UnexpectedError { .. } => 63, - StarknetApiError::ProofLimitExceeded => 10000, + StarknetApiError::ProofLimitExceeded { .. } => 1000, } } @@ -128,8 +142,10 @@ impl StarknetApiError { pub fn data(&self) -> Option { match self { StarknetApiError::ContractError { .. } - | StarknetApiError::UnexpectedError { .. } | StarknetApiError::PageSizeTooBig { .. } + | StarknetApiError::UnexpectedError { .. } + | StarknetApiError::ProofLimitExceeded { .. } + | StarknetApiError::StorageProofNotSupported { .. } | StarknetApiError::TransactionExecutionError { .. } => Some(serde_json::json!(self)), StarknetApiError::InvalidTransactionNonce { reason } @@ -284,7 +300,6 @@ mod tests { #[case(StarknetApiError::InvalidMessageSelector, 21, "Invalid message selector")] #[case(StarknetApiError::NonAccount, 58, "Sender address in not an account contract")] #[case(StarknetApiError::InvalidTxnIndex, 27, "Invalid transaction index in a block")] - #[case(StarknetApiError::ProofLimitExceeded, 10000, "Too many storage keys requested")] #[case(StarknetApiError::TooManyKeysInFilter, 34, "Too many keys provided in a filter")] #[case(StarknetApiError::ContractClassSizeIsTooLarge, 57, "Contract class size is too large")] #[case(StarknetApiError::FailedToFetchPendingTransactions, 38, "Failed to fetch pending transactions")] @@ -372,6 +387,30 @@ mod tests { "max_allowed": 500 }), )] + #[case( + StarknetApiError::StorageProofNotSupported { + oldest_block: 10, + requested_block: 9 + }, + 42, + "The node doesn't support storage proofs for blocks that are too far in the past", + json!({ + "oldest_block": 10, + "requested_block": 9 + }), + )] + #[case( + StarknetApiError::ProofLimitExceeded { + limit: 5, + total: 10 + }, + 1000, + "Proof limit exceeded", + json!({ + "limit": 5, + "total": 10 + }), + )] fn test_starknet_api_error_to_error_conversion_data_some( #[case] starknet_error: StarknetApiError, #[case] expected_code: i32, diff --git a/crates/katana/rpc/rpc-types/src/lib.rs b/crates/katana/rpc/rpc-types/src/lib.rs index 708b905994..3ee07c1a19 100644 --- a/crates/katana/rpc/rpc-types/src/lib.rs +++ b/crates/katana/rpc/rpc-types/src/lib.rs @@ -5,6 +5,7 @@ pub mod account; pub mod block; +pub mod class; pub mod error; pub mod event; pub mod message; @@ -12,6 +13,7 @@ pub mod receipt; pub mod state_update; pub mod trace; pub mod transaction; +pub mod trie; mod utils; use std::ops::Deref; @@ -49,7 +51,7 @@ pub type FunctionCall = starknet::core::types::FunctionCall; pub type FeeEstimate = starknet::core::types::FeeEstimate; -pub type ContractClass = starknet::core::types::ContractClass; +// pub type ContractClass = starknet::core::types::ContractClass; pub type SimulationFlagForEstimateFee = starknet::core::types::SimulationFlagForEstimateFee; diff --git a/crates/katana/rpc/rpc-types/src/state_update.rs b/crates/katana/rpc/rpc-types/src/state_update.rs index d4248b1a1f..ae9cf1d37c 100644 --- a/crates/katana/rpc/rpc-types/src/state_update.rs +++ b/crates/katana/rpc/rpc-types/src/state_update.rs @@ -1,3 +1,4 @@ +use katana_primitives::class::ClassHash; use serde::{Deserialize, Serialize}; use starknet::core::types::{ ContractStorageDiffItem, DeclaredClassItem, DeployedContractItem, NonceUpdate, StorageEntry, @@ -49,6 +50,9 @@ impl From for StateDiff { .map(|(addr, nonce)| NonceUpdate { nonce, contract_address: addr.into() }) .collect(); + let deprecated_declared_classes: Vec = + value.deprecated_declared_classes.into_iter().collect(); + let declared_classes: Vec = value .declared_classes .into_iter() @@ -81,8 +85,8 @@ impl From for StateDiff { storage_diffs, declared_classes, deployed_contracts, + deprecated_declared_classes, replaced_classes: Default::default(), - deprecated_declared_classes: Default::default(), }) } } diff --git a/crates/katana/rpc/rpc-types/src/transaction.rs b/crates/katana/rpc/rpc-types/src/transaction.rs index 9af3598d80..4c3e7b6dc6 100644 --- a/crates/katana/rpc/rpc-types/src/transaction.rs +++ b/crates/katana/rpc/rpc-types/src/transaction.rs @@ -3,12 +3,9 @@ use std::sync::Arc; use anyhow::Result; use derive_more::Deref; use katana_primitives::chain::ChainId; -use katana_primitives::class::ClassHash; +use katana_primitives::class::{ClassHash, ContractClass}; use katana_primitives::contract::ContractAddress; -use katana_primitives::conversion::rpc::{ - compiled_class_hash_from_flattened_sierra_class, flattened_sierra_to_compiled_class, - legacy_rpc_to_compiled_class, -}; +use katana_primitives::conversion::rpc::compiled_class_hash_from_flattened_sierra_class; use katana_primitives::da::DataAvailabilityMode; use katana_primitives::fee::{ResourceBounds, ResourceBoundsMapping}; use katana_primitives::transaction::{ @@ -25,6 +22,7 @@ use starknet::core::types::{ }; use starknet::core::utils::get_contract_address; +use crate::class::{RpcContractClass, RpcLegacyContractClass, RpcSierraContractClass}; use crate::receipt::TxReceiptWithBlockInfo; pub const CHUNK_SIZE_DEFAULT: u64 = 100; @@ -94,72 +92,73 @@ impl BroadcastedDeclareTx { Ok(is_valid) } + // TODO: change the contract class type for the broadcasted tx to katana-rpc-types instead for + // easier conversion. /// This function assumes that the compiled class hash is valid. pub fn try_into_tx_with_chain_id(self, chain_id: ChainId) -> Result { match self.0 { BroadcastedDeclareTransaction::V1(tx) => { - let (class_hash, compiled_class) = - legacy_rpc_to_compiled_class(&tx.contract_class)?; - - Ok(DeclareTxWithClass { - compiled_class, - sierra_class: None, - transaction: DeclareTx::V1(DeclareTxV1 { - chain_id, - class_hash, - nonce: tx.nonce, - signature: tx.signature, - sender_address: tx.sender_address.into(), - max_fee: tx.max_fee.to_u128().expect("max fee is too large"), - }), - }) + let rpc_class = Arc::unwrap_or_clone(tx.contract_class); + let rpc_class = RpcLegacyContractClass::try_from(rpc_class).unwrap(); + let class = ContractClass::try_from(RpcContractClass::Legacy(rpc_class)).unwrap(); + + let class_hash = class.class_hash().unwrap(); + + let tx = DeclareTx::V1(DeclareTxV1 { + chain_id, + class_hash, + nonce: tx.nonce, + signature: tx.signature, + sender_address: tx.sender_address.into(), + max_fee: tx.max_fee.to_u128().expect("max fee is too large"), + }); + + Ok(DeclareTxWithClass::new(tx, class)) } BroadcastedDeclareTransaction::V2(tx) => { - // TODO: avoid computing the class hash again - let (class_hash, _, compiled_class) = - flattened_sierra_to_compiled_class(&tx.contract_class)?; - - Ok(DeclareTxWithClass { - compiled_class, - sierra_class: Arc::into_inner(tx.contract_class), - transaction: DeclareTx::V2(DeclareTxV2 { - chain_id, - class_hash, - nonce: tx.nonce, - signature: tx.signature, - sender_address: tx.sender_address.into(), - compiled_class_hash: tx.compiled_class_hash, - max_fee: tx.max_fee.to_u128().expect("max fee is too large"), - }), - }) + let class_hash = tx.contract_class.class_hash(); + + let rpc_class = Arc::unwrap_or_clone(tx.contract_class); + let rpc_class = RpcSierraContractClass::try_from(rpc_class).unwrap(); + let class = ContractClass::try_from(RpcContractClass::Class(rpc_class)).unwrap(); + + let tx = DeclareTx::V2(DeclareTxV2 { + chain_id, + class_hash, + nonce: tx.nonce, + signature: tx.signature, + sender_address: tx.sender_address.into(), + compiled_class_hash: tx.compiled_class_hash, + max_fee: tx.max_fee.to_u128().expect("max fee is too large"), + }); + + Ok(DeclareTxWithClass::new(tx, class)) } BroadcastedDeclareTransaction::V3(tx) => { - // TODO: avoid computing the class hash again - let (class_hash, _, compiled_class) = - flattened_sierra_to_compiled_class(&tx.contract_class)?; - - Ok(DeclareTxWithClass { - compiled_class, - sierra_class: Arc::into_inner(tx.contract_class), - transaction: DeclareTx::V3(DeclareTxV3 { - chain_id, - class_hash, - nonce: tx.nonce, - signature: tx.signature, - sender_address: tx.sender_address.into(), - compiled_class_hash: tx.compiled_class_hash, - tip: tx.tip, - paymaster_data: tx.paymaster_data, - account_deployment_data: tx.account_deployment_data, - resource_bounds: from_rpc_resource_bounds(tx.resource_bounds), - fee_data_availability_mode: from_rpc_da_mode(tx.fee_data_availability_mode), - nonce_data_availability_mode: from_rpc_da_mode( - tx.nonce_data_availability_mode, - ), - }), - }) + let class_hash = tx.contract_class.class_hash(); + + let rpc_class = Arc::unwrap_or_clone(tx.contract_class); + let rpc_class = RpcSierraContractClass::try_from(rpc_class).unwrap(); + let class = ContractClass::try_from(RpcContractClass::Class(rpc_class)).unwrap(); + + let tx = DeclareTx::V3(DeclareTxV3 { + chain_id, + class_hash, + tip: tx.tip, + nonce: tx.nonce, + signature: tx.signature, + paymaster_data: tx.paymaster_data, + sender_address: tx.sender_address.into(), + compiled_class_hash: tx.compiled_class_hash, + account_deployment_data: tx.account_deployment_data, + resource_bounds: from_rpc_resource_bounds(tx.resource_bounds), + fee_data_availability_mode: from_rpc_da_mode(tx.fee_data_availability_mode), + nonce_data_availability_mode: from_rpc_da_mode(tx.nonce_data_availability_mode), + }); + + Ok(DeclareTxWithClass::new(tx, class)) } } } @@ -265,6 +264,19 @@ impl From for Tx { let transaction_hash = value.hash; let tx = match value.transaction { InternalTx::Invoke(invoke) => match invoke { + InvokeTx::V0(tx) => starknet::core::types::Transaction::Invoke( + starknet::core::types::InvokeTransaction::V0( + starknet::core::types::InvokeTransactionV0 { + transaction_hash, + calldata: tx.calldata, + signature: tx.signature, + max_fee: tx.max_fee.into(), + contract_address: tx.contract_address.into(), + entry_point_selector: tx.entry_point_selector, + }, + ), + ), + InvokeTx::V1(tx) => starknet::core::types::Transaction::Invoke( starknet::core::types::InvokeTransaction::V1( starknet::core::types::InvokeTransactionV1 { @@ -302,6 +314,16 @@ impl From for Tx { }, InternalTx::Declare(tx) => starknet::core::types::Transaction::Declare(match tx { + DeclareTx::V0(tx) => starknet::core::types::DeclareTransaction::V0( + starknet::core::types::DeclareTransactionV0 { + transaction_hash, + signature: tx.signature, + class_hash: tx.class_hash, + max_fee: tx.max_fee.into(), + sender_address: tx.sender_address.into(), + }, + ), + DeclareTx::V1(tx) => starknet::core::types::DeclareTransaction::V1( starknet::core::types::DeclareTransactionV1 { nonce: tx.nonce, @@ -391,6 +413,16 @@ impl From for Tx { ), }) } + + InternalTx::Deploy(tx) => starknet::core::types::Transaction::Deploy( + starknet::core::types::DeployTransaction { + constructor_calldata: tx.constructor_calldata, + contract_address_salt: tx.contract_address_salt, + class_hash: tx.class_hash, + version: tx.version, + transaction_hash, + }, + ), }; Tx(tx) diff --git a/crates/katana/rpc/rpc-types/src/trie.rs b/crates/katana/rpc/rpc-types/src/trie.rs new file mode 100644 index 0000000000..00d310efb6 --- /dev/null +++ b/crates/katana/rpc/rpc-types/src/trie.rs @@ -0,0 +1,227 @@ +use std::ops::{Deref, DerefMut}; + +use katana_primitives::contract::StorageKey; +use katana_primitives::hash::StarkHash; +use katana_primitives::{ContractAddress, Felt}; +use katana_trie::bitvec::view::BitView; +use katana_trie::{BitVec, MultiProof, Path, ProofNode}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct ContractStorageKeys { + #[serde(rename = "contract_address")] + pub address: ContractAddress, + #[serde(rename = "storage_keys")] + pub keys: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct GlobalRoots { + /// The associated block hash (needed in case the caller used a block tag for the block_id + /// parameter). + pub block_hash: Felt, + pub classes_tree_root: Felt, + pub contracts_tree_root: Felt, +} + +/// Node in the Merkle-Patricia trie. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum MerkleNode { + /// Represents a path to the highest non-zero descendant node. + Edge { + /// An integer whose binary representation represents the path from the current node to its + /// highest non-zero descendant (bounded by 2^251) + path: Felt, + /// The length of the path (bounded by 251). + length: u8, + /// The hash of the unique non-zero maximal-height descendant node. + child: Felt, + }, + + /// An internal node whose both children are non-zero. + Binary { + /// The hash of the left child. + left: Felt, + /// The hash of the right child. + right: Felt, + }, +} + +impl MerkleNode { + // Taken from `bonsai-trie`: https://github.com/madara-alliance/bonsai-trie/blob/bfc6ad47b3cb8b75b1326bf630ca16e581f194c5/src/trie/merkle_node.rs#L234-L248 + pub fn compute_hash(&self) -> Felt { + match self { + Self::Binary { left, right } => Hash::hash(left, right), + Self::Edge { child, path, length } => { + let mut length_bytes = [0u8; 32]; + length_bytes[31] = *length; + let length = Felt::from_bytes_be(&length_bytes); + Hash::hash(child, path) + length + } + } + } +} + +/// The response type for `starknet_getStorageProof` method. +/// +/// The requested storage proofs. Note that if a requested leaf has the default value, the path to +/// it may end in an edge node whose path is not a prefix of the requested leaf, thus effectively +/// proving non-membership +#[derive(Debug, Serialize, Deserialize)] +pub struct GetStorageProofResponse { + pub global_roots: GlobalRoots, + pub classes_proof: ClassesProof, + pub contracts_proof: ContractsProof, + pub contracts_storage_proofs: ContractStorageProofs, +} + +#[derive(Debug, Default, Serialize, Deserialize)] +#[serde(transparent)] +pub struct ClassesProof { + pub nodes: Nodes, +} + +#[derive(Debug, Default, Serialize, Deserialize)] +pub struct ContractsProof { + /// The nodes in the union of the paths from the contracts tree root to the requested leaves. + pub nodes: Nodes, + /// The nonce and class hash for each requested contract address, in the order in which they + /// appear in the request. These values are needed to construct the associated leaf node. + pub contract_leaves_data: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ContractLeafData { + // NOTE: This field is not specified in the RPC specs, but the contract storage root is + // required to compute the contract state hash (ie the value of the contracts trie). We + // include this in the response for now to ease the conversions over on SNOS side. + pub storage_root: Felt, + pub nonce: Felt, + pub class_hash: Felt, +} + +#[derive(Debug, Default, Serialize, Deserialize)] +#[serde(transparent)] +pub struct ContractStorageProofs { + pub nodes: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct NodeWithHash { + pub node_hash: Felt, + pub node: MerkleNode, +} + +#[derive(Debug, Default, Serialize, Deserialize)] +#[serde(transparent)] +pub struct Nodes(pub Vec); + +impl Deref for Nodes { + type Target = Vec; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Nodes { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +// --- Conversion from/to internal types for convenience + +impl From for Nodes { + fn from(value: MultiProof) -> Self { + Self( + value + .0 + .into_iter() + .map(|(hash, node)| NodeWithHash { node_hash: hash, node: MerkleNode::from(node) }) + .collect(), + ) + } +} + +impl From for MultiProof { + fn from(value: Nodes) -> Self { + Self(value.0.into_iter().map(|node| (node.node_hash, ProofNode::from(node.node))).collect()) + } +} + +impl From for MerkleNode { + fn from(value: ProofNode) -> Self { + match value { + ProofNode::Binary { left, right } => MerkleNode::Binary { left, right }, + ProofNode::Edge { child, path } => { + MerkleNode::Edge { child, length: path.len() as u8, path: path_to_felt(path) } + } + } + } +} + +impl From for ProofNode { + fn from(value: MerkleNode) -> Self { + match value { + MerkleNode::Binary { left, right } => Self::Binary { left, right }, + MerkleNode::Edge { path, child, length } => { + Self::Edge { child, path: felt_to_path(path, length) } + } + } + } +} + +fn felt_to_path(felt: Felt, length: u8) -> Path { + let length = length as usize; + let mut bits = BitVec::new(); + + // This function converts a Felt to a Path by preserving leading zeros + // that are semantically important in the Merkle tree path representation. + // + // Example: + // For a path "0000100" (length=7): + // - As an integer/hex: 0x4 (leading zeros get truncated) + // - As a Path: [0,0,0,0,1,0,0] (leading zeros preserved) + // + // We need to preserve these leading zeros because in a Merkle tree path: + // - Each bit represents a direction (left=0, right=1) + // - The position/index of each bit matters for the path traversal + // - "0000100" and "100" would represent different paths in the tree + for bit in &felt.to_bits_be()[256 - length..] { + bits.push(*bit); + } + + Path(bits) +} + +fn path_to_felt(path: Path) -> Felt { + let mut bytes = [0u8; 32]; + bytes.view_bits_mut()[256 - path.len()..].copy_from_bitslice(&path); + Felt::from_bytes_be(&bytes) +} + +#[cfg(test)] +mod tests { + use katana_trie::BitVec; + + use super::*; + + // Test cases taken from `bonsai-trie` crate + #[rstest::rstest] + #[case(&[0b10101010, 0b10101010])] + #[case(&[])] + #[case(&[0b10101010])] + #[case(&[0b00000000])] + #[case(&[0b11111111])] + #[case(&[0b11111111, 0b00000000, 0b10101010, 0b10101010, 0b11111111, 0b00000000, 0b10101010, 0b10101010, 0b11111111, 0b00000000, 0b10101010, 0b10101010])] + fn path_felt_rt(#[case] input: &[u8]) { + let path = Path(BitVec::from_slice(input)); + + let converted_felt = path_to_felt(path.clone()); + let converted_path = felt_to_path(converted_felt, path.len() as u8); + + assert_eq!(path, converted_path); + assert_eq!(path.len(), converted_path.len()); + } +} diff --git a/crates/katana/rpc/rpc/Cargo.toml b/crates/katana/rpc/rpc/Cargo.toml index 6ffc5703ac..5d032a4a0b 100644 --- a/crates/katana/rpc/rpc/Cargo.toml +++ b/crates/katana/rpc/rpc/Cargo.toml @@ -7,10 +7,6 @@ repository.workspace = true version.workspace = true [dependencies] -anyhow.workspace = true -dojo-metrics.workspace = true -futures.workspace = true -jsonrpsee = { workspace = true, features = [ "server" ] } katana-core.workspace = true katana-executor.workspace = true katana-pool.workspace = true @@ -20,14 +16,28 @@ katana-rpc-api.workspace = true katana-rpc-types.workspace = true katana-rpc-types-builder.workspace = true katana-tasks.workspace = true + +anyhow.workspace = true +dojo-metrics.workspace = true +futures.workspace = true +http.workspace = true +jsonrpsee = { workspace = true, features = [ "server" ] } metrics.workspace = true +serde_json.workspace = true starknet.workspace = true thiserror.workspace = true tokio.workspace = true +tower.workspace = true +tower-http.workspace = true tracing.workspace = true url.workspace = true [dev-dependencies] +katana-cairo.workspace = true +katana-node.workspace = true +katana-rpc-api = { workspace = true, features = [ "client" ] } +katana-trie.workspace = true + alloy = { git = "https://github.com/alloy-rs/alloy", features = [ "contract", "network", "node-bindings", "provider-http", "providers", "signer-local" ] } alloy-primitives = { workspace = true, features = [ "serde" ] } assert_matches.workspace = true @@ -36,9 +46,6 @@ dojo-test-utils.workspace = true dojo-utils.workspace = true indexmap.workspace = true jsonrpsee = { workspace = true, features = [ "client" ] } -katana-cairo.workspace = true -katana-node.workspace = true -katana-rpc-api = { workspace = true, features = [ "client" ] } num-traits.workspace = true rand.workspace = true rstest.workspace = true diff --git a/crates/katana/rpc/rpc/src/cors.rs b/crates/katana/rpc/rpc/src/cors.rs new file mode 100644 index 0000000000..a31453bef8 --- /dev/null +++ b/crates/katana/rpc/rpc/src/cors.rs @@ -0,0 +1,154 @@ +pub use http::HeaderValue; +use tower::Layer; +use tower_http::cors::{self, Any}; +pub use tower_http::cors::{AllowHeaders, AllowMethods}; + +/// Layer that applies the [`Cors`] middleware which adds headers for [CORS][mdn]. +/// +/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS +/// [`Cors`]: cors::Cors +#[derive(Debug, Clone, Default)] +pub struct Cors(cors::CorsLayer); + +impl Cors { + pub fn new() -> Self { + Self::default() + } + + /// A permissive configuration: + /// + /// - All request headers allowed. + /// - All methods allowed. + /// - All origins allowed. + /// - All headers exposed. + pub fn permissive() -> Self { + Self(cors::CorsLayer::permissive()) + } + + /// A very permissive configuration: + /// + /// - **Credentials allowed.** + /// - The method received in `Access-Control-Request-Method` is sent back as an allowed method. + /// - The origin of the preflight request is sent back as an allowed origin. + /// - The header names received in `Access-Control-Request-Headers` are sent back as allowed + /// headers. + /// - No headers are currently exposed, but this may change in the future. + pub fn very_permissive() -> Self { + Self(cors::CorsLayer::very_permissive()) + } + + pub fn allow_origins(self, origins: impl Into) -> Self { + Self(self.0.allow_origin(origins.into())) + } + + pub fn allow_methods(self, methods: impl Into) -> Self { + Self(self.0.allow_methods(methods)) + } + + pub fn allow_headers(self, headers: impl Into) -> Self { + Self(self.0.allow_headers(headers)) + } +} + +impl Layer for Cors { + type Service = cors::Cors; + + fn layer(&self, inner: S) -> Self::Service { + self.0.layer(inner) + } +} + +const WILDCARD: HeaderValue = HeaderValue::from_static("*"); + +/// Holds configuration for how to set the [`Access-Control-Allow-Origin`][mdn] header. +/// +/// This is just a lightweight wrapper of [`cors::AllowOrigin`] that doesn't fail when a wildcard, +/// `*`, is passed to [`cors::AllowOrigin::list`]. See [`cors::AllowOrigin`] for more details. +/// +/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin +#[derive(Debug, Clone, Default)] +pub struct AllowOrigins(cors::AllowOrigin); + +impl AllowOrigins { + /// Allow any origin by sending a wildcard (`*`). + pub fn any() -> Self { + Self(cors::AllowOrigin::any()) + } + + /// Set a single allowed origin. + pub fn exact(origin: HeaderValue) -> Self { + Self(cors::AllowOrigin::exact(origin)) + } + + /// Allow any origin, by mirroring the request origin. + pub fn mirror_request() -> Self { + Self(cors::AllowOrigin::mirror_request()) + } + + /// Set multiple allowed origins. + /// + /// This will not return an error if a wildcard, `*`, is in the list. + pub fn list(origins: I) -> Self + where + I: IntoIterator, + { + let origins = origins.into_iter().collect::>(); + if origins.iter().any(|o| o == WILDCARD) { + Self(cors::AllowOrigin::any()) + } else { + Self(cors::AllowOrigin::list(origins)) + } + } +} + +impl From for AllowOrigins { + fn from(value: cors::AllowOrigin) -> Self { + Self(value) + } +} + +impl From for cors::AllowOrigin { + fn from(value: AllowOrigins) -> Self { + value.0 + } +} + +impl From for AllowOrigins { + fn from(_: Any) -> Self { + Self(cors::AllowOrigin::any()) + } +} + +impl From for AllowOrigins { + fn from(val: HeaderValue) -> Self { + Self(cors::AllowOrigin::exact(val)) + } +} + +impl From<[HeaderValue; N]> for AllowOrigins { + fn from(arr: [HeaderValue; N]) -> Self { + Self::list(arr) + } +} + +impl From> for AllowOrigins { + fn from(vec: Vec) -> Self { + Self::list(vec) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn wildcard_in_list() { + let origins = vec![ + HeaderValue::from_static("http://example.com"), + HeaderValue::from_static("*"), + HeaderValue::from_static("http://other.com"), + ]; + + let _ = AllowOrigins::list(origins); + } +} diff --git a/crates/katana/rpc/rpc/src/health.rs b/crates/katana/rpc/rpc/src/health.rs new file mode 100644 index 0000000000..3299e56d90 --- /dev/null +++ b/crates/katana/rpc/rpc/src/health.rs @@ -0,0 +1,29 @@ +use jsonrpsee::core::server::rpc_module::Methods; +use jsonrpsee::server::middleware::proxy_get_request::ProxyGetRequestLayer; +use jsonrpsee::RpcModule; +use serde_json::json; + +/// Simple health check endpoint. +#[derive(Debug)] +pub struct HealthCheck; + +impl HealthCheck { + const METHOD: &'static str = "health"; + const PROXY_PATH: &'static str = "/"; + + pub(crate) fn proxy() -> ProxyGetRequestLayer { + Self::proxy_with_path(Self::PROXY_PATH) + } + + fn proxy_with_path(path: impl Into) -> ProxyGetRequestLayer { + ProxyGetRequestLayer::new(path, Self::METHOD).expect("path starts with /") + } +} + +impl From for Methods { + fn from(_: HealthCheck) -> Self { + let mut module = RpcModule::new(()); + module.register_method(HealthCheck::METHOD, |_, _| Ok(json!({ "health": true }))).unwrap(); + module.into() + } +} diff --git a/crates/katana/rpc/rpc/src/lib.rs b/crates/katana/rpc/rpc/src/lib.rs index b24f787512..05811354a5 100644 --- a/crates/katana/rpc/rpc/src/lib.rs +++ b/crates/katana/rpc/rpc/src/lib.rs @@ -3,10 +3,155 @@ #![allow(clippy::blocks_in_conditions)] #![cfg_attr(not(test), warn(unused_crate_dependencies))] +use std::net::SocketAddr; +use std::time::Duration; + +use jsonrpsee::server::{AllowHosts, ServerBuilder, ServerHandle}; +use jsonrpsee::RpcModule; +use tower::ServiceBuilder; +use tracing::info; + +pub mod cors; pub mod dev; +pub mod health; pub mod metrics; pub mod saya; pub mod starknet; pub mod torii; - mod utils; + +use cors::Cors; +use health::HealthCheck; +use metrics::RpcServerMetrics; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error(transparent)] + Jsonrpsee(#[from] jsonrpsee::core::Error), + + #[error("RPC server has already been stopped")] + AlreadyStopped, +} + +/// The RPC server handle. +#[derive(Debug, Clone)] +pub struct RpcServerHandle { + /// The actual address that the server is binded to. + addr: SocketAddr, + /// The handle to the spawned [`jsonrpsee::server::Server`]. + handle: ServerHandle, +} + +impl RpcServerHandle { + /// Tell the server to stop without waiting for the server to stop. + pub fn stop(&self) -> Result<(), Error> { + self.handle.stop().map_err(|_| Error::AlreadyStopped) + } + + /// Wait until the server has stopped. + pub async fn stopped(self) { + self.handle.stopped().await + } + + /// Returns the socket address the server is listening on. + pub fn addr(&self) -> &SocketAddr { + &self.addr + } +} + +#[derive(Debug)] +pub struct RpcServer { + metrics: bool, + cors: Option, + health_check: bool, + module: RpcModule<()>, + max_connections: u32, +} + +impl RpcServer { + pub fn new() -> Self { + Self { + cors: None, + metrics: false, + health_check: false, + max_connections: 100, + module: RpcModule::new(()), + } + } + + /// Collect metrics about the RPC server. + /// + /// See top level module of [`crate::metrics`] to see what metrics are collected. + pub fn metrics(mut self) -> Self { + self.metrics = true; + self + } + + /// Enables health checking endpoint via HTTP `GET /health` + pub fn health_check(mut self) -> Self { + self.health_check = true; + self + } + + pub fn cors(mut self, cors: Cors) -> Self { + self.cors = Some(cors); + self + } + + pub fn module(mut self, module: RpcModule<()>) -> Self { + self.module = module; + self + } + + pub async fn start(&self, addr: SocketAddr) -> Result { + let mut modules = self.module.clone(); + + let health_check_proxy = if self.health_check { + modules.merge(HealthCheck)?; + Some(HealthCheck::proxy()) + } else { + None + }; + + let middleware = ServiceBuilder::new() + .option_layer(self.cors.clone()) + .option_layer(health_check_proxy) + .timeout(Duration::from_secs(20)); + + let builder = ServerBuilder::new() + .set_middleware(middleware) + .set_host_filtering(AllowHosts::Any) + .max_connections(self.max_connections); + + let handle = if self.metrics { + let logger = RpcServerMetrics::new(&modules); + let server = builder.set_logger(logger).build(addr).await?; + + let addr = server.local_addr()?; + let handle = server.start(modules)?; + + RpcServerHandle { addr, handle } + } else { + let server = builder.build(addr).await?; + + let addr = server.local_addr()?; + let handle = server.start(modules)?; + + RpcServerHandle { addr, handle } + }; + + // The socket address that we log out must be from the RPC handle, in the case that the + // `addr` passed to this method has port number 0. As the 0 port will be resolved to + // a free port during the call to `ServerBuilder::build(addr)`. + + info!(target: "rpc", addr = %handle.addr, "RPC server started."); + + Ok(handle) + } +} + +impl Default for RpcServer { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/katana/rpc/rpc/src/starknet/config.rs b/crates/katana/rpc/rpc/src/starknet/config.rs new file mode 100644 index 0000000000..73e29b3a5f --- /dev/null +++ b/crates/katana/rpc/rpc/src/starknet/config.rs @@ -0,0 +1,12 @@ +#[derive(Debug, Clone)] +pub struct StarknetApiConfig { + /// The max chunk size that can be served from the `getEvents` method. + /// + /// If `None`, the maximum chunk size is bounded by [`u64::MAX`]. + pub max_event_page_size: Option, + + /// The max keys whose proofs can be requested for from the `getStorageProof` method. + /// + /// If `None`, the maximum keys size is bounded by [`u64::MAX`]. + pub max_proof_keys: Option, +} diff --git a/crates/katana/rpc/rpc/src/starknet/mod.rs b/crates/katana/rpc/rpc/src/starknet/mod.rs index 4f0fa00b94..3828b2c332 100644 --- a/crates/katana/rpc/rpc/src/starknet/mod.rs +++ b/crates/katana/rpc/rpc/src/starknet/mod.rs @@ -1,34 +1,27 @@ //! Server implementation for the Starknet JSON-RPC API. -pub mod forking; -mod read; -mod trace; -mod write; - use std::sync::Arc; -use forking::ForkedClient; use katana_core::backend::Backend; use katana_core::service::block_producer::{BlockProducer, BlockProducerMode, PendingExecutor}; use katana_executor::{ExecutionResult, ExecutorFactory}; -use katana_pool::validation::stateful::TxValidator; use katana_pool::{TransactionPool, TxPool}; use katana_primitives::block::{ BlockHash, BlockHashOrNumber, BlockIdOrTag, BlockNumber, BlockTag, FinalityStatus, PartialHeader, }; -use katana_primitives::class::{ClassHash, CompiledClass}; +use katana_primitives::class::ClassHash; use katana_primitives::contract::{ContractAddress, Nonce, StorageKey, StorageValue}; -use katana_primitives::conversion::rpc::legacy_inner_to_rpc_class; use katana_primitives::da::L1DataAvailabilityMode; use katana_primitives::env::BlockEnv; use katana_primitives::event::MaybeForkedContinuationToken; use katana_primitives::transaction::{ExecutableTxWithHash, TxHash, TxWithHash}; use katana_primitives::Felt; +use katana_provider::error::ProviderError; use katana_provider::traits::block::{BlockHashProvider, BlockIdReader, BlockNumberProvider}; use katana_provider::traits::contract::ContractClassProvider; use katana_provider::traits::env::BlockEnvProvider; -use katana_provider::traits::state::{StateFactoryProvider, StateProvider}; +use katana_provider::traits::state::{StateFactoryProvider, StateProvider, StateRootProvider}; use katana_provider::traits::transaction::{ ReceiptProvider, TransactionProvider, TransactionStatusProvider, }; @@ -36,90 +29,105 @@ use katana_rpc_types::block::{ MaybePendingBlockWithReceipts, MaybePendingBlockWithTxHashes, MaybePendingBlockWithTxs, PendingBlockWithReceipts, PendingBlockWithTxHashes, PendingBlockWithTxs, }; +use katana_rpc_types::class::RpcContractClass; use katana_rpc_types::error::starknet::StarknetApiError; use katana_rpc_types::event::{EventFilterWithPage, EventsPage}; use katana_rpc_types::receipt::{ReceiptBlock, TxReceiptWithBlockInfo}; use katana_rpc_types::state_update::MaybePendingStateUpdate; use katana_rpc_types::transaction::Tx; +use katana_rpc_types::trie::{ + ClassesProof, ContractLeafData, ContractStorageKeys, ContractStorageProofs, ContractsProof, + GetStorageProofResponse, GlobalRoots, Nodes, +}; use katana_rpc_types::FeeEstimate; use katana_rpc_types_builder::ReceiptBuilder; use katana_tasks::{BlockingTaskPool, TokioTaskSpawner}; use starknet::core::types::{ - ContractClass, PriceUnit, ResultPageRequest, TransactionExecutionStatus, TransactionStatus, + PriceUnit, ResultPageRequest, TransactionExecutionStatus, TransactionStatus, }; use crate::utils; use crate::utils::events::{Cursor, EventBlockId}; -pub type StarknetApiResult = Result; +mod config; +pub mod forking; +mod read; +mod trace; +mod write; -#[allow(missing_debug_implementations)] -pub struct StarknetApi { - inner: Arc>, -} +pub use config::StarknetApiConfig; +use forking::ForkedClient; -#[derive(Debug, Clone)] -pub struct StarknetApiConfig { - pub max_event_page_size: Option, -} +type StarknetApiResult = Result; -impl Clone for StarknetApi { - fn clone(&self) -> Self { - Self { inner: Arc::clone(&self.inner) } - } +/// Handler for the Starknet JSON-RPC server. +/// +/// This struct implements all the JSON-RPC traits required to serve the Starknet API (ie, +/// [read](katana_rpc_api::starknet::StarknetApi), +/// [write](katana_rpc_api::starknet::StarknetWriteApi), and +/// [trace](katana_rpc_api::starknet::StarknetTraceApi) APIs. +#[allow(missing_debug_implementations)] +pub struct StarknetApi +where + EF: ExecutorFactory, +{ + inner: Arc>, } -struct Inner { - validator: TxValidator, +struct StarknetApiInner +where + EF: ExecutorFactory, +{ pool: TxPool, backend: Arc>, - block_producer: BlockProducer, - blocking_task_pool: BlockingTaskPool, forked_client: Option, + blocking_task_pool: BlockingTaskPool, + block_producer: Option>, config: StarknetApiConfig, } -impl StarknetApi { +impl StarknetApi +where + EF: ExecutorFactory, +{ pub fn new( backend: Arc>, pool: TxPool, - block_producer: BlockProducer, - validator: TxValidator, + block_producer: Option>, config: StarknetApiConfig, ) -> Self { - Self::new_inner(backend, pool, block_producer, validator, None, config) + Self::new_inner(backend, pool, block_producer, None, config) } pub fn new_forked( backend: Arc>, pool: TxPool, block_producer: BlockProducer, - validator: TxValidator, forked_client: ForkedClient, config: StarknetApiConfig, ) -> Self { - Self::new_inner(backend, pool, block_producer, validator, Some(forked_client), config) + Self::new_inner(backend, pool, Some(block_producer), Some(forked_client), config) } fn new_inner( backend: Arc>, pool: TxPool, - block_producer: BlockProducer, - validator: TxValidator, + block_producer: Option>, forked_client: Option, config: StarknetApiConfig, ) -> Self { let blocking_task_pool = BlockingTaskPool::new().expect("failed to create blocking task pool"); - let inner = Inner { + + let inner = StarknetApiInner { pool, backend, block_producer, blocking_task_pool, - validator, forked_client, config, }; + Self { inner: Arc::new(inner) } } @@ -184,10 +192,10 @@ impl StarknetApi { /// Returns the pending state if the sequencer is running in _interval_ mode. Otherwise `None`. fn pending_executor(&self) -> Option { - match &*self.inner.block_producer.producer.read() { + self.inner.block_producer.as_ref().and_then(|bp| match &*bp.producer.read() { BlockProducerMode::Instant(_) => None, BlockProducerMode::Interval(producer) => Some(producer.executor()), - } + }) } fn state(&self, block_id: &BlockIdOrTag) -> StarknetApiResult> { @@ -253,7 +261,7 @@ impl StarknetApi { &self, block_id: BlockIdOrTag, class_hash: ClassHash, - ) -> StarknetApiResult { + ) -> StarknetApiResult { self.on_io_blocking_task(move |this| { let state = this.state(&block_id)?; @@ -261,18 +269,7 @@ impl StarknetApi { return Err(StarknetApiError::ClassHashNotFound); }; - match class { - CompiledClass::Deprecated(class) => Ok(legacy_inner_to_rpc_class(class)?), - CompiledClass::Class(_) => { - let Some(sierra) = state.sierra_class(class_hash)? else { - return Err(StarknetApiError::UnexpectedError { - reason: "Class hash exist, but its Sierra class is missing".to_string(), - }); - }; - - Ok(ContractClass::Sierra(sierra)) - } - } + Ok(RpcContractClass::try_from(class).unwrap()) }) .await } @@ -283,6 +280,12 @@ impl StarknetApi { contract_address: ContractAddress, ) -> StarknetApiResult { self.on_io_blocking_task(move |this| { + // Contract address 0x1 is special system contract and does not + // have a class. See https://docs.starknet.io/architecture-and-concepts/network-architecture/starknet-state/#address_0x1. + if contract_address.0 == Felt::ONE { + return Ok(ClassHash::ZERO); + } + let state = this.state(&block_id)?; let class_hash = state.class_hash_of_contract(contract_address)?; class_hash.ok_or(StarknetApiError::ContractNotFound) @@ -294,7 +297,7 @@ impl StarknetApi { &self, block_id: BlockIdOrTag, contract_address: ContractAddress, - ) -> StarknetApiResult { + ) -> StarknetApiResult { let hash = self.class_hash_at_address(block_id, contract_address).await?; let class = self.class_at_hash(block_id, hash).await?; Ok(class) @@ -308,10 +311,14 @@ impl StarknetApi { ) -> StarknetApiResult { let state = self.state(&block_id)?; - // check that contract exist by checking the class hash of the contract - let Some(_) = state.class_hash_of_contract(contract_address)? else { + // Check that contract exist by checking the class hash of the contract, + // unless its address 0x1 which is special system contract and does not + // have a class. See https://docs.starknet.io/architecture-and-concepts/network-architecture/starknet-state/#address_0x1. + if contract_address.0 != Felt::ONE + && state.class_hash_of_contract(contract_address)?.is_none() + { return Err(StarknetApiError::ContractNotFound); - }; + } let value = state.storage(contract_address, storage_key)?; Ok(value.unwrap_or_default()) @@ -368,7 +375,7 @@ impl StarknetApi { // TODO: this is a temporary solution, we should have a better way to handle this. // perhaps a pending/pool state provider that implements all the state provider traits. let result = if let BlockIdOrTag::Tag(BlockTag::Pending) = block_id { - this.inner.validator.pool_nonce(contract_address)? + this.inner.pool.validator().pool_nonce(contract_address)? } else { let state = this.state(&block_id)?; state.nonce(contract_address)? @@ -1132,4 +1139,101 @@ impl StarknetApi { Ok(id) } + + async fn get_proofs( + &self, + block_id: BlockIdOrTag, + class_hashes: Option>, + contract_addresses: Option>, + contracts_storage_keys: Option>, + ) -> StarknetApiResult { + self.on_io_blocking_task(move |this| { + let provider = this.inner.backend.blockchain.provider(); + + let Some(block_num) = provider.convert_block_id(block_id)? else { + return Err(StarknetApiError::BlockNotFound); + }; + + // Check if the total number of keys requested exceeds the RPC limit. + if let Some(limit) = this.inner.config.max_proof_keys { + let total_keys = class_hashes.as_ref().map(|v| v.len()).unwrap_or(0) + + contract_addresses.as_ref().map(|v| v.len()).unwrap_or(0) + + contracts_storage_keys.as_ref().map(|v| v.len()).unwrap_or(0); + + let total_keys = total_keys as u64; + if total_keys > limit { + return Err(StarknetApiError::ProofLimitExceeded { limit, total: total_keys }); + } + } + + // TODO: the way we handle the block id is very clanky. change it! + let state = this.state(&BlockIdOrTag::Number(block_num))?; + let block_hash = provider + .block_hash_by_num(block_num)? + .ok_or(ProviderError::MissingBlockHeader(block_num))?; + + // --- Get classes proof (if any) + + let classes_proof = if let Some(classes) = class_hashes { + let proofs = state.class_multiproof(classes)?; + ClassesProof { nodes: proofs.into() } + } else { + ClassesProof::default() + }; + + // --- Get contracts proof (if any) + + let contracts_proof = if let Some(addresses) = contract_addresses { + let proofs = state.contract_multiproof(addresses.clone())?; + let mut contract_leaves_data = Vec::new(); + + for address in addresses { + let nonce = state.nonce(address)?.unwrap_or_default(); + let class_hash = state.class_hash_of_contract(address)?.unwrap_or_default(); + let storage_root = state.storage_root(address)?.unwrap_or_default(); + contract_leaves_data.push(ContractLeafData { storage_root, class_hash, nonce }); + } + + ContractsProof { nodes: proofs.into(), contract_leaves_data } + } else { + ContractsProof::default() + }; + + // --- Get contracts storage proof (if any) + + let contracts_storage_proofs = if let Some(contract_storage) = contracts_storage_keys { + let mut nodes: Vec = Vec::new(); + + for ContractStorageKeys { address, keys } in contract_storage { + let proofs = state.storage_multiproof(address, keys)?; + nodes.push(proofs.into()); + } + + ContractStorageProofs { nodes } + } else { + ContractStorageProofs::default() + }; + + let classes_tree_root = state.classes_root()?; + let contracts_tree_root = state.contracts_root()?; + let global_roots = GlobalRoots { block_hash, classes_tree_root, contracts_tree_root }; + + Ok(GetStorageProofResponse { + global_roots, + classes_proof, + contracts_proof, + contracts_storage_proofs, + }) + }) + .await + } +} + +impl Clone for StarknetApi +where + EF: ExecutorFactory, +{ + fn clone(&self) -> Self { + Self { inner: Arc::clone(&self.inner) } + } } diff --git a/crates/katana/rpc/rpc/src/starknet/read.rs b/crates/katana/rpc/rpc/src/starknet/read.rs index 1aa5a07686..a48cfe5f5b 100644 --- a/crates/katana/rpc/rpc/src/starknet/read.rs +++ b/crates/katana/rpc/rpc/src/starknet/read.rs @@ -1,22 +1,23 @@ use jsonrpsee::core::{async_trait, Error, RpcResult}; use katana_executor::{EntryPointCall, ExecutorFactory}; use katana_primitives::block::BlockIdOrTag; +use katana_primitives::class::ClassHash; use katana_primitives::transaction::{ExecutableTx, ExecutableTxWithHash, TxHash}; -use katana_primitives::Felt; +use katana_primitives::{ContractAddress, Felt}; use katana_rpc_api::starknet::StarknetApiServer; use katana_rpc_types::block::{ BlockHashAndNumber, MaybePendingBlockWithReceipts, MaybePendingBlockWithTxHashes, MaybePendingBlockWithTxs, }; +use katana_rpc_types::class::RpcContractClass; use katana_rpc_types::error::starknet::StarknetApiError; use katana_rpc_types::event::{EventFilterWithPage, EventsPage}; use katana_rpc_types::message::MsgFromL1; use katana_rpc_types::receipt::TxReceiptWithBlockInfo; use katana_rpc_types::state_update::MaybePendingStateUpdate; use katana_rpc_types::transaction::{BroadcastedTx, Tx}; -use katana_rpc_types::{ - ContractClass, FeeEstimate, FeltAsHex, FunctionCall, SimulationFlagForEstimateFee, -}; +use katana_rpc_types::trie::{ContractStorageKeys, GetStorageProofResponse}; +use katana_rpc_types::{FeeEstimate, FeltAsHex, FunctionCall, SimulationFlagForEstimateFee}; use starknet::core::types::TransactionStatus; use super::StarknetApi; @@ -51,7 +52,7 @@ impl StarknetApiServer for StarknetApi { &self, block_id: BlockIdOrTag, contract_address: Felt, - ) -> RpcResult { + ) -> RpcResult { Ok(self.class_at_address(block_id, contract_address.into()).await?) } @@ -115,7 +116,7 @@ impl StarknetApiServer for StarknetApi { &self, block_id: BlockIdOrTag, class_hash: Felt, - ) -> RpcResult { + ) -> RpcResult { Ok(self.class_at_hash(block_id, class_hash).await?) } @@ -266,4 +267,17 @@ impl StarknetApiServer for StarknetApi { ) -> RpcResult { Ok(self.transaction_status(transaction_hash).await?) } + + async fn get_storage_proof( + &self, + block_id: BlockIdOrTag, + class_hashes: Option>, + contract_addresses: Option>, + contracts_storage_keys: Option>, + ) -> RpcResult { + let proofs = self + .get_proofs(block_id, class_hashes, contract_addresses, contracts_storage_keys) + .await?; + Ok(proofs) + } } diff --git a/crates/katana/rpc/rpc/src/starknet/trace.rs b/crates/katana/rpc/rpc/src/starknet/trace.rs index ef23f69439..5f0594bd6f 100644 --- a/crates/katana/rpc/rpc/src/starknet/trace.rs +++ b/crates/katana/rpc/rpc/src/starknet/trace.rs @@ -263,6 +263,10 @@ fn to_rpc_trace(trace: TxExecInfo) -> TransactionTrace { state_diff, }) } + + TxType::Deploy => { + unimplemented!("unsupported legacy tx type") + } } } diff --git a/crates/katana/rpc/rpc/src/utils/events.rs b/crates/katana/rpc/rpc/src/utils/events.rs index b761af08f2..fe03ab9fd2 100644 --- a/crates/katana/rpc/rpc/src/utils/events.rs +++ b/crates/katana/rpc/rpc/src/utils/events.rs @@ -337,7 +337,7 @@ fn fetch_tx_events( if buffer.len() >= chunk_size { // the next time we have to fetch the events, we will start from this index. let new_last_event = if total_can_take == 0 { - // start from the the same event pointed by the + // start from the same event pointed by the // current cursor.. last_event_idx } else { diff --git a/crates/katana/rpc/rpc/tests/dev.rs b/crates/katana/rpc/rpc/tests/dev.rs index 8b53f42c4a..4708e19326 100644 --- a/crates/katana/rpc/rpc/tests/dev.rs +++ b/crates/katana/rpc/rpc/tests/dev.rs @@ -99,6 +99,16 @@ async fn test_increase_next_block_timestamp() { ); } +#[tokio::test] +async fn test_dev_api_enabled() { + let sequencer = create_test_sequencer().await; + + let client = HttpClientBuilder::default().build(sequencer.url()).unwrap(); + + let accounts = client.predeployed_accounts().await.unwrap(); + assert!(!accounts.is_empty(), "predeployed accounts should not be empty"); +} + // #[tokio::test] // async fn test_set_storage_at_on_instant_mode() { // let sequencer = create_test_sequencer().await; diff --git a/crates/katana/rpc/rpc/tests/proofs.rs b/crates/katana/rpc/rpc/tests/proofs.rs new file mode 100644 index 0000000000..c07a8a21ba --- /dev/null +++ b/crates/katana/rpc/rpc/tests/proofs.rs @@ -0,0 +1,292 @@ +use std::path::PathBuf; + +use assert_matches::assert_matches; +use dojo_test_utils::sequencer::{get_default_test_config, TestSequencer}; +use jsonrpsee::http_client::HttpClientBuilder; +use katana_node::config::rpc::DEFAULT_RPC_MAX_PROOF_KEYS; +use katana_node::config::SequencingConfig; +use katana_primitives::block::BlockIdOrTag; +use katana_primitives::class::{ClassHash, CompiledClassHash}; +use katana_primitives::contract::{StorageKey, StorageValue}; +use katana_primitives::{hash, ContractAddress, Felt}; +use katana_rpc_api::starknet::StarknetApiClient; +use katana_rpc_types::trie::ContractStorageKeys; +use katana_trie::{ + compute_classes_trie_value, compute_contract_state_hash, ClassesMultiProof, MultiProof, +}; +use starknet::accounts::{Account, ConnectedAccount, SingleOwnerAccount}; +use starknet::core::types::BlockTag; +use starknet::providers::jsonrpc::HttpTransport; +use starknet::providers::JsonRpcClient; +use starknet::signers::LocalWallet; + +mod common; + +#[tokio::test] +async fn proofs_limit() { + use jsonrpsee::core::Error; + use jsonrpsee::types::error::CallError; + use serde_json::json; + + let sequencer = + TestSequencer::start(get_default_test_config(SequencingConfig::default())).await; + + // We need to use the jsonrpsee client because `starknet-rs` doesn't yet support RPC 0 + let client = HttpClientBuilder::default().build(sequencer.url()).unwrap(); + + // Because we're using the default configuration for instantiating the node, the RPC limit is + // set to 100. The total keys is 35 + 35 + 35 = 105. + + // Generate dummy keys + let mut classes = Vec::new(); + let mut contracts = Vec::new(); + let mut storages = Vec::new(); + + for i in 0..35 { + storages.push(Default::default()); + classes.push(ClassHash::from(i as u64)); + contracts.push(Felt::from(i as u64).into()); + } + + let err = client + .get_storage_proof( + BlockIdOrTag::Tag(BlockTag::Latest), + Some(classes), + Some(contracts), + Some(storages), + ) + .await + .expect_err("rpc should enforce limit"); + + assert_matches!(err, Error::Call(CallError::Custom(e)) => { + assert_eq!(e.code(), 1000); + assert_eq!(&e.message(), &"Proof limit exceeded"); + + let expected_data = json!({ + "total": 105, + "limit": DEFAULT_RPC_MAX_PROOF_KEYS, + }); + + let actual_data = e.data().expect("must have data"); + let actual_data = serde_json::to_value(actual_data).unwrap(); + + assert_eq!(actual_data, expected_data); + }); +} + +#[tokio::test] +async fn genesis_states() { + let cfg = get_default_test_config(SequencingConfig::default()); + + let sequencer = TestSequencer::start(cfg).await; + let genesis_states = sequencer.backend().chain_spec.state_updates(); + + // We need to use the jsonrpsee client because `starknet-rs` doesn't yet support RPC 0.8.0 + let client = HttpClientBuilder::default().build(sequencer.url()).unwrap(); + + // Check class declarations + let genesis_classes = + genesis_states.state_updates.declared_classes.keys().cloned().collect::>(); + + // Check contract deployments + let genesis_contracts = genesis_states + .state_updates + .deployed_contracts + .keys() + .cloned() + .collect::>(); + + // Check contract storage + let genesis_contract_storages = genesis_states + .state_updates + .storage_updates + .iter() + .map(|(address, keys)| ContractStorageKeys { + address: *address, + keys: keys.keys().cloned().collect(), + }) + .collect::>(); + + let proofs = client + .get_storage_proof( + BlockIdOrTag::Tag(BlockTag::Latest), + Some(genesis_classes.clone()), + Some(genesis_contracts.clone()), + Some(genesis_contract_storages.clone()), + ) + .await + .expect("failed to get state proofs"); + + // ----------------------------------------------------------------------- + // Verify classes proofs + + let classes_proof = MultiProof::from(proofs.classes_proof.nodes); + let classes_tree_root = proofs.global_roots.classes_tree_root; + let classes_verification_result = katana_trie::verify_proof::( + &classes_proof, + classes_tree_root, + genesis_classes, + ); + + // Compute the classes trie values + let class_trie_entries = genesis_states + .state_updates + .declared_classes + .values() + .map(|compiled_hash| compute_classes_trie_value(*compiled_hash)) + .collect::>(); + + assert_eq!(class_trie_entries, classes_verification_result); + + // ----------------------------------------------------------------------- + // Verify contracts proofs + + let contracts_proof = MultiProof::from(proofs.contracts_proof.nodes); + let contracts_tree_root = proofs.global_roots.contracts_tree_root; + let contracts_verification_result = katana_trie::verify_proof::( + &contracts_proof, + contracts_tree_root, + genesis_contracts.into_iter().map(Felt::from).collect(), + ); + + // Compute the classes trie values + let contracts_trie_entries = proofs + .contracts_proof + .contract_leaves_data + .into_iter() + .map(|d| compute_contract_state_hash(&d.class_hash, &d.storage_root, &d.nonce)) + .collect::>(); + + assert_eq!(contracts_trie_entries, contracts_verification_result); + + // ----------------------------------------------------------------------- + // Verify contracts proofs + + let storages_updates = &genesis_states.state_updates.storage_updates.values(); + let storages_proofs = proofs.contracts_storage_proofs.nodes; + + // The order of which the proofs are returned is of the same order of the proofs requests. + for (storages, proofs) in storages_updates.clone().zip(storages_proofs) { + let storage_keys = storages.keys().cloned().collect::>(); + let storage_values = storages.values().cloned().collect::>(); + + let contracts_storages_proof = MultiProof::from(proofs); + let (storage_tree_root, ..) = contracts_storages_proof.0.first().unwrap(); + + let storages_verification_result = katana_trie::verify_proof::( + &contracts_storages_proof, + *storage_tree_root, + storage_keys, + ); + + assert_eq!(storage_values, storages_verification_result); + } +} + +#[tokio::test] +async fn classes_proofs() { + let cfg = get_default_test_config(SequencingConfig::default()); + + let sequencer = TestSequencer::start(cfg).await; + let account = sequencer.account(); + + let (class_hash1, compiled_class_hash1) = + declare(&account, "tests/test_data/cairo1_contract.json").await; + let (class_hash2, compiled_class_hash2) = + declare(&account, "tests/test_data/cairo_l1_msg_contract.json").await; + let (class_hash3, compiled_class_hash3) = + declare(&account, "tests/test_data/test_sierra_contract.json").await; + + // We need to use the jsonrpsee client because `starknet-rs` doesn't yet support RPC 0.8.0 + let client = HttpClientBuilder::default().build(sequencer.url()).unwrap(); + + { + let class_hash = class_hash1; + let trie_entry = compute_classes_trie_value(compiled_class_hash1); + + let proofs = client + .get_storage_proof(BlockIdOrTag::Number(1), Some(vec![class_hash]), None, None) + .await + .expect("failed to get storage proof"); + + let results = ClassesMultiProof::from(MultiProof::from(proofs.classes_proof.nodes)) + .verify(proofs.global_roots.classes_tree_root, vec![class_hash]); + + assert_eq!(vec![trie_entry], results); + } + + { + let class_hash = class_hash2; + let trie_entry = compute_classes_trie_value(compiled_class_hash2); + + let proofs = client + .get_storage_proof(BlockIdOrTag::Number(2), Some(vec![class_hash]), None, None) + .await + .expect("failed to get storage proof"); + + let results = ClassesMultiProof::from(MultiProof::from(proofs.classes_proof.nodes)) + .verify(proofs.global_roots.classes_tree_root, vec![class_hash]); + + assert_eq!(vec![trie_entry], results); + } + + { + let class_hash = class_hash3; + let trie_entry = compute_classes_trie_value(compiled_class_hash3); + + let proofs = client + .get_storage_proof(BlockIdOrTag::Number(3), Some(vec![class_hash]), None, None) + .await + .expect("failed to get storage proof"); + + let results = ClassesMultiProof::from(MultiProof::from(proofs.classes_proof.nodes)) + .verify(proofs.global_roots.classes_tree_root, vec![class_hash]); + + assert_eq!(vec![trie_entry], results); + } + + { + let class_hashes = vec![class_hash1, class_hash2, class_hash3]; + let trie_entries = vec![ + compute_classes_trie_value(compiled_class_hash1), + compute_classes_trie_value(compiled_class_hash2), + compute_classes_trie_value(compiled_class_hash3), + ]; + + let proofs = client + .get_storage_proof( + BlockIdOrTag::Tag(BlockTag::Latest), + Some(class_hashes.clone()), + None, + None, + ) + .await + .expect("failed to get storage proof"); + + let results = ClassesMultiProof::from(MultiProof::from(proofs.classes_proof.nodes)) + .verify(proofs.global_roots.classes_tree_root, class_hashes.clone()); + + assert_eq!(trie_entries, results); + } +} + +async fn declare( + account: &SingleOwnerAccount, LocalWallet>, + path: impl Into, +) -> (ClassHash, CompiledClassHash) { + let (contract, compiled_class_hash) = common::prepare_contract_declaration_params(&path.into()) + .expect("failed to prepare class declaration params"); + + let class_hash = contract.class_hash(); + let res = account + .declare_v2(contract.into(), compiled_class_hash) + .send() + .await + .expect("failed to send declare tx"); + + dojo_utils::TransactionWaiter::new(res.transaction_hash, account.provider()) + .await + .expect("failed to wait on tx"); + + (class_hash, compiled_class_hash) +} diff --git a/crates/katana/rpc/rpc/tests/starknet.rs b/crates/katana/rpc/rpc/tests/starknet.rs index 9e4e07a286..0dce2e039a 100644 --- a/crates/katana/rpc/rpc/tests/starknet.rs +++ b/crates/katana/rpc/rpc/tests/starknet.rs @@ -23,8 +23,10 @@ use starknet::accounts::{ use starknet::core::types::contract::legacy::LegacyContractClass; use starknet::core::types::{ BlockId, BlockTag, Call, DeclareTransactionReceipt, DeployAccountTransactionReceipt, - EventFilter, EventsPage, ExecutionResult, Felt, StarknetError, TransactionExecutionStatus, - TransactionFinalityStatus, TransactionReceipt, TransactionTrace, + EventFilter, EventsPage, ExecutionResult, Felt, MaybePendingBlockWithReceipts, + MaybePendingBlockWithTxHashes, MaybePendingBlockWithTxs, MaybePendingStateUpdate, + StarknetError, TransactionExecutionStatus, TransactionFinalityStatus, TransactionReceipt, + TransactionTrace, }; use starknet::core::utils::get_contract_address; use starknet::macros::{felt, selector}; @@ -55,6 +57,18 @@ async fn declare_and_deploy_contract() -> Result<()> { // check that the class is actually declared assert!(provider.get_class(BlockId::Tag(BlockTag::Pending), class_hash).await.is_ok()); + // check state update includes class in declared_classes + let state_update = provider.get_state_update(BlockId::Tag(BlockTag::Latest)).await?; + match state_update { + MaybePendingStateUpdate::Update(update) => { + assert!( + update.state_diff.declared_classes.iter().any(|item| item.class_hash == class_hash + && item.compiled_class_hash == compiled_class_hash) + ); + } + _ => panic!("Expected Update, got PendingUpdate"), + } + let ctor_args = vec![Felt::ONE, Felt::TWO]; let calldata = [ vec![ @@ -109,6 +123,15 @@ async fn declare_and_deploy_legacy_contract() -> Result<()> { // check that the class is actually declared assert!(provider.get_class(BlockId::Tag(BlockTag::Pending), class_hash).await.is_ok()); + // check state update includes class in deprecated_declared_classes + let state_update = provider.get_state_update(BlockId::Tag(BlockTag::Latest)).await?; + match state_update { + MaybePendingStateUpdate::Update(update) => { + assert!(update.state_diff.deprecated_declared_classes.contains(&class_hash)); + } + _ => panic!("Expected Update, got PendingUpdate"), + } + let ctor_args = vec![Felt::ONE]; let calldata = [ vec![ @@ -951,3 +974,197 @@ async fn v3_transactions() -> Result<()> { Ok(()) } + +#[tokio::test] +async fn fetch_pending_blocks() { + let config = + get_default_test_config(SequencingConfig { no_mining: true, ..Default::default() }); + let sequencer = TestSequencer::start(config).await; + + // create a json rpc client to interact with the dev api. + let dev_client = HttpClientBuilder::default().build(sequencer.url()).unwrap(); + let provider = sequencer.provider(); + let account = sequencer.account(); + + // setup contract to interact with (can be any existing contract that can be interacted with) + let contract = Erc20Contract::new(DEFAULT_ETH_FEE_TOKEN_ADDRESS.into(), &account); + + // setup contract function params + let recipient = felt!("0x1"); + let amount = Uint256 { low: felt!("0x1"), high: Felt::ZERO }; + + // list of tx hashes that we've sent + let mut txs = Vec::new(); + + for _ in 0..3 { + let res = contract.transfer(&recipient, &amount).send().await.unwrap(); + dojo_utils::TransactionWaiter::new(res.transaction_hash, &provider).await.unwrap(); + txs.push(res.transaction_hash); + } + + let block_id = BlockId::Tag(BlockTag::Pending); + + // ----------------------------------------------------------------------- + + let latest_block_hash_n_num = provider.block_hash_and_number().await.unwrap(); + let latest_block_hash = latest_block_hash_n_num.block_hash; + + let block_with_txs = provider.get_block_with_txs(block_id).await.unwrap(); + + if let MaybePendingBlockWithTxs::PendingBlock(block) = block_with_txs { + assert_eq!(block.transactions.len(), txs.len()); + assert_eq!(block.parent_hash, latest_block_hash); + assert_eq!(txs[0], *block.transactions[0].transaction_hash()); + assert_eq!(txs[1], *block.transactions[1].transaction_hash()); + assert_eq!(txs[2], *block.transactions[2].transaction_hash()); + } else { + panic!("expected pending block with transactions") + } + + let block_with_tx_hashes = provider.get_block_with_tx_hashes(block_id).await.unwrap(); + if let MaybePendingBlockWithTxHashes::PendingBlock(block) = block_with_tx_hashes { + assert_eq!(block.transactions.len(), txs.len()); + assert_eq!(block.parent_hash, latest_block_hash); + assert_eq!(txs[0], block.transactions[0]); + assert_eq!(txs[1], block.transactions[1]); + assert_eq!(txs[2], block.transactions[2]); + } else { + panic!("expected pending block with transaction hashes") + } + + let block_with_receipts = provider.get_block_with_receipts(block_id).await.unwrap(); + if let MaybePendingBlockWithReceipts::PendingBlock(block) = block_with_receipts { + assert_eq!(block.transactions.len(), txs.len()); + assert_eq!(block.parent_hash, latest_block_hash); + assert_eq!(txs[0], *block.transactions[0].transaction.transaction_hash()); + assert_eq!(txs[1], *block.transactions[1].transaction.transaction_hash()); + assert_eq!(txs[2], *block.transactions[2].transaction.transaction_hash()); + } else { + panic!("expected pending block with transaction receipts") + } + + // Close the current pending block + dev_client.generate_block().await.unwrap(); + + // ----------------------------------------------------------------------- + + let latest_block_hash_n_num = provider.block_hash_and_number().await.unwrap(); + let latest_block_hash = latest_block_hash_n_num.block_hash; + let block_with_txs = provider.get_block_with_txs(block_id).await.unwrap(); + + assert_matches!(block_with_txs, MaybePendingBlockWithTxs::PendingBlock(_)); + if let MaybePendingBlockWithTxs::PendingBlock(block) = block_with_txs { + assert_eq!(block.transactions.len(), 0); + assert_eq!(block.parent_hash, latest_block_hash); + } else { + panic!("expected pending block with transactions") + } + + let block_with_tx_hashes = provider.get_block_with_tx_hashes(block_id).await.unwrap(); + if let MaybePendingBlockWithTxHashes::PendingBlock(block) = block_with_tx_hashes { + assert_eq!(block.transactions.len(), 0); + assert_eq!(block.parent_hash, latest_block_hash); + } else { + panic!("expected pending block with transaction hashes") + } + + let block_with_receipts = provider.get_block_with_receipts(block_id).await.unwrap(); + if let MaybePendingBlockWithReceipts::PendingBlock(block) = block_with_receipts { + assert_eq!(block.transactions.len(), 0); + assert_eq!(block.parent_hash, latest_block_hash); + } else { + panic!("expected pending block with transaction receipts") + } +} + +// Querying for pending blocks in instant mining mode will always return the last accepted block. +#[tokio::test] +async fn fetch_pending_blocks_in_instant_mode() { + let config = get_default_test_config(SequencingConfig::default()); + let sequencer = TestSequencer::start(config).await; + + // create a json rpc client to interact with the dev api. + let dev_client = HttpClientBuilder::default().build(sequencer.url()).unwrap(); + let provider = sequencer.provider(); + let account = sequencer.account(); + + // Get the latest block hash before sending the tx beacuse the tx will generate a new block. + let latest_block_hash_n_num = provider.block_hash_and_number().await.unwrap(); + let latest_block_hash = latest_block_hash_n_num.block_hash; + + // setup contract to interact with (can be any existing contract that can be interacted with) + let contract = Erc20Contract::new(DEFAULT_ETH_FEE_TOKEN_ADDRESS.into(), &account); + + // setup contract function params + let recipient = felt!("0x1"); + let amount = Uint256 { low: felt!("0x1"), high: Felt::ZERO }; + + let res = contract.transfer(&recipient, &amount).send().await.unwrap(); + dojo_utils::TransactionWaiter::new(res.transaction_hash, &provider).await.unwrap(); + + let block_id = BlockId::Tag(BlockTag::Pending); + + // ----------------------------------------------------------------------- + + let block_with_txs = provider.get_block_with_txs(block_id).await.unwrap(); + + if let MaybePendingBlockWithTxs::Block(block) = block_with_txs { + assert_eq!(block.transactions.len(), 1); + assert_eq!(block.parent_hash, latest_block_hash); + assert_eq!(*block.transactions[0].transaction_hash(), res.transaction_hash); + } else { + panic!("expected pending block with transactions") + } + + let block_with_tx_hashes = provider.get_block_with_tx_hashes(block_id).await.unwrap(); + if let MaybePendingBlockWithTxHashes::Block(block) = block_with_tx_hashes { + assert_eq!(block.transactions.len(), 1); + assert_eq!(block.parent_hash, latest_block_hash); + assert_eq!(block.transactions[0], res.transaction_hash); + } else { + panic!("expected pending block with transaction hashes") + } + + let block_with_receipts = provider.get_block_with_receipts(block_id).await.unwrap(); + if let MaybePendingBlockWithReceipts::Block(block) = block_with_receipts { + assert_eq!(block.transactions.len(), 1); + assert_eq!(block.parent_hash, latest_block_hash); + assert_eq!(*block.transactions[0].transaction.transaction_hash(), res.transaction_hash); + } else { + panic!("expected pending block with transaction receipts") + } + + // Get the recently generated block from the sent tx + let latest_block_hash_n_num = provider.block_hash_and_number().await.unwrap(); + let latest_block_hash = latest_block_hash_n_num.block_hash; + + // Generate an empty block + dev_client.generate_block().await.unwrap(); + + // ----------------------------------------------------------------------- + + let block_with_txs = provider.get_block_with_txs(block_id).await.unwrap(); + + if let MaybePendingBlockWithTxs::Block(block) = block_with_txs { + assert_eq!(block.transactions.len(), 0); + assert_eq!(block.parent_hash, latest_block_hash); + } else { + panic!("expected block with transactions") + } + + let block_with_tx_hashes = provider.get_block_with_tx_hashes(block_id).await.unwrap(); + if let MaybePendingBlockWithTxHashes::Block(block) = block_with_tx_hashes { + assert_eq!(block.transactions.len(), 0); + assert_eq!(block.parent_hash, latest_block_hash); + } else { + panic!("expected block with transaction hashes") + } + + let block_with_receipts = provider.get_block_with_receipts(block_id).await.unwrap(); + if let MaybePendingBlockWithReceipts::Block(block) = block_with_receipts { + assert_eq!(block.transactions.len(), 0); + assert_eq!(block.parent_hash, latest_block_hash); + } else { + panic!("expected block with transaction receipts") + } +} diff --git a/crates/katana/rpc/rpc/tests/test_data/test_sierra_contract.json b/crates/katana/rpc/rpc/tests/test_data/test_sierra_contract.json new file mode 120000 index 0000000000..e92339e522 --- /dev/null +++ b/crates/katana/rpc/rpc/tests/test_data/test_sierra_contract.json @@ -0,0 +1 @@ +../../../../contracts/build/account_with_dummy_validate.sierra.json \ No newline at end of file diff --git a/crates/katana/rpc/rpc/tests/torii.rs b/crates/katana/rpc/rpc/tests/torii.rs index 32abc4ed15..18ffa0e855 100644 --- a/crates/katana/rpc/rpc/tests/torii.rs +++ b/crates/katana/rpc/rpc/tests/torii.rs @@ -13,7 +13,7 @@ use katana_rpc_api::starknet::StarknetApiClient; use katana_rpc_api::torii::ToriiApiClient; use katana_rpc_types::transaction::{TransactionsPage, TransactionsPageCursor}; use starknet::accounts::{Account, ConnectedAccount}; -use starknet::core::types::{Call, Felt, TransactionStatus}; +use starknet::core::types::{Call, DeclareTransactionResult, Felt, TransactionStatus}; use starknet::core::utils::get_selector_from_name; use tokio::time::sleep; @@ -194,10 +194,10 @@ async fn test_get_transactions_with_instant_mining() { // Should return successfully when no transactions have been mined. let cursor = TransactionsPageCursor { block_number: 0, transaction_index: 0, chunk_size: 100 }; - let declare_res = + let DeclareTransactionResult { transaction_hash, class_hash } = account.declare_v2(contract.clone(), compiled_class_hash).send().await.unwrap(); - sleep(Duration::from_millis(1000)).await; + dojo_utils::TransactionWaiter::new(transaction_hash, &sequencer.provider()).await.unwrap(); // Should return successfully with single txn. let response: TransactionsPage = client.get_transactions(cursor).await.unwrap(); @@ -208,7 +208,7 @@ async fn test_get_transactions_with_instant_mining() { // Should block on cursor at end of page and return on new txn let long_poll_future = client.get_transactions(response.cursor); - let deploy_call = build_deploy_contract_call(declare_res.class_hash, Felt::ZERO); + let deploy_call = build_deploy_contract_call(class_hash, Felt::ZERO); let deploy_txn = account.execute_v1(vec![deploy_call]); let deploy_txn_future = deploy_txn.send(); @@ -225,7 +225,7 @@ async fn test_get_transactions_with_instant_mining() { } } - let deploy_call = build_deploy_contract_call(declare_res.class_hash, Felt::ONE); + let deploy_call = build_deploy_contract_call(class_hash, Felt::ONE); let deploy_txn = account.execute_v1(vec![deploy_call]); let deploy_txn_future = deploy_txn.send().await.unwrap(); diff --git a/crates/katana/runner/macro/Cargo.toml b/crates/katana/runner/macro/Cargo.toml index a8e2689be0..7c270bd298 100644 --- a/crates/katana/runner/macro/Cargo.toml +++ b/crates/katana/runner/macro/Cargo.toml @@ -8,6 +8,6 @@ version.workspace = true proc-macro = true [dependencies] -proc-macro2 = "1.0.86" -quote = "1.0" -syn = { version = "2.0", features = [ "fold", "full" ] } +proc-macro2.workspace = true +quote.workspace = true +syn = { workspace = true, features = [ "fold", "full" ] } diff --git a/crates/katana/runner/src/lib.rs b/crates/katana/runner/src/lib.rs index f771e28511..f4af3df571 100644 --- a/crates/katana/runner/src/lib.rs +++ b/crates/katana/runner/src/lib.rs @@ -251,7 +251,7 @@ impl KatanaRunner { } /// Determines the default program path for the katana runner based on the KATANA_RUNNER_BIN -/// environment variable. If not set, try to to use katana from the PATH. +/// environment variable. If not set, try to use katana from the PATH. fn determine_default_program_path() -> String { if let Ok(bin) = std::env::var("KATANA_RUNNER_BIN") { bin } else { "katana".to_string() } } diff --git a/crates/katana/storage/codecs/derive/Cargo.toml b/crates/katana/storage/codecs/derive/Cargo.toml index 986d362e50..f4e0cc5248 100644 --- a/crates/katana/storage/codecs/derive/Cargo.toml +++ b/crates/katana/storage/codecs/derive/Cargo.toml @@ -9,10 +9,10 @@ version.workspace = true proc-macro = true [dependencies] -proc-macro2 = "1.0.70" -quote = "1.0.33" +proc-macro2.workspace = true +quote.workspace = true serde.workspace = true -syn = { version = "2.0.41", features = [ "extra-traits", "full" ] } +syn = { workspace = true, features = [ "extra-traits", "full" ] } [package.metadata.cargo-udeps.ignore] normal = [ "serde" ] diff --git a/crates/katana/storage/db/Cargo.toml b/crates/katana/storage/db/Cargo.toml index b92f75b51a..47ff7f3464 100644 --- a/crates/katana/storage/db/Cargo.toml +++ b/crates/katana/storage/db/Cargo.toml @@ -18,14 +18,11 @@ parking_lot.workspace = true roaring = { version = "0.10.3", features = [ "serde" ] } serde.workspace = true serde_json.workspace = true -starknet.workspace = true -starknet-types-core.workspace = true tempfile.workspace = true thiserror.workspace = true tracing.workspace = true # codecs -bitvec.workspace = true postcard = { workspace = true, optional = true } smallvec = "1.13.2" @@ -37,6 +34,9 @@ rev = "b34b0d3" [dev-dependencies] arbitrary.workspace = true criterion.workspace = true +proptest = "1.6.0" +rstest.workspace = true +starknet.workspace = true [features] default = [ "postcard" ] diff --git a/crates/katana/storage/db/src/abstraction/transaction.rs b/crates/katana/storage/db/src/abstraction/transaction.rs index 9f62a7a572..0280e65c07 100644 --- a/crates/katana/storage/db/src/abstraction/transaction.rs +++ b/crates/katana/storage/db/src/abstraction/transaction.rs @@ -66,3 +66,115 @@ pub trait DbTxMut: DbTx { /// Clears all entries in the given database. This will empty the database. fn clear(&self) -> Result<(), DatabaseError>; } + +// --- Reference variations + +pub trait DbTxRef<'a>: Clone { + /// The cursor type. + type Cursor: DbCursor; + + /// The cursor type for dupsort table. + // TODO: ideally we should only define the cursor type once, + // find a way to not have to define both cursor types in both traits + type DupCursor: DbDupSortCursor; + + /// Creates a cursor to iterate over a table items. + fn cursor(&self) -> Result, DatabaseError>; + + /// Creates a cursor to iterate over a dupsort table items. + fn cursor_dup(&self) -> Result, DatabaseError>; + + /// Gets a value from a table using the given key. + fn get(&self, key: T::Key) -> Result, DatabaseError>; + + /// Returns number of entries in the table. + fn entries(&self) -> Result; +} + +pub trait DbTxMutRef<'a>: DbTxRef<'a> { + /// The mutable cursor type. + type Cursor: DbCursorMut; + + /// The mutable cursor type for dupsort table. + // TODO: find a way to not have to define both cursor types in both traits + type DupCursor: DbDupSortCursorMut; + + /// Creates a cursor to mutably iterate over a table items. + fn cursor_mut(&self) -> Result<>::Cursor, DatabaseError>; + + /// Creates a cursor to iterate over a dupsort table items. + fn cursor_dup_mut( + &self, + ) -> Result<>::DupCursor, DatabaseError>; + + /// Inserts an item into a database. + /// + /// This function stores key/data pairs in the database. The default behavior is to enter the + /// new key/data pair, replacing any previously existing key if duplicates are disallowed, or + /// adding a duplicate data item if duplicates are allowed (DatabaseFlags::DUP_SORT). + fn put(&self, key: T::Key, value: T::Value) -> Result<(), DatabaseError>; + + /// Delete items from a database, removing the key/data pair if it exists. + /// + /// If the data parameter is [Some] only the matching data item will be deleted. Otherwise, if + /// data parameter is [None], any/all value(s) for specified key will be deleted. + /// + /// Returns `true` if the key/value pair was present. + fn delete(&self, key: T::Key, value: Option) + -> Result; + + /// Clears all entries in the given database. This will empty the database. + fn clear(&self) -> Result<(), DatabaseError>; +} + +impl<'a, Tx: DbTx> DbTxRef<'a> for &'a Tx { + type Cursor = ::Cursor; + type DupCursor = ::DupCursor; + + fn cursor(&self) -> Result, DatabaseError> { + ::cursor::(self) + } + + fn cursor_dup(&self) -> Result, DatabaseError> { + ::cursor_dup::(self) + } + + fn entries(&self) -> Result { + ::entries::(self) + } + + fn get(&self, key: T::Key) -> Result, DatabaseError> { + ::get::(self, key) + } +} + +impl<'a, Tx: DbTxMut> DbTxMutRef<'a> for &'a Tx { + type Cursor = ::Cursor; + type DupCursor = ::DupCursor; + + fn cursor_mut(&self) -> Result<>::Cursor, DatabaseError> { + ::cursor_mut::(self) + } + + fn cursor_dup_mut( + &self, + ) -> Result<>::DupCursor, DatabaseError> { + ::cursor_dup_mut::(self) + } + + fn put(&self, key: T::Key, value: T::Value) -> Result<(), DatabaseError> { + ::put::(self, key, value) + } + + fn delete( + &self, + key: T::Key, + value: Option, + ) -> Result { + ::delete::(self, key, value) + } + + fn clear(&self) -> Result<(), DatabaseError> { + ::clear::(self) + } +} diff --git a/crates/katana/storage/db/src/codecs/mod.rs b/crates/katana/storage/db/src/codecs/mod.rs index 049168919c..f777f3899a 100644 --- a/crates/katana/storage/db/src/codecs/mod.rs +++ b/crates/katana/storage/db/src/codecs/mod.rs @@ -2,7 +2,7 @@ pub mod postcard; use katana_primitives::block::FinalityStatus; -use katana_primitives::class::FlattenedSierraClass; +use katana_primitives::class::ContractClass; use katana_primitives::contract::ContractAddress; use katana_primitives::Felt; @@ -72,14 +72,27 @@ macro_rules! impl_encode_and_decode_for_felts { impl_encode_and_decode_for_uints!(u64); impl_encode_and_decode_for_felts!(Felt, ContractAddress); -impl Compress for FlattenedSierraClass { +impl Encode for String { + type Encoded = Vec; + fn encode(self) -> Self::Encoded { + self.into_bytes() + } +} + +impl Decode for String { + fn decode>(bytes: B) -> Result { + String::from_utf8(bytes.as_ref().to_vec()).map_err(|e| CodecError::Decode(e.to_string())) + } +} + +impl Compress for ContractClass { type Compressed = Vec; fn compress(self) -> Self::Compressed { serde_json::to_vec(&self).unwrap() } } -impl Decompress for FlattenedSierraClass { +impl Decompress for ContractClass { fn decompress>(bytes: B) -> Result { serde_json::from_slice(bytes.as_ref()).map_err(|e| CodecError::Decode(e.to_string())) } diff --git a/crates/katana/storage/db/src/codecs/postcard.rs b/crates/katana/storage/db/src/codecs/postcard.rs index 2a154fb756..5b5dfb27c5 100644 --- a/crates/katana/storage/db/src/codecs/postcard.rs +++ b/crates/katana/storage/db/src/codecs/postcard.rs @@ -11,6 +11,7 @@ use crate::error::CodecError; use crate::models::block::StoredBlockBodyIndices; use crate::models::contract::ContractInfoChangeList; use crate::models::list::BlockList; +use crate::models::stage::StageCheckpoint; use crate::models::trie::TrieDatabaseValue; macro_rules! impl_compress_and_decompress_for_table_values { @@ -42,6 +43,7 @@ impl_compress_and_decompress_for_table_values!( TrieDatabaseValue, ContractAddress, BlockList, + StageCheckpoint, GenericContractInfo, StoredBlockBodyIndices, ContractInfoChangeList diff --git a/crates/katana/storage/db/src/mdbx/tx.rs b/crates/katana/storage/db/src/mdbx/tx.rs index da902c9f5d..a1d6529045 100644 --- a/crates/katana/storage/db/src/mdbx/tx.rs +++ b/crates/katana/storage/db/src/mdbx/tx.rs @@ -33,7 +33,7 @@ pub struct Tx { impl Tx { /// Creates new `Tx` object with a `RO` or `RW` transaction. pub fn new(inner: libmdbx::Transaction) -> Self { - Self { inner, db_handles: Default::default() } + Self { inner, db_handles: RwLock::new([None; NUM_TABLES]) } } pub fn get_dbi(&self) -> Result { diff --git a/crates/katana/storage/db/src/models/mod.rs b/crates/katana/storage/db/src/models/mod.rs index 09fe7d1e0c..426c7ea447 100644 --- a/crates/katana/storage/db/src/models/mod.rs +++ b/crates/katana/storage/db/src/models/mod.rs @@ -2,5 +2,6 @@ pub mod block; pub mod class; pub mod contract; pub mod list; +pub mod stage; pub mod storage; pub mod trie; diff --git a/crates/katana/storage/db/src/models/stage.rs b/crates/katana/storage/db/src/models/stage.rs new file mode 100644 index 0000000000..7bd32d2220 --- /dev/null +++ b/crates/katana/storage/db/src/models/stage.rs @@ -0,0 +1,13 @@ +use katana_primitives::block::BlockNumber; +use serde::{Deserialize, Serialize}; + +/// Unique identifier for a pipeline stage. +pub type StageId = String; + +/// Pipeline stage checkpoint. +#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)] +#[cfg_attr(test, derive(::arbitrary::Arbitrary))] +pub struct StageCheckpoint { + /// The block number that the stage has processed up to. + pub block: BlockNumber, +} diff --git a/crates/katana/storage/db/src/models/trie.rs b/crates/katana/storage/db/src/models/trie.rs index b10456af57..4f5cc49ec3 100644 --- a/crates/katana/storage/db/src/models/trie.rs +++ b/crates/katana/storage/db/src/models/trie.rs @@ -1,18 +1,65 @@ use katana_trie::bonsai::ByteVec; use serde::{Deserialize, Serialize}; -use crate::codecs::{Decode, Encode}; +use crate::codecs::{Compress, Decode, Decompress, Encode}; use crate::error::CodecError; +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct TrieHistoryEntry { + pub key: TrieDatabaseKey, + pub value: TrieDatabaseValue, +} + +impl Compress for TrieHistoryEntry { + type Compressed = Vec; + + fn compress(self) -> Self::Compressed { + let mut buf = Vec::new(); + buf.extend(self.key.encode()); + buf.extend(self.value.compress()); + buf + } +} + +impl Decompress for TrieHistoryEntry { + fn decompress>(bytes: B) -> Result { + let bytes = bytes.as_ref(); + + let key = TrieDatabaseKey::decode(bytes)?; + // first byte is the key type, second byte is the actual key length + let key_bytes_length = 1 + 1 + key.key.len(); + let value = TrieDatabaseValue::decompress(&bytes[key_bytes_length..])?; + + Ok(Self { key, value }) + } +} + #[repr(u8)] -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)] pub enum TrieDatabaseKeyType { Trie = 0, Flat, TrieLog, } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Debug, thiserror::Error)] +#[error("unknown trie key type: {0}")] +pub struct TrieDatabaseKeyTypeTryFromError(u8); + +impl TryFrom for TrieDatabaseKeyType { + type Error = TrieDatabaseKeyTypeTryFromError; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Self::Trie), + 1 => Ok(Self::Flat), + 2 => Ok(Self::TrieLog), + invalid => Err(TrieDatabaseKeyTypeTryFromError(invalid)), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] pub struct TrieDatabaseKey { pub r#type: TrieDatabaseKeyType, pub key: Vec, @@ -26,6 +73,7 @@ impl Encode for TrieDatabaseKey { fn encode(self) -> Self::Encoded { let mut encoded = Vec::new(); encoded.push(self.r#type as u8); + encoded.push(self.key.len() as u8); // Encode key length encoded.extend(self.key); encoded } @@ -34,18 +82,21 @@ impl Encode for TrieDatabaseKey { impl Decode for TrieDatabaseKey { fn decode>(bytes: B) -> Result { let bytes = bytes.as_ref(); - if bytes.is_empty() { - panic!("emptyy buffer") + + if bytes.len() < 2 { + // Need at least type and length bytes + panic!("empty buffer") } - let r#type = match bytes[0] { - 0 => TrieDatabaseKeyType::Trie, - 1 => TrieDatabaseKeyType::Flat, - 2 => TrieDatabaseKeyType::TrieLog, - _ => panic!("Invalid trie database key type"), - }; + let r#type = + TrieDatabaseKeyType::try_from(bytes[0]).expect("Invalid trie database key type"); + let key_len = bytes[1] as usize; + + if bytes.len() < 2 + key_len { + panic!("Buffer too short for key length"); + } - let key = bytes[1..].to_vec(); + let key = bytes[2..2 + key_len].to_vec(); Ok(TrieDatabaseKey { r#type, key }) } diff --git a/crates/katana/storage/db/src/tables.rs b/crates/katana/storage/db/src/tables.rs index 3ebfc050d0..c3ca7c5f44 100644 --- a/crates/katana/storage/db/src/tables.rs +++ b/crates/katana/storage/db/src/tables.rs @@ -1,5 +1,5 @@ use katana_primitives::block::{BlockHash, BlockNumber, FinalityStatus, Header}; -use katana_primitives::class::{ClassHash, CompiledClass, CompiledClassHash, FlattenedSierraClass}; +use katana_primitives::class::{ClassHash, CompiledClass, CompiledClassHash, ContractClass}; use katana_primitives::contract::{ContractAddress, GenericContractInfo, StorageKey}; use katana_primitives::receipt::Receipt; use katana_primitives::trace::TxExecInfo; @@ -9,8 +9,9 @@ use crate::codecs::{Compress, Decode, Decompress, Encode}; use crate::models::block::StoredBlockBodyIndices; use crate::models::contract::{ContractClassChange, ContractInfoChangeList, ContractNonceChange}; use crate::models::list::BlockList; +use crate::models::stage::{StageCheckpoint, StageId}; use crate::models::storage::{ContractStorageEntry, ContractStorageKey, StorageEntry}; -use crate::models::trie::{TrieDatabaseKey, TrieDatabaseValue}; +use crate::models::trie::{TrieDatabaseKey, TrieDatabaseValue, TrieHistoryEntry}; pub trait Key: Encode + Decode + Clone + std::fmt::Debug {} pub trait Value: Compress + Decompress + std::fmt::Debug {} @@ -19,7 +20,7 @@ impl Key for T where T: Encode + Decode + Clone + std::fmt::Debug {} impl Value for T where T: Compress + Decompress + std::fmt::Debug {} /// An asbtraction for a table. -pub trait Table { +pub trait Table: 'static { /// The name of the table. const NAME: &'static str; /// The key type of the table. @@ -36,7 +37,12 @@ pub trait DupSort: Table { type SubKey: Key; } -pub trait Trie: Table {} +pub trait Trie: Table { + /// Table for storing the trie entries according to the block its was committed. + type History: DupSort; + /// Table for storing the trie change set. + type Changeset: Table; +} /// Enum for the types of tables present in libmdbx. #[derive(Debug, PartialEq, Copy, Clone)] @@ -47,7 +53,7 @@ pub enum TableType { DupSort, } -pub const NUM_TABLES: usize = 26; +pub const NUM_TABLES: usize = 33; /// Macro to declare `libmdbx` tables. #[macro_export] @@ -161,7 +167,7 @@ define_tables_enum! {[ (Receipts, TableType::Table), (CompiledClassHashes, TableType::Table), (CompiledClasses, TableType::Table), - (SierraClasses, TableType::Table), + (Classes, TableType::Table), (ContractInfo, TableType::Table), (ContractStorage, TableType::DupSort), (ClassDeclarationBlock, TableType::Table), @@ -171,12 +177,22 @@ define_tables_enum! {[ (ClassChangeHistory, TableType::DupSort), (StorageChangeHistory, TableType::DupSort), (StorageChangeSet, TableType::Table), - (ClassTrie, TableType::Table), - (ContractTrie, TableType::Table), - (ContractStorageTrie, TableType::Table) + (StageCheckpoints, TableType::Table), + (ClassesTrie, TableType::Table), + (ContractsTrie, TableType::Table), + (StoragesTrie, TableType::Table), + (ClassesTrieHistory, TableType::DupSort), + (ContractsTrieHistory, TableType::DupSort), + (StoragesTrieHistory, TableType::DupSort), + (ClassesTrieChangeSet, TableType::Table), + (ContractsTrieChangeSet, TableType::Table), + (StoragesTrieChangeSet, TableType::Table) ]} tables! { + /// Pipeline stages checkpoint + StageCheckpoints: (StageId) => StageCheckpoint, + /// Store canonical block headers Headers: (BlockNumber) => Header, /// Stores block hashes according to its block number @@ -202,10 +218,10 @@ tables! { Receipts: (TxNumber) => Receipt, /// Store compiled classes CompiledClassHashes: (ClassHash) => CompiledClassHash, - /// Store compiled contract classes according to its compiled class hash + /// Store compiled contract classes according to its class hash CompiledClasses: (ClassHash) => CompiledClass, - /// Store Sierra classes according to its class hash - SierraClasses: (ClassHash) => FlattenedSierraClass, + /// Store contract classes according to its class hash + Classes: (ClassHash) => ContractClass, /// Store contract information according to its contract address ContractInfo: (ContractAddress) => GenericContractInfo, /// Store contract storage @@ -231,16 +247,41 @@ tables! { StorageChangeHistory: (BlockNumber, ContractStorageKey) => ContractStorageEntry, /// Class trie - ClassTrie: (TrieDatabaseKey) => TrieDatabaseValue, + ClassesTrie: (TrieDatabaseKey) => TrieDatabaseValue, /// Contract trie - ContractTrie: (TrieDatabaseKey) => TrieDatabaseValue, + ContractsTrie: (TrieDatabaseKey) => TrieDatabaseValue, /// Contract storage trie - ContractStorageTrie: (TrieDatabaseKey) => TrieDatabaseValue + StoragesTrie: (TrieDatabaseKey) => TrieDatabaseValue, + + /// Class trie history + ClassesTrieHistory: (BlockNumber, TrieDatabaseKey) => TrieHistoryEntry, + /// Contract trie history + ContractsTrieHistory: (BlockNumber, TrieDatabaseKey) => TrieHistoryEntry, + /// Contract storage trie history + StoragesTrieHistory: (BlockNumber, TrieDatabaseKey) => TrieHistoryEntry, + + /// Class trie change set + ClassesTrieChangeSet: (TrieDatabaseKey) => BlockList, + /// contract trie change set + ContractsTrieChangeSet: (TrieDatabaseKey) => BlockList, + /// contract storage trie change set + StoragesTrieChangeSet: (TrieDatabaseKey) => BlockList +} + +impl Trie for ClassesTrie { + type History = ClassesTrieHistory; + type Changeset = ClassesTrieChangeSet; } -impl Trie for ClassTrie {} -impl Trie for ContractTrie {} -impl Trie for ContractStorageTrie {} +impl Trie for ContractsTrie { + type History = ContractsTrieHistory; + type Changeset = ContractsTrieChangeSet; +} + +impl Trie for StoragesTrie { + type History = StoragesTrieHistory; + type Changeset = StoragesTrieChangeSet; +} #[cfg(test)] mod tests { @@ -263,7 +304,7 @@ mod tests { assert_eq!(Tables::ALL[10].name(), Receipts::NAME); assert_eq!(Tables::ALL[11].name(), CompiledClassHashes::NAME); assert_eq!(Tables::ALL[12].name(), CompiledClasses::NAME); - assert_eq!(Tables::ALL[13].name(), SierraClasses::NAME); + assert_eq!(Tables::ALL[13].name(), Classes::NAME); assert_eq!(Tables::ALL[14].name(), ContractInfo::NAME); assert_eq!(Tables::ALL[15].name(), ContractStorage::NAME); assert_eq!(Tables::ALL[16].name(), ClassDeclarationBlock::NAME); @@ -273,8 +314,16 @@ mod tests { assert_eq!(Tables::ALL[20].name(), ClassChangeHistory::NAME); assert_eq!(Tables::ALL[21].name(), StorageChangeHistory::NAME); assert_eq!(Tables::ALL[22].name(), StorageChangeSet::NAME); - assert_eq!(Tables::ALL[23].name(), ClassTrie::NAME); - assert_eq!(Tables::ALL[24].name(), ContractTrie::NAME); + assert_eq!(Tables::ALL[23].name(), StageCheckpoints::NAME); + assert_eq!(Tables::ALL[24].name(), ClassesTrie::NAME); + assert_eq!(Tables::ALL[25].name(), ContractsTrie::NAME); + assert_eq!(Tables::ALL[26].name(), StoragesTrie::NAME); + assert_eq!(Tables::ALL[27].name(), ClassesTrieHistory::NAME); + assert_eq!(Tables::ALL[28].name(), ContractsTrieHistory::NAME); + assert_eq!(Tables::ALL[29].name(), StoragesTrieHistory::NAME); + assert_eq!(Tables::ALL[30].name(), ClassesTrieChangeSet::NAME); + assert_eq!(Tables::ALL[31].name(), ContractsTrieChangeSet::NAME); + assert_eq!(Tables::ALL[32].name(), StoragesTrieChangeSet::NAME); assert_eq!(Tables::Headers.table_type(), TableType::Table); assert_eq!(Tables::BlockHashes.table_type(), TableType::Table); @@ -289,7 +338,7 @@ mod tests { assert_eq!(Tables::Receipts.table_type(), TableType::Table); assert_eq!(Tables::CompiledClassHashes.table_type(), TableType::Table); assert_eq!(Tables::CompiledClasses.table_type(), TableType::Table); - assert_eq!(Tables::SierraClasses.table_type(), TableType::Table); + assert_eq!(Tables::Classes.table_type(), TableType::Table); assert_eq!(Tables::ContractInfo.table_type(), TableType::Table); assert_eq!(Tables::ContractStorage.table_type(), TableType::DupSort); assert_eq!(Tables::ClassDeclarationBlock.table_type(), TableType::Table); @@ -299,8 +348,16 @@ mod tests { assert_eq!(Tables::ClassChangeHistory.table_type(), TableType::DupSort); assert_eq!(Tables::StorageChangeHistory.table_type(), TableType::DupSort); assert_eq!(Tables::StorageChangeSet.table_type(), TableType::Table); - assert_eq!(Tables::ClassTrie.table_type(), TableType::Table); - assert_eq!(Tables::ContractTrie.table_type(), TableType::Table); + assert_eq!(Tables::StageCheckpoints.table_type(), TableType::Table); + assert_eq!(Tables::ClassesTrie.table_type(), TableType::Table); + assert_eq!(Tables::ContractsTrie.table_type(), TableType::Table); + assert_eq!(Tables::StoragesTrie.table_type(), TableType::Table); + assert_eq!(Tables::ClassesTrieHistory.table_type(), TableType::DupSort); + assert_eq!(Tables::ContractsTrieHistory.table_type(), TableType::DupSort); + assert_eq!(Tables::StoragesTrieHistory.table_type(), TableType::DupSort); + assert_eq!(Tables::ClassesTrieChangeSet.table_type(), TableType::Table); + assert_eq!(Tables::ContractsTrieChangeSet.table_type(), TableType::Table); + assert_eq!(Tables::StoragesTrieChangeSet.table_type(), TableType::Table); } use katana_primitives::address; @@ -320,6 +377,9 @@ mod tests { }; use crate::models::list::BlockList; use crate::models::storage::{ContractStorageEntry, ContractStorageKey, StorageEntry}; + use crate::models::trie::{ + TrieDatabaseKey, TrieDatabaseKeyType, TrieDatabaseValue, TrieHistoryEntry, + }; macro_rules! assert_key_encode_decode { { $( ($name:ty, $key:expr) ),* } => { @@ -378,7 +438,7 @@ mod tests { (BlockNumber, 99), (TxExecInfo, TxExecInfo::default()), (CompiledClassHash, felt!("211")), - (CompiledClass, CompiledClass::Deprecated(Default::default())), + (CompiledClass, CompiledClass::Legacy(Default::default())), (GenericContractInfo, GenericContractInfo::default()), (StorageEntry, StorageEntry::default()), (ContractInfoChangeList, ContractInfoChangeList::default()), @@ -387,12 +447,17 @@ mod tests { (BlockList, BlockList::default()), (ContractStorageEntry, ContractStorageEntry::default()), (Receipt, Receipt::Invoke(InvokeTxReceipt { - revert_error: None, - events: Vec::new(), - messages_sent: Vec::new(), - execution_resources: Default::default(), - fee: TxFeeInfo { gas_consumed: 0, gas_price: 0, overall_fee: 0, unit: PriceUnit::Wei }, - })) + revert_error: None, + events: Vec::new(), + messages_sent: Vec::new(), + execution_resources: Default::default(), + fee: TxFeeInfo { gas_consumed: 0, gas_price: 0, overall_fee: 0, unit: PriceUnit::Wei }, + })), + (TrieDatabaseValue, TrieDatabaseValue::default()), + (TrieHistoryEntry, TrieHistoryEntry { + value: TrieDatabaseValue::default(), + key: TrieDatabaseKey { key: Vec::default(), r#type: TrieDatabaseKeyType::Flat }, + }) } } } diff --git a/crates/katana/storage/db/src/trie/class.rs b/crates/katana/storage/db/src/trie/class.rs deleted file mode 100644 index 4e853829e9..0000000000 --- a/crates/katana/storage/db/src/trie/class.rs +++ /dev/null @@ -1,55 +0,0 @@ -use bitvec::order::Msb0; -use bitvec::vec::BitVec; -use bitvec::view::AsBits; -use katana_primitives::block::BlockNumber; -use katana_primitives::class::{ClassHash, CompiledClassHash}; -use katana_primitives::Felt; -use katana_trie::bonsai::id::BasicId; -use katana_trie::bonsai::{BonsaiStorage, BonsaiStorageConfig}; -use starknet::macros::short_string; -use starknet_types_core::hash::{Poseidon, StarkHash}; - -use crate::abstraction::DbTxMut; -use crate::tables; -use crate::trie::TrieDb; - -// https://docs.starknet.io/architecture-and-concepts/network-architecture/starknet-state/#classes_trie -const CONTRACT_CLASS_LEAF_V0: Felt = short_string!("CONTRACT_CLASS_LEAF_V0"); - -#[derive(Debug)] -pub struct ClassTrie { - inner: BonsaiStorage, Poseidon>, -} - -impl ClassTrie { - pub fn new(tx: Tx) -> Self { - let config = BonsaiStorageConfig { - max_saved_trie_logs: Some(0), - max_saved_snapshots: Some(0), - snapshot_interval: u64::MAX, - }; - - let db = TrieDb::::new(tx); - let inner = BonsaiStorage::new(db, config).unwrap(); - - Self { inner } - } - - pub fn insert(&mut self, hash: ClassHash, compiled_hash: CompiledClassHash) { - let value = Poseidon::hash(&CONTRACT_CLASS_LEAF_V0, &compiled_hash); - let key: BitVec = hash.to_bytes_be().as_bits()[5..].to_owned(); - self.inner.insert(self.bonsai_identifier(), &key, &value).unwrap(); - } - - pub fn commit(&mut self, block_number: BlockNumber) { - self.inner.commit(BasicId::new(block_number)).unwrap(); - } - - pub fn root(&self) -> Felt { - self.inner.root_hash(self.bonsai_identifier()).unwrap() - } - - fn bonsai_identifier(&self) -> &'static [u8] { - b"1" - } -} diff --git a/crates/katana/storage/db/src/trie/contract.rs b/crates/katana/storage/db/src/trie/contract.rs deleted file mode 100644 index 5e460739aa..0000000000 --- a/crates/katana/storage/db/src/trie/contract.rs +++ /dev/null @@ -1,83 +0,0 @@ -use bitvec::order::Msb0; -use bitvec::vec::BitVec; -use bitvec::view::AsBits; -use katana_primitives::block::BlockNumber; -use katana_primitives::contract::{StorageKey, StorageValue}; -use katana_primitives::{ContractAddress, Felt}; -use katana_trie::bonsai::id::BasicId; -use katana_trie::bonsai::{BonsaiStorage, BonsaiStorageConfig}; -use starknet_types_core::hash::Poseidon; - -use crate::abstraction::DbTxMut; -use crate::tables; -use crate::trie::TrieDb; - -#[derive(Debug)] -pub struct StorageTrie { - inner: BonsaiStorage, Poseidon>, -} - -impl StorageTrie { - pub fn new(tx: Tx) -> Self { - let config = BonsaiStorageConfig { - max_saved_trie_logs: Some(0), - max_saved_snapshots: Some(0), - snapshot_interval: u64::MAX, - }; - - let db = TrieDb::::new(tx); - let inner = BonsaiStorage::new(db, config).unwrap(); - - Self { inner } - } - - pub fn insert(&mut self, address: ContractAddress, key: StorageKey, value: StorageValue) { - let key: BitVec = key.to_bytes_be().as_bits()[5..].to_owned(); - self.inner.insert(&address.to_bytes_be(), &key, &value).unwrap(); - } - - pub fn commit(&mut self, block_number: BlockNumber) { - self.inner.commit(BasicId::new(block_number)).unwrap(); - } - - pub fn root(&self, address: &ContractAddress) -> Felt { - self.inner.root_hash(&address.to_bytes_be()).unwrap() - } -} - -#[derive(Debug)] -pub struct ContractTrie { - inner: BonsaiStorage, Poseidon>, -} - -impl ContractTrie { - pub fn new(tx: Tx) -> Self { - let config = BonsaiStorageConfig { - max_saved_trie_logs: Some(0), - max_saved_snapshots: Some(0), - snapshot_interval: u64::MAX, - }; - - let db = TrieDb::::new(tx); - let inner = BonsaiStorage::new(db, config).unwrap(); - - Self { inner } - } - - pub fn insert(&mut self, address: ContractAddress, state_hash: Felt) { - let key: BitVec = address.to_bytes_be().as_bits()[5..].to_owned(); - self.inner.insert(self.bonsai_identifier(), &key, &state_hash).unwrap(); - } - - pub fn commit(&mut self, block_number: BlockNumber) { - self.inner.commit(BasicId::new(block_number)).unwrap(); - } - - pub fn root(&self) -> Felt { - self.inner.root_hash(self.bonsai_identifier()).unwrap() - } - - fn bonsai_identifier(&self) -> &'static [u8] { - b"1" - } -} diff --git a/crates/katana/storage/db/src/trie/mod.rs b/crates/katana/storage/db/src/trie/mod.rs index 823c9f28ba..7301b66dc4 100644 --- a/crates/katana/storage/db/src/trie/mod.rs +++ b/crates/katana/storage/db/src/trie/mod.rs @@ -1,20 +1,23 @@ +use core::fmt; +use std::collections::HashMap; +use std::fmt::Debug; use std::marker::PhantomData; use anyhow::Result; -use katana_trie::bonsai::id::BasicId; -use katana_trie::bonsai::{self, ByteVec, DatabaseKey}; +use katana_primitives::block::BlockNumber; +use katana_primitives::ContractAddress; +use katana_trie::bonsai::{BonsaiDatabase, BonsaiPersistentDatabase, ByteVec, DatabaseKey}; +use katana_trie::CommitId; use smallvec::ToSmallVec; -use crate::abstraction::{DbCursor, DbTxMut}; -use crate::models::trie::{TrieDatabaseKey, TrieDatabaseKeyType}; +use crate::abstraction::{DbCursor, DbTxMutRef, DbTxRef}; +use crate::models::trie::{TrieDatabaseKey, TrieDatabaseKeyType, TrieHistoryEntry}; use crate::models::{self}; -use crate::tables; +use crate::tables::{self, Trie}; -mod class; -mod contract; +mod snapshot; -pub use class::ClassTrie; -pub use contract::{ContractTrie, StorageTrie}; +pub use snapshot::SnapshotTrieDb; #[derive(Debug, thiserror::Error)] #[error(transparent)] @@ -23,25 +26,224 @@ pub struct Error(#[from] crate::error::DatabaseError); impl katana_trie::bonsai::DBError for Error {} #[derive(Debug)] -pub struct TrieDb { +pub struct TrieDbFactory<'a, Tx: DbTxRef<'a>> { tx: Tx, - _table: PhantomData, + _phantom: &'a PhantomData<()>, } -impl TrieDb +impl<'a, Tx: DbTxRef<'a>> TrieDbFactory<'a, Tx> { + pub fn new(tx: Tx) -> Self { + Self { tx, _phantom: &PhantomData } + } + + pub fn latest(&self) -> GlobalTrie<'a, Tx> { + GlobalTrie { tx: self.tx.clone(), _phantom: &PhantomData } + } + + // TODO: check that the snapshot for the block number is available + pub fn historical(&self, block: BlockNumber) -> Option> { + Some(HistoricalGlobalTrie { tx: self.tx.clone(), block, _phantom: &PhantomData }) + } +} + +/// Provides access to the latest tries. +#[derive(Debug)] +pub struct GlobalTrie<'a, Tx: DbTxRef<'a>> { + tx: Tx, + _phantom: &'a PhantomData<()>, +} + +impl<'a, Tx> GlobalTrie<'a, Tx> +where + Tx: DbTxRef<'a> + Debug, +{ + /// Returns the contracts trie. + pub fn contracts_trie( + &self, + ) -> katana_trie::ContractsTrie> { + katana_trie::ContractsTrie::new(TrieDb::new(self.tx.clone())) + } + + /// Returns the classes trie. + pub fn classes_trie(&self) -> katana_trie::ClassesTrie> { + katana_trie::ClassesTrie::new(TrieDb::new(self.tx.clone())) + } + + // TODO: makes this return an Option + /// Returns the storages trie. + pub fn storages_trie( + &self, + address: ContractAddress, + ) -> katana_trie::StoragesTrie> { + katana_trie::StoragesTrie::new(TrieDb::new(self.tx.clone()), address) + } +} + +/// Historical tries, allowing access to the state tries at each block. +#[derive(Debug)] +pub struct HistoricalGlobalTrie<'a, Tx: DbTxRef<'a>> { + /// The database transaction. + tx: Tx, + /// The block number at which the trie was constructed. + block: BlockNumber, + _phantom: &'a PhantomData<()>, +} + +impl<'a, Tx> HistoricalGlobalTrie<'a, Tx> +where + Tx: DbTxRef<'a> + Debug, +{ + /// Returns the historical contracts trie. + pub fn contracts_trie( + &self, + ) -> katana_trie::ContractsTrie> { + let commit = CommitId::new(self.block); + katana_trie::ContractsTrie::new(SnapshotTrieDb::new(self.tx.clone(), commit)) + } + + /// Returns the historical classes trie. + pub fn classes_trie( + &self, + ) -> katana_trie::ClassesTrie> { + let commit = CommitId::new(self.block); + katana_trie::ClassesTrie::new(SnapshotTrieDb::new(self.tx.clone(), commit)) + } + + // TODO: makes this return an Option + /// Returns the historical storages trie. + pub fn storages_trie( + &self, + address: ContractAddress, + ) -> katana_trie::StoragesTrie> { + let commit = CommitId::new(self.block); + katana_trie::StoragesTrie::new(SnapshotTrieDb::new(self.tx.clone(), commit), address) + } +} + +// --- Trie's database implementations. These are implemented based on the Bonsai Trie +// functionalities and abstractions. + +pub struct TrieDb<'a, Tb, Tx> +where + Tb: Trie, + Tx: DbTxRef<'a>, +{ + tx: Tx, + _phantom: &'a PhantomData, +} + +impl<'a, Tb, Tx> fmt::Debug for TrieDb<'a, Tb, Tx> +where + Tb: Trie, + Tx: DbTxRef<'a> + fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("TrieDbMut").field("tx", &self.tx).finish() + } +} + +impl<'a, Tb, Tx> TrieDb<'a, Tb, Tx> +where + Tb: Trie, + Tx: DbTxRef<'a>, +{ + pub(crate) fn new(tx: Tx) -> Self { + Self { tx, _phantom: &PhantomData } + } +} + +impl<'a, Tb, Tx> BonsaiDatabase for TrieDb<'a, Tb, Tx> +where + Tb: Trie, + Tx: DbTxRef<'a> + fmt::Debug, +{ + type Batch = (); + type DatabaseError = Error; + + fn create_batch(&self) -> Self::Batch {} + + fn remove_by_prefix(&mut self, _: &DatabaseKey<'_>) -> Result<(), Self::DatabaseError> { + Ok(()) + } + + fn get(&self, key: &DatabaseKey<'_>) -> Result, Self::DatabaseError> { + let value = self.tx.get::(to_db_key(key))?; + Ok(value) + } + + fn get_by_prefix( + &self, + _: &DatabaseKey<'_>, + ) -> Result, Self::DatabaseError> { + todo!() + } + + fn insert( + &mut self, + _: &DatabaseKey<'_>, + _: &[u8], + _: Option<&mut Self::Batch>, + ) -> Result, Self::DatabaseError> { + unimplemented!("not supported in read-only transaction") + } + + fn remove( + &mut self, + _: &DatabaseKey<'_>, + _: Option<&mut Self::Batch>, + ) -> Result, Self::DatabaseError> { + unimplemented!("not supported in read-only transaction") + } + + fn contains(&self, key: &DatabaseKey<'_>) -> Result { + let key = to_db_key(key); + let value = self.tx.get::(key)?; + Ok(value.is_some()) + } + + fn write_batch(&mut self, _: Self::Batch) -> Result<(), Self::DatabaseError> { + unimplemented!("not supported in read-only transaction") + } +} + +pub struct TrieDbMut<'tx, Tb, Tx> +where + Tb: Trie, + Tx: DbTxMutRef<'tx>, +{ + tx: Tx, + /// List of key-value pairs that has been added throughout the duration of the trie + /// transaction. + /// + /// This will be used to create the trie snapshot. + write_cache: HashMap, + _phantom: &'tx PhantomData, +} + +impl<'tx, Tb, Tx> fmt::Debug for TrieDbMut<'tx, Tb, Tx> where - Tb: tables::Trie, - Tx: DbTxMut, + Tb: Trie, + Tx: DbTxMutRef<'tx> + fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("TrieDbMut").field("tx", &self.tx).finish() + } +} + +impl<'tx, Tb, Tx> TrieDbMut<'tx, Tb, Tx> +where + Tb: Trie, + Tx: DbTxMutRef<'tx>, { pub fn new(tx: Tx) -> Self { - Self { tx, _table: PhantomData } + Self { tx, write_cache: HashMap::new(), _phantom: &PhantomData } } } -impl bonsai::BonsaiDatabase for TrieDb +impl<'tx, Tb, Tx> BonsaiDatabase for TrieDbMut<'tx, Tb, Tx> where - Tb: tables::Trie, - Tx: DbTxMut, + Tb: Trie, + Tx: DbTxMutRef<'tx> + fmt::Debug, { type Batch = (); type DatabaseError = Error; @@ -85,23 +287,30 @@ where &mut self, key: &DatabaseKey<'_>, value: &[u8], - _batch: Option<&mut Self::Batch>, + batch: Option<&mut Self::Batch>, ) -> Result, Self::DatabaseError> { + let _ = batch; let key = to_db_key(key); let value: ByteVec = value.to_smallvec(); + let old_value = self.tx.get::(key.clone())?; - self.tx.put::(key, value)?; + self.tx.put::(key.clone(), value.clone())?; + + self.write_cache.insert(key, value); Ok(old_value) } fn remove( &mut self, key: &DatabaseKey<'_>, - _batch: Option<&mut Self::Batch>, + batch: Option<&mut Self::Batch>, ) -> Result, Self::DatabaseError> { + let _ = batch; let key = to_db_key(key); + let old_value = self.tx.get::(key.clone())?; self.tx.delete::(key, None)?; + Ok(old_value) } @@ -111,27 +320,55 @@ where Ok(value.is_some()) } - fn write_batch(&mut self, _batch: Self::Batch) -> Result<(), Self::DatabaseError> { + fn write_batch(&mut self, _: Self::Batch) -> Result<(), Self::DatabaseError> { Ok(()) } } -impl bonsai::BonsaiPersistentDatabase for TrieDb +impl<'tx, Tb, Tx> BonsaiPersistentDatabase for TrieDbMut<'tx, Tb, Tx> where - Tb: tables::Trie, - Tx: DbTxMut, + Tb: Trie, + Tx: DbTxMutRef<'tx> + fmt::Debug + 'tx, { type DatabaseError = Error; - type Transaction = TrieDb; + type Transaction<'a> = SnapshotTrieDb<'tx, Tb, Tx> where Self: 'a; + + fn snapshot(&mut self, id: CommitId) { + let block_number: BlockNumber = id.into(); + + let entries = std::mem::take(&mut self.write_cache); + let entries = entries.into_iter().map(|(key, value)| TrieHistoryEntry { key, value }); - fn snapshot(&mut self, _: BasicId) {} + for entry in entries { + let mut set = self + .tx + .get::(entry.key.clone()) + .expect("failed to get trie change set") + .unwrap_or_default(); + set.insert(block_number); - fn merge(&mut self, _: Self::Transaction) -> Result<(), Self::DatabaseError> { - todo!(); + self.tx + .put::(entry.key.clone(), set) + .expect("failed to put trie change set"); + + self.tx + .put::(block_number, entry) + .expect("failed to put trie history entry"); + } } - fn transaction(&self, _: BasicId) -> Option { - todo!(); + // merging should recompute the trie again + fn merge<'a>(&mut self, transaction: Self::Transaction<'a>) -> Result<(), Self::DatabaseError> + where + Self: 'a, + { + let _ = transaction; + unimplemented!(); + } + + // TODO: check if the snapshot exist + fn transaction(&self, id: CommitId) -> Option<(CommitId, Self::Transaction<'_>)> { + Some((id, SnapshotTrieDb::new(self.tx.clone(), id))) } } @@ -148,3 +385,104 @@ fn to_db_key(key: &DatabaseKey<'_>) -> models::trie::TrieDatabaseKey { } } } + +#[cfg(test)] +mod tests { + use katana_primitives::hash::{Poseidon, StarkHash}; + use katana_primitives::{felt, hash}; + use katana_trie::{verify_proof, ClassesTrie, CommitId}; + use starknet::macros::short_string; + + use super::TrieDbMut; + use crate::abstraction::Database; + use crate::mdbx::test_utils; + use crate::tables; + use crate::trie::SnapshotTrieDb; + + #[test] + fn snapshot() { + let db = test_utils::create_test_db(); + let db_tx = db.tx_mut().expect("failed to get tx"); + + let mut trie = ClassesTrie::new(TrieDbMut::::new(&db_tx)); + + let root0 = { + let entries = [ + (felt!("0x9999"), felt!("0xdead")), + (felt!("0x5555"), felt!("0xbeef")), + (felt!("0x1337"), felt!("0xdeadbeef")), + ]; + + for (key, value) in entries { + trie.insert(key, value); + } + + trie.commit(0); + trie.root() + }; + + let root1 = { + let entries = [ + (felt!("0x6969"), felt!("0x80085")), + (felt!("0x3333"), felt!("0x420")), + (felt!("0x2222"), felt!("0x7171")), + ]; + + for (key, value) in entries { + trie.insert(key, value); + } + + trie.commit(1); + trie.root() + }; + + assert_ne!(root0, root1); + + { + let db = SnapshotTrieDb::::new(&db_tx, CommitId::new(0)); + let mut snapshot0 = ClassesTrie::new(db); + + let snapshot_root0 = snapshot0.root(); + assert_eq!(snapshot_root0, root0); + + let proofs0 = snapshot0.multiproof(vec![felt!("0x9999")]); + let verify_result0 = + verify_proof::(&proofs0, snapshot_root0, vec![felt!("0x9999")]); + + let value = + hash::Poseidon::hash(&short_string!("CONTRACT_CLASS_LEAF_V0"), &felt!("0xdead")); + assert_eq!(vec![value], verify_result0); + } + + { + let commit = CommitId::new(1); + let mut snapshot1 = + ClassesTrie::new(SnapshotTrieDb::::new(&db_tx, commit)); + + let snapshot_root1 = snapshot1.root(); + assert_eq!(snapshot_root1, root1); + + let proofs1 = snapshot1.multiproof(vec![felt!("0x6969")]); + let verify_result1 = + verify_proof::(&proofs1, snapshot_root1, vec![felt!("0x6969")]); + + let value = + hash::Poseidon::hash(&short_string!("CONTRACT_CLASS_LEAF_V0"), &felt!("0x80085")); + assert_eq!(vec![value], verify_result1); + } + + { + let root = trie.root(); + let proofs = trie.multiproof(vec![felt!("0x6969"), felt!("0x9999")]); + let result = + verify_proof::(&proofs, root, vec![felt!("0x6969"), felt!("0x9999")]); + + let value0 = + hash::Poseidon::hash(&short_string!("CONTRACT_CLASS_LEAF_V0"), &felt!("0x80085")); + let value1 = + hash::Poseidon::hash(&short_string!("CONTRACT_CLASS_LEAF_V0"), &felt!("0xdead")); + + assert_eq!(vec![value0, value1], result); + } + } +} diff --git a/crates/katana/storage/db/src/trie/snapshot.rs b/crates/katana/storage/db/src/trie/snapshot.rs new file mode 100644 index 0000000000..a9012888f9 --- /dev/null +++ b/crates/katana/storage/db/src/trie/snapshot.rs @@ -0,0 +1,244 @@ +use core::fmt; +use std::marker::PhantomData; + +use anyhow::Result; +use katana_primitives::block::BlockNumber; +use katana_trie::bonsai::{BonsaiDatabase, ByteVec, DatabaseKey}; +use katana_trie::CommitId; + +use super::Error; +use crate::abstraction::{DbDupSortCursor, DbTxRef}; +use crate::models::list::BlockList; +use crate::tables::Trie; +use crate::trie::to_db_key; + +pub struct SnapshotTrieDb<'tx, Tb, Tx> +where + Tb: Trie, + Tx: DbTxRef<'tx>, +{ + tx: Tx, + snapshot_id: CommitId, + _table: &'tx PhantomData, +} + +impl<'a, Tb, Tx> fmt::Debug for SnapshotTrieDb<'a, Tb, Tx> +where + Tb: Trie, + Tx: DbTxRef<'a> + fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("SnapshotTrieDb").field("tx", &self.tx).finish() + } +} + +/// This is a helper function for getting the block number of the most +/// recent change that occurred relative to the given block number. +/// +/// ## Arguments +/// +/// * `block_list`: A list of block numbers where a change in value occur. +fn recent_change_from_block(target: BlockNumber, block_list: &BlockList) -> Option { + // if the rank is 0, then it's either; + // 1. the list is empty + // 2. there are no prior changes occured before/at `block_number` + let rank = block_list.rank(target); + if rank == 0 { None } else { block_list.select(rank - 1) } +} + +impl<'tx, Tb, Tx> SnapshotTrieDb<'tx, Tb, Tx> +where + Tb: Trie, + Tx: DbTxRef<'tx>, +{ + pub(crate) fn new(tx: Tx, id: CommitId) -> Self { + Self { tx, snapshot_id: id, _table: &PhantomData } + } +} + +impl<'tx, Tb, Tx> BonsaiDatabase for SnapshotTrieDb<'tx, Tb, Tx> +where + Tb: Trie, + Tx: DbTxRef<'tx> + fmt::Debug, +{ + type Batch = (); + type DatabaseError = Error; + + fn create_batch(&self) -> Self::Batch {} + + fn remove_by_prefix(&mut self, _: &DatabaseKey<'_>) -> Result<(), Self::DatabaseError> { + unimplemented!("modifying trie snapshot is not supported") + } + + fn get(&self, key: &DatabaseKey<'_>) -> Result, Self::DatabaseError> { + let key = to_db_key(key); + let block_number = self.snapshot_id.into(); + + let change_set = self.tx.get::(key.clone())?; + if let Some(num) = change_set.and_then(|set| recent_change_from_block(block_number, &set)) { + let mut cursor = self.tx.cursor_dup::()?; + let entry = cursor + .seek_by_key_subkey(num, key.clone())? + .expect("entry should exist if in change set"); + + if entry.key == key { + return Ok(Some(entry.value)); + } + } + + Ok(None) + } + + fn get_by_prefix( + &self, + _: &DatabaseKey<'_>, + ) -> Result, Self::DatabaseError> { + todo!() + } + + fn insert( + &mut self, + _: &DatabaseKey<'_>, + _: &[u8], + _: Option<&mut Self::Batch>, + ) -> Result, Self::DatabaseError> { + unimplemented!("modifying trie snapshot is not supported") + } + + fn remove( + &mut self, + _: &DatabaseKey<'_>, + _: Option<&mut Self::Batch>, + ) -> Result, Self::DatabaseError> { + unimplemented!("modifying trie snapshot is not supported") + } + + fn contains(&self, _: &DatabaseKey<'_>) -> Result { + todo!() + } + + fn write_batch(&mut self, _: Self::Batch) -> Result<(), Self::DatabaseError> { + unimplemented!("modifying trie snapshot is not supported") + } +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use katana_primitives::felt; + use katana_trie::bonsai::DatabaseKey; + use katana_trie::{BonsaiPersistentDatabase, CommitId}; + use proptest::prelude::*; + use proptest::strategy; + + use super::*; + use crate::abstraction::{Database, DbTx}; + use crate::mdbx::test_utils; + use crate::models::trie::TrieDatabaseKeyType; + use crate::tables; + use crate::trie::{SnapshotTrieDb, TrieDbMut}; + + #[allow(unused)] + fn arb_db_key_type() -> BoxedStrategy { + prop_oneof![ + Just(TrieDatabaseKeyType::Trie), + Just(TrieDatabaseKeyType::Flat), + Just(TrieDatabaseKeyType::TrieLog), + ] + .boxed() + } + + #[derive(Debug)] + struct Case { + number: BlockNumber, + keyvalues: HashMap<(TrieDatabaseKeyType, [u8; 32]), [u8; 32]>, + } + + prop_compose! { + // This create a strategy that generates a random values but always a hardcoded key + fn arb_keyvalues_with_fixed_key() ( + value in any::<[u8;32]>() + ) -> HashMap<(TrieDatabaseKeyType, [u8; 32]), [u8; 32]> { + let key = (TrieDatabaseKeyType::Trie, felt!("0x112345678921541231").to_bytes_be()); + HashMap::from_iter([(key, value)]) + } + } + + prop_compose! { + fn arb_keyvalues() ( + keyvalues in prop::collection::hash_map( + (arb_db_key_type(), any::<[u8;32]>()), + any::<[u8;32]>(), + 1..100 + ) + ) -> HashMap<(TrieDatabaseKeyType, [u8; 32]), [u8; 32]> { + keyvalues + } + } + + prop_compose! { + fn arb_block(count: u64, step: u64) ( + number in (count * step)..((count * step) + step), + keyvalues in arb_keyvalues_with_fixed_key() + ) -> Case { + Case { number, keyvalues } + } + } + + /// Strategy for generating a list of blocks with `count` size where each block is within a + /// range of `step` size. See [`arb_block`]. + fn arb_blocklist(step: u64, count: usize) -> impl strategy::Strategy> { + let mut strats = Vec::with_capacity(count); + for i in 0..count { + strats.push(arb_block(i as u64, step)); + } + strategy::Strategy::prop_map(strats, move |strats| strats) + } + + proptest! { + #[test] + fn test_get_insert(blocks in arb_blocklist(10, 1000)) { + let db = test_utils::create_test_db(); + let tx = db.tx_mut().expect("failed to create rw tx"); + + for block in &blocks { + let mut trie = TrieDbMut::::new(&tx); + + // Insert key/value pairs + for ((r#type, key), value) in &block.keyvalues { + let db_key = match r#type { + TrieDatabaseKeyType::Trie => DatabaseKey::Trie(key.as_ref()), + TrieDatabaseKeyType::Flat => DatabaseKey::Flat(key.as_ref()), + TrieDatabaseKeyType::TrieLog => DatabaseKey::TrieLog(key.as_ref()), + }; + + trie.insert(&db_key, value.as_ref(), None).expect("failed to insert"); + } + + let snapshot_id = CommitId::from(block.number); + trie.snapshot(snapshot_id); + } + + tx.commit().expect("failed to commit tx"); + let tx = db.tx().expect("failed to create ro tx"); + + for block in &blocks { + let snapshot_id = CommitId::from(block.number); + let snapshot_db = SnapshotTrieDb::::new(&tx, snapshot_id); + + // Verify snapshots + for ((r#type, key), value) in &block.keyvalues { + let db_key = match r#type { + TrieDatabaseKeyType::Trie => DatabaseKey::Trie(key.as_ref()), + TrieDatabaseKeyType::Flat => DatabaseKey::Flat(key.as_ref()), + TrieDatabaseKeyType::TrieLog => DatabaseKey::TrieLog(key.as_ref()), + }; + + let result = snapshot_db.get(&db_key).unwrap(); + prop_assert_eq!(result.as_ref().map(|x| x.as_slice()), Some(value.as_slice())); + } + } + } + } +} diff --git a/crates/katana/storage/db/src/version.rs b/crates/katana/storage/db/src/version.rs index f0c1fe0f31..c7797c39b7 100644 --- a/crates/katana/storage/db/src/version.rs +++ b/crates/katana/storage/db/src/version.rs @@ -5,7 +5,7 @@ use std::mem; use std::path::{Path, PathBuf}; /// Current version of the database. -pub const CURRENT_DB_VERSION: u32 = 4; +pub const CURRENT_DB_VERSION: u32 = 6; /// Name of the version file. const DB_VERSION_FILE_NAME: &str = "db.version"; @@ -81,6 +81,6 @@ mod tests { #[test] fn test_current_version() { use super::CURRENT_DB_VERSION; - assert_eq!(CURRENT_DB_VERSION, 4, "Invalid current database version") + assert_eq!(CURRENT_DB_VERSION, 6, "Invalid current database version") } } diff --git a/crates/katana/storage/provider/Cargo.toml b/crates/katana/storage/provider/Cargo.toml index fa7860cee3..c561bf7e2f 100644 --- a/crates/katana/storage/provider/Cargo.toml +++ b/crates/katana/storage/provider/Cargo.toml @@ -8,7 +8,7 @@ version.workspace = true [dependencies] katana-db = { workspace = true, features = [ "test-utils" ] } -katana-primitives = { workspace = true, features = [ "rpc" ] } +katana-primitives.workspace = true katana-trie.workspace = true anyhow.workspace = true @@ -26,13 +26,13 @@ starknet = { workspace = true, optional = true } tokio = { workspace = true, optional = true } alloy-primitives = { workspace = true, optional = true } -serde_json = { workspace = true, optional = true } +serde_json.workspace = true [features] default = [ "fork", "in-memory" ] fork = [ "dep:futures", "dep:starknet", "dep:tokio", "in-memory" ] in-memory = [ ] -test-utils = [ "dep:alloy-primitives", "dep:serde_json" ] +test-utils = [ "dep:alloy-primitives" ] [dev-dependencies] alloy-primitives.workspace = true diff --git a/crates/katana/storage/provider/src/error.rs b/crates/katana/storage/provider/src/error.rs index 3876bbeec7..4445edd795 100644 --- a/crates/katana/storage/provider/src/error.rs +++ b/crates/katana/storage/provider/src/error.rs @@ -1,6 +1,6 @@ use katana_db::error::DatabaseError; use katana_primitives::block::BlockNumber; -use katana_primitives::class::ClassHash; +use katana_primitives::class::{ClassHash, ContractClassCompilationError}; use katana_primitives::contract::{ContractAddress, StorageKey}; use katana_primitives::transaction::TxNumber; @@ -96,6 +96,9 @@ pub enum ProviderError { storage_key: StorageKey, }, + #[error(transparent)] + ContractClassCompilation(#[from] ContractClassCompilationError), + /// Error returned by the database implementation. #[error(transparent)] Database(#[from] DatabaseError), diff --git a/crates/katana/storage/provider/src/lib.rs b/crates/katana/storage/provider/src/lib.rs index cc37ce473e..191af03cdd 100644 --- a/crates/katana/storage/provider/src/lib.rs +++ b/crates/katana/storage/provider/src/lib.rs @@ -1,25 +1,27 @@ use std::collections::BTreeMap; use std::ops::{Range, RangeInclusive}; +use std::sync::Arc; use katana_db::models::block::StoredBlockBodyIndices; use katana_primitives::block::{ Block, BlockHash, BlockHashOrNumber, BlockNumber, BlockWithTxHashes, FinalityStatus, Header, SealedBlockWithStatus, }; -use katana_primitives::class::{ClassHash, CompiledClass, CompiledClassHash, FlattenedSierraClass}; +use katana_primitives::class::{ClassHash, CompiledClass, CompiledClassHash, ContractClass}; use katana_primitives::contract::{ContractAddress, StorageKey, StorageValue}; use katana_primitives::env::BlockEnv; use katana_primitives::receipt::Receipt; -use katana_primitives::state::{StateUpdates, StateUpdatesWithDeclaredClasses}; +use katana_primitives::state::{StateUpdates, StateUpdatesWithClasses}; use katana_primitives::trace::TxExecInfo; use katana_primitives::transaction::{TxHash, TxNumber, TxWithHash}; use katana_primitives::Felt; use traits::block::{BlockIdReader, BlockStatusProvider, BlockWriter}; -use traits::contract::{ContractClassProvider, ContractClassWriter}; +use traits::contract::{ContractClassWriter, ContractClassWriterExt}; use traits::env::BlockEnvProvider; -use traits::state::{StateRootProvider, StateWriter}; +use traits::stage::StageCheckpointProvider; +use traits::state::StateWriter; use traits::transaction::{TransactionStatusProvider, TransactionTraceProvider}; -use traits::trie::{ClassTrieWriter, ContractTrieWriter}; +use traits::trie::TrieWriter; pub mod error; pub mod providers; @@ -42,12 +44,18 @@ pub type ProviderResult = Result; /// operation is done through this provider. #[derive(Debug)] pub struct BlockchainProvider { - provider: Db, + provider: Arc, } impl BlockchainProvider { pub fn new(provider: Db) -> Self { - Self { provider } + Self { provider: Arc::new(provider) } + } +} + +impl Clone for BlockchainProvider { + fn clone(&self) -> Self { + Self { provider: self.provider.clone() } } } @@ -131,7 +139,7 @@ where fn insert_block_with_states_and_receipts( &self, block: SealedBlockWithStatus, - states: StateUpdatesWithDeclaredClasses, + states: StateUpdatesWithClasses, receipts: Vec, executions: Vec, ) -> ProviderResult<()> { @@ -238,53 +246,6 @@ where } } -impl StateProvider for BlockchainProvider -where - Db: StateProvider, -{ - fn nonce( - &self, - address: ContractAddress, - ) -> ProviderResult> { - self.provider.nonce(address) - } - - fn storage( - &self, - address: ContractAddress, - storage_key: StorageKey, - ) -> ProviderResult> { - self.provider.storage(address, storage_key) - } - - fn class_hash_of_contract( - &self, - address: ContractAddress, - ) -> ProviderResult> { - self.provider.class_hash_of_contract(address) - } -} - -impl ContractClassProvider for BlockchainProvider -where - Db: ContractClassProvider, -{ - fn compiled_class_hash_of_class_hash( - &self, - hash: ClassHash, - ) -> ProviderResult> { - self.provider.compiled_class_hash_of_class_hash(hash) - } - - fn class(&self, hash: ClassHash) -> ProviderResult> { - self.provider.class(hash) - } - - fn sierra_class(&self, hash: ClassHash) -> ProviderResult> { - self.provider.sierra_class(hash) - } -} - impl StateFactoryProvider for BlockchainProvider where Db: StateFactoryProvider, @@ -308,14 +269,19 @@ where fn state_update(&self, block_id: BlockHashOrNumber) -> ProviderResult> { self.provider.state_update(block_id) } -} -impl StateRootProvider for BlockchainProvider -where - Db: StateRootProvider, -{ - fn state_root(&self, block_id: BlockHashOrNumber) -> ProviderResult> { - self.provider.state_root(block_id) + fn declared_classes( + &self, + block_id: BlockHashOrNumber, + ) -> ProviderResult>> { + self.provider.declared_classes(block_id) + } + + fn deployed_contracts( + &self, + block_id: BlockHashOrNumber, + ) -> ProviderResult>> { + self.provider.deployed_contracts(block_id) } } @@ -323,7 +289,7 @@ impl ContractClassWriter for BlockchainProvider where Db: ContractClassWriter, { - fn set_class(&self, hash: ClassHash, class: CompiledClass) -> ProviderResult<()> { + fn set_class(&self, hash: ClassHash, class: ContractClass) -> ProviderResult<()> { self.provider.set_class(hash, class) } @@ -334,13 +300,14 @@ where ) -> ProviderResult<()> { self.provider.set_compiled_class_hash_of_class_hash(hash, compiled_hash) } +} - fn set_sierra_class( - &self, - hash: ClassHash, - sierra: FlattenedSierraClass, - ) -> ProviderResult<()> { - self.provider.set_sierra_class(hash, sierra) +impl ContractClassWriterExt for BlockchainProvider +where + Db: ContractClassWriterExt, +{ + fn set_compiled_class(&self, hash: ClassHash, class: CompiledClass) -> ProviderResult<()> { + self.provider.set_compiled_class(hash, class) } } @@ -383,28 +350,36 @@ where } } -impl ClassTrieWriter for BlockchainProvider +impl TrieWriter for BlockchainProvider where - Db: ClassTrieWriter, + Db: TrieWriter, { - fn insert_updates( + fn trie_insert_declared_classes( &self, block_number: BlockNumber, updates: &BTreeMap, ) -> ProviderResult { - self.provider.insert_updates(block_number, updates) + self.provider.trie_insert_declared_classes(block_number, updates) } -} -impl ContractTrieWriter for BlockchainProvider -where - Db: ContractTrieWriter, -{ - fn insert_updates( + fn trie_insert_contract_updates( &self, block_number: BlockNumber, state_updates: &StateUpdates, ) -> ProviderResult { - self.provider.insert_updates(block_number, state_updates) + self.provider.trie_insert_contract_updates(block_number, state_updates) + } +} + +impl StageCheckpointProvider for BlockchainProvider +where + Db: StageCheckpointProvider, +{ + fn checkpoint(&self, id: &str) -> ProviderResult> { + self.provider.checkpoint(id) + } + + fn set_checkpoint(&self, id: &str, block_number: BlockNumber) -> ProviderResult<()> { + self.provider.set_checkpoint(id, block_number) } } diff --git a/crates/katana/storage/provider/src/providers/db/mod.rs b/crates/katana/storage/provider/src/providers/db/mod.rs index 4d06be8dda..ac93a12b0e 100644 --- a/crates/katana/storage/provider/src/providers/db/mod.rs +++ b/crates/katana/storage/provider/src/providers/db/mod.rs @@ -1,7 +1,7 @@ pub mod state; pub mod trie; -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use std::fmt::Debug; use std::ops::{Range, RangeInclusive}; @@ -14,6 +14,7 @@ use katana_db::models::contract::{ ContractClassChange, ContractInfoChangeList, ContractNonceChange, }; use katana_db::models::list::BlockList; +use katana_db::models::stage::StageCheckpoint; use katana_db::models::storage::{ContractStorageEntry, ContractStorageKey, StorageEntry}; use katana_db::tables::{self, DupSort, Table}; use katana_db::utils::KeyValue; @@ -27,10 +28,9 @@ use katana_primitives::contract::{ }; use katana_primitives::env::BlockEnv; use katana_primitives::receipt::Receipt; -use katana_primitives::state::{StateUpdates, StateUpdatesWithDeclaredClasses}; +use katana_primitives::state::{StateUpdates, StateUpdatesWithClasses}; use katana_primitives::trace::TxExecInfo; use katana_primitives::transaction::{TxHash, TxNumber, TxWithHash}; -use katana_primitives::Felt; use crate::error::ProviderError; use crate::traits::block::{ @@ -38,7 +38,8 @@ use crate::traits::block::{ HeaderProvider, }; use crate::traits::env::BlockEnvProvider; -use crate::traits::state::{StateFactoryProvider, StateProvider, StateRootProvider}; +use crate::traits::stage::StageCheckpointProvider; +use crate::traits::state::{StateFactoryProvider, StateProvider}; use crate::traits::state_update::StateUpdateProvider; use crate::traits::transaction::{ ReceiptProvider, TransactionProvider, TransactionStatusProvider, TransactionTraceProvider, @@ -48,7 +49,7 @@ use crate::ProviderResult; /// A provider implementation that uses a persistent database as the backend. // TODO: remove the default generic type -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct DbProvider(Db); impl DbProvider { @@ -254,47 +255,28 @@ impl BlockStatusProvider for DbProvider { } } -impl StateRootProvider for DbProvider { - fn state_root(&self, block_id: BlockHashOrNumber) -> ProviderResult> { - let db_tx = self.0.tx()?; - - let block_num = match block_id { - BlockHashOrNumber::Num(num) => Some(num), - BlockHashOrNumber::Hash(hash) => db_tx.get::(hash)?, - }; - - if let Some(block_num) = block_num { - let header = db_tx.get::(block_num)?; - db_tx.commit()?; - Ok(header.map(|h| h.state_root)) - } else { - Ok(None) - } - } +// A helper function that iterates over all entries in a dupsort table and collects the +// results into `V`. If `key` is not found, `V::default()` is returned. +fn dup_entries( + db_tx: &::Tx, + key: ::Key, + f: impl FnMut(Result, DatabaseError>) -> ProviderResult, +) -> ProviderResult +where + Db: Database, + Tb: DupSort + Debug, + V: FromIterator + Default, +{ + Ok(db_tx + .cursor_dup::()? + .walk_dup(Some(key), None)? + .map(|walker| walker.map(f).collect::>()) + .transpose()? + .unwrap_or_default()) } impl StateUpdateProvider for DbProvider { fn state_update(&self, block_id: BlockHashOrNumber) -> ProviderResult> { - // A helper function that iterates over all entries in a dupsort table and collects the - // results into `V`. If `key` is not found, `V::default()` is returned. - fn dup_entries( - db_tx: &::Tx, - key: ::Key, - f: impl FnMut(Result, DatabaseError>) -> ProviderResult, - ) -> ProviderResult - where - Db: Database, - Tb: DupSort + Debug, - V: FromIterator + Default, - { - Ok(db_tx - .cursor_dup::()? - .walk_dup(Some(key), None)? - .map(|walker| walker.map(f).collect::>()) - .transpose()? - .unwrap_or_default()) - } - let db_tx = self.0.tx()?; let block_num = self.block_number_by_id(block_id)?; @@ -319,20 +301,24 @@ impl StateUpdateProvider for DbProvider { Ok((contract_address, class_hash)) })?; - let declared_classes = dup_entries::< - Db, - tables::ClassDeclarations, - BTreeMap, - _, - >(&db_tx, block_num, |entry| { - let (_, class_hash) = entry?; - - let compiled_hash = db_tx - .get::(class_hash)? - .ok_or(ProviderError::MissingCompiledClassHash(class_hash))?; + let mut declared_classes = BTreeMap::new(); + let mut deprecated_declared_classes = BTreeSet::new(); - Ok((class_hash, compiled_hash)) - })?; + if let Some(block_entries) = + db_tx.cursor_dup::()?.walk_dup(Some(block_num), None)? + { + for entry in block_entries { + let (_, class_hash) = entry?; + match db_tx.get::(class_hash)? { + Some(compiled_hash) => { + declared_classes.insert(class_hash, compiled_hash); + } + None => { + deprecated_declared_classes.insert(class_hash); + } + } + } + } let storage_updates = { let entries = dup_entries::< @@ -355,17 +341,74 @@ impl StateUpdateProvider for DbProvider { }; db_tx.commit()?; + Ok(Some(StateUpdates { nonce_updates, storage_updates, deployed_contracts, declared_classes, - ..Default::default() + deprecated_declared_classes, + replaced_classes: BTreeMap::default(), })) } else { Ok(None) } } + + fn declared_classes( + &self, + block_id: BlockHashOrNumber, + ) -> ProviderResult>> { + let db_tx = self.0.tx()?; + let block_num = self.block_number_by_id(block_id)?; + + if let Some(block_num) = block_num { + let declared_classes = dup_entries::< + Db, + tables::ClassDeclarations, + BTreeMap, + _, + >(&db_tx, block_num, |entry| { + let (_, class_hash) = entry?; + + let compiled_hash = db_tx + .get::(class_hash)? + .ok_or(ProviderError::MissingCompiledClassHash(class_hash))?; + + Ok((class_hash, compiled_hash)) + })?; + + db_tx.commit()?; + Ok(Some(declared_classes)) + } else { + Ok(None) + } + } + + fn deployed_contracts( + &self, + block_id: BlockHashOrNumber, + ) -> ProviderResult>> { + let db_tx = self.0.tx()?; + let block_num = self.block_number_by_id(block_id)?; + + if let Some(block_num) = block_num { + let deployed_contracts = dup_entries::< + Db, + tables::ClassChangeHistory, + BTreeMap, + _, + >(&db_tx, block_num, |entry| { + let (_, ContractClassChange { contract_address, class_hash }) = entry?; + Ok((contract_address, class_hash)) + })?; + + db_tx.commit()?; + Ok(Some(deployed_contracts)) + } else { + Ok(None) + } + } } impl TransactionProvider for DbProvider { @@ -611,7 +654,7 @@ impl BlockWriter for DbProvider { fn insert_block_with_states_and_receipts( &self, block: SealedBlockWithStatus, - states: StateUpdatesWithDeclaredClasses, + states: StateUpdatesWithClasses, receipts: Vec, executions: Vec, ) -> ProviderResult<()> { @@ -633,13 +676,8 @@ impl BlockWriter for DbProvider { db_tx.put::(block_number, block_header)?; db_tx.put::(block_number, block_body_indices)?; - for (i, (transaction, receipt, execution)) in transactions - .into_iter() - .zip(receipts.into_iter()) - .zip(executions.into_iter()) - .map(|((transaction, receipt), execution)| (transaction, receipt, execution)) - .enumerate() - { + // Store base transaction details + for (i, transaction) in transactions.into_iter().enumerate() { let tx_number = tx_offset + i as u64; let tx_hash = transaction.hash; @@ -647,7 +685,17 @@ impl BlockWriter for DbProvider { db_tx.put::(tx_hash, tx_number)?; db_tx.put::(tx_number, block_number)?; db_tx.put::(tx_number, transaction.transaction)?; + } + + // Store transaction receipts + for (i, receipt) in receipts.into_iter().enumerate() { + let tx_number = tx_offset + i as u64; db_tx.put::(tx_number, receipt)?; + } + + // Store execution traces + for (i, execution) in executions.into_iter().enumerate() { + let tx_number = tx_offset + i as u64; db_tx.put::(tx_number, execution)?; } @@ -660,12 +708,16 @@ impl BlockWriter for DbProvider { db_tx.put::(block_number, class_hash)? } - for (hash, compiled_class) in states.declared_compiled_classes { - db_tx.put::(hash, compiled_class)?; + for class_hash in states.state_updates.deprecated_declared_classes { + db_tx.put::(class_hash, block_number)?; + db_tx.put::(block_number, class_hash)? } - for (class_hash, sierra_class) in states.declared_sierra_classes { - db_tx.put::(class_hash, sierra_class)?; + for (class_hash, class) in states.classes { + // generate the compiled class + let compiled = class.clone().compile()?; + db_tx.put::(class_hash, class)?; + db_tx.put::(class_hash, compiled)?; } // insert storage changes @@ -775,6 +827,26 @@ impl BlockWriter for DbProvider { } } +impl StageCheckpointProvider for DbProvider { + fn checkpoint(&self, id: &str) -> ProviderResult> { + let tx = self.0.tx()?; + let result = tx.get::(id.to_string())?; + tx.commit()?; + Ok(result.map(|x| x.block)) + } + + fn set_checkpoint(&self, id: &str, block_number: BlockNumber) -> ProviderResult<()> { + let tx = self.0.tx_mut()?; + + let key = id.to_string(); + let value = StageCheckpoint { block: block_number }; + tx.put::(key, value)?; + + tx.commit()?; + Ok(()) + } +} + #[cfg(test)] mod tests { use std::collections::BTreeMap; @@ -786,7 +858,7 @@ mod tests { use katana_primitives::contract::ContractAddress; use katana_primitives::fee::{PriceUnit, TxFeeInfo}; use katana_primitives::receipt::{InvokeTxReceipt, Receipt}; - use katana_primitives::state::{StateUpdates, StateUpdatesWithDeclaredClasses}; + use katana_primitives::state::{StateUpdates, StateUpdatesWithClasses}; use katana_primitives::trace::TxExecInfo; use katana_primitives::transaction::{InvokeTx, Tx, TxHash, TxWithHash}; use starknet::macros::felt; @@ -811,8 +883,8 @@ mod tests { SealedBlockWithStatus { block, status: FinalityStatus::AcceptedOnL2 } } - fn create_dummy_state_updates() -> StateUpdatesWithDeclaredClasses { - StateUpdatesWithDeclaredClasses { + fn create_dummy_state_updates() -> StateUpdatesWithClasses { + StateUpdatesWithClasses { state_updates: StateUpdates { nonce_updates: BTreeMap::from([ (address!("1"), felt!("1")), @@ -836,8 +908,8 @@ mod tests { } } - fn create_dummy_state_updates_2() -> StateUpdatesWithDeclaredClasses { - StateUpdatesWithDeclaredClasses { + fn create_dummy_state_updates_2() -> StateUpdatesWithClasses { + StateUpdatesWithClasses { state_updates: StateUpdates { nonce_updates: BTreeMap::from([ (address!("1"), felt!("5")), diff --git a/crates/katana/storage/provider/src/providers/db/state.rs b/crates/katana/storage/provider/src/providers/db/state.rs index 75de28092b..e7864e3bfa 100644 --- a/crates/katana/storage/provider/src/providers/db/state.rs +++ b/crates/katana/storage/provider/src/providers/db/state.rs @@ -5,16 +5,18 @@ use katana_db::models::contract::ContractInfoChangeList; use katana_db::models::list::BlockList; use katana_db::models::storage::{ContractStorageKey, StorageEntry}; use katana_db::tables; +use katana_db::trie::TrieDbFactory; use katana_primitives::block::BlockNumber; -use katana_primitives::class::{ClassHash, CompiledClass, CompiledClassHash, FlattenedSierraClass}; +use katana_primitives::class::{ClassHash, CompiledClass, CompiledClassHash, ContractClass}; use katana_primitives::contract::{ ContractAddress, GenericContractInfo, Nonce, StorageKey, StorageValue, }; +use katana_primitives::Felt; use super::DbProvider; use crate::error::ProviderError; -use crate::traits::contract::{ContractClassProvider, ContractClassWriter}; -use crate::traits::state::{StateProvider, StateWriter}; +use crate::traits::contract::{ContractClassProvider, ContractClassWriter, ContractClassWriterExt}; +use crate::traits::state::{StateProofProvider, StateProvider, StateRootProvider, StateWriter}; use crate::ProviderResult; impl StateWriter for DbProvider { @@ -70,9 +72,9 @@ impl StateWriter for DbProvider { } impl ContractClassWriter for DbProvider { - fn set_class(&self, hash: ClassHash, class: CompiledClass) -> ProviderResult<()> { + fn set_class(&self, hash: ClassHash, class: ContractClass) -> ProviderResult<()> { self.0.update(move |db_tx| -> ProviderResult<()> { - db_tx.put::(hash, class)?; + db_tx.put::(hash, class)?; Ok(()) })? } @@ -87,14 +89,12 @@ impl ContractClassWriter for DbProvider { Ok(()) })? } +} - fn set_sierra_class( - &self, - hash: ClassHash, - sierra: FlattenedSierraClass, - ) -> ProviderResult<()> { +impl ContractClassWriterExt for DbProvider { + fn set_compiled_class(&self, hash: ClassHash, class: CompiledClass) -> ProviderResult<()> { self.0.update(move |db_tx| -> ProviderResult<()> { - db_tx.put::(hash, sierra)?; + db_tx.put::(hash, class)?; Ok(()) })? } @@ -114,9 +114,12 @@ impl ContractClassProvider for LatestStateProvider where Tx: DbTx + Send + Sync, { - fn class(&self, hash: ClassHash) -> ProviderResult> { - let class = self.0.get::(hash)?; - Ok(class) + fn class(&self, hash: ClassHash) -> ProviderResult> { + Ok(self.0.get::(hash)?) + } + + fn compiled_class(&self, hash: ClassHash) -> ProviderResult> { + Ok(self.0.get::(hash)?) } fn compiled_class_hash_of_class_hash( @@ -126,11 +129,6 @@ where let hash = self.0.get::(hash)?; Ok(hash) } - - fn sierra_class(&self, hash: ClassHash) -> ProviderResult> { - let class = self.0.get::(hash)?; - Ok(class) - } } impl StateProvider for LatestStateProvider @@ -164,6 +162,56 @@ where } } +impl StateProofProvider for LatestStateProvider +where + Tx: DbTx + fmt::Debug + Send + Sync, +{ + fn class_multiproof(&self, classes: Vec) -> ProviderResult { + let mut trie = TrieDbFactory::new(&self.0).latest().classes_trie(); + let proofs = trie.multiproof(classes); + Ok(proofs) + } + + fn contract_multiproof( + &self, + addresses: Vec, + ) -> ProviderResult { + let mut trie = TrieDbFactory::new(&self.0).latest().contracts_trie(); + let proofs = trie.multiproof(addresses); + Ok(proofs) + } + + fn storage_multiproof( + &self, + address: ContractAddress, + storage_keys: Vec, + ) -> ProviderResult { + let mut trie = TrieDbFactory::new(&self.0).latest().storages_trie(address); + let proofs = trie.multiproof(storage_keys); + Ok(proofs) + } +} + +impl StateRootProvider for LatestStateProvider +where + Tx: DbTx + fmt::Debug + Send + Sync, +{ + fn classes_root(&self) -> ProviderResult { + let trie = TrieDbFactory::new(&self.0).latest().classes_trie(); + Ok(trie.root()) + } + + fn contracts_root(&self) -> ProviderResult { + let trie = TrieDbFactory::new(&self.0).latest().contracts_trie(); + Ok(trie.root()) + } + + fn storage_root(&self, contract: ContractAddress) -> ProviderResult> { + let trie = TrieDbFactory::new(&self.0).latest().storages_trie(contract); + Ok(Some(trie.root())) + } +} + /// A historical state provider. #[derive(Debug)] pub(super) struct HistoricalStateProvider { @@ -177,40 +225,41 @@ impl HistoricalStateProvider { pub fn new(tx: Tx, block_number: u64) -> Self { Self { tx, block_number } } + + /// Check if the class was declared before the pinned block number. + fn is_class_declared_before_block(&self, hash: ClassHash) -> ProviderResult { + let decl_block_num = self.tx.get::(hash)?; + let is_declared = decl_block_num.is_some_and(|num| num <= self.block_number); + Ok(is_declared) + } } impl ContractClassProvider for HistoricalStateProvider where Tx: DbTx + fmt::Debug + Send + Sync, { - fn compiled_class_hash_of_class_hash( - &self, - hash: ClassHash, - ) -> ProviderResult> { - // check that the requested class hash was declared before the pinned block number - if self - .tx - .get::(hash)? - .is_some_and(|num| num <= self.block_number) - { - Ok(self.tx.get::(hash)?) + fn class(&self, hash: ClassHash) -> ProviderResult> { + if self.is_class_declared_before_block(hash)? { + Ok(self.tx.get::(hash)?) } else { Ok(None) } } - fn class(&self, hash: ClassHash) -> ProviderResult> { - if self.compiled_class_hash_of_class_hash(hash)?.is_some() { - let contract = self.tx.get::(hash)?; - Ok(contract) + fn compiled_class(&self, hash: ClassHash) -> ProviderResult> { + if self.is_class_declared_before_block(hash)? { + Ok(self.tx.get::(hash)?) } else { Ok(None) } } - fn sierra_class(&self, hash: ClassHash) -> ProviderResult> { - if self.compiled_class_hash_of_class_hash(hash)?.is_some() { - self.tx.get::(hash).map_err(|e| e.into()) + fn compiled_class_hash_of_class_hash( + &self, + hash: ClassHash, + ) -> ProviderResult> { + if self.is_class_declared_before_block(hash)? { + Ok(self.tx.get::(hash)?) } else { Ok(None) } @@ -298,8 +347,88 @@ where } } +impl StateProofProvider for HistoricalStateProvider +where + Tx: DbTx + fmt::Debug + Send + Sync, +{ + fn class_multiproof(&self, classes: Vec) -> ProviderResult { + let proofs = TrieDbFactory::new(&self.tx) + .historical(self.block_number) + .expect("should exist") + .classes_trie() + .multiproof(classes); + Ok(proofs) + } + + fn contract_multiproof( + &self, + addresses: Vec, + ) -> ProviderResult { + let proofs = TrieDbFactory::new(&self.tx) + .historical(self.block_number) + .expect("should exist") + .contracts_trie() + .multiproof(addresses); + Ok(proofs) + } + + fn storage_multiproof( + &self, + address: ContractAddress, + storage_keys: Vec, + ) -> ProviderResult { + let proofs = TrieDbFactory::new(&self.tx) + .historical(self.block_number) + .expect("should exist") + .storages_trie(address) + .multiproof(storage_keys); + Ok(proofs) + } +} + +impl StateRootProvider for HistoricalStateProvider +where + Tx: DbTx + fmt::Debug + Send + Sync, +{ + fn classes_root(&self) -> ProviderResult { + let root = TrieDbFactory::new(&self.tx) + .historical(self.block_number) + .expect("should exist") + .classes_trie() + .root(); + Ok(root) + } + + fn contracts_root(&self) -> ProviderResult { + let root = TrieDbFactory::new(&self.tx) + .historical(self.block_number) + .expect("should exist") + .contracts_trie() + .root(); + Ok(root) + } + + fn storage_root(&self, contract: ContractAddress) -> ProviderResult> { + let root = TrieDbFactory::new(&self.tx) + .historical(self.block_number) + .expect("should exist") + .storages_trie(contract) + .root(); + Ok(Some(root)) + } + + fn state_root(&self) -> ProviderResult { + let header = self.tx.get::(self.block_number)?.expect("should exist"); + Ok(header.state_root) + } +} + /// This is a helper function for getting the block number of the most /// recent change that occurred relative to the given block number. +/// +/// ## Arguments +/// +/// * `block_list`: A list of block numbers where a change in value occur. fn recent_change_from_block( block_number: BlockNumber, block_list: &BlockList, diff --git a/crates/katana/storage/provider/src/providers/db/trie.rs b/crates/katana/storage/provider/src/providers/db/trie.rs index 65b5b96984..539835e764 100644 --- a/crates/katana/storage/provider/src/providers/db/trie.rs +++ b/crates/katana/storage/provider/src/providers/db/trie.rs @@ -2,17 +2,19 @@ use std::collections::{BTreeMap, HashMap}; use std::fmt::Debug; use katana_db::abstraction::Database; -use katana_db::trie; -use katana_db::trie::{ContractTrie, StorageTrie}; +use katana_db::tables; +use katana_db::trie::TrieDbMut; use katana_primitives::block::BlockNumber; use katana_primitives::class::{ClassHash, CompiledClassHash}; use katana_primitives::state::StateUpdates; use katana_primitives::{ContractAddress, Felt}; -use katana_trie::compute_contract_state_hash; +use katana_trie::{compute_contract_state_hash, ClassesTrie, ContractsTrie, StoragesTrie}; +use crate::error::ProviderError; use crate::providers::db::DbProvider; use crate::traits::state::{StateFactoryProvider, StateProvider}; -use crate::traits::trie::{ClassTrieWriter, ContractTrieWriter}; +use crate::traits::trie::TrieWriter; +use crate::ProviderResult; #[derive(Debug, Default)] struct ContractLeaf { @@ -21,80 +23,89 @@ struct ContractLeaf { pub nonce: Option, } -impl ClassTrieWriter for DbProvider { - fn insert_updates( +impl TrieWriter for DbProvider { + fn trie_insert_declared_classes( &self, block_number: BlockNumber, updates: &BTreeMap, - ) -> crate::ProviderResult { - let mut trie = trie::ClassTrie::new(self.0.tx_mut()?); + ) -> ProviderResult { + self.0.update(|tx| { + let mut trie = ClassesTrie::new(TrieDbMut::::new(tx)); - for (class_hash, compiled_hash) in updates { - trie.insert(*class_hash, *compiled_hash); - } + for (class_hash, compiled_hash) in updates { + trie.insert(*class_hash, *compiled_hash); + } - trie.commit(block_number); - Ok(trie.root()) + trie.commit(block_number); + Ok(trie.root()) + })? } -} -impl ContractTrieWriter for DbProvider { - fn insert_updates( + fn trie_insert_contract_updates( &self, block_number: BlockNumber, state_updates: &StateUpdates, - ) -> crate::ProviderResult { - let mut contract_leafs: HashMap = HashMap::new(); - - let leaf_hashes: Vec<_> = { - let mut storage_trie_db = StorageTrie::new(self.0.tx_mut()?); - - // First we insert the contract storage changes - for (address, storage_entries) in &state_updates.storage_updates { - for (key, value) in storage_entries { - storage_trie_db.insert(*address, *key, *value); + ) -> ProviderResult { + self.0.update(|tx| { + let mut contract_trie_db = + ContractsTrie::new(TrieDbMut::::new(tx)); + + let mut contract_leafs: HashMap = HashMap::new(); + + let leaf_hashes: Vec<_> = { + // First we insert the contract storage changes + for (address, storage_entries) in &state_updates.storage_updates { + let mut storage_trie_db = + StoragesTrie::new(TrieDbMut::::new(tx), *address); + + for (key, value) in storage_entries { + storage_trie_db.insert(*key, *value); + } + // insert the contract address in the contract_leafs to put the storage root + // later + contract_leafs.insert(*address, Default::default()); + + // Then we commit them + storage_trie_db.commit(block_number); } - // insert the contract address in the contract_leafs to put the storage root later - contract_leafs.insert(*address, Default::default()); - } - // Then we commit them - storage_trie_db.commit(block_number); + for (address, nonce) in &state_updates.nonce_updates { + contract_leafs.entry(*address).or_default().nonce = Some(*nonce); + } - for (address, nonce) in &state_updates.nonce_updates { - contract_leafs.entry(*address).or_default().nonce = Some(*nonce); - } + for (address, class_hash) in &state_updates.deployed_contracts { + contract_leafs.entry(*address).or_default().class_hash = Some(*class_hash); + } - for (address, class_hash) in &state_updates.deployed_contracts { - contract_leafs.entry(*address).or_default().class_hash = Some(*class_hash); - } + for (address, class_hash) in &state_updates.replaced_classes { + contract_leafs.entry(*address).or_default().class_hash = Some(*class_hash); + } - for (address, class_hash) in &state_updates.replaced_classes { - contract_leafs.entry(*address).or_default().class_hash = Some(*class_hash); + contract_leafs + .into_iter() + .map(|(address, mut leaf)| { + let storage_trie = StoragesTrie::new( + TrieDbMut::::new(tx), + address, + ); + let storage_root = storage_trie.root(); + leaf.storage_root = Some(storage_root); + + let latest_state = self.latest()?; + let leaf_hash = contract_state_leaf_hash(latest_state, &address, &leaf); + + Ok((address, leaf_hash)) + }) + .collect::, ProviderError>>()? + }; + + for (k, v) in leaf_hashes { + contract_trie_db.insert(k, v); } - contract_leafs - .into_iter() - .map(|(address, mut leaf)| { - let storage_root = storage_trie_db.root(&address); - leaf.storage_root = Some(storage_root); - - let latest_state = self.latest().unwrap(); - let leaf_hash = contract_state_leaf_hash(latest_state, &address, &leaf); - - (address, leaf_hash) - }) - .collect::>() - }; - - let mut contract_trie_db = ContractTrie::new(self.0.tx_mut()?); - - for (k, v) in leaf_hashes { - contract_trie_db.insert(k, v); - } - - contract_trie_db.commit(block_number); - Ok(contract_trie_db.root()) + contract_trie_db.commit(block_number); + Ok(contract_trie_db.root()) + })? } } diff --git a/crates/katana/storage/provider/src/providers/fork/backend.rs b/crates/katana/storage/provider/src/providers/fork/backend.rs index 265aa502a3..b6113532b5 100644 --- a/crates/katana/storage/provider/src/providers/fork/backend.rs +++ b/crates/katana/storage/provider/src/providers/fork/backend.rs @@ -1,4 +1,5 @@ -use std::collections::VecDeque; +use std::collections::hash_map::Entry; +use std::collections::{HashMap, VecDeque}; use std::pin::Pin; use std::sync::mpsc::{ channel as oneshot, Receiver as OneshotReceiver, RecvError, Sender as OneshotSender, @@ -7,16 +8,16 @@ use std::sync::Arc; use std::task::{Context, Poll}; use std::{io, thread}; +use anyhow::anyhow; use futures::channel::mpsc::{channel as async_channel, Receiver, SendError, Sender}; use futures::future::BoxFuture; use futures::stream::Stream; use futures::{Future, FutureExt}; use katana_primitives::block::BlockHashOrNumber; -use katana_primitives::class::{ClassHash, CompiledClass, CompiledClassHash, FlattenedSierraClass}; +use katana_primitives::class::{ClassHash, CompiledClass, CompiledClassHash, ContractClass}; use katana_primitives::contract::{ContractAddress, Nonce, StorageKey, StorageValue}; use katana_primitives::conversion::rpc::{ - compiled_class_hash_from_flattened_sierra_class, flattened_sierra_to_compiled_class, - legacy_rpc_to_compiled_class, + compiled_class_hash_from_flattened_sierra_class, legacy_rpc_to_class, }; use katana_primitives::Felt; use parking_lot::Mutex; @@ -27,35 +28,41 @@ use tracing::{error, trace}; use crate::error::ProviderError; use crate::providers::in_memory::cache::CacheStateDb; use crate::traits::contract::ContractClassProvider; -use crate::traits::state::StateProvider; +use crate::traits::state::{StateProofProvider, StateProvider, StateRootProvider}; use crate::ProviderResult; const LOG_TARGET: &str = "forking::backend"; type BackendResult = Result; -type GetNonceResult = BackendResult; -type GetStorageResult = BackendResult; -type GetClassHashAtResult = BackendResult; -type GetClassAtResult = BackendResult; +/// The types of response from [`Backend`]. +#[derive(Debug, Clone)] +enum BackendResponse { + Nonce(BackendResult), + Storage(BackendResult), + ClassHashAt(BackendResult), + ClassAt(BackendResult), +} -#[derive(Debug, thiserror::Error)] +#[derive(Debug, thiserror::Error, Clone)] pub enum BackendError { #[error("failed to send request to backend: {0}")] FailedSendRequest(#[from] SendError), #[error("failed to receive result from backend: {0}")] FailedReceiveResult(#[from] RecvError), #[error("compute class hash error: {0}")] - ComputeClassHashError(anyhow::Error), + ComputeClassHashError(Arc), #[error("failed to spawn backend thread: {0}")] - BackendThreadInit(#[from] io::Error), + BackendThreadInit(#[from] Arc), #[error("rpc provider error: {0}")] - StarknetProvider(#[from] starknet::providers::ProviderError), + StarknetProvider(#[from] Arc), + #[error("unexpected received result: {0}")] + UnexpectedReceiveResult(Arc), } -struct Request { +struct Request

{ payload: P, - sender: OneshotSender>, + sender: OneshotSender, } /// The types of request that can be sent to [`Backend`]. @@ -63,10 +70,10 @@ struct Request { /// Each request consists of a payload and the sender half of a oneshot channel that will be used /// to send the result back to the backend handle. enum BackendRequest { - Nonce(Request), - Class(Request), - ClassHash(Request), - Storage(Request<(ContractAddress, StorageKey), StorageValue>), + Nonce(Request), + Class(Request), + ClassHash(Request), + Storage(Request<(ContractAddress, StorageKey)>), // Test-only request kind for requesting the backend stats #[cfg(test)] Stats(OneshotSender), @@ -74,21 +81,19 @@ enum BackendRequest { impl BackendRequest { /// Create a new request for fetching the nonce of a contract. - fn nonce(address: ContractAddress) -> (BackendRequest, OneshotReceiver) { + fn nonce(address: ContractAddress) -> (BackendRequest, OneshotReceiver) { let (sender, receiver) = oneshot(); (BackendRequest::Nonce(Request { payload: address, sender }), receiver) } /// Create a new request for fetching the class definitions of a contract. - fn class(hash: ClassHash) -> (BackendRequest, OneshotReceiver) { + fn class(hash: ClassHash) -> (BackendRequest, OneshotReceiver) { let (sender, receiver) = oneshot(); (BackendRequest::Class(Request { payload: hash, sender }), receiver) } /// Create a new request for fetching the class hash of a contract. - fn class_hash( - address: ContractAddress, - ) -> (BackendRequest, OneshotReceiver) { + fn class_hash(address: ContractAddress) -> (BackendRequest, OneshotReceiver) { let (sender, receiver) = oneshot(); (BackendRequest::ClassHash(Request { payload: address, sender }), receiver) } @@ -97,7 +102,7 @@ impl BackendRequest { fn storage( address: ContractAddress, key: StorageKey, - ) -> (BackendRequest, OneshotReceiver) { + ) -> (BackendRequest, OneshotReceiver) { let (sender, receiver) = oneshot(); (BackendRequest::Storage(Request { payload: (address, key), sender }), receiver) } @@ -109,7 +114,17 @@ impl BackendRequest { } } -type BackendRequestFuture = BoxFuture<'static, ()>; +type BackendRequestFuture = BoxFuture<'static, BackendResponse>; + +// Identifier for pending requests. +// This is used for request deduplication. +#[derive(Eq, Hash, PartialEq, Clone, Copy, Debug)] +enum BackendRequestIdentifier { + Nonce(ContractAddress), + Class(ClassHash), + ClassHash(ContractAddress), + Storage((ContractAddress, StorageKey)), +} /// The backend for the forked provider. /// @@ -119,8 +134,10 @@ type BackendRequestFuture = BoxFuture<'static, ()>; pub struct Backend

{ /// The Starknet RPC provider that will be used to fetch data from. provider: Arc

, + // HashMap that keep track of current requests, for dedup purposes. + request_dedup_map: HashMap>>, /// Requests that are currently being poll. - pending_requests: Vec, + pending_requests: Vec<(BackendRequestIdentifier, BackendRequestFuture)>, /// Requests that are queued to be polled. queued_requests: VecDeque, /// A channel for receiving requests from the [BackendHandle]s. @@ -150,7 +167,7 @@ where .expect("failed to create tokio runtime") .block_on(backend); }) - .map_err(BackendError::BackendThreadInit)?; + .map_err(|e| BackendError::BackendThreadInit(Arc::new(e)))?; trace!(target: LOG_TARGET, "Forking backend started."); @@ -169,6 +186,7 @@ where block, incoming: rx, provider: Arc::new(provider), + request_dedup_map: HashMap::new(), pending_requests: Vec::new(), queued_requests: VecDeque::new(), }; @@ -182,57 +200,73 @@ where let block = self.block; let provider = self.provider.clone(); + // Check if there are similar requests in the queue before sending the request match request { BackendRequest::Nonce(Request { payload, sender }) => { - let fut = Box::pin(async move { - let res = provider - .get_nonce(block, Felt::from(payload)) - .await - .map_err(BackendError::StarknetProvider); - - sender.send(res).expect("failed to send nonce result") - }); - - self.pending_requests.push(fut); + let req_key = BackendRequestIdentifier::Nonce(payload); + + self.dedup_request( + req_key, + sender, + Box::pin(async move { + let res = provider + .get_nonce(block, Felt::from(payload)) + .await + .map_err(|e| BackendError::StarknetProvider(Arc::new(e))); + BackendResponse::Nonce(res) + }), + ); } BackendRequest::Storage(Request { payload: (addr, key), sender }) => { - let fut = Box::pin(async move { - let res = provider - .get_storage_at(Felt::from(addr), key, block) - .await - .map_err(BackendError::StarknetProvider); - - sender.send(res).expect("failed to send storage result") - }); - - self.pending_requests.push(fut); + let req_key = BackendRequestIdentifier::Storage((addr, key)); + + self.dedup_request( + req_key, + sender, + Box::pin(async move { + let res = provider + .get_storage_at(Felt::from(addr), key, block) + .await + .map_err(|e| BackendError::StarknetProvider(Arc::new(e))); + + BackendResponse::Storage(res) + }), + ); } BackendRequest::ClassHash(Request { payload, sender }) => { - let fut = Box::pin(async move { - let res = provider - .get_class_hash_at(block, Felt::from(payload)) - .await - .map_err(BackendError::StarknetProvider); - - sender.send(res).expect("failed to send class hash result") - }); - - self.pending_requests.push(fut); + let req_key = BackendRequestIdentifier::ClassHash(payload); + + self.dedup_request( + req_key, + sender, + Box::pin(async move { + let res = provider + .get_class_hash_at(block, Felt::from(payload)) + .await + .map_err(|e| BackendError::StarknetProvider(Arc::new(e))); + + BackendResponse::ClassHashAt(res) + }), + ); } BackendRequest::Class(Request { payload, sender }) => { - let fut = Box::pin(async move { - let res = provider - .get_class(block, payload) - .await - .map_err(BackendError::StarknetProvider); - - sender.send(res).expect("failed to send class result") - }); - - self.pending_requests.push(fut); + let req_key = BackendRequestIdentifier::Class(payload); + + self.dedup_request( + req_key, + sender, + Box::pin(async move { + let res = provider + .get_class(block, payload) + .await + .map_err(|e| BackendError::StarknetProvider(Arc::new(e))); + + BackendResponse::ClassAt(res) + }), + ); } #[cfg(test)] @@ -242,6 +276,29 @@ where } } } + + fn dedup_request( + &mut self, + req_key: BackendRequestIdentifier, + sender: OneshotSender, + rpc_call_future: BoxFuture<'static, BackendResponse>, + ) { + if let Entry::Vacant(e) = self.request_dedup_map.entry(req_key) { + self.pending_requests.push((req_key, rpc_call_future)); + e.insert(vec![sender]); + } else { + match self.request_dedup_map.get_mut(&req_key) { + Some(sender_vec) => { + sender_vec.push(sender); + } + None => { + // Log this and do nothing here, as this should never happen. + // If this does happen it is an unexpected bug. + error!(target: LOG_TARGET, "failed to get current request dedup vector"); + } + } + } + } } impl

Future for Backend

@@ -275,11 +332,28 @@ where // poll all pending requests for n in (0..pin.pending_requests.len()).rev() { - let mut fut = pin.pending_requests.swap_remove(n); + let (fut_key, mut fut) = pin.pending_requests.swap_remove(n); // poll the future and if the future is still pending, push it back to the // pending requests so that it will be polled again - if fut.poll_unpin(cx).is_pending() { - pin.pending_requests.push(fut); + match fut.poll_unpin(cx) { + Poll::Pending => { + pin.pending_requests.push((fut_key, fut)); + } + Poll::Ready(res) => { + let sender_vec = pin + .request_dedup_map + .get(&fut_key) + .expect("failed to get sender vector"); + + // Send the response to all the senders waiting on the same request + sender_vec.iter().for_each(|sender| { + sender.send(res.clone()).unwrap_or_else(|error| { + error!(target: LOG_TARGET, key = ?fut_key, %error, "Failed to send result.") + }); + }); + + pin.request_dedup_map.remove(&fut_key); + } } } @@ -309,7 +383,12 @@ impl BackendHandle { trace!(target: LOG_TARGET, %address, "Requesting contract nonce."); let (req, rx) = BackendRequest::nonce(address); self.request(req)?; - rx.recv()? + match rx.recv()? { + BackendResponse::Nonce(res) => res, + response => { + Err(BackendError::UnexpectedReceiveResult(Arc::new(anyhow!("{:?}", response)))) + } + } } pub fn get_storage( @@ -320,21 +399,36 @@ impl BackendHandle { trace!(target: LOG_TARGET, %address, key = %format!("{key:#x}"), "Requesting contract storage."); let (req, rx) = BackendRequest::storage(address, key); self.request(req)?; - rx.recv()? + match rx.recv()? { + BackendResponse::Storage(res) => res, + response => { + Err(BackendError::UnexpectedReceiveResult(Arc::new(anyhow!("{:?}", response)))) + } + } } pub fn get_class_hash_at(&self, address: ContractAddress) -> Result { trace!(target: LOG_TARGET, %address, "Requesting contract class hash."); let (req, rx) = BackendRequest::class_hash(address); self.request(req)?; - rx.recv()? + match rx.recv()? { + BackendResponse::ClassHashAt(res) => res, + response => { + Err(BackendError::UnexpectedReceiveResult(Arc::new(anyhow!("{:?}", response)))) + } + } } pub fn get_class_at(&self, class_hash: ClassHash) -> Result { trace!(target: LOG_TARGET, class_hash = %format!("{class_hash:#x}"), "Requesting class."); let (req, rx) = BackendRequest::class(class_hash); self.request(req)?; - rx.recv()? + match rx.recv()? { + BackendResponse::ClassAt(res) => res, + response => { + Err(BackendError::UnexpectedReceiveResult(Arc::new(anyhow!("{:?}", response)))) + } + } } pub fn get_compiled_class_hash( @@ -349,7 +443,7 @@ impl BackendHandle { RpcContractClass::Legacy(_) => Ok(class_hash), RpcContractClass::Sierra(sierra_class) => { compiled_class_hash_from_flattened_sierra_class(&sierra_class) - .map_err(BackendError::ComputeClassHashError) + .map_err(|e| BackendError::ComputeClassHashError(Arc::new(e))) } } } @@ -480,28 +574,68 @@ impl StateProvider for SharedStateProvider { } impl ContractClassProvider for SharedStateProvider { - fn sierra_class(&self, hash: ClassHash) -> ProviderResult> { - if let class @ Some(_) = self.0.shared_contract_classes.sierra_classes.read().get(&hash) { - return Ok(class.cloned()); + fn class(&self, hash: ClassHash) -> ProviderResult> { + if let Some(class) = self.0.shared_contract_classes.classes.read().get(&hash) { + return Ok(Some(class.clone())); } let Some(class) = handle_not_found_err(self.0.get_class_at(hash)).map_err(|error| { - error!(target: LOG_TARGET, hash = %format!("{hash:#x}"), %error, "Fetching sierra class."); + error!(target: LOG_TARGET, hash = %format!("{hash:#x}"), %error, "Fetching class."); error })? else { return Ok(None); }; - match class { - starknet::core::types::ContractClass::Legacy(_) => Ok(None), - starknet::core::types::ContractClass::Sierra(sierra_class) => { + let (class_hash, class) = match class { + RpcContractClass::Legacy(class) => { + let (_, class) = legacy_rpc_to_class(&class).map_err(|error| { + error!(target: LOG_TARGET, hash = %format!("{hash:#x}"), %error, "Parsing legacy class."); + ProviderError::ParsingError(error.to_string()) + })?; + + (hash, class) + } + + RpcContractClass::Sierra(class) => { + let value = serde_json::to_value(class).unwrap(); + let class = serde_json::from_value(value).unwrap(); + (hash, ContractClass::Class(class)) + } + }; + + self.0.shared_contract_classes.classes.write().entry(class_hash).or_insert(class.clone()); + Ok(Some(class)) + } + + fn compiled_class(&self, hash: ClassHash) -> ProviderResult> { + if let compiled @ Some(..) = + self.0.shared_contract_classes.compiled_classes.read().get(&hash) + { + Ok(compiled.cloned()) + } + // If doesn't exist in the cache, try to fetch it from the forked provider. + else { + // The Starknet RPC specs doesn't provide an endpoint for fetching the compiled class. + // If the uncompiled version doesn't yet exist locally, we fetch from the forked + // provider (from the `starknet_getClsas` method) and compile it. + let class = self.0.shared_contract_classes.classes.read().get(&hash).cloned(); + let class = if class.is_some() { class } else { self.class(hash)? }; + let compiled = class.map(|c| c.compile()).transpose().inspect_err(|error|{ + error!(target: LOG_TARGET, hash = %format!("{hash:#x}"), %error, "Failed to compile class."); + })?; + + if let Some(compiled) = compiled { + // Store the compiled class in the cache. self.0 .shared_contract_classes - .sierra_classes + .compiled_classes .write() - .insert(hash, sierra_class.clone()); - Ok(Some(sierra_class)) + .entry(hash) + .or_insert(compiled.clone()); + Ok(Some(compiled)) + } else { + Ok(None) } } } @@ -526,60 +660,44 @@ impl ContractClassProvider for SharedStateProvider { Ok(None) } } +} - fn class(&self, hash: ClassHash) -> ProviderResult> { - if let Some(class) = self.0.shared_contract_classes.compiled_classes.read().get(&hash) { - return Ok(Some(class.clone())); - } - - let Some(class) = handle_not_found_err(self.0.get_class_at(hash)).map_err(|error| { - error!(target: LOG_TARGET, hash = %format!("{hash:#x}"), %error, "Fetching class."); - error - })? - else { - return Ok(None); - }; - - let (class_hash, compiled_class_hash, casm, sierra) = match class { - RpcContractClass::Legacy(class) => { - let (_, compiled_class) = legacy_rpc_to_compiled_class(&class).map_err(|error| { - error!(target: LOG_TARGET, hash = %format!("{hash:#x}"), %error, "Parsing legacy class."); - ProviderError::ParsingError(error.to_string()) - })?; - - (hash, hash, compiled_class, None) - } +impl StateProofProvider for SharedStateProvider { + fn class_multiproof(&self, classes: Vec) -> ProviderResult { + let _ = classes; + unimplemented!("not supported in forked mode") + } - RpcContractClass::Sierra(sierra_class) => { - let (_, compiled_class_hash, compiled_class) = - flattened_sierra_to_compiled_class(&sierra_class).map_err(|error| { - error!(target: LOG_TARGET, hash = %format!("{hash:#x}"), %error, "Parsing sierra class."); - ProviderError::ParsingError(error.to_string()) - })?; + fn contract_multiproof( + &self, + addresses: Vec, + ) -> ProviderResult { + let _ = addresses; + unimplemented!("not supported in forked mode") + } - (hash, compiled_class_hash, compiled_class, Some(sierra_class)) - } - }; + fn storage_multiproof( + &self, + address: ContractAddress, + key: Vec, + ) -> ProviderResult { + let _ = address; + let _ = key; + unimplemented!("not supported in forked mode") + } +} - self.0.compiled_class_hashes.write().insert(class_hash, compiled_class_hash); +impl StateRootProvider for SharedStateProvider { + fn classes_root(&self) -> ProviderResult { + unimplemented!("not supported in forked mode") + } - self.0 - .shared_contract_classes - .compiled_classes - .write() - .entry(class_hash) - .or_insert(casm.clone()); - - if let Some(sierra) = sierra { - self.0 - .shared_contract_classes - .sierra_classes - .write() - .entry(class_hash) - .or_insert(sierra); - } + fn contracts_root(&self) -> ProviderResult { + unimplemented!("not supported in forked mode") + } - Ok(Some(casm)) + fn storage_root(&self, _: ContractAddress) -> ProviderResult> { + unimplemented!("not supported in forked mode") } } @@ -592,9 +710,12 @@ fn handle_not_found_err(result: Result) -> Result, match result { Ok(value) => Ok(Some(value)), - Err(BackendError::StarknetProvider(StarknetProviderError::StarknetError( - StarknetError::ContractNotFound | StarknetError::ClassHashNotFound, - ))) => Ok(None), + Err(BackendError::StarknetProvider(err_in_arc)) => match err_in_arc.as_ref() { + StarknetProviderError::StarknetError( + StarknetError::ContractNotFound | StarknetError::ClassHashNotFound, + ) => Ok(None), + _ => Err(BackendError::StarknetProvider(err_in_arc)), + }, Err(e) => Err(e), } @@ -603,11 +724,12 @@ fn handle_not_found_err(result: Result) -> Result, #[cfg(test)] pub(crate) mod test_utils { - use std::sync::mpsc::sync_channel; + use std::sync::mpsc::{sync_channel, SyncSender}; use katana_primitives::block::BlockNumber; use starknet::providers::jsonrpc::HttpTransport; use starknet::providers::JsonRpcClient; + use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::TcpListener; use url::Url; @@ -620,13 +742,13 @@ pub(crate) mod test_utils { } // Starts a TCP server that never close the connection. - pub fn start_tcp_server() { + pub fn start_tcp_server(addr: String) { use tokio::runtime::Builder; let (tx, rx) = sync_channel::<()>(1); thread::spawn(move || { Builder::new_current_thread().enable_all().build().unwrap().block_on(async move { - let listener = TcpListener::bind("127.0.0.1:8080").await.unwrap(); + let listener = TcpListener::bind(addr).await.unwrap(); let mut connections = Vec::new(); tx.send(()).unwrap(); @@ -640,11 +762,48 @@ pub(crate) mod test_utils { rx.recv().unwrap(); } + + // Helper function to start a TCP server that returns predefined JSON-RPC responses + pub fn start_mock_rpc_server(addr: String, response: String) -> SyncSender<()> { + use tokio::runtime::Builder; + let (tx, rx) = sync_channel::<()>(1); + + thread::spawn(move || { + Builder::new_current_thread().enable_all().build().unwrap().block_on(async move { + let listener = TcpListener::bind(addr).await.unwrap(); + + loop { + let (mut socket, _) = listener.accept().await.unwrap(); + + // Read the request, so hyper would not close the connection + let mut buffer = [0; 1024]; + let _ = socket.read(&mut buffer).await.unwrap(); + + // Wait for a signal to return the response. + rx.recv().unwrap(); + + // After reading, we send the pre-determined response + let http_response = format!( + "HTTP/1.1 200 OK\r\ncontent-length: {}\r\ncontent-type: \ + application/json\r\n\r\n{}", + response.len(), + response + ); + + socket.write_all(http_response.as_bytes()).await.unwrap(); + socket.flush().await.unwrap(); + } + }); + }); + + // Returning the sender to allow controlling the response timing. + tx + } } #[cfg(test)] mod tests { - + use std::sync::Mutex; use std::time::Duration; use katana_primitives::contract::GenericContractInfo; @@ -667,7 +826,7 @@ mod tests { #[test] fn handle_incoming_requests() { // start a mock remote network - start_tcp_server(); + start_tcp_server("127.0.0.1:8080".to_string()); let handle = create_forked_backend("http://127.0.0.1:8080", 1); @@ -686,7 +845,7 @@ mod tests { }); let h3 = handle.clone(); thread::spawn(move || { - h3.get_compiled_class_hash(felt!("0x1")).expect(ERROR_SEND_REQUEST); + h3.get_compiled_class_hash(felt!("0x2")).expect(ERROR_SEND_REQUEST); }); let h4 = handle.clone(); thread::spawn(move || { @@ -705,6 +864,319 @@ mod tests { assert_eq!(stats, 5, "Backend should have 5 ongoing requests.") } + #[test] + fn get_nonce_request_should_be_deduplicated() { + // start a mock remote network + start_tcp_server("127.0.0.1:8081".to_string()); + + let handle = create_forked_backend("http://127.0.0.1:8081", 1); + + // check no pending requests + let stats = handle.stats().expect(ERROR_STATS); + assert_eq!(stats, 0, "Backend should not have any ongoing requests."); + + // send requests to the backend + let h1 = handle.clone(); + thread::spawn(move || { + h1.get_nonce(felt!("0x1").into()).expect(ERROR_SEND_REQUEST); + }); + let h2 = handle.clone(); + thread::spawn(move || { + h2.get_nonce(felt!("0x1").into()).expect(ERROR_SEND_REQUEST); + }); + + // wait for the requests to be handled + thread::sleep(Duration::from_secs(1)); + + // check current request count + let stats = handle.stats().expect(ERROR_STATS); + assert_eq!(stats, 1, "Backend should have 1 ongoing requests."); + + // Different request, should be counted + let h3 = handle.clone(); + thread::spawn(move || { + h3.get_nonce(felt!("0x2").into()).expect(ERROR_SEND_REQUEST); + }); + + // wait for the requests to be handled + thread::sleep(Duration::from_secs(1)); + + // check request are handled + let stats = handle.stats().expect(ERROR_STATS); + assert_eq!(stats, 2, "Backend should only have 2 ongoing requests.") + } + + #[test] + fn get_class_at_request_should_be_deduplicated() { + // start a mock remote network + start_tcp_server("127.0.0.1:8082".to_string()); + + let handle = create_forked_backend("http://127.0.0.1:8082", 1); + + // check no pending requests + let stats = handle.stats().expect(ERROR_STATS); + assert_eq!(stats, 0, "Backend should not have any ongoing requests."); + + // send requests to the backend + let h1 = handle.clone(); + thread::spawn(move || { + h1.get_class_at(felt!("0x1")).expect(ERROR_SEND_REQUEST); + }); + let h2 = handle.clone(); + thread::spawn(move || { + h2.get_class_at(felt!("0x1")).expect(ERROR_SEND_REQUEST); + }); + + // wait for the requests to be handled + thread::sleep(Duration::from_secs(1)); + + // check current request count + let stats = handle.stats().expect(ERROR_STATS); + assert_eq!(stats, 1, "Backend should have 1 ongoing requests."); + + // Different request, should be counted + let h3 = handle.clone(); + thread::spawn(move || { + h3.get_class_at(felt!("0x2")).expect(ERROR_SEND_REQUEST); + }); + + // wait for the requests to be handled + thread::sleep(Duration::from_secs(1)); + + // check request are handled + let stats = handle.stats().expect(ERROR_STATS); + assert_eq!(stats, 2, "Backend should only have 2 ongoing requests.") + } + + #[test] + fn get_compiled_class_hash_request_should_be_deduplicated() { + // start a mock remote network + start_tcp_server("127.0.0.1:8083".to_string()); + + let handle = create_forked_backend("http://127.0.0.1:8083", 1); + + // check no pending requests + let stats = handle.stats().expect(ERROR_STATS); + assert_eq!(stats, 0, "Backend should not have any ongoing requests."); + + // send requests to the backend + let h1 = handle.clone(); + thread::spawn(move || { + h1.get_compiled_class_hash(felt!("0x1")).expect(ERROR_SEND_REQUEST); + }); + let h2 = handle.clone(); + thread::spawn(move || { + h2.get_compiled_class_hash(felt!("0x1")).expect(ERROR_SEND_REQUEST); + }); + + // wait for the requests to be handled + thread::sleep(Duration::from_secs(1)); + + // check current request count + let stats = handle.stats().expect(ERROR_STATS); + assert_eq!(stats, 1, "Backend should have 1 ongoing requests."); + + // Different request, should be counted + let h3 = handle.clone(); + thread::spawn(move || { + h3.get_compiled_class_hash(felt!("0x2")).expect(ERROR_SEND_REQUEST); + }); + + // wait for the requests to be handled + thread::sleep(Duration::from_secs(1)); + + // check request are handled + let stats = handle.stats().expect(ERROR_STATS); + assert_eq!(stats, 2, "Backend should only have 2 ongoing requests.") + } + + #[test] + fn get_class_at_and_get_compiled_class_hash_request_should_be_deduplicated() { + // start a mock remote network + start_tcp_server("127.0.0.1:8084".to_string()); + + let handle = create_forked_backend("http://127.0.0.1:8084", 1); + + // check no pending requests + let stats = handle.stats().expect(ERROR_STATS); + assert_eq!(stats, 0, "Backend should not have any ongoing requests."); + + // send requests to the backend + let h1 = handle.clone(); + thread::spawn(move || { + h1.get_class_at(felt!("0x1")).expect(ERROR_SEND_REQUEST); + }); + // Since this also calls to the same request as the previous one, it should be deduped + let h2 = handle.clone(); + thread::spawn(move || { + h2.get_compiled_class_hash(felt!("0x1")).expect(ERROR_SEND_REQUEST); + }); + + // wait for the requests to be handled + thread::sleep(Duration::from_secs(1)); + + // check current request count + let stats = handle.stats().expect(ERROR_STATS); + assert_eq!(stats, 1, "Backend should have 1 ongoing requests."); + + // Different request, should be counted + let h3 = handle.clone(); + thread::spawn(move || { + h3.get_class_at(felt!("0x2")).expect(ERROR_SEND_REQUEST); + }); + + // wait for the requests to be handled + thread::sleep(Duration::from_secs(1)); + + // check request are handled + let stats = handle.stats().expect(ERROR_STATS); + assert_eq!(stats, 2, "Backend should only have 2 ongoing requests.") + } + + #[test] + fn get_class_hash_at_request_should_be_deduplicated() { + // start a mock remote network + start_tcp_server("127.0.0.1:8085".to_string()); + + let handle = create_forked_backend("http://127.0.0.1:8085", 1); + + // check no pending requests + let stats = handle.stats().expect(ERROR_STATS); + assert_eq!(stats, 0, "Backend should not have any ongoing requests."); + + // send requests to the backend + let h1 = handle.clone(); + thread::spawn(move || { + h1.get_class_hash_at(felt!("0x1").into()).expect(ERROR_SEND_REQUEST); + }); + let h2 = handle.clone(); + thread::spawn(move || { + h2.get_class_hash_at(felt!("0x1").into()).expect(ERROR_SEND_REQUEST); + }); + + // wait for the requests to be handled + thread::sleep(Duration::from_secs(1)); + + // check current request count + let stats = handle.stats().expect(ERROR_STATS); + assert_eq!(stats, 1, "Backend should have 1 ongoing requests."); + + // Different request, should be counted + let h3 = handle.clone(); + thread::spawn(move || { + h3.get_class_hash_at(felt!("0x2").into()).expect(ERROR_SEND_REQUEST); + }); + + // wait for the requests to be handled + thread::sleep(Duration::from_secs(1)); + + // check request are handled + let stats = handle.stats().expect(ERROR_STATS); + assert_eq!(stats, 2, "Backend should only have 2 ongoing requests.") + } + + #[test] + fn get_storage_request_should_be_deduplicated() { + // start a mock remote network + start_tcp_server("127.0.0.1:8086".to_string()); + + let handle = create_forked_backend("http://127.0.0.1:8086", 1); + + // check no pending requests + let stats = handle.stats().expect(ERROR_STATS); + assert_eq!(stats, 0, "Backend should not have any ongoing requests."); + + // send requests to the backend + let h1 = handle.clone(); + thread::spawn(move || { + h1.get_storage(felt!("0x1").into(), felt!("0x1")).expect(ERROR_SEND_REQUEST); + }); + let h2 = handle.clone(); + thread::spawn(move || { + h2.get_storage(felt!("0x1").into(), felt!("0x1")).expect(ERROR_SEND_REQUEST); + }); + + // wait for the requests to be handled + thread::sleep(Duration::from_secs(1)); + + // check current request count + let stats = handle.stats().expect(ERROR_STATS); + assert_eq!(stats, 1, "Backend should have 1 ongoing requests."); + + // Different request, should be counted + let h3 = handle.clone(); + thread::spawn(move || { + h3.get_storage(felt!("0x2").into(), felt!("0x3")).expect(ERROR_SEND_REQUEST); + }); + + // wait for the requests to be handled + thread::sleep(Duration::from_secs(1)); + + // check request are handled + let stats = handle.stats().expect(ERROR_STATS); + assert_eq!(stats, 2, "Backend should only have 2 ongoing requests.") + } + + #[test] + fn get_storage_request_on_same_address_with_different_key_should_be_deduplicated() { + // start a mock remote network + start_tcp_server("127.0.0.1:8087".to_string()); + + let handle = create_forked_backend("http://127.0.0.1:8087", 1); + + // check no pending requests + let stats = handle.stats().expect(ERROR_STATS); + assert_eq!(stats, 0, "Backend should not have any ongoing requests."); + + // send requests to the backend + let h1 = handle.clone(); + thread::spawn(move || { + h1.get_storage(felt!("0x1").into(), felt!("0x1")).expect(ERROR_SEND_REQUEST); + }); + let h2 = handle.clone(); + thread::spawn(move || { + h2.get_storage(felt!("0x1").into(), felt!("0x1")).expect(ERROR_SEND_REQUEST); + }); + + // wait for the requests to be handled + thread::sleep(Duration::from_secs(1)); + + // check current request count + let stats = handle.stats().expect(ERROR_STATS); + assert_eq!(stats, 1, "Backend should have 1 ongoing requests."); + + // Different request, should be counted + let h3 = handle.clone(); + thread::spawn(move || { + h3.get_storage(felt!("0x1").into(), felt!("0x3")).expect(ERROR_SEND_REQUEST); + }); + // Different request, should be counted + let h4 = handle.clone(); + thread::spawn(move || { + h4.get_storage(felt!("0x1").into(), felt!("0x6")).expect(ERROR_SEND_REQUEST); + }); + + // wait for the requests to be handled + thread::sleep(Duration::from_secs(1)); + + // check current request count + let stats = handle.stats().expect(ERROR_STATS); + assert_eq!(stats, 3, "Backend should have 3 ongoing requests."); + + // Same request as the last one, shouldn't be counted + let h5 = handle.clone(); + thread::spawn(move || { + h5.get_storage(felt!("0x1").into(), felt!("0x6")).expect(ERROR_SEND_REQUEST); + }); + + // wait for the requests to be handled + thread::sleep(Duration::from_secs(1)); + + // check request are handled + let stats = handle.stats().expect(ERROR_STATS); + assert_eq!(stats, 3, "Backend should only have 3 ongoing requests.") + } + #[test] fn get_from_cache_if_exist() { // setup @@ -736,6 +1208,53 @@ mod tests { ); } + #[test] + fn test_deduplicated_request_should_return_similar_results() { + // Start mock server with a predefined nonce response + let response = r#"{"jsonrpc":"2.0","result":"0x123","id":1}"#; + let sender = start_mock_rpc_server("127.0.0.1:8090".to_string(), response.to_string()); + + let handle = create_forked_backend("http://127.0.0.1:8090", 1); + let addr = ContractAddress(felt!("0x1")); + + // Collect results from multiple identical nonce requests + let results: Arc>>> = + Arc::new(Mutex::new(Vec::new())); + let handles: Vec<_> = (0..5) + .map(|_| { + let h = handle.clone(); + let results = results.clone(); + thread::spawn(move || { + let res = h.get_nonce(addr); + results.lock().unwrap().push(res); + }) + }) + .collect(); + + // wait for the requests to be sent to the rpc server + thread::sleep(Duration::from_secs(1)); + + // Check that there's only one request, meaning it is deduplicated. + let stats = handle.stats().expect(ERROR_STATS); + assert_eq!(stats, 1, "Backend should only have 1 ongoing requests."); + + // Send the signal to tell the mock rpc server to return the response + sender.send(()).unwrap(); + + // Join all request threads + handles.into_iter().for_each(|h| h.join().unwrap()); + + // Verify all results are identical + let results = results.lock().unwrap(); + for result in results.iter() { + assert_eq!( + "0x123", + format!("{:#x}", result.as_ref().unwrap()), + "All deduplicated nonce requests should return the same result" + ); + } + } + // TODO: unignore this once we have separate the spawning of the backend thread from the backend // creation #[test] diff --git a/crates/katana/storage/provider/src/providers/fork/mod.rs b/crates/katana/storage/provider/src/providers/fork/mod.rs index 115c6dd45a..09342cc860 100644 --- a/crates/katana/storage/provider/src/providers/fork/mod.rs +++ b/crates/katana/storage/provider/src/providers/fork/mod.rs @@ -10,11 +10,11 @@ use katana_primitives::block::{ Block, BlockHash, BlockHashOrNumber, BlockNumber, BlockWithTxHashes, FinalityStatus, Header, SealedBlockWithStatus, }; -use katana_primitives::class::{ClassHash, CompiledClass, CompiledClassHash, FlattenedSierraClass}; +use katana_primitives::class::{ClassHash, CompiledClass, CompiledClassHash, ContractClass}; use katana_primitives::contract::ContractAddress; use katana_primitives::env::BlockEnv; use katana_primitives::receipt::Receipt; -use katana_primitives::state::{StateUpdates, StateUpdatesWithDeclaredClasses}; +use katana_primitives::state::{StateUpdates, StateUpdatesWithClasses}; use katana_primitives::trace::TxExecInfo; use katana_primitives::transaction::{Tx, TxHash, TxNumber, TxWithHash}; use katana_primitives::Felt; @@ -30,15 +30,16 @@ use crate::traits::block::{ BlockHashProvider, BlockNumberProvider, BlockProvider, BlockStatusProvider, BlockWriter, HeaderProvider, }; -use crate::traits::contract::ContractClassWriter; +use crate::traits::contract::{ContractClassWriter, ContractClassWriterExt}; use crate::traits::env::BlockEnvProvider; -use crate::traits::state::{StateFactoryProvider, StateProvider, StateRootProvider, StateWriter}; +use crate::traits::stage::StageCheckpointProvider; +use crate::traits::state::{StateFactoryProvider, StateProvider, StateWriter}; use crate::traits::state_update::StateUpdateProvider; use crate::traits::transaction::{ ReceiptProvider, TransactionProvider, TransactionStatusProvider, TransactionTraceProvider, TransactionsProviderExt, }; -use crate::traits::trie::{ClassTrieWriter, ContractTrieWriter}; +use crate::traits::trie::TrieWriter; use crate::ProviderResult; #[derive(Debug)] @@ -409,17 +410,17 @@ impl ReceiptProvider for ForkedProvider { } } -impl StateRootProvider for ForkedProvider { - fn state_root( - &self, - block_id: BlockHashOrNumber, - ) -> ProviderResult> { - let state_root = self.block_number_by_id(block_id)?.and_then(|num| { - self.storage.read().block_headers.get(&num).map(|header| header.state_root) - }); - Ok(state_root) - } -} +// impl StateRootProvider for ForkedProvider { +// fn state_root( +// &self, +// block_id: BlockHashOrNumber, +// ) -> ProviderResult> { +// let state_root = self.block_number_by_id(block_id)?.and_then(|num| { +// self.storage.read().block_headers.get(&num).map(|header| header.state_root) +// }); +// Ok(state_root) +// } +// } impl StateUpdateProvider for ForkedProvider { fn state_update(&self, block_id: BlockHashOrNumber) -> ProviderResult> { @@ -432,6 +433,20 @@ impl StateUpdateProvider for ForkedProvider { block_num.and_then(|num| self.storage.read().state_update.get(&num).cloned()); Ok(state_update) } + + fn declared_classes( + &self, + block_id: BlockHashOrNumber, + ) -> ProviderResult>> { + Ok(self.state_update(block_id)?.map(|su| su.declared_classes)) + } + + fn deployed_contracts( + &self, + block_id: BlockHashOrNumber, + ) -> ProviderResult>> { + Ok(self.state_update(block_id)?.map(|su| su.deployed_contracts)) + } } impl StateFactoryProvider for ForkedProvider { @@ -466,7 +481,7 @@ impl BlockWriter for ForkedProvider { fn insert_block_with_states_and_receipts( &self, block: SealedBlockWithStatus, - states: StateUpdatesWithDeclaredClasses, + states: StateUpdatesWithClasses, receipts: Vec, executions: Vec, ) -> ProviderResult<()> { @@ -520,17 +535,8 @@ impl BlockWriter for ForkedProvider { } impl ContractClassWriter for ForkedProvider { - fn set_class(&self, hash: ClassHash, class: CompiledClass) -> ProviderResult<()> { - self.state.shared_contract_classes.compiled_classes.write().insert(hash, class); - Ok(()) - } - - fn set_sierra_class( - &self, - hash: ClassHash, - sierra: FlattenedSierraClass, - ) -> ProviderResult<()> { - self.state.shared_contract_classes.sierra_classes.write().insert(hash, sierra); + fn set_class(&self, hash: ClassHash, class: ContractClass) -> ProviderResult<()> { + self.state.shared_contract_classes.classes.write().insert(hash, class); Ok(()) } @@ -544,6 +550,13 @@ impl ContractClassWriter for ForkedProvider { } } +impl ContractClassWriterExt for ForkedProvider { + fn set_compiled_class(&self, hash: ClassHash, class: CompiledClass) -> ProviderResult<()> { + self.state.shared_contract_classes.compiled_classes.write().insert(hash, class); + Ok(()) + } +} + impl StateWriter for ForkedProvider { fn set_storage( &self, @@ -586,8 +599,8 @@ impl BlockEnvProvider for ForkedProvider { } } -impl ClassTrieWriter for ForkedProvider { - fn insert_updates( +impl TrieWriter for ForkedProvider { + fn trie_insert_declared_classes( &self, block_number: BlockNumber, updates: &BTreeMap, @@ -596,10 +609,8 @@ impl ClassTrieWriter for ForkedProvider { let _ = updates; Ok(Felt::ZERO) } -} -impl ContractTrieWriter for ForkedProvider { - fn insert_updates( + fn trie_insert_contract_updates( &self, block_number: BlockNumber, state_updates: &StateUpdates, @@ -609,3 +620,16 @@ impl ContractTrieWriter for ForkedProvider { Ok(Felt::ZERO) } } + +impl StageCheckpointProvider for ForkedProvider { + fn checkpoint(&self, id: &str) -> ProviderResult> { + let _ = id; + unimplemented!("syncing is not supported for forked provider") + } + + fn set_checkpoint(&self, id: &str, block_number: BlockNumber) -> ProviderResult<()> { + let _ = id; + let _ = block_number; + unimplemented!("syncing is not supported for forked provider") + } +} diff --git a/crates/katana/storage/provider/src/providers/fork/state.rs b/crates/katana/storage/provider/src/providers/fork/state.rs index 77e0605205..7ab28ca869 100644 --- a/crates/katana/storage/provider/src/providers/fork/state.rs +++ b/crates/katana/storage/provider/src/providers/fork/state.rs @@ -1,13 +1,13 @@ use std::sync::Arc; -use katana_primitives::class::{ClassHash, CompiledClass, CompiledClassHash, FlattenedSierraClass}; +use katana_primitives::class::{ClassHash, CompiledClass, CompiledClassHash, ContractClass}; use katana_primitives::contract::{ContractAddress, Nonce, StorageKey, StorageValue}; use super::backend::SharedStateProvider; use crate::providers::in_memory::cache::CacheStateDb; use crate::providers::in_memory::state::StateSnapshot; use crate::traits::contract::ContractClassProvider; -use crate::traits::state::StateProvider; +use crate::traits::state::{StateProofProvider, StateProvider, StateRootProvider}; use crate::ProviderResult; pub type ForkedStateDb = CacheStateDb; @@ -76,12 +76,19 @@ impl StateProvider for ForkedStateDb { } } -impl ContractClassProvider for CacheStateDb { - fn sierra_class(&self, hash: ClassHash) -> ProviderResult> { - if let class @ Some(_) = self.shared_contract_classes.sierra_classes.read().get(&hash) { +impl ContractClassProvider for ForkedStateDb { + fn class(&self, hash: ClassHash) -> ProviderResult> { + if let class @ Some(_) = self.shared_contract_classes.classes.read().get(&hash) { return Ok(class.cloned()); } - ContractClassProvider::sierra_class(&self.db, hash) + ContractClassProvider::class(&self.db, hash) + } + + fn compiled_class(&self, hash: ClassHash) -> ProviderResult> { + if let class @ Some(_) = self.shared_contract_classes.compiled_classes.read().get(&hash) { + return Ok(class.cloned()); + } + ContractClassProvider::compiled_class(&self.db, hash) } fn compiled_class_hash_of_class_hash( @@ -93,12 +100,40 @@ impl ContractClassProvider for CacheStateDb { } ContractClassProvider::compiled_class_hash_of_class_hash(&self.db, hash) } +} - fn class(&self, hash: ClassHash) -> ProviderResult> { - if let class @ Some(_) = self.shared_contract_classes.compiled_classes.read().get(&hash) { - return Ok(class.cloned()); - } - ContractClassProvider::class(&self.db, hash) +impl StateProofProvider for ForkedStateDb { + fn class_multiproof(&self, _: Vec) -> ProviderResult { + Ok(katana_trie::MultiProof(Default::default())) + } + + fn contract_multiproof( + &self, + _: Vec, + ) -> ProviderResult { + Ok(katana_trie::MultiProof(Default::default())) + } + + fn storage_multiproof( + &self, + _: ContractAddress, + _: Vec, + ) -> ProviderResult { + Ok(katana_trie::MultiProof(Default::default())) + } +} + +impl StateRootProvider for ForkedStateDb { + fn classes_root(&self) -> ProviderResult { + Ok(katana_primitives::Felt::ZERO) + } + + fn contracts_root(&self) -> ProviderResult { + Ok(katana_primitives::Felt::ZERO) + } + + fn storage_root(&self, _: ContractAddress) -> ProviderResult> { + Ok(Some(katana_primitives::Felt::ZERO)) } } @@ -127,12 +162,12 @@ impl StateProvider for LatestStateProvider { } impl ContractClassProvider for LatestStateProvider { - fn sierra_class(&self, hash: ClassHash) -> ProviderResult> { - ContractClassProvider::sierra_class(&self.0, hash) + fn class(&self, hash: ClassHash) -> ProviderResult> { + ContractClassProvider::class(&self.0, hash) } - fn class(&self, hash: ClassHash) -> ProviderResult> { - ContractClassProvider::class(&self.0, hash) + fn compiled_class(&self, hash: ClassHash) -> ProviderResult> { + ContractClassProvider::compiled_class(&self.0, hash) } fn compiled_class_hash_of_class_hash( @@ -143,6 +178,41 @@ impl ContractClassProvider for LatestStateProvider { } } +impl StateProofProvider for LatestStateProvider { + fn class_multiproof(&self, _: Vec) -> ProviderResult { + Ok(katana_trie::MultiProof(Default::default())) + } + + fn contract_multiproof( + &self, + _: Vec, + ) -> ProviderResult { + Ok(katana_trie::MultiProof(Default::default())) + } + + fn storage_multiproof( + &self, + _: ContractAddress, + _: Vec, + ) -> ProviderResult { + Ok(katana_trie::MultiProof(Default::default())) + } +} + +impl StateRootProvider for LatestStateProvider { + fn classes_root(&self) -> ProviderResult { + Ok(katana_primitives::Felt::ZERO) + } + + fn contracts_root(&self) -> ProviderResult { + Ok(katana_primitives::Felt::ZERO) + } + + fn storage_root(&self, _: ContractAddress) -> ProviderResult> { + Ok(Some(katana_primitives::Felt::ZERO)) + } +} + impl StateProvider for ForkedSnapshot { fn nonce(&self, address: ContractAddress) -> ProviderResult> { if let nonce @ Some(_) = self @@ -188,11 +258,19 @@ impl StateProvider for ForkedSnapshot { } impl ContractClassProvider for ForkedSnapshot { - fn sierra_class(&self, hash: ClassHash) -> ProviderResult> { + fn class(&self, hash: ClassHash) -> ProviderResult> { if self.inner.compiled_class_hashes.contains_key(&hash) { - Ok(self.classes.sierra_classes.read().get(&hash).cloned()) + Ok(self.classes.classes.read().get(&hash).cloned()) } else { - ContractClassProvider::sierra_class(&self.inner.db, hash) + ContractClassProvider::class(&self.inner.db, hash) + } + } + + fn compiled_class(&self, hash: ClassHash) -> ProviderResult> { + if self.inner.compiled_class_hashes.contains_key(&hash) { + Ok(self.classes.compiled_classes.read().get(&hash).cloned()) + } else { + ContractClassProvider::compiled_class(&self.inner.db, hash) } } @@ -205,13 +283,40 @@ impl ContractClassProvider for ForkedSnapshot { } ContractClassProvider::compiled_class_hash_of_class_hash(&self.inner.db, hash) } +} - fn class(&self, hash: ClassHash) -> ProviderResult> { - if self.inner.compiled_class_hashes.contains_key(&hash) { - Ok(self.classes.compiled_classes.read().get(&hash).cloned()) - } else { - ContractClassProvider::class(&self.inner.db, hash) - } +impl StateProofProvider for ForkedSnapshot { + fn class_multiproof(&self, _: Vec) -> ProviderResult { + Ok(katana_trie::MultiProof(Default::default())) + } + + fn contract_multiproof( + &self, + _: Vec, + ) -> ProviderResult { + Ok(katana_trie::MultiProof(Default::default())) + } + + fn storage_multiproof( + &self, + _: ContractAddress, + _: Vec, + ) -> ProviderResult { + Ok(katana_trie::MultiProof(Default::default())) + } +} + +impl StateRootProvider for ForkedSnapshot { + fn classes_root(&self) -> ProviderResult { + Ok(katana_primitives::Felt::ZERO) + } + + fn contracts_root(&self) -> ProviderResult { + Ok(katana_primitives::Felt::ZERO) + } + + fn storage_root(&self, _: ContractAddress) -> ProviderResult> { + Ok(Some(katana_primitives::Felt::ZERO)) } } @@ -220,7 +325,7 @@ mod tests { use std::collections::BTreeMap; use katana_primitives::address; - use katana_primitives::state::{StateUpdates, StateUpdatesWithDeclaredClasses}; + use katana_primitives::state::{StateUpdates, StateUpdatesWithClasses}; use starknet::macros::felt; use super::*; @@ -255,7 +360,7 @@ mod tests { let local = ForkedStateDb::new(remote.clone()); let nonce_updates = BTreeMap::from([(address, remote_nonce)]); - let updates = StateUpdatesWithDeclaredClasses { + let updates = StateUpdatesWithClasses { state_updates: StateUpdates { nonce_updates, ..Default::default() }, ..Default::default() }; @@ -276,7 +381,7 @@ mod tests { let nonce_updates = BTreeMap::from([(address, remote_nonce)]); let deployed_contracts = BTreeMap::from([(address, class_hash)]); - let updates = StateUpdatesWithDeclaredClasses { + let updates = StateUpdatesWithClasses { state_updates: StateUpdates { nonce_updates, deployed_contracts, @@ -287,7 +392,7 @@ mod tests { remote.0.insert_updates(updates); let nonce_updates = BTreeMap::from([(address, local_nonce)]); - let updates = StateUpdatesWithDeclaredClasses { + let updates = StateUpdatesWithClasses { state_updates: StateUpdates { nonce_updates, ..Default::default() }, ..Default::default() }; @@ -308,7 +413,7 @@ mod tests { let deployed_contracts = BTreeMap::from([(address, class_hash)]); let nonce_updates = BTreeMap::from([(address, local_nonce)]); - let updates = StateUpdatesWithDeclaredClasses { + let updates = StateUpdatesWithClasses { state_updates: StateUpdates { nonce_updates, deployed_contracts, @@ -332,7 +437,7 @@ mod tests { let local = ForkedStateDb::new(remote.clone()); let deployed_contracts = BTreeMap::from([(address, class_hash)]); - let updates = StateUpdatesWithDeclaredClasses { + let updates = StateUpdatesWithClasses { state_updates: StateUpdates { deployed_contracts, ..Default::default() }, ..Default::default() }; diff --git a/crates/katana/storage/provider/src/providers/in_memory/cache.rs b/crates/katana/storage/provider/src/providers/in_memory/cache.rs index f80d608da4..2263b4ec91 100644 --- a/crates/katana/storage/provider/src/providers/in_memory/cache.rs +++ b/crates/katana/storage/provider/src/providers/in_memory/cache.rs @@ -3,10 +3,10 @@ use std::sync::Arc; use katana_db::models::block::StoredBlockBodyIndices; use katana_primitives::block::{BlockHash, BlockNumber, FinalityStatus, Header}; -use katana_primitives::class::{ClassHash, CompiledClass, CompiledClassHash, FlattenedSierraClass}; +use katana_primitives::class::{ClassHash, CompiledClass, CompiledClassHash, ContractClass}; use katana_primitives::contract::{ContractAddress, GenericContractInfo, StorageKey, StorageValue}; use katana_primitives::receipt::Receipt; -use katana_primitives::state::{StateUpdates, StateUpdatesWithDeclaredClasses}; +use katana_primitives::state::{StateUpdates, StateUpdatesWithClasses}; use katana_primitives::trace::TxExecInfo; use katana_primitives::transaction::{Tx, TxHash, TxNumber}; use parking_lot::RwLock; @@ -14,13 +14,13 @@ use parking_lot::RwLock; type ContractStorageMap = HashMap>; type ContractStateMap = HashMap; -type SierraClassesMap = HashMap; +type ClassesMap = HashMap; type CompiledClassesMap = HashMap; type CompiledClassHashesMap = HashMap; #[derive(Default, Debug)] pub struct SharedContractClasses { - pub(crate) sierra_classes: RwLock, + pub(crate) classes: RwLock, pub(crate) compiled_classes: RwLock, } @@ -43,11 +43,11 @@ pub struct CacheStateDb { impl CacheStateDb { /// Applies the given state updates to the cache. - pub fn insert_updates(&self, updates: StateUpdatesWithDeclaredClasses) { + pub fn insert_updates(&self, updates: StateUpdatesWithClasses) { let mut storage = self.storage.write(); let mut contract_state = self.contract_state.write(); let mut compiled_class_hashes = self.compiled_class_hashes.write(); - let mut sierra_classes = self.shared_contract_classes.sierra_classes.write(); + let mut classes = self.shared_contract_classes.classes.write(); let mut compiled_classes = self.shared_contract_classes.compiled_classes.write(); for (contract_address, nonce) in updates.state_updates.nonce_updates { @@ -65,9 +65,13 @@ impl CacheStateDb { contract_storage.extend(storage_changes); } + for (hash, class) in updates.classes.clone() { + let compiled = class.compile().expect("failed to compiled class"); + compiled_classes.insert(hash, compiled); + } + + classes.extend(updates.classes); compiled_class_hashes.extend(updates.state_updates.declared_classes); - sierra_classes.extend(updates.declared_sierra_classes); - compiled_classes.extend(updates.declared_compiled_classes); } } diff --git a/crates/katana/storage/provider/src/providers/in_memory/state.rs b/crates/katana/storage/provider/src/providers/in_memory/state.rs index 7f1f1102ab..a309cf275c 100644 --- a/crates/katana/storage/provider/src/providers/in_memory/state.rs +++ b/crates/katana/storage/provider/src/providers/in_memory/state.rs @@ -96,7 +96,7 @@ impl Default for InMemoryStateDb { storage: Default::default(), contract_state: Default::default(), shared_contract_classes: Arc::new(SharedContractClasses { - sierra_classes: Default::default(), + classes: Default::default(), compiled_classes: Default::default(), }), compiled_class_hashes: Default::default(), diff --git a/crates/katana/storage/provider/src/test_utils.rs b/crates/katana/storage/provider/src/test_utils.rs index 97dde7b2a9..bf3fdeb113 100644 --- a/crates/katana/storage/provider/src/test_utils.rs +++ b/crates/katana/storage/provider/src/test_utils.rs @@ -4,13 +4,12 @@ use alloy_primitives::U256; use katana_db::mdbx::test_utils; use katana_primitives::block::{BlockHash, FinalityStatus}; use katana_primitives::chain_spec::ChainSpec; -use katana_primitives::class::CompiledClass; use katana_primitives::contract::ContractAddress; use katana_primitives::genesis::allocation::{ DevGenesisAccount, GenesisAccountAlloc, GenesisAllocation, }; use katana_primitives::genesis::{Genesis, GenesisClass}; -use katana_primitives::utils::class::parse_compiled_class_v1; +use katana_primitives::utils::class::parse_sierra_class; use katana_primitives::{address, chain_spec}; use starknet::macros::felt; @@ -50,14 +49,8 @@ pub fn create_chain_for_testing() -> ChainSpec { // TODO: we should have a genesis builder that can do all of this for us. let class = { let json = include_str!("../test-data/simple_account.sierra.json"); - let json = serde_json::from_str(json).unwrap(); - let sierra = parse_compiled_class_v1(json).unwrap(); - - GenesisClass { - sierra: None, - compiled_class_hash: class_hash, - casm: Arc::new(CompiledClass::Class(sierra)), - } + let class = parse_sierra_class(json).unwrap(); + GenesisClass { compiled_class_hash: class_hash, class: Arc::new(class) } }; // setup test account diff --git a/crates/katana/storage/provider/src/traits/block.rs b/crates/katana/storage/provider/src/traits/block.rs index 070d016210..ec4d99bd9a 100644 --- a/crates/katana/storage/provider/src/traits/block.rs +++ b/crates/katana/storage/provider/src/traits/block.rs @@ -6,7 +6,7 @@ use katana_primitives::block::{ FinalityStatus, Header, SealedBlockWithStatus, }; use katana_primitives::receipt::Receipt; -use katana_primitives::state::StateUpdatesWithDeclaredClasses; +use katana_primitives::state::StateUpdatesWithClasses; use katana_primitives::trace::TxExecInfo; use super::transaction::{TransactionProvider, TransactionsProviderExt}; @@ -146,7 +146,7 @@ pub trait BlockWriter: Send + Sync { fn insert_block_with_states_and_receipts( &self, block: SealedBlockWithStatus, - states: StateUpdatesWithDeclaredClasses, + states: StateUpdatesWithClasses, receipts: Vec, executions: Vec, ) -> ProviderResult<()>; diff --git a/crates/katana/storage/provider/src/traits/contract.rs b/crates/katana/storage/provider/src/traits/contract.rs index 8ad2d5c700..7a7a7ea08b 100644 --- a/crates/katana/storage/provider/src/traits/contract.rs +++ b/crates/katana/storage/provider/src/traits/contract.rs @@ -1,37 +1,44 @@ -use katana_primitives::class::{ClassHash, CompiledClass, CompiledClassHash, FlattenedSierraClass}; +use katana_primitives::class::{ClassHash, CompiledClass, CompiledClassHash, ContractClass}; use crate::ProviderResult; /// A provider trait for retrieving contract class related information. #[auto_impl::auto_impl(&, Box, Arc)] pub trait ContractClassProvider: Send + Sync { + /// Returns the compiled class definition of a contract class given its class hash. + fn class(&self, hash: ClassHash) -> ProviderResult>; + + /// Returns the compiled class definition of a contract class given its class hash. + /// + /// It depends on the provider implementation on how to store/manage the compiled classes, be it + /// compiling on demand (default implementation), or storing the compiled class in the database + /// or volatile cache. + fn compiled_class(&self, hash: ClassHash) -> ProviderResult> { + if let Some(class) = self.class(hash)? { Ok(Some(class.compile()?)) } else { Ok(None) } + } + /// Returns the compiled class hash for the given class hash. fn compiled_class_hash_of_class_hash( &self, hash: ClassHash, ) -> ProviderResult>; - - /// Returns the compiled class definition of a contract class given its class hash. - fn class(&self, hash: ClassHash) -> ProviderResult>; - - /// Retrieves the Sierra class definition of a contract class given its class hash. - fn sierra_class(&self, hash: ClassHash) -> ProviderResult>; } -// TEMP: added mainly for compatibility reason. might be removed in the future. #[auto_impl::auto_impl(&, Box, Arc)] pub trait ContractClassWriter: Send + Sync { - /// Returns the compiled class hash for the given class hash. + /// Sets the compiled class hash for the given class hash. fn set_compiled_class_hash_of_class_hash( &self, hash: ClassHash, compiled_hash: CompiledClassHash, ) -> ProviderResult<()>; - /// Returns the compiled class definition of a contract class given its class hash. - fn set_class(&self, hash: ClassHash, class: CompiledClass) -> ProviderResult<()>; + /// Sets the contract class for the given class hash. + fn set_class(&self, hash: ClassHash, class: ContractClass) -> ProviderResult<()>; +} - /// Retrieves the Sierra class definition of a contract class given its class hash. - fn set_sierra_class(&self, hash: ClassHash, sierra: FlattenedSierraClass) - -> ProviderResult<()>; +#[auto_impl::auto_impl(&, Box, Arc)] +pub trait ContractClassWriterExt: ContractClassWriter { + /// Set the compiled class for the given class hash. + fn set_compiled_class(&self, hash: ClassHash, class: CompiledClass) -> ProviderResult<()>; } diff --git a/crates/katana/storage/provider/src/traits/mod.rs b/crates/katana/storage/provider/src/traits/mod.rs index 1fdbcf3de2..eb9f5df0eb 100644 --- a/crates/katana/storage/provider/src/traits/mod.rs +++ b/crates/katana/storage/provider/src/traits/mod.rs @@ -1,6 +1,11 @@ +//! `-Ext` suffixed traits means they are not meant to be used in the main program flow. Usually as +//! a way to restrict certain operations/functions from being accessed at certain parts of the +//! program. + pub mod block; pub mod contract; pub mod env; +pub mod stage; pub mod state; pub mod state_update; pub mod transaction; diff --git a/crates/katana/storage/provider/src/traits/stage.rs b/crates/katana/storage/provider/src/traits/stage.rs new file mode 100644 index 0000000000..ff5e2c2267 --- /dev/null +++ b/crates/katana/storage/provider/src/traits/stage.rs @@ -0,0 +1,12 @@ +use katana_primitives::block::BlockNumber; + +use crate::ProviderResult; + +#[auto_impl::auto_impl(&, Box, Arc)] +pub trait StageCheckpointProvider: Send + Sync { + /// Returns the number of the last block that was successfully processed by the stage. + fn checkpoint(&self, id: &str) -> ProviderResult>; + + /// Sets the checkpoint for a stage to the given block number. + fn set_checkpoint(&self, id: &str, block_number: BlockNumber) -> ProviderResult<()>; +} diff --git a/crates/katana/storage/provider/src/traits/state.rs b/crates/katana/storage/provider/src/traits/state.rs index 38046c2b5b..36e81d62d0 100644 --- a/crates/katana/storage/provider/src/traits/state.rs +++ b/crates/katana/storage/provider/src/traits/state.rs @@ -2,18 +2,39 @@ use katana_primitives::block::BlockHashOrNumber; use katana_primitives::class::ClassHash; use katana_primitives::contract::{ContractAddress, Nonce, StorageKey, StorageValue}; use katana_primitives::Felt; +use katana_trie::MultiProof; +use starknet::macros::short_string; +use starknet_types_core::hash::StarkHash; use super::contract::ContractClassProvider; use crate::ProviderResult; #[auto_impl::auto_impl(&, Box, Arc)] pub trait StateRootProvider: Send + Sync { - /// Retrieves the state root of a block. - fn state_root(&self, block_id: BlockHashOrNumber) -> ProviderResult>; + /// Retrieves the root of the global state trie. + fn state_root(&self) -> ProviderResult { + // https://docs.starknet.io/architecture-and-concepts/network-architecture/starknet-state/#state_commitment + Ok(starknet_types_core::hash::Poseidon::hash_array(&[ + short_string!("STARKNET_STATE_V0"), + self.contracts_root()?, + self.classes_root()?, + ])) + } + + /// Retrieves the root of the classes trie. + fn classes_root(&self) -> ProviderResult; + + /// Retrieves the root of the contracts trie. + fn contracts_root(&self) -> ProviderResult; + + /// Retrieves the root of a contract's storage trie. + fn storage_root(&self, contract: ContractAddress) -> ProviderResult>; } #[auto_impl::auto_impl(&, Box, Arc)] -pub trait StateProvider: ContractClassProvider + Send + Sync + std::fmt::Debug { +pub trait StateProvider: + ContractClassProvider + StateProofProvider + StateRootProvider + Send + Sync + std::fmt::Debug +{ /// Returns the nonce of a contract. fn nonce(&self, address: ContractAddress) -> ProviderResult>; @@ -63,3 +84,16 @@ pub trait StateWriter: Send + Sync { class_hash: ClassHash, ) -> ProviderResult<()>; } + +#[auto_impl::auto_impl(&, Box, Arc)] +pub trait StateProofProvider: Send + Sync { + fn storage_multiproof( + &self, + address: ContractAddress, + key: Vec, + ) -> ProviderResult; + + fn contract_multiproof(&self, addresses: Vec) -> ProviderResult; + + fn class_multiproof(&self, classes: Vec) -> ProviderResult; +} diff --git a/crates/katana/storage/provider/src/traits/state_update.rs b/crates/katana/storage/provider/src/traits/state_update.rs index 762f4a4f05..7d706a2856 100644 --- a/crates/katana/storage/provider/src/traits/state_update.rs +++ b/crates/katana/storage/provider/src/traits/state_update.rs @@ -1,5 +1,9 @@ +use std::collections::BTreeMap; + use katana_primitives::block::BlockHashOrNumber; +use katana_primitives::class::{ClassHash, CompiledClassHash}; use katana_primitives::state::StateUpdates; +use katana_primitives::ContractAddress; use crate::ProviderResult; @@ -7,4 +11,16 @@ use crate::ProviderResult; pub trait StateUpdateProvider: Send + Sync { /// Returns the state update at the given block. fn state_update(&self, block_id: BlockHashOrNumber) -> ProviderResult>; + + /// Returns all declared class hashes at the given block. + fn declared_classes( + &self, + block_id: BlockHashOrNumber, + ) -> ProviderResult>>; + + /// Returns all deployed contracts at the given block. + fn deployed_contracts( + &self, + block_id: BlockHashOrNumber, + ) -> ProviderResult>>; } diff --git a/crates/katana/storage/provider/src/traits/trie.rs b/crates/katana/storage/provider/src/traits/trie.rs index 8570a30b88..6a63ec3c40 100644 --- a/crates/katana/storage/provider/src/traits/trie.rs +++ b/crates/katana/storage/provider/src/traits/trie.rs @@ -8,17 +8,14 @@ use katana_primitives::Felt; use crate::ProviderResult; #[auto_impl::auto_impl(&, Box, Arc)] -pub trait ClassTrieWriter: Send + Sync { - fn insert_updates( +pub trait TrieWriter: Send + Sync { + fn trie_insert_declared_classes( &self, block_number: BlockNumber, updates: &BTreeMap, ) -> ProviderResult; -} -#[auto_impl::auto_impl(&, Box, Arc)] -pub trait ContractTrieWriter: Send + Sync { - fn insert_updates( + fn trie_insert_contract_updates( &self, block_number: BlockNumber, state_updates: &StateUpdates, diff --git a/crates/katana/storage/provider/tests/block.rs b/crates/katana/storage/provider/tests/block.rs index 5125e8e361..1be22a5e92 100644 --- a/crates/katana/storage/provider/tests/block.rs +++ b/crates/katana/storage/provider/tests/block.rs @@ -3,7 +3,7 @@ use katana_primitives::block::{ Block, BlockHashOrNumber, BlockNumber, BlockWithTxHashes, FinalityStatus, }; use katana_primitives::env::BlockEnv; -use katana_primitives::state::StateUpdatesWithDeclaredClasses; +use katana_primitives::state::StateUpdatesWithClasses; use katana_primitives::transaction::TxWithHash; use katana_provider::providers::db::DbProvider; use katana_provider::providers::fork::ForkedProvider; @@ -11,7 +11,7 @@ use katana_provider::traits::block::{ BlockHashProvider, BlockNumberProvider, BlockProvider, BlockStatusProvider, BlockWriter, }; use katana_provider::traits::env::BlockEnvProvider; -use katana_provider::traits::state::StateRootProvider; +use katana_provider::traits::state::{StateFactoryProvider, StateRootProvider}; use katana_provider::traits::state_update::StateUpdateProvider; use katana_provider::traits::transaction::{ ReceiptProvider, TransactionProvider, TransactionStatusProvider, TransactionTraceProvider, @@ -29,6 +29,7 @@ use fixtures::{ use katana_primitives::Felt; #[apply(insert_block_cases)] +#[ignore = "trie computation not supported yet for forked mode yet"] fn insert_block_with_fork_provider( #[from(fork_provider)] provider: BlockchainProvider, #[case] block_count: u64, @@ -45,6 +46,7 @@ fn insert_block_with_db_provider( } #[apply(insert_block_cases)] +#[ignore = "trie computation not supported yet for forked mode yet"] fn insert_block_empty_with_fork_provider( #[from(fork_provider)] provider: BlockchainProvider, #[case] block_count: u64, @@ -65,7 +67,7 @@ where Db: BlockProvider + BlockWriter + ReceiptProvider - + StateRootProvider + + StateFactoryProvider + TransactionStatusProvider + TransactionTraceProvider + BlockEnvProvider, @@ -119,7 +121,8 @@ where let actual_block = provider.block(block_id)?; let actual_block_txs = provider.transactions_by_block(block_id)?; let actual_status = provider.block_status(block_id)?; - let actual_state_root = provider.state_root(block_id)?; + let actual_state_root = + provider.historical(block_id)?.map(|s| s.state_root()).transpose()?; let actual_block_tx_count = provider.transaction_count_by_block(block_id)?; let actual_receipts = provider.receipts_by_block(block_id)?; @@ -174,7 +177,7 @@ where Db: BlockProvider + BlockWriter + ReceiptProvider - + StateRootProvider + + StateFactoryProvider + TransactionStatusProvider + TransactionTraceProvider + BlockEnvProvider, @@ -225,7 +228,8 @@ where let actual_block = provider.block(block_id)?; let actual_block_txs = provider.transactions_by_block(block_id)?; let actual_status = provider.block_status(block_id)?; - let actual_state_root = provider.state_root(block_id)?; + let actual_state_root = + provider.historical(block_id)?.map(|s| s.state_root()).transpose()?; let actual_block_tx_count = provider.transaction_count_by_block(block_id)?; let actual_receipts = provider.receipts_by_block(block_id)?; @@ -278,7 +282,7 @@ fn test_read_state_update_with_fork_provider( ForkedProvider, >, #[case] block_num: BlockNumber, - #[case] expected_state_update: StateUpdatesWithDeclaredClasses, + #[case] expected_state_update: StateUpdatesWithClasses, ) -> Result<()> { test_read_state_update_impl(provider, block_num, expected_state_update) } @@ -287,7 +291,7 @@ fn test_read_state_update_with_fork_provider( fn test_read_state_update_with_db_provider( #[with(db_provider())] provider: BlockchainProvider, #[case] block_num: BlockNumber, - #[case] expected_state_update: StateUpdatesWithDeclaredClasses, + #[case] expected_state_update: StateUpdatesWithClasses, ) -> Result<()> { test_read_state_update_impl(provider, block_num, expected_state_update) } @@ -295,7 +299,7 @@ fn test_read_state_update_with_db_provider( fn test_read_state_update_impl( provider: BlockchainProvider, block_num: BlockNumber, - expected_state_update: StateUpdatesWithDeclaredClasses, + expected_state_update: StateUpdatesWithClasses, ) -> Result<()> where Db: StateUpdateProvider, @@ -317,11 +321,11 @@ fn insert_block_cases(#[case] block_count: u64) {} #[rstest::rstest] #[case::state_update_at_block_1(1, mock_state_updates()[0].clone())] #[case::state_update_at_block_2(2, mock_state_updates()[1].clone())] -#[case::state_update_at_block_3(3, StateUpdatesWithDeclaredClasses::default())] +#[case::state_update_at_block_3(3, StateUpdatesWithClasses::default())] #[case::state_update_at_block_5(5, mock_state_updates()[2].clone())] fn test_read_state_update( #[from(provider_with_states)] provider: BlockchainProvider, #[case] block_num: BlockNumber, - #[case] expected_state_update: StateUpdatesWithDeclaredClasses, + #[case] expected_state_update: StateUpdatesWithClasses, ) { } diff --git a/crates/katana/storage/provider/tests/class.rs b/crates/katana/storage/provider/tests/class.rs index cb31ba11ff..4e0c75c493 100644 --- a/crates/katana/storage/provider/tests/class.rs +++ b/crates/katana/storage/provider/tests/class.rs @@ -2,44 +2,35 @@ mod fixtures; use anyhow::Result; use fixtures::{ - fork_provider_with_spawned_fork_network, provider_with_states, DOJO_WORLD_COMPILED_CLASS, - DOJO_WORLD_SIERRA_CLASS, + fork_provider_with_spawned_fork_network, provider_with_states, DOJO_WORLD_SIERRA_CLASS, }; use katana_primitives::block::{BlockHashOrNumber, BlockNumber}; -use katana_primitives::class::{ClassHash, CompiledClass, CompiledClassHash, FlattenedSierraClass}; -use katana_primitives::genesis::constant::{DEFAULT_LEGACY_ERC20_CASM, DEFAULT_LEGACY_UDC_CASM}; +use katana_primitives::class::{ClassHash, CompiledClassHash, ContractClass}; +use katana_primitives::genesis::constant::{DEFAULT_LEGACY_ERC20_CLASS, DEFAULT_LEGACY_UDC_CLASS}; use katana_provider::providers::fork::ForkedProvider; use katana_provider::traits::state::{StateFactoryProvider, StateProvider}; use katana_provider::BlockchainProvider; use rstest_reuse::{self, *}; use starknet::macros::felt; -type ClassHashAndClasses = - (ClassHash, Option, Option, Option); +type ClassHashAndClasses = (ClassHash, Option, Option); fn assert_state_provider_class( state_provider: Box, expected_class: Vec, ) -> Result<()> { - for (class_hash, expected_compiled_hash, expected_compiled_class, expected_sierra_class) in - expected_class - { + for (class_hash, expected_compiled_hash, expected_class) in expected_class { + let actual_class = state_provider.class(class_hash)?; + let actual_compiled_class = state_provider.compiled_class(class_hash)?; let actual_compiled_hash = state_provider.compiled_class_hash_of_class_hash(class_hash)?; - let actual_compiled_class = state_provider.class(class_hash)?; - let actual_sierra_class = state_provider.sierra_class(class_hash)?; - - assert!( - if let Some(CompiledClass::Class(_)) = &actual_compiled_class { - actual_sierra_class.is_some() - } else { - actual_sierra_class.is_none() - }, - "V1 compiled class should have its Sierra class" - ); - - assert_eq!(actual_compiled_hash, expected_compiled_hash); - assert_eq!(actual_compiled_class, expected_compiled_class); - assert_eq!(actual_sierra_class, expected_sierra_class); + + if let Some(expected_class) = expected_class { + let expected_compiled = expected_class.clone().compile().expect("fail to compile"); + + assert_eq!(actual_class, Some(expected_class)); + assert_eq!(actual_compiled_hash, expected_compiled_hash); + assert_eq!(actual_compiled_class, Some(expected_compiled)); + } } Ok(()) } @@ -62,9 +53,9 @@ mod latest { #[rstest::rstest] #[case( vec![ - (felt!("11"), Some(felt!("1000")), Some(DEFAULT_LEGACY_ERC20_CASM.clone()), None), - (felt!("22"), Some(felt!("2000")), Some(DEFAULT_LEGACY_UDC_CASM.clone()), None), - (felt!("33"), Some(felt!("3000")), Some((*DOJO_WORLD_COMPILED_CLASS).clone()), Some((*DOJO_WORLD_SIERRA_CLASS).clone())), + (felt!("11"), Some(felt!("1000")), Some(DEFAULT_LEGACY_ERC20_CLASS.clone())), + (felt!("22"), Some(felt!("2000")), Some(DEFAULT_LEGACY_UDC_CLASS.clone())), + (felt!("33"), Some(felt!("3000")), Some(ContractClass::Class((*DOJO_WORLD_SIERRA_CLASS).clone()))), ] )] fn test_latest_class_read( @@ -78,17 +69,17 @@ mod latest { #[with(fork_provider_with_spawned_fork_network::default())] provider: BlockchainProvider< ForkedProvider, >, - #[case] expected_class: Vec, + #[case] expected_classes: Vec, ) -> Result<()> { - assert_latest_class(provider, expected_class) + assert_latest_class(provider, expected_classes) } #[apply(test_latest_class_read)] fn read_class_from_db_provider( #[with(db_provider())] provider: BlockchainProvider, - #[case] expected_class: Vec, + #[case] expected_classes: Vec, ) -> Result<()> { - assert_latest_class(provider, expected_class) + assert_latest_class(provider, expected_classes) } } @@ -116,33 +107,33 @@ mod historical { #[case::class_hash_at_block_0( 0, vec![ - (felt!("11"), None, None, None), - (felt!("22"), None, None, None), - (felt!("33"), None, None, None) + (felt!("11"), None, None), + (felt!("22"), None, None), + (felt!("33"), None, None) ]) ] #[case::class_hash_at_block_1( 1, vec![ - (felt!("11"), Some(felt!("1000")), Some(DEFAULT_LEGACY_ERC20_CASM.clone()), None), - (felt!("22"), None, None, None), - (felt!("33"), None, None, None), + (felt!("11"), Some(felt!("1000")), Some(DEFAULT_LEGACY_ERC20_CLASS.clone())), + (felt!("22"), None, None), + (felt!("33"), None, None), ]) ] #[case::class_hash_at_block_4( 4, vec![ - (felt!("11"), Some(felt!("1000")), Some(DEFAULT_LEGACY_ERC20_CASM.clone()), None), - (felt!("22"), Some(felt!("2000")), Some(DEFAULT_LEGACY_UDC_CASM.clone()), None), - (felt!("33"), None, None, None), + (felt!("11"), Some(felt!("1000")), Some(DEFAULT_LEGACY_ERC20_CLASS.clone())), + (felt!("22"), Some(felt!("2000")), Some(DEFAULT_LEGACY_UDC_CLASS.clone())), + (felt!("33"), None, None), ]) ] #[case::class_hash_at_block_5( 5, vec![ - (felt!("11"), Some(felt!("1000")), Some(DEFAULT_LEGACY_ERC20_CASM.clone()), None), - (felt!("22"), Some(felt!("2000")), Some(DEFAULT_LEGACY_UDC_CASM.clone()), None), - (felt!("33"), Some(felt!("3000")), Some((*DOJO_WORLD_COMPILED_CLASS).clone()), Some((*DOJO_WORLD_SIERRA_CLASS).clone())), + (felt!("11"), Some(felt!("1000")), Some(DEFAULT_LEGACY_ERC20_CLASS.clone())), + (felt!("22"), Some(felt!("2000")), Some(DEFAULT_LEGACY_UDC_CLASS.clone())), + (felt!("33"), Some(felt!("3000")), Some(ContractClass::Class((*DOJO_WORLD_SIERRA_CLASS).clone()))), ]) ] fn test_historical_class_read( @@ -158,17 +149,17 @@ mod historical { ForkedProvider, >, #[case] block_num: BlockNumber, - #[case] expected_class: Vec, + #[case] expected_classes: Vec, ) -> Result<()> { - assert_historical_class(provider, block_num, expected_class) + assert_historical_class(provider, block_num, expected_classes) } #[apply(test_historical_class_read)] fn read_class_from_db_provider( #[with(db_provider())] provider: BlockchainProvider, #[case] block_num: BlockNumber, - #[case] expected_class: Vec, + #[case] expected_classes: Vec, ) -> Result<()> { - assert_historical_class(provider, block_num, expected_class) + assert_historical_class(provider, block_num, expected_classes) } } diff --git a/crates/katana/storage/provider/tests/contract.rs b/crates/katana/storage/provider/tests/contract.rs index 0a7541f5d1..46339dfa5c 100644 --- a/crates/katana/storage/provider/tests/contract.rs +++ b/crates/katana/storage/provider/tests/contract.rs @@ -43,8 +43,8 @@ mod latest { #[rstest::rstest] #[case( vec![ - (ContractAddress::from(felt!("1")), Some(felt!("22")), Some(felt!("3"))), - (ContractAddress::from(felt!("2")), Some(felt!("33")), Some(felt!("2"))), + (ContractAddress::from(felt!("1337")), Some(felt!("22")), Some(felt!("3"))), + (ContractAddress::from(felt!("80085")), Some(felt!("33")), Some(felt!("2"))), ] )] fn test_latest_contract_info_read( @@ -95,31 +95,31 @@ mod historical { #[case::storage_at_block_0( 0, vec![ - (ContractAddress::from(felt!("1")), None, None), - (ContractAddress::from(felt!("2")), None, None) - ]) - ] + (ContractAddress::from(felt!("1337")), None, None), + (ContractAddress::from(felt!("80085")), None, None) + ]) +] #[case::storage_at_block_1( - 1, - vec![ - (ContractAddress::from(felt!("1")), Some(felt!("11")), Some(felt!("1"))), - (ContractAddress::from(felt!("2")), Some(felt!("11")), Some(felt!("1"))), - ]) - ] + 1, + vec![ + (ContractAddress::from(felt!("1337")), Some(felt!("11")), Some(felt!("1"))), + (ContractAddress::from(felt!("80085")), Some(felt!("11")), Some(felt!("1"))), + ]) +] #[case::storage_at_block_4( - 4, - vec![ - (ContractAddress::from(felt!("1")), Some(felt!("11")), Some(felt!("2"))), - (ContractAddress::from(felt!("2")), Some(felt!("22")), Some(felt!("1"))), - ]) - ] + 4, + vec![ + (ContractAddress::from(felt!("1337")), Some(felt!("11")), Some(felt!("2"))), + (ContractAddress::from(felt!("80085")), Some(felt!("22")), Some(felt!("1"))), + ]) +] #[case::storage_at_block_5( - 5, - vec![ - (ContractAddress::from(felt!("1")), Some(felt!("22")), Some(felt!("3"))), - (ContractAddress::from(felt!("2")), Some(felt!("33")), Some(felt!("2"))), - ]) - ] + 5, + vec![ + (ContractAddress::from(felt!("1337")), Some(felt!("22")), Some(felt!("3"))), + (ContractAddress::from(felt!("80085")), Some(felt!("33")), Some(felt!("2"))), + ]) +] fn test_historical_storage_read( #[from(provider_with_states)] provider: BlockchainProvider, #[case] block_num: BlockNumber, diff --git a/crates/katana/storage/provider/tests/fixtures.rs b/crates/katana/storage/provider/tests/fixtures.rs index 4c717f3f66..44d8e66a89 100644 --- a/crates/katana/storage/provider/tests/fixtures.rs +++ b/crates/katana/storage/provider/tests/fixtures.rs @@ -6,10 +6,10 @@ use katana_primitives::address; use katana_primitives::block::{ BlockHashOrNumber, FinalityStatus, Header, SealedBlock, SealedBlockWithStatus, }; -use katana_primitives::class::{CompiledClass, FlattenedSierraClass, SierraClass}; +use katana_primitives::class::{CompiledClass, ContractClass, SierraContractClass}; use katana_primitives::contract::ContractAddress; -use katana_primitives::genesis::constant::{DEFAULT_LEGACY_ERC20_CASM, DEFAULT_LEGACY_UDC_CASM}; -use katana_primitives::state::{StateUpdates, StateUpdatesWithDeclaredClasses}; +use katana_primitives::genesis::constant::{DEFAULT_LEGACY_ERC20_CLASS, DEFAULT_LEGACY_UDC_CLASS}; +use katana_primitives::state::{StateUpdates, StateUpdatesWithClasses}; use katana_primitives::utils::class::parse_compiled_class; use katana_provider::providers::db::DbProvider; use katana_provider::providers::fork::ForkedProvider; @@ -35,11 +35,9 @@ lazy_static! { .unwrap(); parse_compiled_class(json).unwrap() }; - pub static ref DOJO_WORLD_SIERRA_CLASS: FlattenedSierraClass = { - let sierra_class: SierraClass = - serde_json::from_str(include_str!("../../db/benches/artifacts/dojo_world_240.json")) - .unwrap(); - sierra_class.flatten().unwrap() + pub static ref DOJO_WORLD_SIERRA_CLASS: SierraContractClass = { + serde_json::from_str(include_str!("../../db/benches/artifacts/dojo_world_240.json")) + .unwrap() }; } @@ -70,9 +68,9 @@ pub fn db_provider() -> BlockchainProvider { } #[rstest::fixture] -pub fn mock_state_updates() -> [StateUpdatesWithDeclaredClasses; 3] { - let address_1 = address!("1"); - let address_2 = address!("2"); +pub fn mock_state_updates() -> [StateUpdatesWithClasses; 3] { + let address_1 = address!("1337"); + let address_2 = address!("80085"); let class_hash_1 = felt!("11"); let compiled_class_hash_1 = felt!("1000"); @@ -83,7 +81,7 @@ pub fn mock_state_updates() -> [StateUpdatesWithDeclaredClasses; 3] { let class_hash_3 = felt!("33"); let compiled_class_hash_3 = felt!("3000"); - let state_update_1 = StateUpdatesWithDeclaredClasses { + let state_update_1 = StateUpdatesWithClasses { state_updates: StateUpdates { nonce_updates: BTreeMap::from([(address_1, 1u8.into()), (address_2, 1u8.into())]), storage_updates: BTreeMap::from([ @@ -103,14 +101,10 @@ pub fn mock_state_updates() -> [StateUpdatesWithDeclaredClasses; 3] { ]), ..Default::default() }, - declared_compiled_classes: BTreeMap::from([( - class_hash_1, - DEFAULT_LEGACY_ERC20_CASM.clone(), - )]), - ..Default::default() + classes: BTreeMap::from([(class_hash_1, DEFAULT_LEGACY_ERC20_CLASS.clone())]), }; - let state_update_2 = StateUpdatesWithDeclaredClasses { + let state_update_2 = StateUpdatesWithClasses { state_updates: StateUpdates { nonce_updates: BTreeMap::from([(address_1, 2u8.into())]), storage_updates: BTreeMap::from([( @@ -121,14 +115,10 @@ pub fn mock_state_updates() -> [StateUpdatesWithDeclaredClasses; 3] { deployed_contracts: BTreeMap::from([(address_2, class_hash_2)]), ..Default::default() }, - declared_compiled_classes: BTreeMap::from([( - class_hash_2, - DEFAULT_LEGACY_UDC_CASM.clone(), - )]), - ..Default::default() + classes: BTreeMap::from([(class_hash_2, DEFAULT_LEGACY_UDC_CLASS.clone())]), }; - let state_update_3 = StateUpdatesWithDeclaredClasses { + let state_update_3 = StateUpdatesWithClasses { state_updates: StateUpdates { nonce_updates: BTreeMap::from([(address_1, 3u8.into()), (address_2, 2u8.into())]), storage_updates: BTreeMap::from([ @@ -145,13 +135,9 @@ pub fn mock_state_updates() -> [StateUpdatesWithDeclaredClasses; 3] { declared_classes: BTreeMap::from([(class_hash_3, compiled_class_hash_3)]), ..Default::default() }, - declared_compiled_classes: BTreeMap::from([( - class_hash_3, - (*DOJO_WORLD_COMPILED_CLASS).clone(), - )]), - declared_sierra_classes: BTreeMap::from([( + classes: BTreeMap::from([( class_hash_3, - (*DOJO_WORLD_SIERRA_CLASS).clone(), + ContractClass::Class((*DOJO_WORLD_SIERRA_CLASS).clone()), )]), }; @@ -162,7 +148,7 @@ pub fn mock_state_updates() -> [StateUpdatesWithDeclaredClasses; 3] { #[default(BlockchainProvider)] pub fn provider_with_states( #[default(db_provider())] provider: BlockchainProvider, - #[from(mock_state_updates)] state_updates: [StateUpdatesWithDeclaredClasses; 3], + #[from(mock_state_updates)] state_updates: [StateUpdatesWithClasses; 3], ) -> BlockchainProvider where Db: BlockWriter + StateFactoryProvider, @@ -174,7 +160,7 @@ where BlockHashOrNumber::Num(1) => state_updates[0].clone(), BlockHashOrNumber::Num(2) => state_updates[1].clone(), BlockHashOrNumber::Num(5) => state_updates[2].clone(), - _ => StateUpdatesWithDeclaredClasses::default(), + _ => StateUpdatesWithClasses::default(), }; provider diff --git a/crates/katana/storage/provider/tests/storage.rs b/crates/katana/storage/provider/tests/storage.rs index f401e189b6..9961d4c6a9 100644 --- a/crates/katana/storage/provider/tests/storage.rs +++ b/crates/katana/storage/provider/tests/storage.rs @@ -39,11 +39,11 @@ mod latest { #[rstest::rstest] #[case( vec![ - (ContractAddress::from(felt!("1")), felt!("1"), Some(felt!("111"))), - (ContractAddress::from(felt!("1")), felt!("2"), Some(felt!("222"))), - (ContractAddress::from(felt!("1")), felt!("3"), Some(felt!("77"))), - (ContractAddress::from(felt!("2")), felt!("1"), Some(felt!("12"))), - (ContractAddress::from(felt!("2")), felt!("2"), Some(felt!("13"))) + (ContractAddress::from(felt!("1337")), felt!("1"), Some(felt!("111"))), + (ContractAddress::from(felt!("1337")), felt!("2"), Some(felt!("222"))), + (ContractAddress::from(felt!("1337")), felt!("3"), Some(felt!("77"))), + (ContractAddress::from(felt!("80085")), felt!("1"), Some(felt!("12"))), + (ContractAddress::from(felt!("80085")), felt!("2"), Some(felt!("13"))) ] )] fn test_latest_storage_read( @@ -95,38 +95,38 @@ mod historical { #[case::storage_at_block_0( 0, vec![ - (ContractAddress::from(felt!("1")), felt!("1"), None), - (ContractAddress::from(felt!("1")), felt!("2"), None), - (ContractAddress::from(felt!("2")), felt!("1"), None), - (ContractAddress::from(felt!("2")), felt!("2"), None) + (ContractAddress::from(felt!("1337")), felt!("1"), None), + (ContractAddress::from(felt!("1337")), felt!("2"), None), + (ContractAddress::from(felt!("80085")), felt!("1"), None), + (ContractAddress::from(felt!("80085")), felt!("2"), None) ]) ] #[case::storage_at_block_1( 1, vec![ - (ContractAddress::from(felt!("1")), felt!("1"), Some(felt!("100"))), - (ContractAddress::from(felt!("1")), felt!("2"), Some(felt!("101"))), - (ContractAddress::from(felt!("2")), felt!("1"), Some(felt!("200"))), - (ContractAddress::from(felt!("2")), felt!("2"), Some(felt!("201"))), + (ContractAddress::from(felt!("1337")), felt!("1"), Some(felt!("100"))), + (ContractAddress::from(felt!("1337")), felt!("2"), Some(felt!("101"))), + (ContractAddress::from(felt!("80085")), felt!("1"), Some(felt!("200"))), + (ContractAddress::from(felt!("80085")), felt!("2"), Some(felt!("201"))), ]) ] #[case::storage_at_block_4( 4, vec![ - (ContractAddress::from(felt!("1")), felt!("1"), Some(felt!("111"))), - (ContractAddress::from(felt!("1")), felt!("2"), Some(felt!("222"))), - (ContractAddress::from(felt!("2")), felt!("1"), Some(felt!("200"))), - (ContractAddress::from(felt!("2")), felt!("2"), Some(felt!("201"))), + (ContractAddress::from(felt!("1337")), felt!("1"), Some(felt!("111"))), + (ContractAddress::from(felt!("1337")), felt!("2"), Some(felt!("222"))), + (ContractAddress::from(felt!("80085")), felt!("1"), Some(felt!("200"))), + (ContractAddress::from(felt!("80085")), felt!("2"), Some(felt!("201"))), ]) ] #[case::storage_at_block_5( 5, vec![ - (ContractAddress::from(felt!("1")), felt!("1"), Some(felt!("111"))), - (ContractAddress::from(felt!("1")), felt!("2"), Some(felt!("222"))), - (ContractAddress::from(felt!("1")), felt!("3"), Some(felt!("77"))), - (ContractAddress::from(felt!("2")), felt!("1"), Some(felt!("12"))), - (ContractAddress::from(felt!("2")), felt!("2"), Some(felt!("13"))), + (ContractAddress::from(felt!("1337")), felt!("1"), Some(felt!("111"))), + (ContractAddress::from(felt!("1337")), felt!("2"), Some(felt!("222"))), + (ContractAddress::from(felt!("1337")), felt!("3"), Some(felt!("77"))), + (ContractAddress::from(felt!("80085")), felt!("1"), Some(felt!("12"))), + (ContractAddress::from(felt!("80085")), felt!("2"), Some(felt!("13"))), ]) ] fn test_historical_storage_read( diff --git a/crates/katana/pipeline/Cargo.toml b/crates/katana/sync/pipeline/Cargo.toml similarity index 64% rename from crates/katana/pipeline/Cargo.toml rename to crates/katana/sync/pipeline/Cargo.toml index 5988c7aa0c..5991fabe87 100644 --- a/crates/katana/pipeline/Cargo.toml +++ b/crates/katana/sync/pipeline/Cargo.toml @@ -6,14 +6,14 @@ repository.workspace = true version.workspace = true [dependencies] -katana-core.workspace = true -katana-executor.workspace = true -katana-pool.workspace = true -katana-tasks.workspace = true +katana-primitives.workspace = true +katana-provider = { workspace = true, features = [ "test-utils" ] } +katana-stage.workspace = true -anyhow.workspace = true -async-trait.workspace = true futures.workspace = true thiserror.workspace = true tokio.workspace = true tracing.workspace = true + +[dev-dependencies] +async-trait.workspace = true diff --git a/crates/katana/sync/pipeline/src/lib.rs b/crates/katana/sync/pipeline/src/lib.rs new file mode 100644 index 0000000000..8bbf3e6fd8 --- /dev/null +++ b/crates/katana/sync/pipeline/src/lib.rs @@ -0,0 +1,233 @@ +#![cfg_attr(not(test), warn(unused_crate_dependencies))] + +use core::future::IntoFuture; + +use futures::future::BoxFuture; +use katana_primitives::block::BlockNumber; +use katana_provider::error::ProviderError; +use katana_provider::traits::stage::StageCheckpointProvider; +use katana_stage::{Stage, StageExecutionInput}; +use tokio::sync::watch; +use tracing::{error, info}; + +/// The result of a pipeline execution. +pub type PipelineResult = Result; + +/// The future type for [Pipeline]'s implementation of [IntoFuture]. +pub type PipelineFut = BoxFuture<'static, PipelineResult<()>>; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("Stage not found: {id}")] + StageNotFound { id: String }, + + #[error(transparent)] + Stage(#[from] katana_stage::Error), + + #[error(transparent)] + Provider(#[from] ProviderError), +} + +#[derive(Debug, Clone)] +pub struct PipelineHandle { + tx: watch::Sender>, +} + +impl PipelineHandle { + pub fn set_tip(&self, tip: BlockNumber) { + info!(target: "pipeline", %tip, "Setting new tip"); + self.tx.send(Some(tip)).expect("channel closed"); + } +} + +/// Syncing pipeline. +/// +/// The pipeline drives the execution of stages, running each stage to completion in the order they +/// were added. +/// +/// Inspired by [`reth`]'s staged sync pipeline. +/// +/// [`reth`]: https://github.com/paradigmxyz/reth/blob/c7aebff0b6bc19cd0b73e295497d3c5150d40ed8/crates/stages/api/src/pipeline/mod.rs#L66 +pub struct Pipeline

{ + chunk_size: u64, + provider: P, + stages: Vec>, + tip_watcher: (watch::Receiver>, watch::Sender>), +} + +impl

Pipeline

{ + /// Create a new empty pipeline. + pub fn new(provider: P, chunk_size: u64) -> (Self, PipelineHandle) { + let (tx, rx) = watch::channel(None); + let handle = PipelineHandle { tx: tx.clone() }; + let pipeline = Self { stages: Vec::new(), tip_watcher: (rx, tx), provider, chunk_size }; + (pipeline, handle) + } + + /// Insert a new stage into the pipeline. + pub fn add_stage(&mut self, stage: S) { + self.stages.push(Box::new(stage)); + } + + /// Insert multiple stages into the pipeline. + /// + /// The stages will be executed in the order they are appear in the iterator. + pub fn add_stages(&mut self, stages: impl Iterator>) { + self.stages.extend(stages); + } + + pub fn handle(&self) -> PipelineHandle { + PipelineHandle { tx: self.tip_watcher.1.clone() } + } +} + +impl Pipeline

{ + /// Run the pipeline in a loop. + pub async fn run(&mut self) -> PipelineResult<()> { + let mut current_chunk_tip = self.chunk_size; + + loop { + let tip = *self.tip_watcher.0.borrow_and_update(); + + while let Some(tip) = tip { + let to = current_chunk_tip.min(tip); + let last_block_processed = self.run_once_until(to).await?; + + if last_block_processed >= tip { + info!(target: "pipeline", %tip, "Finished processing until tip."); + break; + } else { + current_chunk_tip = (last_block_processed + self.chunk_size).min(tip); + } + } + + info!(target: "pipeline", "Waiting for new tip."); + + // If we reach here, that means we have run the pipeline up until the `tip`. + // So, wait until the tip has changed. + if self.tip_watcher.0.changed().await.is_err() { + break; + } + } + + info!(target: "pipeline", "Pipeline finished."); + + Ok(()) + } + + /// Run the pipeline once, until the given block number. + async fn run_once_until(&mut self, to: BlockNumber) -> PipelineResult { + let last_stage_idx = self.stages.len() - 1; + + for (i, stage) in self.stages.iter_mut().enumerate() { + let id = stage.id(); + + // Get the checkpoint for the stage, otherwise default to block number 0 + let checkpoint = self.provider.checkpoint(id)?.unwrap_or_default(); + + // Skip the stage if the checkpoint is greater than or equal to the target block number + if checkpoint >= to { + info!(target: "pipeline", %id, "Skipping stage."); + + if i == last_stage_idx { + return Ok(checkpoint); + } + + continue; + } + + info!(target: "pipeline", %id, from = %checkpoint, %to, "Executing stage."); + + // plus 1 because the checkpoint is inclusive + let input = StageExecutionInput { from: checkpoint + 1, to }; + stage.execute(&input).await?; + self.provider.set_checkpoint(id, to)?; + + info!(target: "pipeline", %id, from = %checkpoint, %to, "Stage execution completed."); + } + + Ok(to) + } +} + +impl

IntoFuture for Pipeline

+where + P: StageCheckpointProvider + 'static, +{ + type Output = PipelineResult<()>; + type IntoFuture = PipelineFut; + + fn into_future(mut self) -> Self::IntoFuture { + Box::pin(async move { + self.run().await.inspect_err(|error| { + error!(target: "pipeline", %error, "Pipeline failed."); + }) + }) + } +} + +impl

core::fmt::Debug for Pipeline

+where + P: core::fmt::Debug, +{ + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("Pipeline") + .field("tip", &self.tip_watcher) + .field("provider", &self.provider) + .field("chunk_size", &self.chunk_size) + .field("stages", &self.stages.iter().map(|s| s.id()).collect::>()) + .finish() + } +} + +#[cfg(test)] +mod tests { + use katana_provider::test_utils::test_provider; + use katana_provider::traits::stage::StageCheckpointProvider; + use katana_stage::StageResult; + + use super::{Pipeline, Stage, StageExecutionInput}; + + struct MockStage; + + #[async_trait::async_trait] + impl Stage for MockStage { + fn id(&self) -> &'static str { + "Mock" + } + + async fn execute(&mut self, _: &StageExecutionInput) -> StageResult { + Ok(()) + } + } + + #[tokio::test] + async fn stage_checkpoint() { + let provider = test_provider(); + + let (mut pipeline, _handle) = Pipeline::new(&provider, 10); + pipeline.add_stage(MockStage); + + // check that the checkpoint was set + let initial_checkpoint = provider.checkpoint("Mock").unwrap(); + assert_eq!(initial_checkpoint, None); + + pipeline.run_once_until(5).await.expect("failed to run the pipeline once"); + + // check that the checkpoint was set + let actual_checkpoint = provider.checkpoint("Mock").unwrap(); + assert_eq!(actual_checkpoint, Some(5)); + + pipeline.run_once_until(10).await.expect("failed to run the pipeline once"); + + // check that the checkpoint was set + let actual_checkpoint = provider.checkpoint("Mock").unwrap(); + assert_eq!(actual_checkpoint, Some(10)); + + pipeline.run_once_until(10).await.expect("failed to run the pipeline once"); + + // check that the checkpoint doesn't change + let actual_checkpoint = provider.checkpoint("Mock").unwrap(); + assert_eq!(actual_checkpoint, Some(10)); + } +} diff --git a/crates/katana/sync/stage/Cargo.toml b/crates/katana/sync/stage/Cargo.toml new file mode 100644 index 0000000000..8dc734d469 --- /dev/null +++ b/crates/katana/sync/stage/Cargo.toml @@ -0,0 +1,26 @@ +[package] +edition.workspace = true +license.workspace = true +name = "katana-stage" +repository.workspace = true +version.workspace = true + +[dependencies] +katana-core.workspace = true +katana-executor.workspace = true +katana-feeder-gateway.workspace = true +katana-pool.workspace = true +katana-primitives.workspace = true +katana-provider.workspace = true +katana-rpc-types.workspace = true +katana-tasks.workspace = true + +anyhow.workspace = true +async-trait.workspace = true +backon = { version = "1.3.0", features = [ "tokio-sleep" ] } +futures.workspace = true +num-traits.workspace = true +starknet.workspace = true +thiserror.workspace = true +tokio.workspace = true +tracing.workspace = true diff --git a/crates/katana/sync/stage/src/blocks.rs b/crates/katana/sync/stage/src/blocks.rs new file mode 100644 index 0000000000..a7dec187c3 --- /dev/null +++ b/crates/katana/sync/stage/src/blocks.rs @@ -0,0 +1,297 @@ +use std::sync::Arc; +use std::time::Duration; + +use anyhow::Result; +use backon::{ExponentialBuilder, Retryable}; +use katana_feeder_gateway::client; +use katana_feeder_gateway::client::SequencerGateway; +use katana_feeder_gateway::types::StateUpdateWithBlock; +use katana_primitives::block::{ + BlockIdOrTag, BlockNumber, FinalityStatus, GasPrices, Header, SealedBlock, + SealedBlockWithStatus, +}; +use katana_primitives::fee::{PriceUnit, TxFeeInfo}; +use katana_primitives::receipt::{ + DeclareTxReceipt, DeployAccountTxReceipt, InvokeTxReceipt, L1HandlerTxReceipt, Receipt, +}; +use katana_primitives::state::{StateUpdates, StateUpdatesWithClasses}; +use katana_primitives::transaction::{Tx, TxWithHash}; +use katana_primitives::Felt; +use katana_provider::traits::block::BlockWriter; +use num_traits::ToPrimitive; +use starknet::core::types::ResourcePrice; +use starknet::providers::sequencer::models::BlockStatus; +use tracing::{debug, error, warn}; + +use super::{Stage, StageExecutionInput, StageResult}; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error(transparent)] + Gateway(#[from] client::Error), +} + +#[derive(Debug)] +pub struct Blocks

{ + provider: P, + downloader: Downloader, +} + +impl

Blocks

{ + pub fn new(provider: P, feeder_gateway: SequencerGateway, download_batch_size: usize) -> Self { + let downloader = Downloader::new(feeder_gateway, download_batch_size); + Self { provider, downloader } + } +} + +#[async_trait::async_trait] +impl Stage for Blocks

{ + fn id(&self) -> &'static str { + "Blocks" + } + + async fn execute(&mut self, input: &StageExecutionInput) -> StageResult { + // Download all blocks from the provided range + let blocks = self.downloader.download_blocks(input.from, input.to).await?; + + if !blocks.is_empty() { + debug!(target: "stage", id = %self.id(), total = %blocks.len(), "Storing blocks to storage."); + + // Store blocks to storage + for block in blocks { + let (block, receipts, state_updates) = extract_block_data(block)?; + + self.provider.insert_block_with_states_and_receipts( + block, + state_updates, + receipts, + Vec::new(), + )?; + } + } + + Ok(()) + } +} + +#[derive(Debug, Clone)] +struct Downloader { + batch_size: usize, + client: Arc, +} + +impl Downloader { + fn new(client: SequencerGateway, batch_size: usize) -> Self { + Self { client: Arc::new(client), batch_size } + } + + /// Fetch blocks in the range [from, to] in batches of `batch_size`. + async fn download_blocks( + &self, + from: BlockNumber, + to: BlockNumber, + ) -> Result, Error> { + debug!(target: "pipeline", %from, %to, "Downloading blocks."); + let mut blocks = Vec::with_capacity(to.saturating_sub(from) as usize); + + for batch_start in (from..=to).step_by(self.batch_size) { + let batch_end = (batch_start + self.batch_size as u64 - 1).min(to); + let batch = self.fetch_blocks_with_retry(batch_start, batch_end).await?; + blocks.extend(batch); + } + + Ok(blocks) + } + + /// Fetch blocks with the given block number with retry mechanism at a batch level. + async fn fetch_blocks_with_retry( + &self, + from: BlockNumber, + to: BlockNumber, + ) -> Result, Error> { + let request = || async move { self.clone().fetch_blocks(from, to).await }; + + // Retry only when being rate limited + let backoff = ExponentialBuilder::default().with_min_delay(Duration::from_secs(9)); + let result = request + .retry(backoff) + .when(|error| matches!(error, Error::Gateway(client::Error::RateLimited))) + .notify(|error, _| { + warn!(target: "pipeline", %from, %to, %error, "Retrying block download."); + }) + .await?; + + Ok(result) + } + + async fn fetch_blocks( + &self, + from: BlockNumber, + to: BlockNumber, + ) -> Result, Error> { + let total = to.saturating_sub(from) as usize; + let mut requests = Vec::with_capacity(total); + + for i in from..=to { + requests.push(self.fetch_block(i)); + } + + let results = futures::future::join_all(requests).await; + results.into_iter().collect() + } + + /// Fetch a single block with the given block number. + async fn fetch_block(&self, block: BlockNumber) -> Result { + let block = self + .client + .get_state_update_with_block(BlockIdOrTag::Number(block)) + .await + .inspect_err(|error| { + if !error.is_rate_limited() { + error!(target: "pipeline", %error, %block, "Failed to fetch block.") + } + })?; + Ok(block) + } +} + +fn extract_block_data( + data: StateUpdateWithBlock, +) -> Result<(SealedBlockWithStatus, Vec, StateUpdatesWithClasses)> { + fn to_gas_prices(prices: ResourcePrice) -> GasPrices { + GasPrices { + eth: prices.price_in_fri.to_u128().expect("valid u128"), + strk: prices.price_in_fri.to_u128().expect("valid u128"), + } + } + + let status = match data.block.status { + BlockStatus::AcceptedOnL2 => FinalityStatus::AcceptedOnL2, + BlockStatus::AcceptedOnL1 => FinalityStatus::AcceptedOnL1, + status => panic!("unsupported block status: {status:?}"), + }; + + let transactions = data + .block + .transactions + .into_iter() + .map(|tx| tx.try_into()) + .collect::, _>>()?; + + let receipts = data + .block + .transaction_receipts + .into_iter() + .zip(transactions.iter()) + .map(|(receipt, tx)| { + let events = receipt.events; + let revert_error = receipt.revert_error; + let messages_sent = receipt.l2_to_l1_messages; + let overall_fee = receipt.actual_fee.to_u128().expect("valid u128"); + + let unit = if tx.transaction.version() >= Felt::THREE { + PriceUnit::Fri + } else { + PriceUnit::Wei + }; + + let fee = TxFeeInfo { + unit, + overall_fee, + gas_price: Default::default(), + gas_consumed: Default::default(), + }; + + match tx.transaction { + Tx::Invoke(_) => Receipt::Invoke(InvokeTxReceipt { + fee, + events, + revert_error, + messages_sent, + execution_resources: Default::default(), + }), + Tx::Declare(_) => Receipt::Declare(DeclareTxReceipt { + fee, + events, + revert_error, + messages_sent, + execution_resources: Default::default(), + }), + Tx::L1Handler(_) => Receipt::L1Handler(L1HandlerTxReceipt { + fee, + events, + messages_sent, + revert_error, + message_hash: Default::default(), + execution_resources: Default::default(), + }), + Tx::DeployAccount(_) => Receipt::DeployAccount(DeployAccountTxReceipt { + fee, + events, + revert_error, + messages_sent, + contract_address: Default::default(), + execution_resources: Default::default(), + }), + Tx::Deploy(_) => unreachable!("Deploy transactions are not supported"), + } + }) + .collect::>(); + + let transaction_count = transactions.len() as u32; + let block = SealedBlock { + body: transactions, + hash: data.block.block_hash.unwrap_or_default(), + header: Header { + transaction_count, + timestamp: data.block.timestamp, + l1_da_mode: data.block.l1_da_mode, + events_count: Default::default(), + parent_hash: data.block.parent_block_hash, + state_diff_length: Default::default(), + receipts_commitment: Default::default(), + state_diff_commitment: Default::default(), + number: data.block.block_number.unwrap_or_default(), + l1_gas_prices: to_gas_prices(data.block.l1_gas_price), + state_root: data.block.state_root.unwrap_or_default(), + l1_data_gas_prices: to_gas_prices(data.block.l1_data_gas_price), + protocol_version: data.block.starknet_version.unwrap_or_default(), + events_commitment: data.block.event_commitment.unwrap_or_default(), + sequencer_address: data.block.sequencer_address.unwrap_or_default(), + transactions_commitment: data.block.transaction_commitment.unwrap_or_default(), + }, + }; + + let state_updates: StateUpdates = data.state_update.state_diff.into(); + let state_updates = StateUpdatesWithClasses { state_updates, ..Default::default() }; + + Ok((SealedBlockWithStatus { block, status }, receipts, state_updates)) +} + +#[cfg(test)] +mod tests { + use katana_feeder_gateway::client::SequencerGateway; + use katana_provider::test_utils::test_provider; + use katana_provider::traits::block::BlockNumberProvider; + + use super::Blocks; + use crate::{Stage, StageExecutionInput}; + + #[tokio::test] + async fn fetch_blocks() { + let from_block = 308919; + let to_block = from_block + 2; + + let provider = test_provider(); + let feeder_gateway = SequencerGateway::sn_sepolia(); + + let mut stage = Blocks::new(&provider, feeder_gateway, 10); + + let input = StageExecutionInput { from: from_block, to: to_block }; + stage.execute(&input).await.expect("failed to execute stage"); + + // check provider storage + let block_number = provider.latest_number().expect("failed to get latest block number"); + assert_eq!(block_number, to_block); + } +} diff --git a/crates/katana/sync/stage/src/classes.rs b/crates/katana/sync/stage/src/classes.rs new file mode 100644 index 0000000000..fe3a71783c --- /dev/null +++ b/crates/katana/sync/stage/src/classes.rs @@ -0,0 +1,162 @@ +use std::sync::Arc; +use std::time::Duration; + +use anyhow::Result; +use backon::{ExponentialBuilder, Retryable}; +use katana_feeder_gateway::client::{self, SequencerGateway}; +use katana_primitives::block::{BlockIdOrTag, BlockNumber}; +use katana_primitives::class::{ClassHash, ContractClass}; +use katana_provider::error::ProviderError; +use katana_provider::traits::contract::{ContractClassWriter, ContractClassWriterExt}; +use katana_provider::traits::state_update::StateUpdateProvider; +use katana_rpc_types::class::ConversionError; +use tracing::{debug, error, warn}; + +use super::{Stage, StageExecutionInput, StageResult}; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("missing declared classes for block {block}")] + MissingBlockDeclaredClasses { + /// The block number whose declared classes are missing. + block: BlockNumber, + }, + + /// Error returnd by the client used to download the classes from. + #[error(transparent)] + Gateway(#[from] client::Error), + + /// Error that can occur when converting the classes types to the internal types. + #[error(transparent)] + Conversion(#[from] ConversionError), + + #[error(transparent)] + Provider(#[from] ProviderError), +} + +#[derive(Debug)] +pub struct Classes

{ + provider: P, + downloader: Downloader, +} + +impl

Classes

{ + pub fn new(provider: P, feeder_gateway: SequencerGateway, download_batch_size: usize) -> Self { + let downloader = Downloader::new(feeder_gateway, download_batch_size); + Self { provider, downloader } + } +} + +#[async_trait::async_trait] +impl

Stage for Classes

+where + P: StateUpdateProvider + ContractClassWriter + ContractClassWriterExt, +{ + fn id(&self) -> &'static str { + "Classes" + } + + async fn execute(&mut self, input: &StageExecutionInput) -> StageResult { + let mut classes: Vec<(ClassHash, ContractClass)> = Vec::new(); + + for block in input.from..=input.to { + // get the classes declared at block `i` + let class_hashes = self + .provider + .declared_classes(block.into())? + .ok_or(Error::MissingBlockDeclaredClasses { block })?; + let class_hashes = class_hashes.keys().copied().collect::>(); + + // fetch the classes artifacts + let class_artifacts = self.downloader.download_classes(&class_hashes, block).await?; + classes.extend(class_hashes.into_iter().zip(class_artifacts)); + } + + if !classes.is_empty() { + debug!(target: "stage", id = self.id(), total = %classes.len(), "Storing class artifacts."); + for (hash, class) in classes { + self.provider.set_class(hash, class)?; + } + } + + Ok(()) + } +} + +#[derive(Debug, Clone)] +struct Downloader { + batch_size: usize, + client: Arc, +} + +impl Downloader { + fn new(client: SequencerGateway, batch_size: usize) -> Self { + Self { client: Arc::new(client), batch_size } + } + + async fn download_classes( + &self, + hashes: &[ClassHash], + block: BlockNumber, + ) -> Result, Error> { + debug!(total = %hashes.len(), %block, "Downloading classes."); + let mut classes = Vec::with_capacity(hashes.len()); + + for chunk in hashes.chunks(self.batch_size) { + let batch = self.fetch_classes_with_retry(chunk, block).await?; + classes.extend(batch); + } + + Ok(classes) + } + + async fn fetch_classes_with_retry( + &self, + classes: &[ClassHash], + block: BlockNumber, + ) -> Result, Error> { + let request = || async move { self.clone().fetch_classes(classes, block).await }; + + // Retry only when being rate limited + let backoff = ExponentialBuilder::default().with_min_delay(Duration::from_secs(3)); + let result = request + .retry(backoff) + .when(|error| matches!(error, Error::Gateway(client::Error::RateLimited))) + .notify(|error, _| { + warn!(target: "pipeline", %error, "Retrying class download."); + }) + .await?; + + Ok(result) + } + + async fn fetch_classes( + &self, + classes: &[ClassHash], + block: BlockNumber, + ) -> Result, Error> { + let mut requests = Vec::with_capacity(classes.len()); + + for class in classes { + requests.push(self.fetch_class(*class, block)); + } + + let results = futures::future::join_all(requests).await; + results.into_iter().collect() + } + + async fn fetch_class( + &self, + hash: ClassHash, + block: BlockNumber, + ) -> Result { + let class = self.client.get_class(hash, BlockIdOrTag::Number(block)).await.inspect_err( + |error| { + if !error.is_rate_limited() { + error!(target: "pipeline", %error, %block, class = %format!("{hash:#x}"), "Fetching class.") + } + }, + )?; + Ok(class.try_into()?) + } +} diff --git a/crates/katana/sync/stage/src/lib.rs b/crates/katana/sync/stage/src/lib.rs new file mode 100644 index 0000000000..ab41da26ed --- /dev/null +++ b/crates/katana/sync/stage/src/lib.rs @@ -0,0 +1,52 @@ +#![cfg_attr(not(test), warn(unused_crate_dependencies))] + +use katana_primitives::block::BlockNumber; +use katana_provider::error::ProviderError; + +mod blocks; +mod classes; +mod sequencing; + +pub use blocks::Blocks; +pub use classes::Classes; +pub use sequencing::Sequencing; + +/// The result type of a stage execution. See [Stage::execute]. +pub type StageResult = Result<(), Error>; + +#[derive(Debug, Default, Clone)] +pub struct StageExecutionInput { + pub from: BlockNumber, + pub to: BlockNumber, +} + +#[derive(Debug, Default)] +pub struct StageExecutionOutput { + pub last_block_processed: BlockNumber, +} + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error(transparent)] + Provider(#[from] ProviderError), + + /// Errors that could happen during the execution of the [`Blocks`](blocks::Blocks) stage. + #[error(transparent)] + Blocks(#[from] blocks::Error), + + /// Errors that could happen during the execution of the [`Classes`](classes::Classes) stage. + #[error(transparent)] + Classes(#[from] classes::Error), + + #[error(transparent)] + Other(#[from] anyhow::Error), +} + +#[async_trait::async_trait] +pub trait Stage: Send + Sync { + /// Returns the id which uniquely identifies the stage. + fn id(&self) -> &'static str; + + /// Executes the stage. + async fn execute(&mut self, input: &StageExecutionInput) -> StageResult; +} diff --git a/crates/katana/pipeline/src/stage/sequencing.rs b/crates/katana/sync/stage/src/sequencing.rs similarity index 65% rename from crates/katana/pipeline/src/stage/sequencing.rs rename to crates/katana/sync/stage/src/sequencing.rs index c988ecab57..c1998e7341 100644 --- a/crates/katana/pipeline/src/stage/sequencing.rs +++ b/crates/katana/sync/stage/src/sequencing.rs @@ -1,7 +1,8 @@ +use std::future::IntoFuture; use std::sync::Arc; use anyhow::Result; -use futures::future; +use futures::future::{self, BoxFuture}; use katana_core::backend::Backend; use katana_core::service::block_producer::{BlockProducer, BlockProductionError}; use katana_core::service::messaging::{MessagingConfig, MessagingService, MessagingTask}; @@ -11,8 +12,7 @@ use katana_pool::{TransactionPool, TxPool}; use katana_tasks::{TaskHandle, TaskSpawner}; use tracing::error; -use super::{StageId, StageResult}; -use crate::Stage; +pub type SequencingFut = BoxFuture<'static, Result<()>>; /// The sequencing stage is responsible for advancing the chain state. #[allow(missing_debug_implementations)] @@ -61,31 +61,30 @@ impl Sequencing { } } -#[async_trait::async_trait] -impl Stage for Sequencing { - fn id(&self) -> StageId { - StageId::Sequencing - } +impl IntoFuture for Sequencing { + type Output = Result<()>; + type IntoFuture = SequencingFut; - #[tracing::instrument(skip(self), name = "Stage", fields(id = %self.id()))] - async fn execute(&mut self) -> StageResult { - // Build the messaging and block production tasks. - let messaging = self.run_messaging().await?; - let block_production = self.run_block_production(); + fn into_future(self) -> Self::IntoFuture { + Box::pin(async move { + // Build the messaging and block production tasks. + let messaging = self.run_messaging().await?; + let block_production = self.run_block_production(); - // Neither of these tasks should complete as they are meant to be run forever, - // but if either of them do complete, the sequencing stage should return. - // - // Select on the tasks completion to prevent the task from failing silently (if any). - tokio::select! { - res = messaging => { - error!(target: "pipeline", reason = ?res, "Messaging task finished unexpectedly."); - }, - res = block_production => { - error!(target: "pipeline", reason = ?res, "Block production task finished unexpectedly."); + // Neither of these tasks should complete as they are meant to be run forever, + // but if either of them do complete, the sequencing stage should return. + // + // Select on the tasks completion to prevent the task from failing silently (if any). + tokio::select! { + res = messaging => { + error!(target: "sequencing", reason = ?res, "Messaging task finished unexpectedly."); + }, + res = block_production => { + error!(target: "sequencing", reason = ?res, "Block production task finished unexpectedly."); + } } - } - Ok(()) + Ok(()) + }) } } diff --git a/crates/katana/trie/Cargo.toml b/crates/katana/trie/Cargo.toml index 759dab5347..5c6f948e4b 100644 --- a/crates/katana/trie/Cargo.toml +++ b/crates/katana/trie/Cargo.toml @@ -17,7 +17,7 @@ starknet-types-core.workspace = true thiserror.workspace = true [dependencies.bonsai-trie] +branch = "kariy/indexmap" default-features = false features = [ "std" ] -git = "https://github.com/madara-alliance/bonsai-trie/" -rev = "56d7d62" +git = "https://github.com/dojoengine/bonsai-trie/" diff --git a/crates/katana/trie/src/classes.rs b/crates/katana/trie/src/classes.rs new file mode 100644 index 0000000000..ff71e3b3b8 --- /dev/null +++ b/crates/katana/trie/src/classes.rs @@ -0,0 +1,70 @@ +use bonsai_trie::{BonsaiDatabase, BonsaiPersistentDatabase, MultiProof}; +use katana_primitives::block::BlockNumber; +use katana_primitives::class::{ClassHash, CompiledClassHash}; +use katana_primitives::hash::{Poseidon, StarkHash}; +use katana_primitives::Felt; +use starknet::macros::short_string; + +use crate::id::CommitId; + +#[derive(Debug)] +pub struct ClassesMultiProof(pub MultiProof); + +impl ClassesMultiProof { + // TODO: maybe perform results check in this method as well. make it accept the compiled class + // hashes + pub fn verify(&self, root: Felt, class_hashes: Vec) -> Vec { + crate::verify_proof::(&self.0, root, class_hashes) + } +} + +impl From for ClassesMultiProof { + fn from(value: MultiProof) -> Self { + Self(value) + } +} + +#[derive(Debug)] +pub struct ClassesTrie { + trie: crate::BonsaiTrie, +} + +////////////////////////////////////////////////////////////// +// ClassesTrie implementations +////////////////////////////////////////////////////////////// + +impl ClassesTrie { + const BONSAI_IDENTIFIER: &'static [u8] = b"classes"; + + pub fn new(db: DB) -> Self { + Self { trie: crate::BonsaiTrie::new(db) } + } + + pub fn root(&self) -> Felt { + self.trie.root(Self::BONSAI_IDENTIFIER) + } + + pub fn multiproof(&mut self, class_hashes: Vec) -> MultiProof { + self.trie.multiproof(Self::BONSAI_IDENTIFIER, class_hashes) + } +} + +impl ClassesTrie +where + DB: BonsaiDatabase + BonsaiPersistentDatabase, +{ + pub fn insert(&mut self, hash: ClassHash, compiled_hash: CompiledClassHash) { + let value = compute_classes_trie_value(compiled_hash); + self.trie.insert(Self::BONSAI_IDENTIFIER, hash, value) + } + + pub fn commit(&mut self, block: BlockNumber) { + self.trie.commit(block.into()) + } +} + +pub fn compute_classes_trie_value(compiled_class_hash: CompiledClassHash) -> Felt { + // https://docs.starknet.io/architecture-and-concepts/network-architecture/starknet-state/#classes_trie + const CONTRACT_CLASS_LEAF_V0: Felt = short_string!("CONTRACT_CLASS_LEAF_V0"); + Poseidon::hash(&CONTRACT_CLASS_LEAF_V0, &compiled_class_hash) +} diff --git a/crates/katana/trie/src/contracts.rs b/crates/katana/trie/src/contracts.rs new file mode 100644 index 0000000000..95e7394ee6 --- /dev/null +++ b/crates/katana/trie/src/contracts.rs @@ -0,0 +1,47 @@ +use bonsai_trie::{BonsaiDatabase, BonsaiPersistentDatabase, MultiProof}; +use katana_primitives::block::BlockNumber; +use katana_primitives::hash::Pedersen; +use katana_primitives::{ContractAddress, Felt}; + +use crate::id::CommitId; + +#[derive(Debug)] +pub struct ContractsTrie { + trie: crate::BonsaiTrie, +} + +////////////////////////////////////////////////////////////// +// ContractsTrie implementations +////////////////////////////////////////////////////////////// + +impl ContractsTrie { + /// NOTE: The identifier value is only relevant if the underlying [`BonsaiDatabase`] + /// implementation is shared across other tries. + const BONSAI_IDENTIFIER: &'static [u8] = b"contracts"; + + pub fn new(db: DB) -> Self { + Self { trie: crate::BonsaiTrie::new(db) } + } + + pub fn root(&self) -> Felt { + self.trie.root(Self::BONSAI_IDENTIFIER) + } + + pub fn multiproof(&mut self, addresses: Vec) -> MultiProof { + let keys = addresses.into_iter().map(Felt::from).collect::>(); + self.trie.multiproof(Self::BONSAI_IDENTIFIER, keys) + } +} + +impl ContractsTrie +where + DB: BonsaiDatabase + BonsaiPersistentDatabase, +{ + pub fn insert(&mut self, address: ContractAddress, state_hash: Felt) { + self.trie.insert(Self::BONSAI_IDENTIFIER, *address, state_hash) + } + + pub fn commit(&mut self, block: BlockNumber) { + self.trie.commit(block.into()) + } +} diff --git a/crates/katana/trie/src/id.rs b/crates/katana/trie/src/id.rs new file mode 100644 index 0000000000..d7a81aa8c1 --- /dev/null +++ b/crates/katana/trie/src/id.rs @@ -0,0 +1,38 @@ +use bonsai_trie::id::Id; +use bonsai_trie::ByteVec; +use katana_primitives::block::BlockNumber; + +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Default)] +pub struct CommitId(BlockNumber); + +impl CommitId { + pub fn new(block_number: BlockNumber) -> Self { + Self(block_number) + } +} + +impl Id for CommitId { + fn as_u64(self) -> u64 { + self.0 + } + + fn from_u64(v: u64) -> Self { + Self(v) + } + + fn to_bytes(&self) -> ByteVec { + ByteVec::from(&self.0.to_be_bytes() as &[_]) + } +} + +impl From for CommitId { + fn from(value: BlockNumber) -> Self { + Self(value) + } +} + +impl From for BlockNumber { + fn from(value: CommitId) -> Self { + value.0 + } +} diff --git a/crates/katana/trie/src/lib.rs b/crates/katana/trie/src/lib.rs index 1a3e858bd6..e0af40b4bc 100644 --- a/crates/katana/trie/src/lib.rs +++ b/crates/katana/trie/src/lib.rs @@ -1,19 +1,99 @@ -use anyhow::Result; -use bitvec::vec::BitVec; -pub use bonsai_trie as bonsai; -use bonsai_trie::id::BasicId; -use bonsai_trie::{BonsaiDatabase, BonsaiPersistentDatabase}; +use bitvec::view::AsBits; +pub use bonsai::{BitVec, MultiProof, Path, ProofNode}; +use bonsai_trie::BonsaiStorage; +pub use bonsai_trie::{BonsaiDatabase, BonsaiPersistentDatabase, BonsaiStorageConfig}; use katana_primitives::class::ClassHash; use katana_primitives::Felt; use starknet_types_core::hash::{Pedersen, StarkHash}; +pub use {bitvec, bonsai_trie as bonsai}; -/// A helper trait to define a database that can be used as a Bonsai Trie. +mod classes; +mod contracts; +mod id; +mod storages; + +pub use classes::*; +pub use contracts::ContractsTrie; +pub use id::CommitId; +pub use storages::StoragesTrie; + +/// A lightweight shim for [`BonsaiStorage`]. /// -/// Basically a short hand for `BonsaiDatabase + BonsaiPersistentDatabase`. -pub trait BonsaiTrieDb: BonsaiDatabase + BonsaiPersistentDatabase {} -impl BonsaiTrieDb for T where T: BonsaiDatabase + BonsaiPersistentDatabase {} +/// This abstract the Bonsai Trie operations - providing a simplified interface without +/// having to handle how to transform the keys into the internal keys used by the trie. +/// This struct is not meant to be used directly, and instead use the specific tries that have +/// been derived from it, [`ClassesTrie`], [`ContractsTrie`], or [`StoragesTrie`]. +pub struct BonsaiTrie +where + DB: BonsaiDatabase, + Hash: StarkHash + Send + Sync, +{ + storage: BonsaiStorage, +} + +impl BonsaiTrie +where + DB: BonsaiDatabase, + Hash: StarkHash + Send + Sync, +{ + pub fn new(db: DB) -> Self { + let config = BonsaiStorageConfig { + // we have our own implementation of storing trie changes + max_saved_trie_logs: Some(0), + // in the bonsai-trie crate, this field seems to be only used in rocksdb impl. + // i dont understand why would they add a config thats implementation specific ???? + // + // this config should be used by our implementation of the + // BonsaiPersistentDatabase::snapshot() + max_saved_snapshots: Some(64usize), + snapshot_interval: 1, + }; + + Self { storage: BonsaiStorage::new(db, config, 251) } + } +} -pub fn compute_merkle_root(values: &[Felt]) -> Result +impl BonsaiTrie +where + DB: BonsaiDatabase, + Hash: StarkHash + Send + Sync, +{ + pub fn root(&self, id: &[u8]) -> Felt { + self.storage.root_hash(id).expect("failed to get trie root") + } + + pub fn multiproof(&mut self, id: &[u8], keys: Vec) -> MultiProof { + let keys = keys.into_iter().map(|key| key.to_bytes_be().as_bits()[5..].to_owned()); + self.storage.get_multi_proof(id, keys).expect("failed to get multiproof") + } +} + +impl BonsaiTrie +where + DB: BonsaiDatabase + BonsaiPersistentDatabase, + Hash: StarkHash + Send + Sync, +{ + pub fn insert(&mut self, id: &[u8], key: Felt, value: Felt) { + let key: BitVec = key.to_bytes_be().as_bits()[5..].to_owned(); + self.storage.insert(id, &key, &value).unwrap(); + } + + pub fn commit(&mut self, id: CommitId) { + self.storage.commit(id).expect("failed to commit trie"); + } +} + +impl std::fmt::Debug for BonsaiTrie +where + DB: BonsaiDatabase, + Hash: StarkHash + Send + Sync, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("BonsaiTrie").field("storage", &self.storage).finish() + } +} + +pub fn compute_merkle_root(values: &[Felt]) -> anyhow::Result where H: StarkHash + Send + Sync, { @@ -25,7 +105,7 @@ where let config = BonsaiStorageConfig::default(); let bonsai_db = databases::HashMapDb::::default(); - let mut bs = BonsaiStorage::<_, _, H>::new(bonsai_db, config).unwrap(); + let mut bs = BonsaiStorage::<_, _, H>::new(bonsai_db, config, 64); for (id, value) in values.iter().enumerate() { let key = BitVec::from_iter(id.to_be_bytes()); @@ -50,6 +130,15 @@ pub fn compute_contract_state_hash( Pedersen::hash(&hash, &CONTRACT_STATE_HASH_VERSION) } +pub fn verify_proof( + proofs: &MultiProof, + root: Felt, + keys: Vec, +) -> Vec { + let keys = keys.into_iter().map(|f| f.to_bytes_be().as_bits()[5..].to_owned()); + proofs.verify_proof::(root, keys, 251).collect::, _>>().unwrap() +} + #[cfg(test)] mod tests { diff --git a/crates/katana/trie/src/storages.rs b/crates/katana/trie/src/storages.rs new file mode 100644 index 0000000000..c7f6bbe912 --- /dev/null +++ b/crates/katana/trie/src/storages.rs @@ -0,0 +1,41 @@ +use bonsai_trie::{BonsaiDatabase, BonsaiPersistentDatabase, MultiProof}; +use katana_primitives::block::BlockNumber; +use katana_primitives::contract::{StorageKey, StorageValue}; +use katana_primitives::hash::Pedersen; +use katana_primitives::{ContractAddress, Felt}; + +use crate::id::CommitId; + +#[derive(Debug)] +pub struct StoragesTrie { + /// The contract address the storage trie belongs to. + address: ContractAddress, + trie: crate::BonsaiTrie, +} + +impl StoragesTrie { + pub fn new(db: DB, address: ContractAddress) -> Self { + Self { address, trie: crate::BonsaiTrie::new(db) } + } + + pub fn root(&self) -> Felt { + self.trie.root(&self.address.to_bytes_be()) + } + + pub fn multiproof(&mut self, storage_keys: Vec) -> MultiProof { + self.trie.multiproof(&self.address.to_bytes_be(), storage_keys) + } +} + +impl StoragesTrie +where + DB: BonsaiDatabase + BonsaiPersistentDatabase, +{ + pub fn insert(&mut self, storage_key: StorageKey, storage_value: StorageValue) { + self.trie.insert(&self.address.to_bytes_be(), storage_key, storage_value) + } + + pub fn commit(&mut self, block: BlockNumber) { + self.trie.commit(block.into()) + } +} diff --git a/crates/saya/core/src/blockchain/mod.rs b/crates/saya/core/src/blockchain/mod.rs index a31be1f2b2..206f8c181c 100644 --- a/crates/saya/core/src/blockchain/mod.rs +++ b/crates/saya/core/src/blockchain/mod.rs @@ -1,14 +1,12 @@ //! Blockchain fetched from Katana. use katana_primitives::block::{BlockHashOrNumber, BlockIdOrTag, BlockTag, SealedBlockWithStatus}; -use katana_primitives::state::StateUpdatesWithDeclaredClasses; +use katana_primitives::state::StateUpdatesWithClasses; use katana_provider::providers::db::DbProvider; use katana_provider::traits::block::{BlockProvider, BlockWriter}; use katana_provider::traits::contract::ContractClassWriter; use katana_provider::traits::env::BlockEnvProvider; -use katana_provider::traits::state::{ - StateFactoryProvider, StateProvider, StateRootProvider, StateWriter, -}; +use katana_provider::traits::state::{StateFactoryProvider, StateProvider, StateWriter}; use katana_provider::traits::state_update::StateUpdateProvider; use katana_provider::traits::transaction::{ ReceiptProvider, TransactionProvider, TransactionStatusProvider, TransactionsProviderExt, @@ -25,7 +23,6 @@ pub trait Database: + TransactionsProviderExt + ReceiptProvider + StateUpdateProvider - + StateRootProvider + StateWriter + ContractClassWriter + StateFactoryProvider @@ -44,7 +41,6 @@ impl Database for T where + TransactionsProviderExt + ReceiptProvider + StateUpdateProvider - + StateRootProvider + StateWriter + ContractClassWriter + StateFactoryProvider @@ -109,7 +105,7 @@ impl Blockchain { } /// Updates the [`Blockchain`] internal state adding the given [`SealedBlockWithStatus`] - /// and the associated [`StateUpdatesWithDeclaredClasses`]. + /// and the associated [`StateUpdatesWithClasses`]. /// /// Currently receipts are ignored. /// @@ -120,7 +116,7 @@ impl Blockchain { pub fn update_state_with_block( &mut self, block: SealedBlockWithStatus, - states: StateUpdatesWithDeclaredClasses, + states: StateUpdatesWithClasses, ) -> SayaResult<()> { let provider = self.provider(); // Receipts are not supported currently. We may need them if some diff --git a/crates/saya/core/src/lib.rs b/crates/saya/core/src/lib.rs index 2efb1ea88a..bcde649d9b 100644 --- a/crates/saya/core/src/lib.rs +++ b/crates/saya/core/src/lib.rs @@ -13,7 +13,7 @@ use dojo_os::piltover::{starknet_apply_piltover, PiltoverCalldata}; use futures::future; use itertools::Itertools; use katana_primitives::block::{BlockNumber, FinalityStatus, SealedBlock, SealedBlockWithStatus}; -use katana_primitives::state::StateUpdatesWithDeclaredClasses; +use katana_primitives::state::StateUpdatesWithClasses; use katana_primitives::transaction::Tx; use katana_rpc_types::trace::TxExecutionInfo; use prover::persistent::{PublishedStateDiff, StarknetOsOutput}; @@ -100,7 +100,7 @@ struct FetchedBlockInfo { block_number: BlockNumber, block: SealedBlock, prev_state_root: Felt, - state_updates: StateUpdatesWithDeclaredClasses, + state_updates: StateUpdatesWithClasses, exec_infos: Vec, } diff --git a/crates/saya/provider/src/provider.rs b/crates/saya/provider/src/provider.rs index c0dafece58..3cfc8eb78c 100644 --- a/crates/saya/provider/src/provider.rs +++ b/crates/saya/provider/src/provider.rs @@ -1,5 +1,5 @@ use katana_primitives::block::{BlockNumber, SealedBlock}; -use katana_primitives::state::StateUpdatesWithDeclaredClasses; +use katana_primitives::state::StateUpdatesWithClasses; use katana_rpc_types::trace::TxExecutionInfo; use starknet::core::types::Felt; @@ -19,7 +19,7 @@ pub trait Provider { async fn fetch_block(&self, block_number: BlockNumber) -> ProviderResult; /// Fetches the state updates related to a given block. - /// Returns the [`StateUpdatesWithDeclaredClasses`] and the serialiazed + /// Returns the [`StateUpdatesWithClasses`] and the serialiazed /// state update for data availability layer. /// /// # Arguments @@ -28,7 +28,7 @@ pub trait Provider { async fn fetch_state_updates( &self, block_number: BlockNumber, - ) -> ProviderResult<(StateUpdatesWithDeclaredClasses, Vec)>; + ) -> ProviderResult<(StateUpdatesWithClasses, Vec)>; /// Fetches the transactions executions info for a given block. /// This method returns the all the executions info for each diff --git a/crates/saya/provider/src/rpc/mod.rs b/crates/saya/provider/src/rpc/mod.rs index 319bf2df67..9f0914047c 100644 --- a/crates/saya/provider/src/rpc/mod.rs +++ b/crates/saya/provider/src/rpc/mod.rs @@ -7,16 +7,19 @@ use anyhow::anyhow; use jsonrpsee::http_client::HttpClientBuilder; use katana_primitives::block::{BlockIdOrTag, BlockNumber, GasPrices, Header, SealedBlock}; use katana_primitives::chain::ChainId; +use katana_primitives::class::ContractClass; use katana_primitives::conversion::rpc as rpc_converter; use katana_primitives::da::L1DataAvailabilityMode; -use katana_primitives::state::StateUpdatesWithDeclaredClasses; +use katana_primitives::state::StateUpdatesWithClasses; use katana_primitives::transaction::TxWithHash; use katana_primitives::version::ProtocolVersion; use katana_rpc_api::saya::SayaApiClient; +use katana_rpc_types::class::{RpcContractClass, RpcSierraContractClass}; use katana_rpc_types::trace::TxExecutionInfo; use num_traits::ToPrimitive; use starknet::core::types::{ - ContractClass, Felt, MaybePendingBlockWithTxs, MaybePendingStateUpdate, + ContractClass as StarknetRsContractClass, Felt, MaybePendingBlockWithTxs, + MaybePendingStateUpdate, }; use starknet::providers::jsonrpc::HttpTransport; use starknet::providers::{JsonRpcClient, Provider as StarknetProvider}; @@ -126,7 +129,7 @@ impl Provider for JsonRpcProvider { async fn fetch_state_updates( &self, block_number: BlockNumber, - ) -> ProviderResult<(StateUpdatesWithDeclaredClasses, Vec)> { + ) -> ProviderResult<(StateUpdatesWithClasses, Vec)> { let rpc_state_update = match self .starknet_provider .get_state_update(BlockIdOrTag::Number(block_number)) @@ -143,7 +146,7 @@ impl Provider for JsonRpcProvider { let state_updates = state_converter::state_updates_from_rpc(&rpc_state_update)?; let mut state_updates_with_classes = - StateUpdatesWithDeclaredClasses { state_updates, ..Default::default() }; + StateUpdatesWithClasses { state_updates, ..Default::default() }; for class_hash in state_updates_with_classes.state_updates.declared_classes.keys() { match self @@ -151,18 +154,19 @@ impl Provider for JsonRpcProvider { .get_class(BlockIdOrTag::Number(block_number), class_hash) .await? { - ContractClass::Legacy(legacy) => { + StarknetRsContractClass::Legacy(legacy) => { trace!(target: LOG_TARGET, version = "cairo 0", %class_hash, "Set contract class."); - let (hash, class) = rpc_converter::legacy_rpc_to_compiled_class(&legacy)?; - state_updates_with_classes.declared_compiled_classes.insert(hash, class); + let (.., class) = rpc_converter::legacy_rpc_to_class(&legacy)?; + state_updates_with_classes.classes.insert(*class_hash, class); } - ContractClass::Sierra(s) => { + StarknetRsContractClass::Sierra(s) => { trace!(target: LOG_TARGET, version = "cairo 1", %class_hash, "Set contract class."); - state_updates_with_classes - .declared_sierra_classes - .insert(*class_hash, s.clone()); + let class = RpcSierraContractClass::try_from(s).unwrap(); + let class = ContractClass::try_from(RpcContractClass::Class(class)).unwrap(); + + state_updates_with_classes.classes.insert(*class_hash, class); } } } diff --git a/crates/sozo/ops/Cargo.toml b/crates/sozo/ops/Cargo.toml index 54a6982f8f..66d1969b2d 100644 --- a/crates/sozo/ops/Cargo.toml +++ b/crates/sozo/ops/Cargo.toml @@ -13,7 +13,7 @@ colored.workspace = true colored_json.workspace = true dojo-types.workspace = true dojo-utils.workspace = true -dojo-world.workspace = true +dojo-world = { workspace = true, features = [ "ipfs" ] } futures.workspace = true num-traits.workspace = true serde.workspace = true @@ -33,7 +33,6 @@ katana-runner = { workspace = true, optional = true } [dev-dependencies] assert_fs.workspace = true dojo-test-utils = { workspace = true, features = [ "build-examples" ] } -ipfs-api-backend-hyper.workspace = true katana-runner.workspace = true scarb.workspace = true sozo-scarbext.workspace = true diff --git a/crates/sozo/ops/src/migrate/mod.rs b/crates/sozo/ops/src/migrate/mod.rs index a5b1ddb52b..c276372945 100644 --- a/crates/sozo/ops/src/migrate/mod.rs +++ b/crates/sozo/ops/src/migrate/mod.rs @@ -20,14 +20,19 @@ use std::collections::HashMap; +use anyhow::anyhow; use cainome::cairo_serde::{ByteArray, ClassHash, ContractAddress}; use dojo_utils::{Declarer, Deployer, Invoker, LabeledClass, TransactionResult, TxnConfig}; use dojo_world::config::calldata_decoder::decode_calldata; -use dojo_world::config::ProfileConfig; +use dojo_world::config::{metadata_config, ProfileConfig, ResourceConfig, WorldMetadata}; +use dojo_world::constants::WORLD; +use dojo_world::contracts::abigen::world::ResourceMetadata; use dojo_world::contracts::WorldContract; use dojo_world::diff::{Manifest, ResourceDiff, WorldDiff, WorldStatus}; use dojo_world::local::ResourceLocal; +use dojo_world::metadata::MetadataStorage; use dojo_world::remote::ResourceRemote; +use dojo_world::services::UploadService; use dojo_world::{utils, ResourceType}; use starknet::accounts::{ConnectedAccount, SingleOwnerAccount}; use starknet::core::types::Call; @@ -103,6 +108,104 @@ where }) } + /// Upload resources metadata to IPFS and update the ResourceMetadata Dojo model. + /// + /// # Arguments + /// + /// # Returns + pub async fn upload_metadata( + &self, + ui: &mut MigrationUi, + service: &mut impl UploadService, + ) -> anyhow::Result<()> { + ui.update_text("Uploading metadata..."); + + let mut invoker = Invoker::new(&self.world.account, self.txn_config); + + // world + let current_hash = self.diff.world_info.metadata_hash; + let new_metadata = WorldMetadata::from(self.diff.profile_config.world.clone()); + + let res = new_metadata.upload_if_changed(service, current_hash).await?; + + if let Some((new_uri, new_hash)) = res { + trace!(new_uri, new_hash = format!("{:#066x}", new_hash), "World metadata updated."); + + invoker.add_call(self.world.set_metadata_getcall(&ResourceMetadata { + resource_id: WORLD, + metadata_uri: ByteArray::from_string(&new_uri)?, + metadata_hash: new_hash, + })); + } + + // contracts + if let Some(configs) = &self.diff.profile_config.contracts { + let calls = self.upload_metadata_from_resource_config(service, configs).await?; + invoker.extend_calls(calls); + } + + // models + if let Some(configs) = &self.diff.profile_config.models { + let calls = self.upload_metadata_from_resource_config(service, configs).await?; + invoker.extend_calls(calls); + } + + // events + if let Some(configs) = &self.diff.profile_config.events { + let calls = self.upload_metadata_from_resource_config(service, configs).await?; + invoker.extend_calls(calls); + } + + if self.do_multicall() { + ui.update_text_boxed(format!("Uploading {} metadata...", invoker.calls.len())); + invoker.multicall().await.map_err(|e| anyhow!(e.to_string()))?; + } else { + ui.update_text_boxed(format!( + "Uploading {} metadata (sequentially)...", + invoker.calls.len() + )); + invoker.invoke_all_sequentially().await.map_err(|e| anyhow!(e.to_string()))?; + } + + Ok(()) + } + + async fn upload_metadata_from_resource_config( + &self, + service: &mut impl UploadService, + config: &[ResourceConfig], + ) -> anyhow::Result> { + let mut calls = vec![]; + + for item in config { + let selector = dojo_types::naming::compute_selector_from_tag_or_name(&item.tag); + + let current_hash = + self.diff.resources.get(&selector).map_or(Felt::ZERO, |r| r.metadata_hash()); + + let new_metadata = metadata_config::ResourceMetadata::from(item.clone()); + + let res = new_metadata.upload_if_changed(service, current_hash).await?; + + if let Some((new_uri, new_hash)) = res { + trace!( + tag = item.tag, + new_uri, + new_hash = format!("{:#066x}", new_hash), + "Resource metadata updated." + ); + + calls.push(self.world.set_metadata_getcall(&ResourceMetadata { + resource_id: selector, + metadata_uri: ByteArray::from_string(&new_uri)?, + metadata_hash: new_hash, + })); + } + } + + Ok(calls) + } + /// Returns whether multicall should be used. By default, it is enabled. fn do_multicall(&self) -> bool { self.profile_config @@ -147,6 +250,7 @@ where // TODO: maybe we want a resource diff with a new variant. Where the migration // is skipped, but we still have the local resource. if self.profile_config.is_skipped(&tag) { + trace!(tag = resource.tag(), "Contract init skipping resource."); continue; } @@ -241,6 +345,7 @@ where // Only takes the local permissions that are not already set onchain to apply them. for (selector, resource) in &self.diff.resources { if self.profile_config.is_skipped(&resource.tag()) { + trace!(tag = resource.tag(), "Sync permissions skipping resource."); continue; } @@ -308,6 +413,7 @@ where // Collects the calls and classes to be declared to sync the resources. for resource in self.diff.resources.values() { if self.profile_config.is_skipped(&resource.tag()) { + trace!(tag = resource.tag(), "Sync skipping resource."); continue; } @@ -665,7 +771,7 @@ where Declarer::declare(labeled_class, &self.world.account, &self.txn_config).await?; - // We want to wait for the receipt to be be able to print the + // We want to wait for the receipt to be able to print the // world block number. let mut txn_config = self.txn_config; txn_config.wait = true; diff --git a/crates/sozo/ops/src/migration_ui.rs b/crates/sozo/ops/src/migration_ui.rs index 1dc629f444..df2e31cd94 100644 --- a/crates/sozo/ops/src/migration_ui.rs +++ b/crates/sozo/ops/src/migration_ui.rs @@ -22,7 +22,7 @@ impl MigrationUi { /// Returns a new instance with the default frames. pub fn new(text: Option<&'static str>) -> Self { if let Some(text) = text { - let frames = spinner!(["⛩️ ", "🥷", "🗡️ "], 500); + let frames = spinner!(["⛩️ ", "🥷 ", "🗡️ "], 500); let spinner = Spinner::new(frames.clone(), text, None); Self { spinner, default_frames: frames, silent: false } } else { diff --git a/crates/sozo/ops/src/tests/migration.rs b/crates/sozo/ops/src/tests/migration.rs index 7cb45dbf0f..99e26736af 100644 --- a/crates/sozo/ops/src/tests/migration.rs +++ b/crates/sozo/ops/src/tests/migration.rs @@ -5,13 +5,16 @@ use anyhow::Result; use dojo_test_utils::compiler::CompilerTestSetup; use dojo_test_utils::migration::copy_spawn_and_move_db; use dojo_utils::TxnConfig; +use dojo_world::config::ResourceConfig; use dojo_world::contracts::WorldContract; use dojo_world::diff::WorldDiff; +use dojo_world::services::MockUploadService; use katana_runner::RunnerCtx; use scarb::compiler::Profile; use sozo_scarbext::WorkspaceExt; use starknet::providers::jsonrpc::HttpTransport; use starknet::providers::JsonRpcClient; +use starknet_crypto::Felt; use crate::migrate::{Migration, MigrationResult}; use crate::migration_ui::MigrationUi; @@ -37,7 +40,7 @@ async fn setup_migration( } /// Migrates the spawn-and-move project from the local environment. -async fn migrate_spawn_and_move(sequencer: &RunnerCtx) -> MigrationResult { +async fn migrate_spawn_and_move(sequencer: &RunnerCtx, with_metadata: bool) -> MigrationResult { let account = sequencer.account(0); let provider = Arc::new(JsonRpcClient::new(HttpTransport::new(sequencer.url()))); @@ -58,13 +61,20 @@ async fn migrate_spawn_and_move(sequencer: &RunnerCtx) -> MigrationResult { let mut ui = MigrationUi::new(None).with_silent(); - migration.migrate(&mut ui).await.expect("Migration spawn-and-move failed.") + let res = migration.migrate(&mut ui).await.expect("Migration spawn-and-move failed."); + + if with_metadata { + let mut service = MockUploadService::default(); + migration.upload_metadata(&mut ui, &mut service).await.expect("Upload metadata failed"); + } + + res } #[tokio::test(flavor = "multi_thread")] #[katana_runner::test(accounts = 10)] async fn migrate_from_local(sequencer: &RunnerCtx) { - let MigrationResult { manifest, has_changes } = migrate_spawn_and_move(sequencer).await; + let MigrationResult { manifest, has_changes } = migrate_spawn_and_move(sequencer, false).await; assert!(has_changes); assert_eq!(manifest.contracts.len(), 4); @@ -73,7 +83,70 @@ async fn migrate_from_local(sequencer: &RunnerCtx) { #[tokio::test(flavor = "multi_thread")] #[katana_runner::test(accounts = 10, db_dir = copy_spawn_and_move_db().as_str())] async fn migrate_no_change(sequencer: &RunnerCtx) { - let MigrationResult { manifest, has_changes } = migrate_spawn_and_move(sequencer).await; + let MigrationResult { manifest, has_changes } = migrate_spawn_and_move(sequencer, false).await; assert!(!has_changes); assert_eq!(manifest.contracts.len(), 4); } + +// helper to check metadata of a list of resources +fn check_resources( + diff: &WorldDiff, + resources: Option>, + expected_count: usize, + checker: &dyn Fn(Felt) -> bool, +) { + assert!(resources.is_some()); + let resources = resources.unwrap(); + + assert_eq!(resources.len(), expected_count); + + for resource in resources { + let selector = dojo_types::naming::compute_selector_from_tag_or_name(&resource.tag); + + let resource = diff.resources.get(&selector); + assert!(resource.is_some()); + + let resource = resource.unwrap(); + + assert!(checker(resource.metadata_hash()), "Bad resource hash: {}", resource.name()); + } +} + +#[tokio::test(flavor = "multi_thread")] +#[katana_runner::test(accounts = 10, db_dir = copy_spawn_and_move_db().as_str())] +async fn upload_metadata(sequencer: &RunnerCtx) { + let is_set = |hash| hash != Felt::ZERO; + let is_not_set = |hash: Felt| hash == Felt::ZERO; + + let provider = Arc::new(JsonRpcClient::new(HttpTransport::new(sequencer.url()))); + + // here, metadata should not be set + let world_diff = setup_migration("spawn-and-move", Profile::DEV, provider.clone()) + .await + .expect("Failed to setup migration"); + let profile_config = world_diff.profile_config.clone(); + + assert!(is_not_set(world_diff.world_info.metadata_hash)); + check_resources(&world_diff, profile_config.contracts, 1, &is_not_set); + check_resources(&world_diff, profile_config.models, 3, &is_not_set); + check_resources(&world_diff, profile_config.events, 1, &is_not_set); + + // no change is expected for the migration itself but metadata + // should be uploaded. + let _ = migrate_spawn_and_move(sequencer, true).await; + + // Note that IPFS upload is deeply tested in dojo-world metadata tests. + // Here we just check that, after migration, resources associated to + // metadata configured in dojo_dev.toml have been successfully updated + // in the `ResourceMetadata` model of the world. + let world_diff = setup_migration("spawn-and-move", Profile::DEV, provider) + .await + .expect("Failed to setup migration"); + let profile_config = world_diff.profile_config.clone(); + + // check world and resources metadata from computed WorldDiff + assert!(is_set(world_diff.world_info.metadata_hash)); + check_resources(&world_diff, profile_config.contracts, 1, &is_set); + check_resources(&world_diff, profile_config.models, 3, &is_set); + check_resources(&world_diff, profile_config.events, 1, &is_set); +} diff --git a/crates/sozo/scarbext/src/workspace.rs b/crates/sozo/scarbext/src/workspace.rs index 224eb410fc..c18582a93c 100644 --- a/crates/sozo/scarbext/src/workspace.rs +++ b/crates/sozo/scarbext/src/workspace.rs @@ -146,7 +146,7 @@ impl WorkspaceExt for Workspace<'_> { let manifest_dir = self.manifest_path().parent().unwrap(); let manifest_dir = Filesystem::new(manifest_dir.into()); - if !manifest_dir.exists() { + if !manifest_dir.child(manifest_name.clone()).exists() { return Ok(None); } diff --git a/crates/sozo/walnut/Cargo.toml b/crates/sozo/walnut/Cargo.toml index 0a9cf690eb..04ad812f0a 100644 --- a/crates/sozo/walnut/Cargo.toml +++ b/crates/sozo/walnut/Cargo.toml @@ -8,18 +8,18 @@ version.workspace = true [dependencies] anyhow.workspace = true console.workspace = true -dojo-world.workspace = true reqwest.workspace = true scarb.workspace = true scarb-ui.workspace = true serde.workspace = true serde_json.workspace = true -sozo-scarbext.workspace = true starknet.workspace = true thiserror.workspace = true url.workspace = true urlencoding = "2.1.3" walkdir.workspace = true +dojo-utils.workspace = true +clap.workspace = true [dev-dependencies] starknet.workspace = true diff --git a/crates/sozo/walnut/src/debugger.rs b/crates/sozo/walnut/src/debugger.rs index 0edd38b80c..97d3ba546e 100644 --- a/crates/sozo/walnut/src/debugger.rs +++ b/crates/sozo/walnut/src/debugger.rs @@ -1,12 +1,11 @@ -use dojo_world::diff::WorldDiff; +use dojo_utils::TransactionResult; use scarb::core::Workspace; use scarb_ui::Ui; -use starknet::core::types::Felt; use url::Url; use crate::transaction::walnut_debug_transaction; -use crate::verification::walnut_verify_migration_strategy; -use crate::{utils, Error}; +use crate::verification::walnut_verify; +use crate::Error; /// A debugger for Starknet transactions embedding the walnut configuration. #[derive(Debug)] @@ -26,7 +25,18 @@ impl WalnutDebugger { } /// Debugs a transaction with Walnut by printing a link to the Walnut debugger page. - pub fn debug_transaction(&self, ui: &Ui, transaction_hash: &Felt) -> Result<(), Error> { + pub fn debug_transaction( + &self, + ui: &Ui, + transaction_result: &TransactionResult, + ) -> Result<(), Error> { + let transaction_hash = match transaction_result { + TransactionResult::Hash(transaction_hash) => transaction_hash, + TransactionResult::Noop => { + return Ok(()); + } + TransactionResult::HashReceipt(transaction_hash, _) => transaction_hash, + }; let url = walnut_debug_transaction(&self.rpc_url, transaction_hash)?; ui.print(format!("Debug transaction with Walnut: {url}")); Ok(()) @@ -34,17 +44,7 @@ impl WalnutDebugger { /// Verifies a migration strategy with Walnut by uploading the source code of the contracts and /// models in the strategy. - pub async fn verify_migration_strategy( - &self, - ws: &Workspace<'_>, - world_diff: &WorldDiff, - ) -> anyhow::Result<()> { - walnut_verify_migration_strategy(ws, self.rpc_url.to_string(), world_diff).await - } - - /// Checks if the Walnut API key is set. - pub fn check_api_key() -> Result<(), Error> { - let _ = utils::walnut_get_api_key()?; - Ok(()) + pub async fn verify(ws: &Workspace<'_>) -> anyhow::Result<()> { + walnut_verify(ws).await } } diff --git a/crates/sozo/walnut/src/lib.rs b/crates/sozo/walnut/src/lib.rs index 96feb2be92..08ec8d767a 100644 --- a/crates/sozo/walnut/src/lib.rs +++ b/crates/sozo/walnut/src/lib.rs @@ -25,6 +25,7 @@ mod debugger; mod transaction; mod utils; mod verification; +pub mod walnut; pub use debugger::WalnutDebugger; diff --git a/crates/sozo/walnut/src/utils.rs b/crates/sozo/walnut/src/utils.rs index ca861249c1..89da582579 100644 --- a/crates/sozo/walnut/src/utils.rs +++ b/crates/sozo/walnut/src/utils.rs @@ -1,10 +1,6 @@ use std::env; -use crate::{Error, WALNUT_API_KEY_ENV_VAR, WALNUT_API_URL, WALNUT_API_URL_ENV_VAR}; - -pub fn walnut_get_api_key() -> Result { - env::var(WALNUT_API_KEY_ENV_VAR).map_err(|_| Error::MissingApiKey) -} +use crate::{WALNUT_API_URL, WALNUT_API_URL_ENV_VAR}; pub fn walnut_get_api_url() -> String { env::var(WALNUT_API_URL_ENV_VAR).unwrap_or_else(|_| WALNUT_API_URL.to_string()) diff --git a/crates/sozo/walnut/src/verification.rs b/crates/sozo/walnut/src/verification.rs index ca11be74ad..ab885ef950 100644 --- a/crates/sozo/walnut/src/verification.rs +++ b/crates/sozo/walnut/src/verification.rs @@ -3,67 +3,31 @@ use std::io; use std::path::Path; use console::{pad_str, Alignment, Style, StyledObject}; -use dojo_world::diff::{ResourceDiff, WorldDiff}; -use dojo_world::local::ResourceLocal; -use dojo_world::remote::ResourceRemote; -use dojo_world::ResourceType; use reqwest::StatusCode; use scarb::core::Workspace; use serde::Serialize; use serde_json::Value; -use sozo_scarbext::WorkspaceExt; use walkdir::WalkDir; -use crate::utils::{walnut_get_api_key, walnut_get_api_url}; +use crate::utils::walnut_get_api_url; use crate::Error; -/// Verifies all classes declared during migration. -/// Only supported on hosted networks (non-localhost). -/// -/// This function verifies all contracts and models in the strategy. For every contract and model, -/// it sends a request to the Walnut backend with the class name, class hash, RPC URL, and source -/// code. Walnut will then build the project with Sozo, compare the Sierra bytecode with the -/// bytecode on the network, and if they are equal, it will store the source code and associate it -/// with the class hash. -pub async fn walnut_verify_migration_strategy( - ws: &Workspace<'_>, - rpc_url: String, - world_diff: &WorldDiff, -) -> anyhow::Result<()> { - let ui = ws.config().ui(); - // Check if rpc_url is localhost - if rpc_url.contains("localhost") || rpc_url.contains("127.0.0.1") { - ui.print(" "); - ui.warn("Verifying classes with Walnut is only supported on hosted networks."); - ui.print(" "); - return Ok(()); - } - - // Check if there are any contracts or models in the strategy - if world_diff.is_synced() { - ui.print(" "); - ui.print("🌰 No contracts or models to verify."); - ui.print(" "); - return Ok(()); - } +#[derive(Debug, Serialize)] +struct VerificationPayload { + /// JSON that contains a map where the key is the path to the file and the value is the content + /// of the file. It should contain all files required to build the Dojo project with Sozo. + pub source_code: Value, - let _profile_config = ws.load_profile_config()?; + pub cairo_version: String, +} - for (_selector, resource) in world_diff.resources.iter() { - if resource.resource_type() == ResourceType::Contract { - match resource { - ResourceDiff::Created(ResourceLocal::Contract(_contract)) => { - // Need to verify created. - } - ResourceDiff::Updated(_, ResourceRemote::Contract(_contract)) => { - // Need to verify updated. - } - _ => { - // Synced, we don't need to verify. - } - } - } - } +/// Verifies all classes in the workspace. +/// +/// This function verifies all contracts and models in the workspace. It sends a single request to +/// the Walnut backend with the source code. Walnut will then build the project and store +/// the source code associated with the class hashes. +pub async fn walnut_verify(ws: &Workspace<'_>) -> anyhow::Result<()> { + let ui = ws.config().ui(); // Notify start of verification ui.print(" "); @@ -71,46 +35,29 @@ pub async fn walnut_verify_migration_strategy( ui.print(" "); // Retrieve the API key and URL from environment variables - let _api_key = walnut_get_api_key()?; - let _api_url = walnut_get_api_url(); + let api_url = walnut_get_api_url(); - // Collect source code - // TODO: now it's the same output as scarb, need to update the dojo fork to output the source - // code, or does scarb supports it already? + // its path to a file so `parent` should never return `None` + let root_dir: &Path = ws.manifest_path().parent().unwrap().as_std_path(); - Ok(()) -} + let source_code = collect_source_code(root_dir)?; + let cairo_version = scarb::version::get().version; -fn _get_class_name_from_artifact_path(path: &Path, namespace: &str) -> Result { - let file_name = path.file_stem().and_then(OsStr::to_str).ok_or(Error::InvalidFileName)?; - let class_name = file_name.strip_prefix(namespace).ok_or(Error::NamespacePrefixNotFound)?; - Ok(class_name.to_string()) -} + let verification_payload = + VerificationPayload { source_code, cairo_version: cairo_version.to_string() }; -#[derive(Debug, Serialize)] -struct _VerificationPayload { - /// The names of the classes we want to verify together with the selector. - pub class_names: Vec, - /// The hashes of the Sierra classes. - pub class_hashes: Vec, - /// The RPC URL of the network where these classes are declared (can only be a hosted network). - pub rpc_url: String, - /// JSON that contains a map where the key is the path to the file and the value is the content - /// of the file. It should contain all files required to build the Dojo project with Sozo. - pub source_code: Value, + // Send verification request + match verify_classes(verification_payload, &api_url).await { + Ok(message) => ui.print(_subtitle(message)), + Err(e) => ui.print(_subtitle(e.to_string())), + } + + Ok(()) } -async fn _verify_classes( - payload: _VerificationPayload, - api_url: &str, - api_key: &str, -) -> Result { - let res = reqwest::Client::new() - .post(format!("{api_url}/v1/verify")) - .header("x-api-key", api_key) - .json(&payload) - .send() - .await?; +async fn verify_classes(payload: VerificationPayload, api_url: &str) -> Result { + let res = + reqwest::Client::new().post(format!("{api_url}/v1/verify")).json(&payload).send().await?; if res.status() == StatusCode::OK { Ok(res.text().await?) @@ -119,7 +66,7 @@ async fn _verify_classes( } } -fn _collect_source_code(root_dir: &Path) -> Result { +fn collect_source_code(root_dir: &Path) -> Result { fn collect_files( root_dir: &Path, search_dir: &Path, diff --git a/crates/sozo/walnut/src/walnut.rs b/crates/sozo/walnut/src/walnut.rs new file mode 100644 index 0000000000..ced939d281 --- /dev/null +++ b/crates/sozo/walnut/src/walnut.rs @@ -0,0 +1,36 @@ +use anyhow::Result; +use clap::{Args, Subcommand}; +use scarb::core::Config; + +use crate::WalnutDebugger; + +#[derive(Debug, Args)] +pub struct WalnutArgs { + #[command(subcommand)] + pub command: WalnutVerifyCommand, +} + +#[derive(Debug, Subcommand)] +pub enum WalnutVerifyCommand { + #[command( + about = "Verify contracts in walnut.dev - essential for debugging source code in Walnut" + )] + Verify(WalnutVerifyOptions), +} + +#[derive(Debug, Args)] +pub struct WalnutVerifyOptions {} + +impl WalnutArgs { + pub fn run(self, config: &Config) -> Result<()> { + let ws = scarb::ops::read_workspace(config.manifest_path(), config)?; + config.tokio_handle().block_on(async { + match self.command { + WalnutVerifyCommand::Verify(_options) => { + WalnutDebugger::verify(&ws).await?; + } + } + Ok(()) + }) + } +} diff --git a/crates/torii/cli/Cargo.toml b/crates/torii/cli/Cargo.toml index e4ed7ab09e..e02f44c640 100644 --- a/crates/torii/cli/Cargo.toml +++ b/crates/torii/cli/Cargo.toml @@ -13,7 +13,7 @@ dojo-utils.workspace = true serde.workspace = true starknet.workspace = true toml.workspace = true -torii-core.workspace = true +torii-sqlite.workspace = true url.workspace = true [dev-dependencies] diff --git a/crates/torii/cli/src/args.rs b/crates/torii/cli/src/args.rs index 8b8a73175b..1d451cc0a4 100644 --- a/crates/torii/cli/src/args.rs +++ b/crates/torii/cli/src/args.rs @@ -35,11 +35,6 @@ pub struct ToriiArgs { )] pub db_dir: Option, - /// The external url of the server, used for configuring the GraphQL Playground in a hosted - /// environment - #[arg(long, value_parser = parse_url, help = "The external url of the server, used for configuring the GraphQL Playground in a hosted environment.")] - pub external_url: Option, - /// Open World Explorer on the browser. #[arg(long, help = "Open World Explorer on the browser.")] pub explorer: bool, @@ -97,10 +92,6 @@ impl ToriiArgs { self.db_dir = config.db_dir; } - if self.external_url.is_none() { - self.external_url = config.external_url; - } - // Currently the comparison it's only at the top level. // Need to make it more granular. @@ -164,7 +155,6 @@ impl TryFrom for ToriiArgsConfig { config.rpc = if args.rpc == Url::parse(DEFAULT_RPC_URL).unwrap() { None } else { Some(args.rpc) }; config.db_dir = args.db_dir; - config.external_url = args.external_url; config.explorer = Some(args.explorer); // Only include the following options if they are not the default. @@ -193,7 +183,7 @@ mod test { use std::net::{IpAddr, Ipv4Addr}; use std::str::FromStr; - use torii_core::types::{Contract, ContractType}; + use torii_sqlite::types::{Contract, ContractType}; use super::*; @@ -228,12 +218,9 @@ mod test { "http://0.0.0.0:6060", "--db-dir", "/tmp/torii-test2", - "--events.raw", - "false", "--events.historical", "a-A", "--indexing.transactions", - "true", "--config", path_str.as_str(), ]; diff --git a/crates/torii/cli/src/options.rs b/crates/torii/cli/src/options.rs index 48fc62915d..c89d3a2475 100644 --- a/crates/torii/cli/src/options.rs +++ b/crates/torii/cli/src/options.rs @@ -2,11 +2,10 @@ use std::net::{IpAddr, Ipv4Addr}; use std::str::FromStr; use anyhow::Context; -use clap::ArgAction; use serde::ser::SerializeSeq; use serde::{Deserialize, Serialize}; use starknet::core::types::Felt; -use torii_core::types::{Contract, ContractType}; +use torii_sqlite::types::{Contract, ContractType}; pub const DEFAULT_HTTP_ADDR: IpAddr = IpAddr::V4(Ipv4Addr::LOCALHOST); pub const DEFAULT_HTTP_PORT: u16 = 8080; @@ -102,7 +101,11 @@ pub struct IndexingOptions { pub blocks_chunk_size: u64, /// Enable indexing pending blocks - #[arg(long = "indexing.pending", action = ArgAction::Set, default_value_t = true, help = "Whether or not to index pending blocks.")] + #[arg( + long = "indexing.pending", + default_value_t = true, + help = "Whether or not to index pending blocks." + )] #[serde(default)] pub pending: bool, @@ -127,7 +130,6 @@ pub struct IndexingOptions { /// Whether or not to index world transactions #[arg( long = "indexing.transactions", - action = ArgAction::Set, default_value_t = false, help = "Whether or not to index world transactions and keep them in the database." )] @@ -155,6 +157,19 @@ pub struct IndexingOptions { )] #[serde(default)] pub namespaces: Vec, + + /// The block number to start indexing the world from. + /// + /// Warning: In the current implementation, this will break the indexing of tokens, if any. + /// Since the tokens require the chain to be indexed from the beginning, to ensure correct + /// balance updates. + #[arg( + long = "indexing.world_block", + help = "The block number to start indexing from.", + default_value_t = 0 + )] + #[serde(default)] + pub world_block: u64, } impl Default for IndexingOptions { @@ -168,6 +183,7 @@ impl Default for IndexingOptions { polling_interval: DEFAULT_POLLING_INTERVAL, max_concurrent_tasks: DEFAULT_MAX_CONCURRENT_TASKS, namespaces: vec![], + world_block: 0, } } } @@ -206,6 +222,10 @@ impl IndexingOptions { if self.namespaces.is_empty() { self.namespaces = other.namespaces.clone(); } + + if self.world_block == 0 { + self.world_block = other.world_block; + } } } } @@ -214,7 +234,11 @@ impl IndexingOptions { #[command(next_help_heading = "Events indexing options")] pub struct EventsOptions { /// Whether or not to index raw events - #[arg(long = "events.raw", action = ArgAction::Set, default_value_t = false, help = "Whether or not to index raw events.")] + #[arg( + long = "events.raw", + default_value_t = false, + help = "Whether or not to index raw events." + )] #[serde(default)] pub raw: bool, diff --git a/crates/torii/client/Cargo.toml b/crates/torii/client/Cargo.toml index d534123d07..fa5d6debf8 100644 --- a/crates/torii/client/Cargo.toml +++ b/crates/torii/client/Cargo.toml @@ -21,7 +21,7 @@ starknet-crypto.workspace = true thiserror.workspace = true tokio = { version = "1.32.0", features = [ "sync" ], default-features = false } torii-grpc = { workspace = true, features = [ "client" ] } -torii-relay = { workspace = true } +torii-relay = { workspace = true, features = [ "client" ] } url.workspace = true [target.'cfg(not(target_arch = "wasm32"))'.dependencies] diff --git a/crates/torii/client/src/client/error.rs b/crates/torii/client/src/client/error.rs index 5a036e19cd..734c068707 100644 --- a/crates/torii/client/src/client/error.rs +++ b/crates/torii/client/src/client/error.rs @@ -16,7 +16,7 @@ pub enum Error { #[error(transparent)] GrpcClient(#[from] torii_grpc::client::Error), #[error(transparent)] - RelayClient(#[from] torii_relay::errors::Error), + RelayClient(#[from] torii_relay::error::Error), #[error(transparent)] Model(#[from] ModelError), #[error("Unsupported query")] diff --git a/crates/torii/client/src/client/mod.rs b/crates/torii/client/src/client/mod.rs index 7fa96c17ab..e2fd1f58bd 100644 --- a/crates/torii/client/src/client/mod.rs +++ b/crates/torii/client/src/client/mod.rs @@ -10,7 +10,9 @@ use starknet::core::types::Felt; use starknet::providers::jsonrpc::HttpTransport; use starknet::providers::JsonRpcClient; use tokio::sync::RwLock as AsyncRwLock; -use torii_grpc::client::{EntityUpdateStreaming, EventUpdateStreaming, IndexerUpdateStreaming}; +use torii_grpc::client::{ + EntityUpdateStreaming, EventUpdateStreaming, IndexerUpdateStreaming, TokenBalanceStreaming, +}; use torii_grpc::proto::world::{ RetrieveEntitiesResponse, RetrieveEventsResponse, RetrieveTokenBalancesResponse, RetrieveTokensResponse, @@ -209,4 +211,37 @@ impl Client { .await?; Ok(stream) } + + /// Subscribes to token balances updates. + /// If no contract addresses are provided, it will subscribe to updates for all contract + /// addresses. If no account addresses are provided, it will subscribe to updates for all + /// account addresses. + pub async fn on_token_balance_updated( + &self, + contract_addresses: Vec, + account_addresses: Vec, + ) -> Result { + let mut grpc_client = self.inner.write().await; + let stream = + grpc_client.subscribe_token_balances(contract_addresses, account_addresses).await?; + Ok(stream) + } + + /// Update the token balances subscription + pub async fn update_token_balance_subscription( + &self, + subscription_id: u64, + contract_addresses: Vec, + account_addresses: Vec, + ) -> Result<(), Error> { + let mut grpc_client = self.inner.write().await; + grpc_client + .update_token_balances_subscription( + subscription_id, + contract_addresses, + account_addresses, + ) + .await?; + Ok(()) + } } diff --git a/crates/torii/core/src/lib.rs b/crates/torii/core/src/lib.rs deleted file mode 100644 index fbf9a1e14b..0000000000 --- a/crates/torii/core/src/lib.rs +++ /dev/null @@ -1,12 +0,0 @@ -#![warn(unused_crate_dependencies)] - -pub mod constants; -pub mod engine; -pub mod error; -pub mod executor; -pub mod model; -pub mod processors; -pub mod simple_broker; -pub mod sql; -pub mod types; -pub mod utils; diff --git a/crates/torii/core/src/model.rs b/crates/torii/core/src/model.rs deleted file mode 100644 index 2266993e9b..0000000000 --- a/crates/torii/core/src/model.rs +++ /dev/null @@ -1,1102 +0,0 @@ -use std::collections::HashMap; -use std::str::FromStr; - -use async_trait::async_trait; -use crypto_bigint::U256; -use dojo_types::naming::get_tag; -use dojo_types::primitive::Primitive; -use dojo_types::schema::{Enum, EnumOption, Member, Struct, Ty}; -use dojo_world::contracts::abigen::model::Layout; -use dojo_world::contracts::model::ModelReader; -use sqlx::sqlite::SqliteRow; -use sqlx::{Pool, Row, Sqlite}; -use starknet::core::types::Felt; - -use super::error::{self, Error}; -use crate::error::{ParseError, QueryError}; - -#[derive(Debug)] -pub struct ModelSQLReader { - /// Namespace of the model - namespace: String, - /// The name of the model - name: String, - /// The selector of the model - selector: Felt, - /// The class hash of the model - class_hash: Felt, - /// The contract address of the model - contract_address: Felt, - pool: Pool, - packed_size: u32, - unpacked_size: u32, - layout: Layout, -} - -impl ModelSQLReader { - pub async fn new(selector: Felt, pool: Pool) -> Result { - let (namespace, name, class_hash, contract_address, packed_size, unpacked_size, layout): ( - String, - String, - String, - String, - u32, - u32, - String, - ) = sqlx::query_as( - "SELECT namespace, name, class_hash, contract_address, packed_size, unpacked_size, \ - layout FROM models WHERE id = ?", - ) - .bind(format!("{:#x}", selector)) - .fetch_one(&pool) - .await?; - - let class_hash = Felt::from_hex(&class_hash).map_err(error::ParseError::FromStr)?; - let contract_address = - Felt::from_hex(&contract_address).map_err(error::ParseError::FromStr)?; - - let layout = serde_json::from_str(&layout).map_err(error::ParseError::FromJsonStr)?; - - Ok(Self { - namespace, - name, - selector, - class_hash, - contract_address, - pool, - packed_size, - unpacked_size, - layout, - }) - } -} - -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -impl ModelReader for ModelSQLReader { - fn namespace(&self) -> &str { - &self.namespace - } - - fn name(&self) -> &str { - &self.name - } - - fn selector(&self) -> Felt { - self.selector - } - - fn class_hash(&self) -> Felt { - self.class_hash - } - - fn contract_address(&self) -> Felt { - self.contract_address - } - - async fn schema(&self) -> Result { - let model_members: Vec = sqlx::query_as( - "SELECT id, model_idx, member_idx, name, type, type_enum, enum_options, key FROM \ - model_members WHERE model_id = ? ORDER BY model_idx ASC, member_idx ASC", - ) - .bind(format!("{:#x}", self.selector)) - .fetch_all(&self.pool) - .await?; - - Ok(parse_sql_model_members(&self.namespace, &self.name, &model_members)) - } - - async fn packed_size(&self) -> Result { - Ok(self.packed_size) - } - - async fn unpacked_size(&self) -> Result { - Ok(self.unpacked_size) - } - - async fn layout(&self) -> Result { - Ok(self.layout.clone()) - } -} - -#[allow(unused)] -#[derive(Debug, sqlx::FromRow)] -pub struct SqlModelMember { - id: String, - model_idx: u32, - member_idx: u32, - name: String, - r#type: String, - type_enum: String, - enum_options: Option, - key: bool, -} - -// assume that the model members are sorted by model_idx and member_idx -// `id` is the type id of the model member -/// A helper function to parse the model members from sql table to `Ty` -pub fn parse_sql_model_members( - namespace: &str, - model: &str, - model_members_all: &[SqlModelMember], -) -> Ty { - fn parse_sql_member(member: &SqlModelMember, model_members_all: &[SqlModelMember]) -> Ty { - match member.type_enum.as_str() { - "Primitive" => Ty::Primitive(member.r#type.parse().unwrap()), - "ByteArray" => Ty::ByteArray("".to_string()), - "Struct" => { - let children = model_members_all - .iter() - .filter(|m| m.id == format!("{}${}", member.id, member.name)) - .map(|child| Member { - key: child.key, - name: child.name.to_owned(), - ty: parse_sql_member(child, model_members_all), - }) - .collect::>(); - - Ty::Struct(Struct { name: member.r#type.clone(), children }) - } - "Enum" => { - let options = member - .enum_options - .as_ref() - .expect("qed; enum_options should exist") - .split(',') - .map(|s| { - let member = if let Some(member) = model_members_all.iter().find(|m| { - m.id == format!("{}${}", member.id, member.name) && m.name == s - }) { - parse_sql_member(member, model_members_all) - } else { - Ty::Tuple(vec![]) - }; - - EnumOption { name: s.to_owned(), ty: member } - }) - .collect::>(); - - Ty::Enum(Enum { option: None, name: member.r#type.clone(), options }) - } - "Tuple" => { - let children = model_members_all - .iter() - .filter(|m| m.id == format!("{}${}", member.id, member.name)) - .map(|child| Member { - key: child.key, - name: child.name.to_owned(), - ty: parse_sql_member(child, model_members_all), - }) - .collect::>(); - - Ty::Tuple(children.into_iter().map(|m| m.ty).collect()) - } - "Array" => { - let children = model_members_all - .iter() - .filter(|m| m.id == format!("{}${}", member.id, member.name)) - .map(|child| Member { - key: child.key, - name: child.name.to_owned(), - ty: parse_sql_member(child, model_members_all), - }) - .collect::>(); - - Ty::Array(children.into_iter().map(|m| m.ty).collect()) - } - ty => { - unimplemented!("unimplemented type_enum: {ty}"); - } - } - } - - Ty::Struct(Struct { - name: get_tag(namespace, model), - children: model_members_all - .iter() - .filter(|m| m.id == get_tag(namespace, model)) - .map(|m| Member { - key: m.key, - name: m.name.to_owned(), - ty: parse_sql_member(m, model_members_all), - }) - .collect::>(), - }) - // parse_sql_model_members_impl(model, model, model_members_all) -} - -/// Creates a query that fetches all models and their nested data. -pub fn build_sql_query( - schemas: &Vec, - entities_table: &str, - entity_relation_column: &str, - where_clause: Option<&str>, - where_clause_arrays: Option<&str>, - limit: Option, - offset: Option, -) -> Result<(String, HashMap, String), Error> { - #[derive(Default)] - struct TableInfo { - table_name: String, - parent_table: Option, - // is_optional: bool, - depth: usize, // Track nesting depth for proper ordering - } - - #[allow(clippy::too_many_arguments)] - fn parse_ty( - path: &str, - name: &str, - ty: &Ty, - selections: &mut Vec, - tables: &mut Vec, - arrays_queries: &mut HashMap, Vec)>, - _parent_is_optional: bool, - depth: usize, - ) { - match &ty { - Ty::Struct(s) => { - let table_name = - if path.is_empty() { name.to_string() } else { format!("{}${}", path, name) }; - - tables.push(TableInfo { - table_name: table_name.clone(), - parent_table: if path.is_empty() { None } else { Some(path.to_string()) }, - // is_optional: parent_is_optional, - depth, - }); - - for child in &s.children { - parse_ty( - &table_name, - &child.name, - &child.ty, - selections, - tables, - arrays_queries, - _parent_is_optional, - depth + 1, - ); - } - } - Ty::Tuple(t) => { - let table_name = format!("{}${}", path, name); - - tables.push(TableInfo { - table_name: table_name.clone(), - parent_table: Some(path.to_string()), - // is_optional: parent_is_optional, - depth, - }); - - for (i, child) in t.iter().enumerate() { - parse_ty( - &table_name, - &format!("_{}", i), - child, - selections, - tables, - arrays_queries, - _parent_is_optional, - depth + 1, - ); - } - } - Ty::Array(t) => { - let table_name = format!("{}${}", path, name); - let is_optional = true; - - let mut array_selections = Vec::new(); - let mut array_tables = vec![TableInfo { - table_name: table_name.clone(), - parent_table: Some(path.to_string()), - // is_optional: true, - depth, - }]; - - parse_ty( - &table_name, - "data", - &t[0], - &mut array_selections, - &mut array_tables, - arrays_queries, - is_optional, - depth + 1, - ); - - arrays_queries.insert(table_name, (array_selections, array_tables)); - } - Ty::Enum(e) => { - let table_name = format!("{}${}", path, name); - let is_optional = true; - - let mut is_typed = false; - for option in &e.options { - if let Ty::Tuple(t) = &option.ty { - if t.is_empty() { - continue; - } - } - - parse_ty( - &table_name, - &option.name, - &option.ty, - selections, - tables, - arrays_queries, - is_optional, - depth + 1, - ); - is_typed = true; - } - - selections.push(format!("[{}].external_{} AS \"{}.{}\"", path, name, path, name)); - if is_typed { - tables.push(TableInfo { - table_name, - parent_table: Some(path.to_string()), - // is_optional: parent_is_optional || is_optional, - depth, - }); - } - } - _ => { - selections.push(format!("[{}].external_{} AS \"{}.{}\"", path, name, path, name)); - } - } - } - - let mut global_selections = Vec::new(); - let mut global_tables = Vec::new(); - let mut arrays_queries: HashMap, Vec)> = HashMap::new(); - - for model in schemas { - parse_ty( - "", - &model.name(), - model, - &mut global_selections, - &mut global_tables, - &mut arrays_queries, - false, - 0, - ); - } - - if global_tables.len() > 64 { - return Err(QueryError::SqliteJoinLimit.into()); - } - - // Sort tables by depth to ensure proper join order - global_tables.sort_by_key(|table| table.depth); - - let selections_clause = global_selections.join(", "); - let join_clause = global_tables - .iter() - .map(|table| { - let join_condition = - format!("{entities_table}.id = [{}].{entity_relation_column}", table.table_name); - format!(" LEFT JOIN [{}] ON {join_condition}", table.table_name) - }) - .collect::>() - .join(" "); - - let mut formatted_arrays_queries: HashMap = arrays_queries - .into_iter() - .map(|(table, (selections, mut tables))| { - let mut selections_clause = selections.join(", "); - if !selections_clause.is_empty() { - selections_clause = format!(", {}", selections_clause); - } - - // Sort array tables by depth - tables.sort_by_key(|table| table.depth); - - let join_clause = tables - .iter() - .enumerate() - .map(|(i, table)| { - if i == 0 { - format!( - " JOIN [{}] ON {entities_table}.id = [{}].{entity_relation_column}", - table.table_name, table.table_name - ) - } else { - format!( - " LEFT JOIN [{}] ON [{}].full_array_id = [{}].full_array_id", - table.table_name, - table.table_name, - table.parent_table.as_ref().unwrap() - ) - } - }) - .collect::>() - .join(" "); - - ( - table, - format!( - "SELECT {entities_table}.id, {entities_table}.keys{selections_clause} FROM \ - {entities_table}{join_clause}", - ), - ) - }) - .collect(); - - let mut query = format!( - "SELECT {entities_table}.id, {entities_table}.keys, {selections_clause} FROM \ - {entities_table}{join_clause}" - ); - let mut count_query = - format!("SELECT COUNT({entities_table}.id) FROM {entities_table}{join_clause}"); - - if let Some(where_clause) = where_clause { - query += &format!(" WHERE {}", where_clause); - count_query += &format!(" WHERE {}", where_clause); - } - query += &format!(" ORDER BY {entities_table}.event_id DESC"); - - if let Some(limit) = limit { - query += &format!(" LIMIT {}", limit); - } - - if let Some(offset) = offset { - query += &format!(" OFFSET {}", offset); - } - - if let Some(where_clause_arrays) = where_clause_arrays { - for (_, formatted_query) in formatted_arrays_queries.iter_mut() { - *formatted_query = format!("{} WHERE {}", formatted_query, where_clause_arrays); - } - } - - Ok((query, formatted_arrays_queries, count_query)) -} - -/// Populate the values of a Ty (schema) from SQLite row. -pub fn map_row_to_ty( - path: &str, - name: &str, - ty: &mut Ty, - // the row that contains non dynamic data for Ty - row: &SqliteRow, - // a hashmap where keys are the paths for the model - // arrays and values are the rows mapping to each element - // in the array - arrays_rows: &HashMap>, -) -> Result<(), Error> { - let column_name = format!("{}.{}", path, name); - - match ty { - Ty::Primitive(primitive) => { - match &primitive { - Primitive::I8(_) => { - let value = row.try_get::(&column_name)?; - primitive.set_i8(Some(value))?; - } - Primitive::I16(_) => { - let value = row.try_get::(&column_name)?; - primitive.set_i16(Some(value))?; - } - Primitive::I32(_) => { - let value = row.try_get::(&column_name)?; - primitive.set_i32(Some(value))?; - } - Primitive::I64(_) => { - let value = row.try_get::(&column_name)?; - primitive.set_i64(Some(value))?; - } - Primitive::I128(_) => { - let value = row.try_get::(&column_name)?; - let hex_str = value.trim_start_matches("0x"); - - if !hex_str.is_empty() { - primitive.set_i128(Some( - i128::from_str_radix(hex_str, 16).map_err(ParseError::ParseIntError)?, - ))?; - } - } - Primitive::U8(_) => { - let value = row.try_get::(&column_name)?; - primitive.set_u8(Some(value))?; - } - Primitive::U16(_) => { - let value = row.try_get::(&column_name)?; - primitive.set_u16(Some(value))?; - } - Primitive::U32(_) => { - let value = row.try_get::(&column_name)?; - primitive.set_u32(Some(value))?; - } - Primitive::U64(_) => { - let value = row.try_get::(&column_name)?; - let hex_str = value.trim_start_matches("0x"); - - if !hex_str.is_empty() { - primitive.set_u64(Some( - u64::from_str_radix(hex_str, 16).map_err(ParseError::ParseIntError)?, - ))?; - } - } - Primitive::U128(_) => { - let value = row.try_get::(&column_name)?; - let hex_str = value.trim_start_matches("0x"); - - if !hex_str.is_empty() { - primitive.set_u128(Some( - u128::from_str_radix(hex_str, 16).map_err(ParseError::ParseIntError)?, - ))?; - } - } - Primitive::U256(_) => { - let value = row.try_get::(&column_name)?; - let hex_str = value.trim_start_matches("0x"); - - if !hex_str.is_empty() { - primitive.set_u256(Some(U256::from_be_hex(hex_str)))?; - } - } - Primitive::USize(_) => { - let value = row.try_get::(&column_name)?; - primitive.set_usize(Some(value))?; - } - Primitive::Bool(_) => { - let value = row.try_get::(&column_name)?; - primitive.set_bool(Some(value))?; - } - Primitive::Felt252(_) => { - let value = row.try_get::(&column_name)?; - if !value.is_empty() { - primitive.set_felt252(Some( - Felt::from_str(&value).map_err(ParseError::FromStr)?, - ))?; - } - } - Primitive::ClassHash(_) => { - let value = row.try_get::(&column_name)?; - if !value.is_empty() { - primitive.set_class_hash(Some( - Felt::from_str(&value).map_err(ParseError::FromStr)?, - ))?; - } - } - Primitive::ContractAddress(_) => { - let value = row.try_get::(&column_name)?; - if !value.is_empty() { - primitive.set_contract_address(Some( - Felt::from_str(&value).map_err(ParseError::FromStr)?, - ))?; - } - } - }; - } - Ty::Enum(enum_ty) => { - let option_name = row.try_get::(&column_name)?; - if !option_name.is_empty() { - enum_ty.set_option(&option_name)?; - } - - let path = [path, name].join("$"); - for option in &mut enum_ty.options { - if option.name != option_name { - continue; - } - - map_row_to_ty(&path, &option.name, &mut option.ty, row, arrays_rows)?; - } - } - Ty::Struct(struct_ty) => { - // struct can be the main entrypoint to our model schema - // so we dont format the table name if the path is empty - let path = - if path.is_empty() { struct_ty.name.clone() } else { [path, name].join("$") }; - - for member in &mut struct_ty.children { - map_row_to_ty(&path, &member.name, &mut member.ty, row, arrays_rows)?; - } - } - Ty::Tuple(ty) => { - let path = [path, name].join("$"); - - for (i, member) in ty.iter_mut().enumerate() { - map_row_to_ty(&path, &format!("_{}", i), member, row, arrays_rows)?; - } - } - Ty::Array(ty) => { - let path = [path, name].join("$"); - // filter by entity id in case we have multiple entities - let rows = arrays_rows - .get(&path) - .expect("qed; rows should exist") - .iter() - .filter(|array_row| array_row.get::("id") == row.get::("id")) - .collect::>(); - - // map each row to the ty of the array - let tys = rows - .iter() - .map(|row| { - let mut ty = ty[0].clone(); - map_row_to_ty(&path, "data", &mut ty, row, arrays_rows).map(|_| ty) - }) - .collect::, _>>()?; - - *ty = tys; - } - Ty::ByteArray(bytearray) => { - let value = row.try_get::(&column_name)?; - *bytearray = value; - } - }; - - Ok(()) -} - -#[cfg(test)] -mod tests { - use dojo_types::schema::{Enum, EnumOption, Member, Struct, Ty}; - - use super::{build_sql_query, SqlModelMember}; - use crate::model::parse_sql_model_members; - - #[test] - fn parse_simple_model_members_to_ty() { - let model_members = vec![ - SqlModelMember { - id: "Test-Position".into(), - name: "x".into(), - r#type: "u256".into(), - key: false, - model_idx: 0, - member_idx: 0, - type_enum: "Primitive".into(), - enum_options: None, - }, - SqlModelMember { - id: "Test-Position".into(), - name: "y".into(), - r#type: "u256".into(), - key: false, - model_idx: 0, - member_idx: 1, - type_enum: "Primitive".into(), - enum_options: None, - }, - SqlModelMember { - id: "Test-PlayerConfig".into(), - name: "name".into(), - r#type: "ByteArray".into(), - key: false, - model_idx: 0, - member_idx: 0, - type_enum: "ByteArray".into(), - enum_options: None, - }, - ]; - - let expected_position = Ty::Struct(Struct { - name: "Test-Position".into(), - children: vec![ - dojo_types::schema::Member { - name: "x".into(), - key: false, - ty: Ty::Primitive("u256".parse().unwrap()), - }, - dojo_types::schema::Member { - name: "y".into(), - key: false, - ty: Ty::Primitive("u256".parse().unwrap()), - }, - ], - }); - - let expected_player_config = Ty::Struct(Struct { - name: "Test-PlayerConfig".into(), - children: vec![dojo_types::schema::Member { - name: "name".into(), - key: false, - ty: Ty::ByteArray("".to_string()), - }], - }); - - assert_eq!(parse_sql_model_members("Test", "Position", &model_members), expected_position); - assert_eq!( - parse_sql_model_members("Test", "PlayerConfig", &model_members), - expected_player_config - ); - } - - #[test] - fn parse_complex_model_members_to_ty() { - let model_members = vec![ - SqlModelMember { - id: "Test-Position".into(), - name: "name".into(), - r#type: "felt252".into(), - key: false, - model_idx: 0, - member_idx: 0, - type_enum: "Primitive".into(), - enum_options: None, - }, - SqlModelMember { - id: "Test-Position".into(), - name: "age".into(), - r#type: "u8".into(), - key: false, - model_idx: 0, - member_idx: 1, - type_enum: "Primitive".into(), - enum_options: None, - }, - SqlModelMember { - id: "Test-Position".into(), - name: "vec".into(), - r#type: "Vec2".into(), - key: false, - model_idx: 0, - member_idx: 1, - type_enum: "Struct".into(), - enum_options: None, - }, - SqlModelMember { - id: "Test-Position$vec".into(), - name: "x".into(), - r#type: "u256".into(), - key: false, - model_idx: 1, - member_idx: 0, - type_enum: "Primitive".into(), - enum_options: None, - }, - SqlModelMember { - id: "Test-Position$vec".into(), - name: "y".into(), - r#type: "u256".into(), - key: false, - model_idx: 1, - member_idx: 1, - type_enum: "Primitive".into(), - enum_options: None, - }, - SqlModelMember { - id: "Test-PlayerConfig".into(), - name: "favorite_item".into(), - r#type: "Option".into(), - key: false, - model_idx: 0, - member_idx: 0, - type_enum: "Enum".into(), - enum_options: Some("None,Some".into()), - }, - SqlModelMember { - id: "Test-PlayerConfig".into(), - name: "items".into(), - r#type: "Array".into(), - key: false, - model_idx: 0, - member_idx: 1, - type_enum: "Array".into(), - enum_options: None, - }, - SqlModelMember { - id: "Test-PlayerConfig$items".into(), - name: "data".into(), - r#type: "PlayerItem".into(), - key: false, - model_idx: 0, - member_idx: 1, - type_enum: "Struct".into(), - enum_options: None, - }, - SqlModelMember { - id: "Test-PlayerConfig$items$data".into(), - name: "item_id".into(), - r#type: "u32".into(), - key: false, - model_idx: 0, - member_idx: 0, - type_enum: "Primitive".into(), - enum_options: None, - }, - SqlModelMember { - id: "Test-PlayerConfig$items$data".into(), - name: "quantity".into(), - r#type: "u32".into(), - key: false, - model_idx: 0, - member_idx: 1, - type_enum: "Primitive".into(), - enum_options: None, - }, - SqlModelMember { - id: "Test-PlayerConfig$favorite_item".into(), - name: "Some".into(), - r#type: "u32".into(), - key: false, - model_idx: 1, - member_idx: 0, - type_enum: "Primitive".into(), - enum_options: None, - }, - SqlModelMember { - id: "Test-PlayerConfig$favorite_item".into(), - name: "option".into(), - r#type: "Option".into(), - key: false, - model_idx: 1, - member_idx: 0, - type_enum: "Enum".into(), - enum_options: Some("None,Some".into()), - }, - ]; - - let expected_position = Ty::Struct(Struct { - name: "Test-Position".into(), - children: vec![ - dojo_types::schema::Member { - name: "name".into(), - key: false, - ty: Ty::Primitive("felt252".parse().unwrap()), - }, - dojo_types::schema::Member { - name: "age".into(), - key: false, - ty: Ty::Primitive("u8".parse().unwrap()), - }, - dojo_types::schema::Member { - name: "vec".into(), - key: false, - ty: Ty::Struct(Struct { - name: "Vec2".into(), - children: vec![ - Member { - name: "x".into(), - key: false, - ty: Ty::Primitive("u256".parse().unwrap()), - }, - Member { - name: "y".into(), - key: false, - ty: Ty::Primitive("u256".parse().unwrap()), - }, - ], - }), - }, - ], - }); - - let expected_player_config = Ty::Struct(Struct { - name: "Test-PlayerConfig".into(), - children: vec![ - dojo_types::schema::Member { - name: "favorite_item".into(), - key: false, - ty: Ty::Enum(Enum { - name: "Option".into(), - option: None, - options: vec![ - EnumOption { name: "None".into(), ty: Ty::Tuple(vec![]) }, - EnumOption { - name: "Some".into(), - ty: Ty::Primitive("u32".parse().unwrap()), - }, - ], - }), - }, - dojo_types::schema::Member { - name: "items".into(), - key: false, - ty: Ty::Array(vec![Ty::Struct(Struct { - name: "PlayerItem".into(), - children: vec![ - Member { - name: "item_id".into(), - key: false, - ty: Ty::Primitive("u32".parse().unwrap()), - }, - Member { - name: "quantity".into(), - key: false, - ty: Ty::Primitive("u32".parse().unwrap()), - }, - ], - })]), - }, - ], - }); - - assert_eq!(parse_sql_model_members("Test", "Position", &model_members), expected_position); - assert_eq!( - parse_sql_model_members("Test", "PlayerConfig", &model_members), - expected_player_config - ); - } - - #[test] - fn parse_model_members_with_enum_to_ty() { - let model_members = vec![SqlModelMember { - id: "Test-Moves".into(), - name: "direction".into(), - r#type: "Direction".into(), - key: false, - model_idx: 0, - member_idx: 0, - type_enum: "Enum".into(), - enum_options: Some("Up,Down,Left,Right".into()), - }]; - - let expected_ty = Ty::Struct(Struct { - name: "Test-Moves".into(), - children: vec![dojo_types::schema::Member { - name: "direction".into(), - key: false, - ty: Ty::Enum(Enum { - name: "Direction".into(), - option: None, - options: vec![ - EnumOption { name: "Up".into(), ty: Ty::Tuple(vec![]) }, - EnumOption { name: "Down".into(), ty: Ty::Tuple(vec![]) }, - EnumOption { name: "Left".into(), ty: Ty::Tuple(vec![]) }, - EnumOption { name: "Right".into(), ty: Ty::Tuple(vec![]) }, - ], - }), - }], - }); - - assert_eq!(parse_sql_model_members("Test", "Moves", &model_members), expected_ty); - } - - #[test] - fn struct_ty_to_query() { - let position = Ty::Struct(Struct { - name: "Test-Position".into(), - children: vec![ - dojo_types::schema::Member { - name: "player".into(), - key: true, - ty: Ty::Primitive("ContractAddress".parse().unwrap()), - }, - dojo_types::schema::Member { - name: "vec".into(), - key: false, - ty: Ty::Struct(Struct { - name: "Vec2".into(), - children: vec![ - Member { - name: "x".into(), - key: false, - ty: Ty::Primitive("u32".parse().unwrap()), - }, - Member { - name: "y".into(), - key: false, - ty: Ty::Primitive("u32".parse().unwrap()), - }, - ], - }), - }, - dojo_types::schema::Member { - name: "test_everything".into(), - key: false, - ty: Ty::Array(vec![Ty::Struct(Struct { - name: "TestEverything".into(), - children: vec![Member { - name: "data".into(), - key: false, - ty: Ty::Tuple(vec![ - Ty::Array(vec![Ty::Primitive("u32".parse().unwrap())]), - Ty::Array(vec![Ty::Array(vec![Ty::Tuple(vec![ - Ty::Primitive("u32".parse().unwrap()), - Ty::Struct(Struct { - name: "Vec2".into(), - children: vec![ - Member { - name: "x".into(), - key: false, - ty: Ty::Primitive("u32".parse().unwrap()), - }, - Member { - name: "y".into(), - key: false, - ty: Ty::Primitive("u32".parse().unwrap()), - }, - ], - }), - ])])]), - ]), - }], - })]), - }, - ], - }); - - let player_config = Ty::Struct(Struct { - name: "Test-PlayerConfig".into(), - children: vec![ - dojo_types::schema::Member { - name: "favorite_item".into(), - key: false, - ty: Ty::Enum(Enum { - name: "Option".into(), - option: None, - options: vec![ - EnumOption { name: "None".into(), ty: Ty::Tuple(vec![]) }, - EnumOption { - name: "Some".into(), - ty: Ty::Primitive("u32".parse().unwrap()), - }, - ], - }), - }, - dojo_types::schema::Member { - name: "items".into(), - key: false, - ty: Ty::Array(vec![Ty::Struct(Struct { - name: "PlayerItem".into(), - children: vec![ - Member { - name: "item_id".into(), - key: false, - ty: Ty::Primitive("u32".parse().unwrap()), - }, - Member { - name: "quantity".into(), - key: false, - ty: Ty::Primitive("u32".parse().unwrap()), - }, - ], - })]), - }, - ], - }); - - let query = build_sql_query( - &vec![position, player_config], - "entities", - "entity_id", - None, - None, - None, - None, - ) - .unwrap(); - - let expected_query = - "SELECT entities.id, entities.keys, [Test-Position].external_player AS \ - \"Test-Position.player\", [Test-Position$vec].external_x AS \"Test-Position$vec.x\", \ - [Test-Position$vec].external_y AS \"Test-Position$vec.y\", \ - [Test-PlayerConfig$favorite_item].external_Some AS \ - \"Test-PlayerConfig$favorite_item.Some\", [Test-PlayerConfig].external_favorite_item \ - AS \"Test-PlayerConfig.favorite_item\" FROM entities LEFT JOIN [Test-Position] ON \ - entities.id = [Test-Position].entity_id LEFT JOIN [Test-PlayerConfig] ON \ - entities.id = [Test-PlayerConfig].entity_id LEFT JOIN [Test-Position$vec] ON \ - entities.id = [Test-Position$vec].entity_id LEFT JOIN \ - [Test-PlayerConfig$favorite_item] ON entities.id = \ - [Test-PlayerConfig$favorite_item].entity_id ORDER BY entities.event_id DESC"; - // todo: completely tests arrays - assert_eq!(query.0, expected_query); - } -} diff --git a/crates/torii/core/src/sql/mod.rs b/crates/torii/core/src/sql/mod.rs deleted file mode 100644 index 5f7a8f5093..0000000000 --- a/crates/torii/core/src/sql/mod.rs +++ /dev/null @@ -1,1318 +0,0 @@ -use std::collections::HashMap; -use std::convert::TryInto; -use std::str::FromStr; -use std::sync::Arc; - -use anyhow::{anyhow, Context, Result}; -use dojo_types::naming::get_tag; -use dojo_types::primitive::Primitive; -use dojo_types::schema::{EnumOption, Member, Struct, Ty}; -use dojo_world::config::WorldMetadata; -use dojo_world::contracts::abigen::model::Layout; -use dojo_world::contracts::naming::compute_selector_from_names; -use sqlx::pool::PoolConnection; -use sqlx::{Pool, Sqlite}; -use starknet::core::types::{Event, Felt, InvokeTransaction, Transaction}; -use starknet_crypto::poseidon_hash_many; -use tokio::sync::mpsc::UnboundedSender; -use utils::felts_to_sql_string; - -use crate::constants::SQL_FELT_DELIMITER; -use crate::executor::{ - Argument, DeleteEntityQuery, EventMessageQuery, QueryMessage, QueryType, ResetCursorsQuery, - SetHeadQuery, UpdateCursorsQuery, -}; -use crate::types::Contract; -use crate::utils::utc_dt_string_from_timestamp; - -type IsEventMessage = bool; -type IsStoreUpdate = bool; - -pub mod cache; -pub mod erc; -#[cfg(test)] -#[path = "test.rs"] -mod test; -pub mod utils; - -use cache::{LocalCache, Model, ModelCache}; - -#[derive(Debug, Clone)] -pub struct Sql { - pub pool: Pool, - pub executor: UnboundedSender, - model_cache: Arc, - // when SQL struct is cloned a empty local_cache is created - local_cache: LocalCache, -} - -#[derive(Debug, Clone)] -pub struct Cursors { - pub cursor_map: HashMap, - pub last_pending_block_tx: Option, - pub head: Option, -} - -impl Sql { - pub async fn new( - pool: Pool, - executor: UnboundedSender, - contracts: &[Contract], - model_cache: Arc, - ) -> Result { - for contract in contracts { - executor.send(QueryMessage::other( - "INSERT OR IGNORE INTO contracts (id, contract_address, contract_type) VALUES (?, \ - ?, ?)" - .to_string(), - vec![ - Argument::FieldElement(contract.address), - Argument::FieldElement(contract.address), - Argument::String(contract.r#type.to_string()), - ], - ))?; - } - - let local_cache = LocalCache::new(pool.clone()).await; - let db = Self { pool: pool.clone(), executor, model_cache, local_cache }; - - db.execute().await?; - - Ok(db) - } - - pub async fn head(&self, contract: Felt) -> Result<(u64, Option, Option)> { - let indexer_query = - sqlx::query_as::<_, (Option, Option, Option, String)>( - "SELECT head, last_pending_block_contract_tx, last_pending_block_tx, \ - contract_type FROM contracts WHERE id = ?", - ) - .bind(format!("{:#x}", contract)); - - let indexer: (Option, Option, Option, String) = indexer_query - .fetch_one(&self.pool) - .await - .with_context(|| format!("Failed to fetch head for contract: {:#x}", contract))?; - Ok(( - indexer - .0 - .map(|h| h.try_into().map_err(|_| anyhow!("Head value {} doesn't fit in u64", h))) - .transpose()? - .unwrap_or(0), - indexer.1.map(|f| Felt::from_str(&f)).transpose()?, - indexer.2.map(|f| Felt::from_str(&f)).transpose()?, - )) - } - - pub async fn set_head( - &mut self, - head: u64, - last_block_timestamp: u64, - world_txns_count: u64, - contract_address: Felt, - ) -> Result<()> { - let head_arg = Argument::Int( - head.try_into().map_err(|_| anyhow!("Head value {} doesn't fit in i64", head))?, - ); - let last_block_timestamp_arg = - Argument::Int(last_block_timestamp.try_into().map_err(|_| { - anyhow!("Last block timestamp value {} doesn't fit in i64", last_block_timestamp) - })?); - let id = Argument::FieldElement(contract_address); - - self.executor.send(QueryMessage::new( - "UPDATE contracts SET head = ?, last_block_timestamp = ? WHERE id = ?".to_string(), - vec![head_arg, last_block_timestamp_arg, id], - QueryType::SetHead(SetHeadQuery { - head, - last_block_timestamp, - txns_count: world_txns_count, - contract_address, - }), - ))?; - - Ok(()) - } - - pub fn set_last_pending_block_contract_tx( - &mut self, - contract: Felt, - last_pending_block_contract_tx: Option, - ) -> Result<()> { - let last_pending_block_contract_tx = if let Some(f) = last_pending_block_contract_tx { - Argument::String(format!("{:#x}", f)) - } else { - Argument::Null - }; - - let id = Argument::FieldElement(contract); - - self.executor.send(QueryMessage::other( - "UPDATE contracts SET last_pending_block_contract_tx = ? WHERE id = ?".to_string(), - vec![last_pending_block_contract_tx, id], - ))?; - - Ok(()) - } - - pub fn set_last_pending_block_tx(&mut self, last_pending_block_tx: Option) -> Result<()> { - let last_pending_block_tx = if let Some(f) = last_pending_block_tx { - Argument::String(format!("{:#x}", f)) - } else { - Argument::Null - }; - - self.executor.send(QueryMessage::other( - "UPDATE contracts SET last_pending_block_tx = ? WHERE 1=1".to_string(), - vec![last_pending_block_tx], - ))?; - - Ok(()) - } - - pub(crate) async fn cursors(&self) -> Result { - let mut conn: PoolConnection = self.pool.acquire().await?; - let cursors = sqlx::query_as::<_, (String, String)>( - "SELECT contract_address, last_pending_block_contract_tx FROM contracts WHERE \ - last_pending_block_contract_tx IS NOT NULL", - ) - .fetch_all(&mut *conn) - .await?; - - let (head, last_pending_block_tx) = sqlx::query_as::<_, (Option, Option)>( - "SELECT head, last_pending_block_tx FROM contracts WHERE 1=1", - ) - .fetch_one(&mut *conn) - .await?; - - let head = head.map(|h| h.try_into().expect("doesn't fit in u64")); - let last_pending_block_tx = - last_pending_block_tx.map(|t| Felt::from_str(&t).expect("its a valid felt")); - Ok(Cursors { - cursor_map: cursors - .into_iter() - .map(|(c, t)| { - ( - Felt::from_str(&c).expect("its a valid felt"), - Felt::from_str(&t).expect("its a valid felt"), - ) - }) - .collect(), - last_pending_block_tx, - head, - }) - } - - // For a given contract address, sets head to the passed value and sets - // last_pending_block_contract_tx and last_pending_block_tx to null - pub fn reset_cursors( - &mut self, - head: u64, - cursor_map: HashMap, - last_block_timestamp: u64, - ) -> Result<()> { - self.executor.send(QueryMessage::new( - "".to_string(), - vec![], - QueryType::ResetCursors(ResetCursorsQuery { - cursor_map, - last_block_timestamp, - last_block_number: head, - }), - ))?; - - Ok(()) - } - - pub fn update_cursors( - &mut self, - head: u64, - last_pending_block_tx: Option, - cursor_map: HashMap, - pending_block_timestamp: u64, - ) -> Result<()> { - self.executor.send(QueryMessage::new( - "".to_string(), - vec![], - QueryType::UpdateCursors(UpdateCursorsQuery { - cursor_map, - last_pending_block_tx, - last_block_number: head, - pending_block_timestamp, - }), - ))?; - Ok(()) - } - - #[allow(clippy::too_many_arguments)] - pub async fn register_model( - &mut self, - namespace: &str, - model: &Ty, - layout: Layout, - class_hash: Felt, - contract_address: Felt, - packed_size: u32, - unpacked_size: u32, - block_timestamp: u64, - upgrade_diff: Option<&Ty>, - ) -> Result<()> { - let selector = compute_selector_from_names(namespace, &model.name()); - let namespaced_name = get_tag(namespace, &model.name()); - - let insert_models = - "INSERT INTO models (id, namespace, name, class_hash, contract_address, layout, \ - packed_size, unpacked_size, executed_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ON \ - CONFLICT(id) DO UPDATE SET contract_address=EXCLUDED.contract_address, \ - class_hash=EXCLUDED.class_hash, layout=EXCLUDED.layout, \ - packed_size=EXCLUDED.packed_size, unpacked_size=EXCLUDED.unpacked_size, \ - executed_at=EXCLUDED.executed_at RETURNING *"; - let arguments = vec![ - Argument::String(format!("{:#x}", selector)), - Argument::String(namespace.to_string()), - Argument::String(model.name().to_string()), - Argument::String(format!("{class_hash:#x}")), - Argument::String(format!("{contract_address:#x}")), - Argument::String(serde_json::to_string(&layout)?), - Argument::Int(packed_size as i64), - Argument::Int(unpacked_size as i64), - Argument::String(utc_dt_string_from_timestamp(block_timestamp)), - ]; - self.executor.send(QueryMessage::new( - insert_models.to_string(), - arguments, - QueryType::RegisterModel, - ))?; - - let mut model_idx = 0_i64; - self.build_register_queries_recursive( - selector, - model, - vec![namespaced_name.clone()], - &mut model_idx, - block_timestamp, - &mut 0, - &mut 0, - upgrade_diff, - )?; - - // we set the model in the cache directly - // because entities might be using it before the query queue is processed - self.model_cache - .set( - selector, - Model { - namespace: namespace.to_string(), - name: model.name().to_string(), - selector, - class_hash, - contract_address, - packed_size, - unpacked_size, - layout, - // we need to update the name of the struct to include the namespace - schema: Ty::Struct(Struct { - name: namespaced_name, - children: model.as_struct().unwrap().children.clone(), - }), - }, - ) - .await; - - Ok(()) - } - - pub async fn set_entity( - &mut self, - entity: Ty, - event_id: &str, - block_timestamp: u64, - entity_id: Felt, - model_id: Felt, - keys_str: Option<&str>, - ) -> Result<()> { - let namespaced_name = entity.name(); - - let entity_id = format!("{:#x}", entity_id); - let model_id = format!("{:#x}", model_id); - - let insert_entities = if keys_str.is_some() { - "INSERT INTO entities (id, event_id, executed_at, keys) VALUES (?, ?, ?, ?) ON \ - CONFLICT(id) DO UPDATE SET updated_at=CURRENT_TIMESTAMP, \ - executed_at=EXCLUDED.executed_at, event_id=EXCLUDED.event_id, keys=EXCLUDED.keys \ - RETURNING *" - } else { - "INSERT INTO entities (id, event_id, executed_at) VALUES (?, ?, ?) ON CONFLICT(id) DO \ - UPDATE SET updated_at=CURRENT_TIMESTAMP, executed_at=EXCLUDED.executed_at, \ - event_id=EXCLUDED.event_id RETURNING *" - }; - - let mut arguments = vec![ - Argument::String(entity_id.clone()), - Argument::String(event_id.to_string()), - Argument::String(utc_dt_string_from_timestamp(block_timestamp)), - ]; - - if let Some(keys) = keys_str { - arguments.push(Argument::String(keys.to_string())); - } - - self.executor.send(QueryMessage::new( - insert_entities.to_string(), - arguments, - QueryType::SetEntity(entity.clone()), - ))?; - - self.executor.send(QueryMessage::other( - "INSERT INTO entity_model (entity_id, model_id) VALUES (?, ?) ON CONFLICT(entity_id, \ - model_id) DO NOTHING" - .to_string(), - vec![Argument::String(entity_id.clone()), Argument::String(model_id.clone())], - ))?; - - let path = vec![namespaced_name]; - self.build_set_entity_queries_recursive( - path, - event_id, - (&entity_id, false), - (&entity, keys_str.is_none()), - block_timestamp, - &vec![], - )?; - - Ok(()) - } - - pub async fn set_event_message( - &mut self, - entity: Ty, - event_id: &str, - block_timestamp: u64, - is_historical: bool, - ) -> Result<()> { - let keys = if let Ty::Struct(s) = &entity { - let mut keys = Vec::new(); - for m in s.keys() { - keys.extend(m.serialize()?); - } - keys - } else { - return Err(anyhow!("Entity is not a struct")); - }; - - let namespaced_name = entity.name(); - let (model_namespace, model_name) = namespaced_name.split_once('-').unwrap(); - - let entity_id = format!("{:#x}", poseidon_hash_many(&keys)); - let model_id = format!("{:#x}", compute_selector_from_names(model_namespace, model_name)); - - let keys_str = felts_to_sql_string(&keys); - let block_timestamp_str = utc_dt_string_from_timestamp(block_timestamp); - - let insert_entities = "INSERT INTO event_messages (id, keys, event_id, executed_at) \ - VALUES (?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET \ - updated_at=CURRENT_TIMESTAMP, executed_at=EXCLUDED.executed_at, \ - event_id=EXCLUDED.event_id RETURNING *"; - self.executor.send(QueryMessage::new( - insert_entities.to_string(), - vec![ - Argument::String(entity_id.clone()), - Argument::String(keys_str.clone()), - Argument::String(event_id.to_string()), - Argument::String(block_timestamp_str.clone()), - ], - QueryType::EventMessage(EventMessageQuery { - entity_id: entity_id.clone(), - model_id: model_id.clone(), - keys_str: keys_str.clone(), - event_id: event_id.to_string(), - block_timestamp: block_timestamp_str.clone(), - ty: entity.clone(), - is_historical, - }), - ))?; - - let path = vec![namespaced_name]; - self.build_set_entity_queries_recursive( - path, - event_id, - (&entity_id, true), - (&entity, false), - block_timestamp, - &vec![], - )?; - - Ok(()) - } - - pub async fn delete_entity( - &mut self, - entity_id: Felt, - model_id: Felt, - entity: Ty, - event_id: &str, - block_timestamp: u64, - ) -> Result<()> { - let entity_id = format!("{:#x}", entity_id); - let path = vec![entity.name()]; - // delete entity models data - self.build_delete_entity_queries_recursive(path, &entity_id, &entity)?; - - self.executor.send(QueryMessage::new( - "DELETE FROM entity_model WHERE entity_id = ? AND model_id = ?".to_string(), - vec![Argument::String(entity_id.clone()), Argument::String(format!("{:#x}", model_id))], - QueryType::DeleteEntity(DeleteEntityQuery { - entity_id: entity_id.clone(), - event_id: event_id.to_string(), - block_timestamp: utc_dt_string_from_timestamp(block_timestamp), - ty: entity.clone(), - }), - ))?; - - Ok(()) - } - - pub fn set_metadata(&mut self, resource: &Felt, uri: &str, block_timestamp: u64) -> Result<()> { - let resource = Argument::FieldElement(*resource); - let uri = Argument::String(uri.to_string()); - let executed_at = Argument::String(utc_dt_string_from_timestamp(block_timestamp)); - - self.executor.send(QueryMessage::other( - "INSERT INTO metadata (id, uri, executed_at) VALUES (?, ?, ?) ON CONFLICT(id) DO \ - UPDATE SET id=excluded.id, executed_at=excluded.executed_at, \ - updated_at=CURRENT_TIMESTAMP" - .to_string(), - vec![resource, uri, executed_at], - ))?; - - Ok(()) - } - - pub fn update_metadata( - &mut self, - resource: &Felt, - uri: &str, - metadata: &WorldMetadata, - icon_img: &Option, - cover_img: &Option, - ) -> Result<()> { - let json = serde_json::to_string(metadata).unwrap(); // safe unwrap - - let mut update = vec!["uri=?", "json=?", "updated_at=CURRENT_TIMESTAMP"]; - let mut arguments = vec![Argument::String(uri.to_string()), Argument::String(json)]; - - if let Some(icon) = icon_img { - update.push("icon_img=?"); - arguments.push(Argument::String(icon.clone())); - } - - if let Some(cover) = cover_img { - update.push("cover_img=?"); - arguments.push(Argument::String(cover.clone())); - } - - let statement = format!("UPDATE metadata SET {} WHERE id = ?", update.join(",")); - arguments.push(Argument::FieldElement(*resource)); - - self.executor.send(QueryMessage::other(statement, arguments))?; - - Ok(()) - } - - pub async fn model(&self, selector: Felt) -> Result { - self.model_cache.model(&selector).await.map_err(|e| e.into()) - } - - pub async fn does_entity_exist(&self, model: String, key: Felt) -> Result { - let sql = format!("SELECT COUNT(*) FROM [{model}] WHERE id = ?"); - - let count: i64 = - sqlx::query_scalar(&sql).bind(format!("{:#x}", key)).fetch_one(&self.pool).await?; - - Ok(count > 0) - } - - pub async fn entities(&self, model: String) -> Result>> { - let query = sqlx::query_as::<_, (i32, String, String)>("SELECT * FROM ?").bind(model); - let mut conn: PoolConnection = self.pool.acquire().await?; - let mut rows = query.fetch_all(&mut *conn).await?; - Ok(rows.drain(..).map(|row| serde_json::from_str(&row.2).unwrap()).collect()) - } - - pub fn store_transaction( - &mut self, - transaction: &Transaction, - transaction_id: &str, - block_timestamp: u64, - ) -> Result<()> { - let id = Argument::String(transaction_id.to_string()); - - let transaction_type = match transaction { - Transaction::Invoke(_) => "INVOKE", - Transaction::L1Handler(_) => "L1_HANDLER", - _ => return Ok(()), - }; - - let (transaction_hash, sender_address, calldata, max_fee, signature, nonce) = - match transaction { - Transaction::Invoke(InvokeTransaction::V1(invoke_v1_transaction)) => ( - Argument::FieldElement(invoke_v1_transaction.transaction_hash), - Argument::FieldElement(invoke_v1_transaction.sender_address), - Argument::String(felts_to_sql_string(&invoke_v1_transaction.calldata)), - Argument::FieldElement(invoke_v1_transaction.max_fee), - Argument::String(felts_to_sql_string(&invoke_v1_transaction.signature)), - Argument::FieldElement(invoke_v1_transaction.nonce), - ), - Transaction::L1Handler(l1_handler_transaction) => ( - Argument::FieldElement(l1_handler_transaction.transaction_hash), - Argument::FieldElement(l1_handler_transaction.contract_address), - Argument::String(felts_to_sql_string(&l1_handler_transaction.calldata)), - Argument::FieldElement(Felt::ZERO), // has no max_fee - Argument::String("".to_string()), // has no signature - Argument::FieldElement((l1_handler_transaction.nonce).into()), - ), - _ => return Ok(()), - }; - - self.executor.send(QueryMessage::other( - "INSERT OR IGNORE INTO transactions (id, transaction_hash, sender_address, calldata, \ - max_fee, signature, nonce, transaction_type, executed_at) VALUES (?, ?, ?, ?, ?, ?, \ - ?, ?, ?)" - .to_string(), - vec![ - id, - transaction_hash, - sender_address, - calldata, - max_fee, - signature, - nonce, - Argument::String(transaction_type.to_string()), - Argument::String(utc_dt_string_from_timestamp(block_timestamp)), - ], - ))?; - - Ok(()) - } - - pub fn store_event( - &mut self, - event_id: &str, - event: &Event, - transaction_hash: Felt, - block_timestamp: u64, - ) -> Result<()> { - let id = Argument::String(event_id.to_string()); - let keys = Argument::String(felts_to_sql_string(&event.keys)); - let data = Argument::String(felts_to_sql_string(&event.data)); - let hash = Argument::FieldElement(transaction_hash); - let executed_at = Argument::String(utc_dt_string_from_timestamp(block_timestamp)); - - self.executor.send(QueryMessage::new( - "INSERT OR IGNORE INTO events (id, keys, data, transaction_hash, executed_at) VALUES \ - (?, ?, ?, ?, ?) RETURNING *" - .to_string(), - vec![id, keys, data, hash, executed_at], - QueryType::StoreEvent, - ))?; - - Ok(()) - } - - #[allow(clippy::too_many_arguments)] - fn build_register_queries_recursive( - &mut self, - selector: Felt, - model: &Ty, - path: Vec, - model_idx: &mut i64, - block_timestamp: u64, - array_idx: &mut usize, - parent_array_idx: &mut usize, - upgrade_diff: Option<&Ty>, - ) -> Result<()> { - if let Ty::Enum(e) = model { - if e.options.iter().all(|o| if let Ty::Tuple(t) = &o.ty { t.is_empty() } else { false }) - { - return Ok(()); - } - } - - self.build_model_query( - selector, - path.clone(), - model, - *model_idx, - block_timestamp, - *array_idx, - *parent_array_idx, - upgrade_diff, - )?; - - let mut build_member = |pathname: &str, member: &Ty| -> Result<()> { - if let Ty::Primitive(_) = member { - return Ok(()); - } else if let Ty::ByteArray(_) = member { - return Ok(()); - } - - let mut path_clone = path.clone(); - path_clone.push(pathname.to_string()); - - self.build_register_queries_recursive( - selector, - member, - path_clone, - &mut (*model_idx + 1), - block_timestamp, - &mut (*array_idx + if let Ty::Array(_) = member { 1 } else { 0 }), - &mut (*parent_array_idx + if let Ty::Array(_) = model { 1 } else { 0 }), - // nested members are not upgrades - None, - )?; - - Ok(()) - }; - - if let Ty::Struct(s) = model { - for member in s.children.iter() { - build_member(&member.name, &member.ty)?; - } - } else if let Ty::Tuple(t) = model { - for (idx, member) in t.iter().enumerate() { - build_member(format!("_{}", idx).as_str(), member)?; - } - } else if let Ty::Array(array) = model { - let ty = &array[0]; - build_member("data", ty)?; - } else if let Ty::Enum(e) = model { - for child in e.options.iter() { - // Skip enum options that have no type / member - if let Ty::Tuple(t) = &child.ty { - if t.is_empty() { - continue; - } - } - - build_member(&child.name, &child.ty)?; - } - } - - Ok(()) - } - - fn build_set_entity_queries_recursive( - &mut self, - path: Vec, - event_id: &str, - // The id of the entity and if the entity is an event message - entity_id: (&str, IsEventMessage), - entity: (&Ty, IsStoreUpdate), - block_timestamp: u64, - indexes: &Vec, - ) -> Result<()> { - let (entity_id, is_event_message) = entity_id; - let (entity, is_store_update_member) = entity; - - let update_members = |members: &[Member], - executor: &mut UnboundedSender, - indexes: &Vec| - -> Result<()> { - let table_id = path.join("$"); - let mut columns = vec![ - "id".to_string(), - "event_id".to_string(), - "executed_at".to_string(), - "updated_at".to_string(), - if is_event_message { - "event_message_id".to_string() - } else { - "entity_id".to_string() - }, - ]; - - let mut arguments = vec![ - Argument::String(if is_event_message { - "event:".to_string() + entity_id - } else { - entity_id.to_string() - }), - Argument::String(event_id.to_string()), - Argument::String(utc_dt_string_from_timestamp(block_timestamp)), - Argument::String(chrono::Utc::now().to_rfc3339()), - Argument::String(entity_id.to_string()), - ]; - - if !indexes.is_empty() { - columns.push("full_array_id".to_string()); - arguments.push(Argument::String( - std::iter::once(entity_id.to_string()) - .chain(indexes.iter().map(|i| i.to_string())) - .collect::>() - .join(SQL_FELT_DELIMITER), - )); - } - - for (column_idx, idx) in indexes.iter().enumerate() { - columns.push(format!("idx_{}", column_idx)); - arguments.push(Argument::Int(*idx)); - } - - for member in members.iter() { - match &member.ty { - Ty::Primitive(ty) => { - columns.push(format!("external_{}", &member.name)); - arguments.push(Argument::String(ty.to_sql_value())); - } - Ty::Enum(e) => { - columns.push(format!("external_{}", &member.name)); - arguments.push(Argument::String(e.to_sql_value())); - } - Ty::ByteArray(b) => { - columns.push(format!("external_{}", &member.name)); - arguments.push(Argument::String(b.clone())); - } - _ => {} - } - } - - let placeholders: Vec<&str> = arguments.iter().map(|_| "?").collect(); - let statement = if is_store_update_member && indexes.is_empty() { - arguments.push(Argument::String(if is_event_message { - "event:".to_string() + entity_id - } else { - entity_id.to_string() - })); - - // row has to exist. update it directly - format!( - "UPDATE [{table_id}] SET {updates} WHERE id = ?", - table_id = table_id, - updates = columns - .iter() - .zip(placeholders.iter()) - .map(|(column, placeholder)| format!("{} = {}", column, placeholder)) - .collect::>() - .join(", ") - ) - } else { - format!( - "INSERT OR REPLACE INTO [{table_id}] ({}) VALUES ({})", - columns.join(","), - placeholders.join(",") - ) - }; - - executor.send(QueryMessage::other(statement, arguments))?; - - Ok(()) - }; - - match entity { - Ty::Struct(s) => { - update_members(&s.children, &mut self.executor, indexes)?; - - for member in s.children.iter() { - let mut path_clone = path.clone(); - path_clone.push(member.name.clone()); - self.build_set_entity_queries_recursive( - path_clone, - event_id, - (entity_id, is_event_message), - (&member.ty, is_store_update_member), - block_timestamp, - indexes, - )?; - } - } - Ty::Enum(e) => { - if e.options.iter().all( - |o| { - if let Ty::Tuple(t) = &o.ty { t.is_empty() } else { false } - }, - ) { - return Ok(()); - } - - let option = e.options[e.option.unwrap() as usize].clone(); - - update_members( - &[ - Member { name: "option".to_string(), ty: Ty::Enum(e.clone()), key: false }, - Member { name: option.name.clone(), ty: option.ty.clone(), key: false }, - ], - &mut self.executor, - indexes, - )?; - - match &option.ty { - // Skip enum options that have no type / member - Ty::Tuple(t) if t.is_empty() => {} - _ => { - let mut path_clone = path.clone(); - path_clone.push(option.name.clone()); - self.build_set_entity_queries_recursive( - path_clone, - event_id, - (entity_id, is_event_message), - (&option.ty, is_store_update_member), - block_timestamp, - indexes, - )?; - } - } - } - Ty::Tuple(t) => { - update_members( - t.iter() - .enumerate() - .map(|(idx, member)| Member { - name: format!("_{}", idx), - ty: member.clone(), - key: false, - }) - .collect::>() - .as_slice(), - &mut self.executor, - indexes, - )?; - - for (idx, member) in t.iter().enumerate() { - let mut path_clone = path.clone(); - path_clone.push(format!("_{}", idx)); - self.build_set_entity_queries_recursive( - path_clone, - event_id, - (entity_id, is_event_message), - (member, is_store_update_member), - block_timestamp, - indexes, - )?; - } - } - Ty::Array(array) => { - // delete all previous array elements with the array indexes - let table_id = path.join("$"); - let mut query = - format!("DELETE FROM [{table_id}] WHERE entity_id = ? ", table_id = table_id); - for idx in 0..indexes.len() { - query.push_str(&format!("AND idx_{} = ? ", idx)); - } - - // flatten indexes with entity id - let mut arguments = vec![Argument::String(entity_id.to_string())]; - arguments.extend(indexes.iter().map(|idx| Argument::Int(*idx))); - - self.executor.send(QueryMessage::other(query, arguments))?; - - // insert the new array elements - for (idx, member) in array.iter().enumerate() { - let mut indexes = indexes.clone(); - indexes.push(idx as i64); - - update_members( - &[Member { name: "data".to_string(), ty: member.clone(), key: false }], - &mut self.executor, - &indexes, - )?; - - let mut path_clone = path.clone(); - path_clone.push("data".to_string()); - self.build_set_entity_queries_recursive( - path_clone, - event_id, - (entity_id, is_event_message), - (member, is_store_update_member), - block_timestamp, - &indexes, - )?; - } - } - _ => {} - } - - Ok(()) - } - - fn build_delete_entity_queries_recursive( - &mut self, - path: Vec, - entity_id: &str, - entity: &Ty, - ) -> Result<()> { - match entity { - Ty::Struct(s) => { - let table_id = path.join("$"); - let statement = format!("DELETE FROM [{table_id}] WHERE entity_id = ?"); - self.executor.send(QueryMessage::other( - statement, - vec![Argument::String(entity_id.to_string())], - ))?; - for member in s.children.iter() { - let mut path_clone = path.clone(); - path_clone.push(member.name.clone()); - self.build_delete_entity_queries_recursive(path_clone, entity_id, &member.ty)?; - } - } - Ty::Enum(e) => { - if e.options - .iter() - .all(|o| if let Ty::Tuple(t) = &o.ty { t.is_empty() } else { false }) - { - return Ok(()); - } - - let table_id = path.join("$"); - let statement = format!("DELETE FROM [{table_id}] WHERE entity_id = ?"); - self.executor.send(QueryMessage::other( - statement, - vec![Argument::String(entity_id.to_string())], - ))?; - - for child in e.options.iter() { - if let Ty::Tuple(t) = &child.ty { - if t.is_empty() { - continue; - } - } - - let mut path_clone = path.clone(); - path_clone.push(child.name.clone()); - self.build_delete_entity_queries_recursive(path_clone, entity_id, &child.ty)?; - } - } - Ty::Array(array) => { - let table_id = path.join("$"); - let statement = format!("DELETE FROM [{table_id}] WHERE entity_id = ?"); - self.executor.send(QueryMessage::other( - statement, - vec![Argument::String(entity_id.to_string())], - ))?; - - for member in array.iter() { - let mut path_clone = path.clone(); - path_clone.push("data".to_string()); - self.build_delete_entity_queries_recursive(path_clone, entity_id, member)?; - } - } - Ty::Tuple(t) => { - let table_id = path.join("$"); - let statement = format!("DELETE FROM [{table_id}] WHERE entity_id = ?"); - self.executor.send(QueryMessage::other( - statement, - vec![Argument::String(entity_id.to_string())], - ))?; - - for (idx, member) in t.iter().enumerate() { - let mut path_clone = path.clone(); - path_clone.push(format!("_{}", idx)); - self.build_delete_entity_queries_recursive(path_clone, entity_id, member)?; - } - } - _ => {} - } - - Ok(()) - } - - #[allow(clippy::too_many_arguments)] - fn build_model_query( - &mut self, - selector: Felt, - path: Vec, - model: &Ty, - model_idx: i64, - block_timestamp: u64, - array_idx: usize, - parent_array_idx: usize, - upgrade_diff: Option<&Ty>, - ) -> Result<()> { - let table_id = path.join("$"); - let mut indices = Vec::new(); - - let mut create_table_query = format!( - "CREATE TABLE IF NOT EXISTS [{table_id}] (id TEXT NOT NULL, event_id TEXT NOT NULL, \ - entity_id TEXT, event_message_id TEXT, " - ); - - let mut alter_table_queries = Vec::new(); - - if array_idx > 0 { - // index columns - for i in 0..array_idx { - let column = format!("idx_{i} INTEGER NOT NULL"); - create_table_query.push_str(&format!("{column}, ")); - - alter_table_queries.push(format!( - "ALTER TABLE [{table_id}] ADD COLUMN idx_{i} INTEGER NOT NULL DEFAULT 0" - )); - } - - // full array id column - create_table_query.push_str("full_array_id TEXT NOT NULL UNIQUE, "); - alter_table_queries.push(format!( - "ALTER TABLE [{table_id}] ADD COLUMN full_array_id TEXT NOT NULL UNIQUE DEFAULT ''" - )); - } - - let mut build_member = |name: &str, ty: &Ty, options: &mut Option| { - if let Ok(cairo_type) = Primitive::from_str(&ty.name()) { - let sql_type = cairo_type.to_sql_type(); - let column = format!("external_{name} {sql_type}"); - - create_table_query.push_str(&format!("{column}, ")); - - alter_table_queries.push(format!( - "ALTER TABLE [{table_id}] ADD COLUMN external_{name} {sql_type}" - )); - - indices.push(format!( - "CREATE INDEX IF NOT EXISTS [idx_{table_id}_{name}] ON [{table_id}] \ - (external_{name});" - )); - } else if let Ty::Enum(e) = &ty { - let all_options = e - .options - .iter() - .map(|c| format!("'{}'", c.name)) - .collect::>() - .join(", "); - - let column = - format!("external_{name} TEXT CHECK(external_{name} IN ({all_options}))",); - - create_table_query.push_str(&format!("{column}, ")); - - alter_table_queries.push(format!("ALTER TABLE [{table_id}] ADD COLUMN {column}")); - - indices.push(format!( - "CREATE INDEX IF NOT EXISTS [idx_{table_id}_{name}] ON [{table_id}] \ - (external_{name});" - )); - - *options = Some(Argument::String( - e.options - .iter() - .map(|c: &dojo_types::schema::EnumOption| c.name.clone()) - .collect::>() - .join(",") - .to_string(), - )); - } else if let Ty::ByteArray(_) = &ty { - let column = format!("external_{name} TEXT"); - - create_table_query.push_str(&format!("{column}, ")); - - alter_table_queries.push(format!("ALTER TABLE [{table_id}] ADD COLUMN {column}")); - - indices.push(format!( - "CREATE INDEX IF NOT EXISTS [idx_{table_id}_{name}] ON [{table_id}] \ - (external_{name});" - )); - } - }; - - match model { - Ty::Struct(s) => { - for (member_idx, member) in s.children.iter().enumerate() { - if let Some(upgrade_diff) = upgrade_diff { - if !upgrade_diff - .as_struct() - .unwrap() - .children - .iter() - .any(|m| m.name == member.name) - { - continue; - } - } - - let name = member.name.clone(); - let mut options = None; // TEMP: doesnt support complex enums yet - - build_member(&name, &member.ty, &mut options); - - // NOTE: this might cause some errors to fail silently - // due to the ignore clause. check migrations for type_enum check - let statement = "INSERT OR IGNORE INTO model_members (id, model_id, \ - model_idx, member_idx, name, type, type_enum, enum_options, \ - key, executed_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; - - let arguments = vec![ - Argument::String(table_id.clone()), - // TEMP: this is temporary until the model hash is precomputed - Argument::String(format!("{:#x}", selector)), - Argument::Int(model_idx), - Argument::Int(member_idx as i64), - Argument::String(name), - Argument::String(member.ty.name()), - Argument::String(member.ty.as_ref().into()), - options.unwrap_or(Argument::Null), - Argument::Bool(member.key), - Argument::String(utc_dt_string_from_timestamp(block_timestamp)), - ]; - - self.executor.send(QueryMessage::other(statement.to_string(), arguments))?; - } - } - Ty::Tuple(tuple) => { - for (idx, member) in tuple.iter().enumerate() { - let mut options = None; // TEMP: doesnt support complex enums yet - - build_member(&format!("_{}", idx), member, &mut options); - - let statement = "INSERT OR IGNORE INTO model_members (id, model_id, \ - model_idx, member_idx, name, type, type_enum, enum_options, \ - key, executed_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; - let arguments = vec![ - Argument::String(table_id.clone()), - // TEMP: this is temporary until the model hash is precomputed - Argument::String(format!("{:#x}", selector)), - Argument::Int(model_idx), - Argument::Int(idx as i64), - Argument::String(format!("_{}", idx)), - Argument::String(member.name()), - Argument::String(member.as_ref().into()), - options.unwrap_or(Argument::Null), - // NOTE: should we consider the case where - // a tuple is used as a key? should its members be keys? - Argument::Bool(false), - Argument::String(utc_dt_string_from_timestamp(block_timestamp)), - ]; - - self.executor.send(QueryMessage::other(statement.to_string(), arguments))?; - } - } - Ty::Array(array) => { - let mut options = None; // TEMP: doesnt support complex enums yet - let ty = &array[0]; - build_member("data", ty, &mut options); - - let statement = "INSERT OR IGNORE INTO model_members (id, model_id, model_idx, \ - member_idx, name, type, type_enum, enum_options, key, \ - executed_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; - let arguments = vec![ - Argument::String(table_id.clone()), - // TEMP: this is temporary until the model hash is precomputed - Argument::String(format!("{:#x}", selector)), - Argument::Int(model_idx), - Argument::Int(0), - Argument::String("data".to_string()), - Argument::String(ty.name()), - Argument::String(ty.as_ref().into()), - options.unwrap_or(Argument::Null), - Argument::Bool(false), - Argument::String(utc_dt_string_from_timestamp(block_timestamp)), - ]; - - self.executor.send(QueryMessage::other(statement.to_string(), arguments))?; - } - Ty::Enum(e) => { - for (idx, child) in e - .options - .iter() - .chain(vec![&EnumOption { - name: "option".to_string(), - ty: Ty::Enum(e.clone()), - }]) - .enumerate() - { - // Skip enum options that have no type / member - if let Ty::Tuple(tuple) = &child.ty { - if tuple.is_empty() { - continue; - } - } - - let mut options = None; // TEMP: doesnt support complex enums yet - build_member(&child.name, &child.ty, &mut options); - - let statement = "INSERT OR IGNORE INTO model_members (id, model_id, \ - model_idx, member_idx, name, type, type_enum, enum_options, \ - key, executed_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; - let arguments = vec![ - Argument::String(table_id.clone()), - // TEMP: this is temporary until the model hash is precomputed - Argument::String(format!("{:#x}", selector)), - Argument::Int(model_idx), - Argument::Int(idx as i64), - Argument::String(child.name.clone()), - Argument::String(child.ty.name()), - Argument::String(child.ty.as_ref().into()), - options.unwrap_or(Argument::Null), - Argument::Bool(false), - Argument::String(utc_dt_string_from_timestamp(block_timestamp)), - ]; - - self.executor.send(QueryMessage::other(statement.to_string(), arguments))?; - } - } - _ => {} - } - - create_table_query.push_str("executed_at DATETIME NOT NULL, "); - create_table_query.push_str("created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, "); - create_table_query.push_str("updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, "); - - // If this is not the Model's root table, create a reference to the parent. - if path.len() > 1 { - let parent_table_id = path[..path.len() - 1].join("$"); - - create_table_query.push_str("FOREIGN KEY (id"); - for i in 0..parent_array_idx { - create_table_query.push_str(&format!(", idx_{i}", i = i)); - } - create_table_query.push_str(&format!( - ") REFERENCES [{parent_table_id}] (id", - parent_table_id = parent_table_id - )); - for i in 0..parent_array_idx { - create_table_query.push_str(&format!(", idx_{i}", i = i)); - } - create_table_query.push_str(") ON DELETE CASCADE, "); - }; - - create_table_query.push_str("PRIMARY KEY (id"); - for i in 0..array_idx { - create_table_query.push_str(&format!(", idx_{i}", i = i)); - } - create_table_query.push_str("), "); - - create_table_query.push_str("FOREIGN KEY (entity_id) REFERENCES entities(id), "); - // create_table_query.push_str("FOREIGN KEY (event_id) REFERENCES events(id), "); - create_table_query - .push_str("FOREIGN KEY (event_message_id) REFERENCES event_messages(id));"); - - if upgrade_diff.is_some() { - for alter_query in alter_table_queries { - self.executor.send(QueryMessage::other(alter_query, vec![]))?; - } - } else { - self.executor.send(QueryMessage::other(create_table_query, vec![]))?; - } - - for index_query in indices { - self.executor.send(QueryMessage::other(index_query, vec![]))?; - } - - Ok(()) - } - - pub async fn execute(&self) -> Result<()> { - let (execute, recv) = QueryMessage::execute_recv(); - self.executor.send(execute)?; - recv.await? - } - - pub async fn flush(&self) -> Result<()> { - let (flush, recv) = QueryMessage::flush_recv(); - self.executor.send(flush)?; - recv.await? - } - - pub async fn rollback(&self) -> Result<()> { - let (rollback, recv) = QueryMessage::rollback_recv(); - self.executor.send(rollback)?; - recv.await? - } -} diff --git a/crates/torii/core/src/utils.rs b/crates/torii/core/src/utils.rs deleted file mode 100644 index 516e739e8a..0000000000 --- a/crates/torii/core/src/utils.rs +++ /dev/null @@ -1,82 +0,0 @@ -use std::time::Duration; - -use anyhow::Result; -use chrono::{DateTime, Utc}; -use futures_util::TryStreamExt; -use ipfs_api_backend_hyper::{IpfsApi, IpfsClient, TryFromUri}; -use tokio_util::bytes::Bytes; -use tracing::info; - -use crate::constants::{ - IPFS_CLIENT_MAX_RETRY, IPFS_CLIENT_PASSWORD, IPFS_CLIENT_URL, IPFS_CLIENT_USERNAME, -}; - -pub fn must_utc_datetime_from_timestamp(timestamp: u64) -> DateTime { - let naive_dt = DateTime::from_timestamp(timestamp as i64, 0) - .expect("Failed to convert timestamp to NaiveDateTime"); - naive_dt.to_utc() -} - -pub fn utc_dt_string_from_timestamp(timestamp: u64) -> String { - must_utc_datetime_from_timestamp(timestamp).to_rfc3339() -} - -pub async fn fetch_content_from_ipfs(cid: &str, mut retries: u8) -> Result { - let client = IpfsClient::from_str(IPFS_CLIENT_URL)? - .with_credentials(IPFS_CLIENT_USERNAME, IPFS_CLIENT_PASSWORD); - while retries > 0 { - let response = client.cat(cid).map_ok(|chunk| chunk.to_vec()).try_concat().await; - match response { - Ok(stream) => return Ok(Bytes::from(stream)), - Err(e) => { - retries -= 1; - if retries > 0 { - info!( - error = %e, - "Fetch uri." - ); - tokio::time::sleep(Duration::from_secs(3)).await; - } - } - } - } - - Err(anyhow::anyhow!(format!( - "Failed to pull data from IPFS after {} attempts, cid: {}", - IPFS_CLIENT_MAX_RETRY, cid - ))) -} -// tests -#[cfg(test)] -mod tests { - use chrono::{DateTime, NaiveDate, NaiveTime, Utc}; - - use super::*; - - #[test] - fn test_must_utc_datetime_from_timestamp() { - let timestamp = 1633027200; - let expected_date = NaiveDate::from_ymd_opt(2021, 9, 30).unwrap(); - let expected_time = NaiveTime::from_hms_opt(18, 40, 0).unwrap(); - let expected = - DateTime::::from_naive_utc_and_offset(expected_date.and_time(expected_time), Utc); - let out = must_utc_datetime_from_timestamp(timestamp); - assert_eq!(out, expected, "Failed to convert timestamp to DateTime"); - } - - #[test] - #[should_panic(expected = "Failed to convert timestamp to NaiveDateTime")] - fn test_must_utc_datetime_from_timestamp_incorrect_timestamp() { - let timestamp = i64::MAX as u64 + 1; - let _result = must_utc_datetime_from_timestamp(timestamp); - } - - #[test] - fn test_utc_dt_string_from_timestamp() { - let timestamp = 1633027200; - let expected = "2021-09-30T18:40:00+00:00"; - let out = utc_dt_string_from_timestamp(timestamp); - println!("{}", out); - assert_eq!(out, expected, "Failed to convert timestamp to String"); - } -} diff --git a/crates/torii/graphql/Cargo.toml b/crates/torii/graphql/Cargo.toml index e34e21f1ac..28bf116330 100644 --- a/crates/torii/graphql/Cargo.toml +++ b/crates/torii/graphql/Cargo.toml @@ -28,7 +28,8 @@ strum_macros.workspace = true thiserror.workspace = true tokio.workspace = true tokio-stream = "0.1.11" -torii-core.workspace = true +torii-sqlite.workspace = true +torii-indexer.workspace = true tracing.workspace = true url.workspace = true warp.workspace = true @@ -37,7 +38,7 @@ warp.workspace = true camino.workspace = true dojo-test-utils = { workspace = true, features = [ "build-examples" ] } dojo-utils.workspace = true -dojo-world = { workspace = true, features = [ "metadata" ] } +dojo-world = { workspace = true, features = [ "ipfs" ] } katana-runner.workspace = true scarb.workspace = true serial_test = "2.0.0" diff --git a/crates/torii/graphql/src/constants.rs b/crates/torii/graphql/src/constants.rs index f309b82b91..fc7d7c0285 100644 --- a/crates/torii/graphql/src/constants.rs +++ b/crates/torii/graphql/src/constants.rs @@ -12,8 +12,8 @@ pub const METADATA_TABLE: &str = "metadata"; pub const ID_COLUMN: &str = "id"; pub const EVENT_ID_COLUMN: &str = "event_id"; -pub const ENTITY_ID_COLUMN: &str = "entity_id"; -pub const EVENT_MESSAGE_ID_COLUMN: &str = "event_message_id"; +pub const ENTITY_ID_COLUMN: &str = "internal_entity_id"; +pub const EVENT_MESSAGE_ID_COLUMN: &str = "internal_event_message_id"; pub const JSON_COLUMN: &str = "json"; pub const TRANSACTION_HASH_COLUMN: &str = "transaction_hash"; diff --git a/crates/torii/graphql/src/mapping.rs b/crates/torii/graphql/src/mapping.rs index 190cbf4b62..02bbd5618d 100644 --- a/crates/torii/graphql/src/mapping.rs +++ b/crates/torii/graphql/src/mapping.rs @@ -100,8 +100,8 @@ lazy_static! { ), ]); pub static ref PAGE_INFO_TYPE_MAPPING: TypeMapping = TypeMapping::from([ - (Name::new("hasPreviousPage"), TypeData::Simple(TypeRef::named(TypeRef::BOOLEAN))), - (Name::new("hasNextPage"), TypeData::Simple(TypeRef::named(TypeRef::BOOLEAN))), + (Name::new("hasPreviousPage"), TypeData::Simple(TypeRef::named_nn(TypeRef::BOOLEAN))), + (Name::new("hasNextPage"), TypeData::Simple(TypeRef::named_nn(TypeRef::BOOLEAN))), ( Name::new("startCursor"), TypeData::Simple(TypeRef::named(GraphqlType::Cursor.to_string())), @@ -160,7 +160,7 @@ lazy_static! { pub static ref ERC20_TOKEN_TYPE_MAPPING: TypeMapping = IndexMap::from([ (Name::new("name"), TypeData::Simple(TypeRef::named_nn(TypeRef::STRING))), (Name::new("symbol"), TypeData::Simple(TypeRef::named_nn(TypeRef::STRING))), - (Name::new("decimals"), TypeData::Simple(TypeRef::named_nn(TypeRef::STRING))), + (Name::new("decimals"), TypeData::Simple(TypeRef::named_nn(TypeRef::INT))), (Name::new("contractAddress"), TypeData::Simple(TypeRef::named_nn(TypeRef::STRING))), (Name::new("amount"), TypeData::Simple(TypeRef::named_nn(TypeRef::STRING))), ]); @@ -171,9 +171,9 @@ lazy_static! { (Name::new("tokenId"), TypeData::Simple(TypeRef::named_nn(TypeRef::STRING))), (Name::new("contractAddress"), TypeData::Simple(TypeRef::named_nn(TypeRef::STRING))), (Name::new("metadata"), TypeData::Simple(TypeRef::named_nn(TypeRef::STRING))), - (Name::new("metadataName"), TypeData::Simple(TypeRef::named_nn(TypeRef::STRING))), - (Name::new("metadataDescription"), TypeData::Simple(TypeRef::named_nn(TypeRef::STRING))), - (Name::new("metadataAttributes"), TypeData::Simple(TypeRef::named_nn(TypeRef::STRING))), + (Name::new("metadataName"), TypeData::Simple(TypeRef::named(TypeRef::STRING))), + (Name::new("metadataDescription"), TypeData::Simple(TypeRef::named(TypeRef::STRING))), + (Name::new("metadataAttributes"), TypeData::Simple(TypeRef::named(TypeRef::STRING))), (Name::new("imagePath"), TypeData::Simple(TypeRef::named_nn(TypeRef::STRING))), ]); diff --git a/crates/torii/graphql/src/object/connection/mod.rs b/crates/torii/graphql/src/object/connection/mod.rs index dc9931ac58..540a59c574 100644 --- a/crates/torii/graphql/src/object/connection/mod.rs +++ b/crates/torii/graphql/src/object/connection/mod.rs @@ -111,13 +111,15 @@ pub fn connection_arguments(field: Field) -> Field { .argument(InputValue::new("limit", TypeRef::named(TypeRef::INT))) } +#[allow(clippy::too_many_arguments)] pub fn connection_output( data: &[SqliteRow], types: &TypeMapping, order: &Option, id_column: &str, total_count: i64, - is_external: bool, + is_internal: bool, + snake_case: bool, page_info: PageInfo, ) -> sqlx::Result { let model_edges = data @@ -125,8 +127,8 @@ pub fn connection_output( .map(|row| { let order_field = match order { Some(order) => { - if is_external { - format!("external_{}", order.field) + if is_internal { + format!("internal_{}", order.field) } else { order.field.to_string() } @@ -136,7 +138,7 @@ pub fn connection_output( let primary_order = row.try_get::(id_column)?; let secondary_order = row.try_get_unchecked::(&order_field)?; let cursor = cursor::encode(&primary_order, &secondary_order); - let value_mapping = value_mapping_from_row(row, types, is_external)?; + let value_mapping = value_mapping_from_row(row, types, is_internal, snake_case)?; let mut edge = ValueMapping::new(); edge.insert(Name::new("node"), Value::Object(value_mapping)); diff --git a/crates/torii/graphql/src/object/entity.rs b/crates/torii/graphql/src/object/entity.rs index 357b428979..1860fec4b2 100644 --- a/crates/torii/graphql/src/object/entity.rs +++ b/crates/torii/graphql/src/object/entity.rs @@ -3,24 +3,21 @@ use async_graphql::dynamic::{ Field, FieldFuture, FieldValue, InputValue, SubscriptionField, SubscriptionFieldFuture, TypeRef, }; use async_graphql::{Name, Value}; -use async_recursion::async_recursion; use dojo_types::naming::get_tag; -use sqlx::pool::PoolConnection; +use dojo_types::schema::Ty; use sqlx::{Pool, Sqlite}; use tokio_stream::StreamExt; -use torii_core::simple_broker::SimpleBroker; -use torii_core::types::Entity; +use torii_sqlite::simple_broker::SimpleBroker; +use torii_sqlite::types::Entity; use super::inputs::keys_input::keys_argument; use super::{BasicObject, ResolvableObject, TypeMapping, ValueMapping}; use crate::constants::{ - DATETIME_FORMAT, ENTITY_ID_COLUMN, ENTITY_NAMES, ENTITY_TABLE, ENTITY_TYPE_NAME, - EVENT_ID_COLUMN, ID_COLUMN, + DATETIME_FORMAT, ENTITY_NAMES, ENTITY_TABLE, ENTITY_TYPE_NAME, EVENT_ID_COLUMN, ID_COLUMN, }; use crate::mapping::ENTITY_TYPE_MAPPING; use crate::object::{resolve_many, resolve_one}; -use crate::query::{type_mapping_query, value_mapping_from_row}; -use crate::types::TypeData; +use crate::query::{build_type_mapping, value_mapping_from_row}; use crate::utils; #[derive(Debug)] pub struct EntityObject; @@ -124,7 +121,7 @@ fn model_union_field() -> Field { // fetch name from the models table // using the model id (hashed model name) let model_ids: Vec<(String, String, String)> = sqlx::query_as( - "SELECT id, namespace, name + "SELECT namespace, name, schema FROM models WHERE id IN ( SELECT model_id @@ -137,25 +134,23 @@ fn model_union_field() -> Field { .await?; let mut results: Vec> = Vec::new(); - for (id, namespace, name) in model_ids { - // the model id in the model mmeebrs table is the hashed model name (id) - let type_mapping = type_mapping_query(&mut conn, &id).await?; + for (namespace, name, schema) in model_ids { + let schema: Ty = serde_json::from_str(&schema).map_err(|e| { + anyhow::anyhow!(format!("Failed to parse model schema: {e}")) + })?; + let type_mapping = build_type_mapping(&namespace, &schema); - // but the table name for the model data is the unhashed model name - let data: ValueMapping = match model_data_recursive_query( - &mut conn, - ENTITY_ID_COLUMN, - vec![get_tag(&namespace, &name)], - &entity_id, - &[], - &type_mapping, - false, - ) - .await? - { - Value::Object(map) => map, - _ => unreachable!(), - }; + // Get the table name + let table_name = get_tag(&namespace, &name); + + // Fetch the row data + let query = + format!("SELECT * FROM [{}] WHERE internal_entity_id = ?", table_name); + let row = + sqlx::query(&query).bind(&entity_id).fetch_one(&mut *conn).await?; + + // Use value_mapping_from_row to handle nested structures + let data = value_mapping_from_row(&row, &type_mapping, false, false)?; results.push(FieldValue::with_type( FieldValue::owned_any(data), @@ -170,116 +165,3 @@ fn model_union_field() -> Field { }) }) } - -// TODO: flatten query -#[async_recursion] -pub async fn model_data_recursive_query( - conn: &mut PoolConnection, - entity_id_column: &str, - path_array: Vec, - entity_id: &str, - indexes: &[i64], - type_mapping: &TypeMapping, - is_list: bool, -) -> sqlx::Result { - // For nested types, we need to remove prefix in path array - let namespace = format!("{}_", path_array[0]); - let table_name = &path_array.join("$").replace(&namespace, ""); - let mut query = - format!("SELECT * FROM [{}] WHERE {entity_id_column} = '{}' ", table_name, entity_id); - for (column_idx, index) in indexes.iter().enumerate() { - query.push_str(&format!("AND idx_{} = {} ", column_idx, index)); - } - - let rows = sqlx::query(&query).fetch_all(conn.as_mut()).await?; - if rows.is_empty() { - return Ok(Value::List(vec![])); - } - - let value_mapping: Value; - let mut nested_value_mappings = Vec::new(); - - for (idx, row) in rows.iter().enumerate() { - let mut nested_value_mapping = value_mapping_from_row(row, type_mapping, true)?; - - for (field_name, type_data) in type_mapping { - if let TypeData::Nested((_, nested_mapping)) = type_data { - let mut nested_path = path_array.clone(); - nested_path.push(field_name.to_string()); - - let nested_values = model_data_recursive_query( - conn, - entity_id_column, - nested_path, - entity_id, - &if is_list { - let mut indexes = indexes.to_vec(); - indexes.push(idx as i64); - indexes - } else { - indexes.to_vec() - }, - nested_mapping, - false, - ) - .await?; - - nested_value_mapping.insert(Name::new(field_name), nested_values); - } else if let TypeData::List(inner) = type_data { - let mut nested_path = path_array.clone(); - nested_path.push(field_name.to_string()); - - let data = match model_data_recursive_query( - conn, - entity_id_column, - nested_path, - entity_id, - // this might need to be changed to support 2d+ arrays - &if is_list { - let mut indexes = indexes.to_vec(); - indexes.push(idx as i64); - indexes - } else { - indexes.to_vec() - }, - &IndexMap::from([(Name::new("data"), *inner.clone())]), - true, - ) - .await? - { - // map our list which uses a data field as a place holder - // for all elements to get the elemnt directly - Value::List(data) => data - .iter() - .map(|v| match v { - Value::Object(map) => map.get(&Name::new("data")).unwrap().clone(), - ty => unreachable!( - "Expected Value::Object for list \"data\" field, got {:?}", - ty - ), - }) - .collect(), - Value::Object(map) => map.get(&Name::new("data")).unwrap().clone(), - ty => { - unreachable!( - "Expected Value::List or Value::Object for list, got {:?}", - ty - ); - } - }; - - nested_value_mapping.insert(Name::new(field_name), data); - } - } - - nested_value_mappings.push(Value::Object(nested_value_mapping)); - } - - if is_list { - value_mapping = Value::List(nested_value_mappings); - } else { - value_mapping = nested_value_mappings.pop().unwrap(); - } - - Ok(value_mapping) -} diff --git a/crates/torii/graphql/src/object/erc/token_balance.rs b/crates/torii/graphql/src/object/erc/token_balance.rs index 7fdcca4fe5..09ef725ed2 100644 --- a/crates/torii/graphql/src/object/erc/token_balance.rs +++ b/crates/torii/graphql/src/object/erc/token_balance.rs @@ -5,8 +5,8 @@ use serde::Deserialize; use sqlx::sqlite::SqliteRow; use sqlx::{FromRow, Pool, Row, Sqlite, SqliteConnection}; use starknet_crypto::Felt; -use torii_core::constants::TOKEN_BALANCE_TABLE; -use torii_core::sql::utils::felt_to_sql_string; +use torii_sqlite::constants::TOKEN_BALANCE_TABLE; +use torii_sqlite::utils::felt_to_sql_string; use tracing::warn; use super::erc_token::{Erc20Token, ErcTokenType}; @@ -103,7 +103,13 @@ async fn fetch_token_balances( JOIN tokens t ON b.token_id = t.id JOIN contracts c ON t.contract_address = c.contract_address" ); - let mut conditions = vec!["b.account_address = ?".to_string()]; + + // Only select balances for the given account address and non-zero balances. + let mut conditions = vec![ + "(b.account_address = ?)".to_string(), + "b.balance != '0x0000000000000000000000000000000000000000000000000000000000000000'" + .to_string(), + ]; let mut cursor_param = &connection.after; if let Some(after_cursor) = &connection.after { @@ -234,21 +240,41 @@ fn token_balances_connection_output<'a>( let token_id = row.token_id.split(':').collect::>(); assert!(token_id.len() == 2); - let metadata: serde_json::Value = - serde_json::from_str(&row.metadata).expect("metadata is always json"); - let metadata_name = - metadata.get("name").map(|v| v.to_string().trim_matches('"').to_string()); - let metadata_description = metadata - .get("description") - .map(|v| v.to_string().trim_matches('"').to_string()); - let metadata_attributes = - metadata.get("attributes").map(|v| v.to_string().trim_matches('"').to_string()); - - let image_path = format!("{}/{}", token_id.join("/"), "image"); + let metadata_str = row.metadata; + let ( + metadata_str, + metadata_name, + metadata_description, + metadata_attributes, + image_path, + ) = if metadata_str.is_empty() { + (String::new(), None, None, None, String::new()) + } else { + let metadata: serde_json::Value = + serde_json::from_str(&metadata_str).expect("metadata is always json"); + let metadata_name = + metadata.get("name").map(|v| v.to_string().trim_matches('"').to_string()); + let metadata_description = metadata + .get("description") + .map(|v| v.to_string().trim_matches('"').to_string()); + let metadata_attributes = metadata + .get("attributes") + .map(|v| v.to_string().trim_matches('"').to_string()); + + let image_path = format!("{}/{}", token_id.join("/"), "image"); + + ( + metadata_str, + metadata_name, + metadata_description, + metadata_attributes, + image_path, + ) + }; let token_metadata = Erc721Token { name: row.name, - metadata: row.metadata, + metadata: metadata_str.to_owned(), contract_address: row.contract_address, symbol: row.symbol, token_id: token_id[1].to_string(), diff --git a/crates/torii/graphql/src/object/erc/token_transfer.rs b/crates/torii/graphql/src/object/erc/token_transfer.rs index 1441937c2e..fe5a256571 100644 --- a/crates/torii/graphql/src/object/erc/token_transfer.rs +++ b/crates/torii/graphql/src/object/erc/token_transfer.rs @@ -5,9 +5,9 @@ use serde::Deserialize; use sqlx::sqlite::SqliteRow; use sqlx::{FromRow, Pool, Row, Sqlite, SqliteConnection}; use starknet_crypto::Felt; -use torii_core::constants::TOKEN_TRANSFER_TABLE; -use torii_core::engine::get_transaction_hash_from_event_id; -use torii_core::sql::utils::felt_to_sql_string; +use torii_indexer::engine::get_transaction_hash_from_event_id; +use torii_sqlite::constants::TOKEN_TRANSFER_TABLE; +use torii_sqlite::utils::felt_to_sql_string; use tracing::warn; use super::erc_token::{Erc20Token, ErcTokenType}; @@ -119,7 +119,10 @@ JOIN "#, ); - let mut conditions = vec!["et.from_address = ? OR et.to_address = ?".to_string()]; + let mut conditions = vec![ + "(et.from_address = ? OR et.to_address = ?)".to_string(), + "(t.metadata IS NULL OR length(t.metadata) > 0)".to_string(), + ]; let mut cursor_param = &connection.after; if let Some(after_cursor) = &connection.after { @@ -263,8 +266,9 @@ fn token_transfers_connection_output<'a>( let token_id = row.token_id.split(':').collect::>(); assert!(token_id.len() == 2); + let metadata_str = row.metadata; let metadata: serde_json::Value = - serde_json::from_str(&row.metadata).expect("metadata is always json"); + serde_json::from_str(&metadata_str).expect("metadata is always json"); let metadata_name = metadata.get("name").map(|v| v.to_string().trim_matches('"').to_string()); let metadata_description = metadata @@ -277,7 +281,7 @@ fn token_transfers_connection_output<'a>( let token_metadata = ErcTokenType::Erc721(Erc721Token { name: row.name, - metadata: row.metadata, + metadata: metadata_str.to_owned(), contract_address: row.contract_address, symbol: row.symbol, token_id: token_id[1].to_string(), diff --git a/crates/torii/graphql/src/object/event.rs b/crates/torii/graphql/src/object/event.rs index e553f4be32..6c9cdd2032 100644 --- a/crates/torii/graphql/src/object/event.rs +++ b/crates/torii/graphql/src/object/event.rs @@ -3,9 +3,9 @@ use async_graphql::dynamic::{ }; use async_graphql::{Name, Result, Value}; use tokio_stream::{Stream, StreamExt}; -use torii_core::constants::SQL_FELT_DELIMITER; -use torii_core::simple_broker::SimpleBroker; -use torii_core::types::Event; +use torii_sqlite::constants::SQL_FELT_DELIMITER; +use torii_sqlite::simple_broker::SimpleBroker; +use torii_sqlite::types::Event; use super::inputs::keys_input::{keys_argument, parse_keys_argument}; use super::{resolve_many, BasicObject, ResolvableObject, TypeMapping}; diff --git a/crates/torii/graphql/src/object/event_message.rs b/crates/torii/graphql/src/object/event_message.rs index 2173140bd2..d0597b643a 100644 --- a/crates/torii/graphql/src/object/event_message.rs +++ b/crates/torii/graphql/src/object/event_message.rs @@ -4,22 +4,23 @@ use async_graphql::dynamic::{ }; use async_graphql::{Name, Value}; use dojo_types::naming::get_tag; +use dojo_types::schema::Ty; use sqlx::{Pool, Sqlite}; use tokio_stream::StreamExt; -use torii_core::simple_broker::SimpleBroker; -use torii_core::types::EventMessage; +use torii_sqlite::simple_broker::SimpleBroker; +use torii_sqlite::types::EventMessage; -use super::entity::model_data_recursive_query; use super::inputs::keys_input::keys_argument; use super::{BasicObject, ResolvableObject, TypeMapping, ValueMapping}; use crate::constants::{ - DATETIME_FORMAT, EVENT_ID_COLUMN, EVENT_MESSAGE_ID_COLUMN, EVENT_MESSAGE_NAMES, - EVENT_MESSAGE_TABLE, EVENT_MESSAGE_TYPE_NAME, ID_COLUMN, + DATETIME_FORMAT, EVENT_ID_COLUMN, EVENT_MESSAGE_NAMES, EVENT_MESSAGE_TABLE, + EVENT_MESSAGE_TYPE_NAME, ID_COLUMN, }; use crate::mapping::ENTITY_TYPE_MAPPING; use crate::object::{resolve_many, resolve_one}; -use crate::query::type_mapping_query; +use crate::query::{build_type_mapping, value_mapping_from_row}; use crate::utils; + #[derive(Debug)] pub struct EventMessageObject; @@ -74,8 +75,6 @@ impl ResolvableObject for EventMessageObject { Some(id) => Some(id.string()?.to_string()), None => None, }; - // if id is None, then subscribe to all entities - // if id is Some, then subscribe to only the entity with that id Ok(SimpleBroker::::subscribe().filter_map( move |entity: EventMessage| { if id.is_none() || id == Some(entity.id.clone()) { @@ -83,7 +82,6 @@ impl ResolvableObject for EventMessageObject { entity, )))) } else { - // id != entity.id , then don't send anything, still listening None } }, @@ -128,9 +126,8 @@ fn model_union_field() -> Field { let entity_id = utils::extract::(indexmap, "id")?; // fetch name from the models table - // using the model id (hashed model name) let model_ids: Vec<(String, String, String)> = sqlx::query_as( - "SELECT id, namespace, name + "SELECT namespace, name, schema FROM models WHERE id IN ( SELECT model_id @@ -143,25 +140,25 @@ fn model_union_field() -> Field { .await?; let mut results: Vec> = Vec::new(); - for (id, namespace, name) in model_ids { - // the model id in the model mmeebrs table is the hashed model name (id) - let type_mapping = type_mapping_query(&mut conn, &id).await?; - - // but the table name for the model data is the unhashed model name - let data: ValueMapping = match model_data_recursive_query( - &mut conn, - EVENT_MESSAGE_ID_COLUMN, - vec![get_tag(&namespace, &name)], - &entity_id, - &[], - &type_mapping, - false, - ) - .await? - { - Value::Object(map) => map, - _ => unreachable!(), - }; + for (namespace, name, schema) in model_ids { + let schema: Ty = serde_json::from_str(&schema).map_err(|e| { + anyhow::anyhow!(format!("Failed to parse model schema: {e}")) + })?; + let type_mapping = build_type_mapping(&namespace, &schema); + + // Get the table name + let table_name = get_tag(&namespace, &name); + + // Fetch the row data + let query = format!( + "SELECT * FROM [{}] WHERE internal_event_message_id = ?", + table_name + ); + let row = + sqlx::query(&query).bind(&entity_id).fetch_one(&mut *conn).await?; + + // Use value_mapping_from_row to handle nested structures + let data = value_mapping_from_row(&row, &type_mapping, false, false)?; results.push(FieldValue::with_type( FieldValue::owned_any(data), diff --git a/crates/torii/graphql/src/object/inputs/where_input.rs b/crates/torii/graphql/src/object/inputs/where_input.rs index 8cea27172e..d78dd454f7 100644 --- a/crates/torii/graphql/src/object/inputs/where_input.rs +++ b/crates/torii/graphql/src/object/inputs/where_input.rs @@ -16,46 +16,84 @@ use crate::types::TypeData; pub struct WhereInputObject { pub type_name: String, pub type_mapping: TypeMapping, + pub nested_inputs: Vec, } impl WhereInputObject { - // Iterate through an object's type mapping and create a new mapping for whereInput. For each of - // the object type (model member), we add 6 additional types for comparators (great than, - // not equal, etc) - pub fn new(type_name: &str, object_types: &TypeMapping) -> Self { - let where_mapping = object_types - .iter() - .filter(|(_, type_data)| !type_data.is_nested() && !type_data.is_list()) - .flat_map(|(type_name, type_data)| { - // TODO: filter on nested and enum objects - if type_data.type_ref() == TypeRef::named("Enum") - || type_data.type_ref() == TypeRef::named("bool") - { - return vec![(Name::new(type_name), type_data.clone())]; + fn build_field_mapping(type_name: &str, type_data: &TypeData) -> Vec<(Name, TypeData)> { + if type_data.type_ref() == TypeRef::named("Enum") + || type_data.type_ref() == TypeRef::named("bool") + { + return vec![(Name::new(type_name), type_data.clone())]; + } + + Comparator::iter().fold( + vec![(Name::new(type_name), type_data.clone())], + |mut acc, comparator| { + let name = format!("{}{}", type_name, comparator.as_ref()); + match comparator { + Comparator::In | Comparator::NotIn => { + acc.push((Name::new(name), TypeData::List(Box::new(type_data.clone())))) + } + _ => { + acc.push((Name::new(name), type_data.clone())); + } } + acc + }, + ) + } - Comparator::iter().fold( - vec![(Name::new(type_name), type_data.clone())], - |mut acc, comparator| { - let name = format!("{}{}", type_name, comparator.as_ref()); - - match comparator { - Comparator::In | Comparator::NotIn => acc.push(( - Name::new(name), - TypeData::List(Box::new(type_data.clone())), + pub fn new(type_name: &str, object_types: &TypeMapping) -> Self { + let mut nested_inputs = Vec::new(); + let mut where_mapping = TypeMapping::new(); + + for (field_name, type_data) in object_types { + if !type_data.is_list() { + match type_data { + TypeData::Nested((_, nested_types)) => { + // Create nested input object + let nested_input = WhereInputObject::new( + &format!("{}_{}", type_name, field_name), + nested_types, + ); + + // Add field for the nested input using TypeData::Nested + where_mapping.insert( + Name::new(field_name), + TypeData::Nested(( + TypeRef::named(&nested_input.type_name), + nested_types.clone(), )), - _ => { - acc.push((Name::new(name), type_data.clone())); - } + ); + nested_inputs.push(nested_input); + } + _ => { + // Add regular field with comparators + for (name, mapped_type) in Self::build_field_mapping(field_name, type_data) + { + where_mapping.insert(name, mapped_type); } + } + } + } + } + + Self { + type_name: format!("{}WhereInput", type_name), + type_mapping: where_mapping, + nested_inputs, + } + } +} - acc - }, - ) - }) - .collect(); - - Self { type_name: format!("{}WhereInput", type_name), type_mapping: where_mapping } +impl WhereInputObject { + pub fn input_objects(&self) -> Vec { + let mut objects = vec![self.input_object()]; + for nested in &self.nested_inputs { + objects.extend(nested.input_objects()); + } + objects } } @@ -79,6 +117,77 @@ pub fn where_argument(field: Field, type_name: &str) -> Field { field.argument(InputValue::new("where", TypeRef::named(format!("{}WhereInput", type_name)))) } +fn parse_nested_where( + input_object: &ValueAccessor<'_>, + type_name: &str, + type_data: &TypeData, +) -> Result> { + match type_data { + TypeData::Nested((_, nested_mapping)) => { + let nested_input = input_object.object()?; + nested_mapping + .iter() + .filter_map(|(field_name, field_type)| { + nested_input.get(field_name).map(|input| { + let nested_filters = parse_where_value( + input, + &format!("{}.{}", type_name, field_name), + field_type, + )?; + Ok(nested_filters) + }) + }) + .collect::>>() + .map(|filters| filters.into_iter().flatten().collect()) + } + _ => Ok(vec![]), + } +} + +fn parse_where_value( + input: ValueAccessor<'_>, + field_path: &str, + type_data: &TypeData, +) -> Result> { + match type_data { + TypeData::Simple(_) => { + if type_data.type_ref() == TypeRef::named("Enum") { + let value = input.string()?; + let mut filter = + parse_filter(&Name::new(field_path), FilterValue::String(value.to_string())); + // complex enums have a nested option field for their variant name. + // we trim the .option suffix to get the actual db field name + filter.field = filter.field.trim_end_matches(".option").to_string(); + return Ok(vec![filter]); + } + + let primitive = Primitive::from_str(&type_data.type_ref().to_string())?; + let filter_value = match primitive.to_sql_type() { + SqlType::Integer => parse_integer(input, field_path, primitive)?, + SqlType::Text => parse_string(input, field_path, primitive)?, + }; + + Ok(vec![parse_filter(&Name::new(field_path), filter_value)]) + } + TypeData::List(inner) => { + let list = input.list()?; + let values = list + .iter() + .map(|value| { + let primitive = Primitive::from_str(&inner.type_ref().to_string())?; + match primitive.to_sql_type() { + SqlType::Integer => parse_integer(value, field_path, primitive), + SqlType::Text => parse_string(value, field_path, primitive), + } + }) + .collect::>>()?; + + Ok(vec![parse_filter(&Name::new(field_path), FilterValue::List(values))]) + } + TypeData::Nested(_) => parse_nested_where(&input, field_path, type_data), + } +} + pub fn parse_where_argument( ctx: &ResolverContext<'_>, where_mapping: &TypeMapping, @@ -87,44 +196,13 @@ pub fn parse_where_argument( let input_object = where_input.object()?; where_mapping .iter() - .filter_map(|(type_name, type_data)| { - input_object.get(type_name).map(|input| match type_data { - TypeData::Simple(_) => { - if type_data.type_ref() == TypeRef::named("Enum") { - let value = input.string().unwrap(); - return Ok(Some(parse_filter( - type_name, - FilterValue::String(value.to_string()), - ))); - } - - let primitive = Primitive::from_str(&type_data.type_ref().to_string())?; - let filter_value = match primitive.to_sql_type() { - SqlType::Integer => parse_integer(input, type_name, primitive)?, - SqlType::Text => parse_string(input, type_name, primitive)?, - }; - - Ok(Some(parse_filter(type_name, filter_value))) - } - TypeData::List(inner) => { - let list = input.list()?; - let values = list - .iter() - .map(|value| { - let primitive = Primitive::from_str(&inner.type_ref().to_string())?; - match primitive.to_sql_type() { - SqlType::Integer => parse_integer(value, type_name, primitive), - SqlType::Text => parse_string(value, type_name, primitive), - } - }) - .collect::>>()?; - - Ok(Some(parse_filter(type_name, FilterValue::List(values)))) - } - _ => Err(GqlError::new("Nested types are not supported")), - }) + .filter_map(|(field_name, type_data)| { + input_object + .get(field_name) + .map(|input| parse_where_value(input, field_name, type_data)) }) - .collect::>>>() + .collect::>>() + .map(|filters| Some(filters.into_iter().flatten().collect())) }) } diff --git a/crates/torii/graphql/src/object/metadata/mod.rs b/crates/torii/graphql/src/object/metadata/mod.rs index 07a7771380..8f5efca950 100644 --- a/crates/torii/graphql/src/object/metadata/mod.rs +++ b/crates/torii/graphql/src/object/metadata/mod.rs @@ -105,7 +105,7 @@ fn metadata_connection_output( .map(|row| { let order = row.try_get::(ID_COLUMN)?; let cursor = cursor::encode(&order, &order); - let mut value_mapping = value_mapping_from_row(row, row_types, false)?; + let mut value_mapping = value_mapping_from_row(row, row_types, false, true)?; value_mapping.insert(Name::new("worldAddress"), Value::from(world_address)); let json_str = row.try_get::(JSON_COLUMN)?; diff --git a/crates/torii/graphql/src/object/mod.rs b/crates/torii/graphql/src/object/mod.rs index b4201e0989..5791d606e2 100644 --- a/crates/torii/graphql/src/object/mod.rs +++ b/crates/torii/graphql/src/object/mod.rs @@ -261,7 +261,7 @@ pub fn resolve_one( let id: String = extract::(ctx.args.as_index_map(), &id_column.to_case(Case::Camel))?; let data = fetch_single_row(&mut conn, &table_name, &id_column, &id).await?; - let model = value_mapping_from_row(&data, &type_mapping, false)?; + let model = value_mapping_from_row(&data, &type_mapping, false, true)?; Ok(Some(Value::Object(model))) }) }) @@ -311,6 +311,7 @@ pub fn resolve_many( &id_column, total_count, false, + true, page_info, )?; diff --git a/crates/torii/graphql/src/object/model.rs b/crates/torii/graphql/src/object/model.rs index c12cf070d4..281187bca4 100644 --- a/crates/torii/graphql/src/object/model.rs +++ b/crates/torii/graphql/src/object/model.rs @@ -4,8 +4,8 @@ use async_graphql::dynamic::{ }; use async_graphql::{Name, Value}; use tokio_stream::StreamExt; -use torii_core::simple_broker::SimpleBroker; -use torii_core::types::Model; +use torii_sqlite::simple_broker::SimpleBroker; +use torii_sqlite::types::Model; use super::{resolve_many, BasicObject, ResolvableObject, TypeMapping, ValueMapping}; use crate::constants::{ diff --git a/crates/torii/graphql/src/object/model_data.rs b/crates/torii/graphql/src/object/model_data.rs index 689226b55c..c1d2a89150 100644 --- a/crates/torii/graphql/src/object/model_data.rs +++ b/crates/torii/graphql/src/object/model_data.rs @@ -1,9 +1,8 @@ use async_graphql::dynamic::{Enum, Field, FieldFuture, InputObject, Object, TypeRef}; use async_graphql::Value; -use chrono::{DateTime, Utc}; use dojo_types::naming::get_tag; -use serde::Deserialize; -use sqlx::{FromRow, Pool, Sqlite}; +use dojo_types::schema::Ty; +use sqlx::{Pool, Sqlite}; use super::connection::{connection_arguments, connection_output, parse_connection_arguments}; use super::inputs::order_input::{order_argument, parse_order_argument, OrderInputObject}; @@ -11,8 +10,8 @@ use super::inputs::where_input::{parse_where_argument, where_argument, WhereInpu use super::inputs::InputObjectTrait; use super::{BasicObject, ResolvableObject, TypeMapping, ValueMapping}; use crate::constants::{ - ENTITY_ID_COLUMN, ENTITY_TABLE, ENTITY_TYPE_NAME, EVENT_ID_COLUMN, EVENT_MESSAGE_TABLE, - EVENT_MESSAGE_TYPE_NAME, ID_COLUMN, INTERNAL_ENTITY_ID_KEY, + ENTITY_ID_COLUMN, ENTITY_TABLE, ENTITY_TYPE_NAME, EVENT_MESSAGE_TABLE, EVENT_MESSAGE_TYPE_NAME, + ID_COLUMN, INTERNAL_ENTITY_ID_KEY, }; use crate::mapping::ENTITY_TYPE_MAPPING; use crate::query::data::{count_rows, fetch_multiple_rows, fetch_single_row}; @@ -20,36 +19,23 @@ use crate::query::value_mapping_from_row; use crate::types::TypeData; use crate::utils; -#[derive(FromRow, Deserialize, PartialEq, Eq, Debug)] -pub struct ModelMember { - pub id: String, - pub model_id: String, - pub model_idx: i64, - pub name: String, - #[serde(rename = "type")] - pub ty: String, - pub type_enum: String, - pub key: bool, - pub executed_at: DateTime, - pub created_at: DateTime, -} - #[derive(Debug)] pub struct ModelDataObject { pub name: String, pub plural_name: String, pub type_name: String, pub type_mapping: TypeMapping, + pub schema: Ty, pub where_input: WhereInputObject, pub order_input: OrderInputObject, } impl ModelDataObject { - pub fn new(name: String, type_name: String, type_mapping: TypeMapping) -> Self { + pub fn new(name: String, type_name: String, type_mapping: TypeMapping, schema: Ty) -> Self { let where_input = WhereInputObject::new(type_name.as_str(), &type_mapping); let order_input = OrderInputObject::new(type_name.as_str(), &type_mapping); let plural_name = format!("{}Models", name); - Self { name, plural_name, type_name, type_mapping, where_input, order_input } + Self { name, plural_name, type_name, type_mapping, schema, where_input, order_input } } } @@ -88,7 +74,10 @@ impl BasicObject for ModelDataObject { impl ResolvableObject for ModelDataObject { fn input_objects(&self) -> Option> { - Some(vec![self.where_input.input_object(), self.order_input.input_object()]) + let mut objects = vec![]; + objects.extend(self.where_input.input_objects()); + objects.push(self.order_input.input_object()); + Some(objects) } fn enum_objects(&self) -> Option> { @@ -119,7 +108,7 @@ impl ResolvableObject for ModelDataObject { let (data, page_info) = fetch_multiple_rows( &mut conn, &table_name, - EVENT_ID_COLUMN, + "internal_event_id", &None, &order, &filters, @@ -131,9 +120,10 @@ impl ResolvableObject for ModelDataObject { &data, &type_mapping, &order, - EVENT_ID_COLUMN, + "internal_event_id", total_count, - true, + false, + false, page_info, )?; @@ -214,7 +204,8 @@ pub fn object(type_name: &str, type_mapping: &TypeMapping, path_array: Vec Field { let entity_id = utils::extract::(indexmap, INTERNAL_ENTITY_ID_KEY)?; let data = fetch_single_row(&mut conn, ENTITY_TABLE, ID_COLUMN, &entity_id).await?; - let entity = value_mapping_from_row(&data, &ENTITY_TYPE_MAPPING, false)?; + let entity = value_mapping_from_row(&data, &ENTITY_TYPE_MAPPING, false, true)?; Ok(Some(Value::Object(entity))) } @@ -276,7 +267,8 @@ fn event_message_field() -> Field { let data = fetch_single_row(&mut conn, EVENT_MESSAGE_TABLE, ID_COLUMN, &entity_id) .await?; - let event_message = value_mapping_from_row(&data, &ENTITY_TYPE_MAPPING, false)?; + let event_message = + value_mapping_from_row(&data, &ENTITY_TYPE_MAPPING, false, true)?; Ok(Some(Value::Object(event_message))) } diff --git a/crates/torii/graphql/src/query/data.rs b/crates/torii/graphql/src/query/data.rs index 5cf1fb5c76..83a5617621 100644 --- a/crates/torii/graphql/src/query/data.rs +++ b/crates/torii/graphql/src/query/data.rs @@ -1,11 +1,11 @@ use async_graphql::connection::PageInfo; use sqlx::sqlite::SqliteRow; use sqlx::{Result, Row, SqliteConnection}; -use torii_core::constants::WORLD_CONTRACT_TYPE; +use torii_sqlite::constants::WORLD_CONTRACT_TYPE; use super::filter::{Filter, FilterValue}; use super::order::{CursorDirection, Direction, Order}; -use crate::constants::{DEFAULT_LIMIT, MODEL_TABLE}; +use crate::constants::DEFAULT_LIMIT; use crate::object::connection::{cursor, ConnectionArguments}; pub async fn count_rows( @@ -87,10 +87,7 @@ pub async fn fetch_multiple_rows( // `first` or `last` param. Explicit ordering take precedence match order { Some(order) => { - let mut column_name = order.field.clone(); - if table_name != MODEL_TABLE { - column_name = format!("external_{}", column_name); - } + let column_name = order.field.clone(); query.push_str(&format!( " ORDER BY {column_name} {}, {id_column} {} LIMIT {limit}", order.direction.as_ref(), @@ -127,9 +124,10 @@ pub async fn fetch_multiple_rows( Ok((data, page_info)) } else if is_cursor_based { let order_field = match order { - Some(order) => format!("external_{}", order.field), + Some(order) => order.field.clone(), None => id_column.to_string(), }; + match cursor_param { Some(cursor_query) => { let first_cursor = cursor::encode( @@ -190,20 +188,17 @@ fn handle_cursor( ) -> Result { match cursor::decode(cursor) { Ok((event_id, field_value)) => match order { - Some(order) => { - let field_name = format!("external_{}", order.field); - Ok(format!( - "(({} {} '{}' AND {} = '{}') OR {} {} '{}')", - id_column, - direction.as_ref(), - event_id, - field_name, - field_value, - field_name, - direction.as_ref(), - field_value - )) - } + Some(order) => Ok(format!( + "(({} {} '{}' AND {} = '{}') OR {} {} '{}')", + id_column, + direction.as_ref(), + event_id, + order.field, + field_value, + order.field, + direction.as_ref(), + field_value + )), None => Ok(format!("{} {} '{}'", id_column, direction.as_ref(), event_id)), }, Err(_) => Err(sqlx::Error::Decode("Invalid cursor format".into())), @@ -227,8 +222,8 @@ fn build_conditions(keys: &Option>, filters: &Option>) - if let Some(filters) = filters { conditions.extend(filters.iter().map(|filter| match &filter.value { - FilterValue::Int(i) => format!("{} {} {}", filter.field, filter.comparator, i), - FilterValue::String(s) => format!("{} {} '{}'", filter.field, filter.comparator, s), + FilterValue::Int(i) => format!("[{}] {} {}", filter.field, filter.comparator, i), + FilterValue::String(s) => format!("[{}] {} '{}'", filter.field, filter.comparator, s), FilterValue::List(list) => { let values = list .iter() @@ -239,7 +234,7 @@ fn build_conditions(keys: &Option>, filters: &Option>) - }) .collect::>() .join(", "); - format!("{} {} ({})", filter.field, filter.comparator, values) + format!("[{}] {} ({})", filter.field, filter.comparator, values) } })); } diff --git a/crates/torii/graphql/src/query/filter.rs b/crates/torii/graphql/src/query/filter.rs index 54e14b768c..0f39c073dc 100644 --- a/crates/torii/graphql/src/query/filter.rs +++ b/crates/torii/graphql/src/query/filter.rs @@ -56,16 +56,10 @@ pub struct Filter { pub fn parse_filter(input: &Name, value: FilterValue) -> Filter { for comparator in Comparator::iter() { if let Some(field) = input.strip_suffix(comparator.as_ref()) { - // Filtering only applies to model members which are stored in db with - // external_{name} - return Filter { - field: format!("external_{}", field), - comparator: comparator.clone(), - value, - }; + return Filter { field: field.to_string(), comparator: comparator.clone(), value }; } } // If no suffix found assume equality comparison - Filter { field: format!("external_{}", input), comparator: Comparator::Eq, value } + Filter { field: input.to_string(), comparator: Comparator::Eq, value } } diff --git a/crates/torii/graphql/src/query/mod.rs b/crates/torii/graphql/src/query/mod.rs index 6586b150c9..cca7ee8816 100644 --- a/crates/torii/graphql/src/query/mod.rs +++ b/crates/torii/graphql/src/query/mod.rs @@ -1,157 +1,103 @@ use std::str::FromStr; +use async_graphql::dynamic::indexmap::IndexMap; use async_graphql::dynamic::TypeRef; use async_graphql::{Name, Value}; use chrono::{DateTime, Utc}; use convert_case::{Case, Casing}; use dojo_types::primitive::{Primitive, SqlType}; +use dojo_types::schema::Ty; use regex::Regex; use sqlx::sqlite::SqliteRow; -use sqlx::{Row, SqliteConnection}; -use torii_core::constants::SQL_FELT_DELIMITER; +use sqlx::Row; +use torii_sqlite::constants::SQL_FELT_DELIMITER; use crate::constants::{ BOOLEAN_TRUE, ENTITY_ID_COLUMN, EVENT_MESSAGE_ID_COLUMN, INTERNAL_ENTITY_ID_KEY, }; -use crate::object::model_data::ModelMember; use crate::types::{TypeData, TypeMapping, ValueMapping}; pub mod data; pub mod filter; pub mod order; -pub async fn type_mapping_query( - conn: &mut SqliteConnection, - model_id: &str, -) -> sqlx::Result { - let model_members = fetch_model_members(conn, model_id).await?; - let (root_members, nested_members): (Vec<&ModelMember>, Vec<&ModelMember>) = - model_members.iter().partition(|member| member.model_idx == 0); +pub fn build_type_mapping(namespace: &str, schema: &Ty) -> TypeMapping { + let model = schema.as_struct().unwrap(); - build_type_mapping(&root_members, &nested_members) -} - -async fn fetch_model_members( - conn: &mut SqliteConnection, - model_id: &str, -) -> sqlx::Result> { - sqlx::query_as( - r#" - SELECT - id, - model_id, - model_idx, - name, - type AS ty, - type_enum, - key, - executed_at, - created_at - from model_members WHERE model_id = ? - "#, - ) - .bind(model_id) - .fetch_all(conn) - .await -} - -fn build_type_mapping( - root_members: &[&ModelMember], - nested_members: &[&ModelMember], -) -> sqlx::Result { - let type_mapping: TypeMapping = root_members + model + .children .iter() - .map(|&member| { - let type_data = member_to_type_data(member, nested_members); - Ok((Name::new(&member.name), type_data)) + .map(|member| { + let type_data = member_to_type_data(namespace, &member.ty); + (Name::new(&member.name), type_data) }) - .collect::>()?; - - Ok(type_mapping) + .collect() } -fn member_to_type_data(member: &ModelMember, nested_members: &[&ModelMember]) -> TypeData { +fn member_to_type_data(namespace: &str, schema: &Ty) -> TypeData { // TODO: convert sql -> Ty directly - match member.type_enum.as_str() { - "Primitive" => TypeData::Simple(TypeRef::named(&member.ty)), - "ByteArray" => TypeData::Simple(TypeRef::named("ByteArray")), - "Array" => TypeData::List(Box::new(member_to_type_data( - nested_members - .iter() - .find(|&nested_member| { - nested_member.model_id == member.model_id - && nested_member.id.ends_with(&member.name) - // TEMP FIX: refer to parse_nested_type - && nested_member - .id - .split('$') - .collect::>() - .starts_with(&member.id.split('$').collect::>()) - }) - .expect("Array type should have nested type"), - nested_members, - ))), + match schema { + Ty::Primitive(primitive) => TypeData::Simple(TypeRef::named(primitive.to_string())), + Ty::ByteArray(_) => TypeData::Simple(TypeRef::named("ByteArray")), + Ty::Array(array) => TypeData::List(Box::new(member_to_type_data(namespace, &array[0]))), // Enums that do not have a nested member are considered as a simple Enum - "Enum" - if !nested_members.iter().any(|&nested_member| { - nested_member.model_id == member.model_id - && nested_member.id.ends_with(&member.name) - && nested_member - .id - .split('$') - .collect::>() - .starts_with(&member.id.split('$').collect::>()) - }) => + Ty::Enum(enum_) + if enum_ + .options + .iter() + .all(|o| if let Ty::Tuple(t) = &o.ty { t.is_empty() } else { false }) => { TypeData::Simple(TypeRef::named("Enum")) } - _ => parse_nested_type(member, nested_members), + _ => parse_nested_type(namespace, schema), } } -fn parse_nested_type(member: &ModelMember, nested_members: &[&ModelMember]) -> TypeData { - let nested_mapping: TypeMapping = nested_members - .iter() - .filter_map(|&nested_member| { - if member.model_id == nested_member.model_id - && nested_member.id.ends_with(&member.name) - // TEMP FIX: a nested member that has the same name as another nested member - // and that both have parents that start with the same id (Model$Test1 and Model$Test2) - // will end up being assigned to the wrong parent - && nested_member - .id - .split('$') - .take(nested_member.id.split('$').count() - 1) - .collect::>() - .eq(&member.id.split('$').collect::>()) - { - // if the nested member is an Enum and the member is an Enum, we need to inject the - // Enum type in order to have a "option" field in the nested Enum - // for the enum variant - if nested_member.type_enum == "Enum" - && nested_member.name == "option" - && member.type_enum == "Enum" - { - return Some((Name::new("option"), TypeData::Simple(TypeRef::named("Enum")))); - } +fn parse_nested_type(namespace: &str, schema: &Ty) -> TypeData { + let type_mapping: TypeMapping = match schema { + Ty::Struct(s) => s + .children + .iter() + .map(|member| { + let type_data = member_to_type_data(namespace, &member.ty); + (Name::new(&member.name), type_data) + }) + .collect(), + Ty::Enum(e) => { + let mut type_mapping = e + .options + .iter() + .filter_map(|option| { + // ignore unit type variants + if let Ty::Tuple(t) = &option.ty { + if t.is_empty() { + return None; + } + } - let type_data = member_to_type_data(nested_member, nested_members); - Some((Name::new(&nested_member.name), type_data)) - } else { - None - } - }) - .collect(); + let type_data = member_to_type_data(namespace, &option.ty); + Some((Name::new(&option.name), type_data)) + }) + .collect::(); + + type_mapping.insert(Name::new("option"), TypeData::Simple(TypeRef::named("Enum"))); + type_mapping + } + Ty::Tuple(t) => t + .iter() + .enumerate() + .map(|(i, ty)| (Name::new(format!("_{}", i)), member_to_type_data(namespace, ty))) + .collect(), + _ => return TypeData::Simple(TypeRef::named(schema.name())), + }; - let model_name = member.id.split('$').next().unwrap(); + let name: String = format!("{}_{}", namespace, schema.name()); // sanitizes the member type string // for eg. Position_Array -> Position_ArrayVec2 // Position_(u8, Vec2) -> Position_u8Vec2 let re = Regex::new(r"[, ()<>-]").unwrap(); - let sanitized_model_name = model_name.replace('-', "_"); - let sanitized_member_type_name = re.replace_all(&member.ty, ""); - let namespaced = format!("{}_{}", sanitized_model_name, sanitized_member_type_name); - TypeData::Nested((TypeRef::named(namespaced), nested_mapping)) + let sanitized_member_type_name = re.replace_all(&name, ""); + TypeData::Nested((TypeRef::named(sanitized_member_type_name), type_mapping)) } fn remove_hex_leading_zeros(value: Value) -> Value { @@ -170,37 +116,150 @@ fn remove_hex_leading_zeros(value: Value) -> Value { pub fn value_mapping_from_row( row: &SqliteRow, types: &TypeMapping, - is_external: bool, + is_internal: bool, + snake_case: bool, ) -> sqlx::Result { - let mut value_mapping = types - .iter() - .filter(|(_, type_data)| { - type_data.is_simple() - // ignore Enum fields because the column is not stored in this row. we inejct it later - // && !(type_data.type_ref().to_string() == "Enum") - }) - .map(|(field_name, type_data)| { - let mut value = - fetch_value(row, field_name, &type_data.type_ref().to_string(), is_external)?; - - // handles felt arrays stored as string (ex: keys) - if let (TypeRef::List(_), Value::String(s)) = (&type_data.type_ref(), &value) { - let mut felts: Vec<_> = s.split(SQL_FELT_DELIMITER).map(Value::from).collect(); - felts.pop(); // removes empty item - value = Value::List(felts); - } + // Retrieve entity ID if present + let entity_id = if let Ok(entity_id) = row.try_get::(ENTITY_ID_COLUMN) { + Some(entity_id) + } else if let Ok(event_message_id) = row.try_get::(EVENT_MESSAGE_ID_COLUMN) { + Some(event_message_id) + } else { + None + }; - Ok((Name::new(field_name), value)) - }) - .collect::>()?; + fn build_value_mapping( + row: &SqliteRow, + types: &TypeMapping, + prefix: &str, + is_internal: bool, + snake_case: bool, + entity_id: &Option, + ) -> sqlx::Result { + let mut value_mapping = ValueMapping::new(); + // Add internal entity ID if present + if let Some(entity_id) = entity_id { + value_mapping.insert(Name::new(INTERNAL_ENTITY_ID_KEY), Value::from(entity_id)); + } - // entity_id is not part of a model's type_mapping but needed to relate to parent entity - if let Ok(entity_id) = row.try_get::(ENTITY_ID_COLUMN) { - value_mapping.insert(Name::new(INTERNAL_ENTITY_ID_KEY), Value::from(entity_id)); - } else if let Ok(event_message_id) = row.try_get::(EVENT_MESSAGE_ID_COLUMN) { - value_mapping.insert(Name::new(INTERNAL_ENTITY_ID_KEY), Value::from(event_message_id)); + for (field_name, type_data) in types { + let column_name = if prefix.is_empty() { + field_name.to_string() + } else { + format!("{}.{}", prefix, field_name) + }; + + match type_data { + TypeData::Simple(type_ref) => { + let mut value = fetch_value( + row, + &column_name, + &type_ref.to_string(), + is_internal, + snake_case, + )?; + + // handles felt arrays stored as string (ex: keys) + if let (TypeRef::List(_), Value::String(s)) = (type_ref, &value) { + let mut felts: Vec<_> = + s.split(SQL_FELT_DELIMITER).map(Value::from).collect(); + felts.pop(); // removes empty item + value = Value::List(felts); + } + + value_mapping.insert(Name::new(field_name), value); + } + TypeData::List(_) => { + let value = fetch_value(row, &column_name, "String", is_internal, snake_case)?; + if let Value::String(json_str) = value { + if json_str.is_empty() { + value_mapping.insert(Name::new(field_name), Value::List(vec![])); + continue; + } + + let mut array_value: Value = + serde_json::from_str(&json_str).map_err(|e| { + sqlx::Error::Protocol(format!("JSON parse error: {}", e)) + })?; + + fn populate_value( + value: &mut Value, + type_data: &TypeData, + entity_id: &Option, + ) { + match value { + Value::Object(obj) => { + for (field_name, field_value) in obj.iter_mut() { + populate_value( + field_value, + &type_data.type_mapping().unwrap()[field_name], + entity_id, + ); + } + + if type_data.type_mapping().map_or(false, |mapping| { + mapping.contains_key(&Name::new("option")) + }) { + obj.insert( + Name::new("option"), + Value::String(obj.keys().next().unwrap().to_string()), + ); + } + + // insert $entity_id$ relation + if let Some(entity_id) = entity_id { + obj.insert( + Name::new(INTERNAL_ENTITY_ID_KEY), + Value::from(entity_id), + ); + } + } + Value::List(inner) => match type_data { + TypeData::List(inner_type_data) => { + for item in inner.iter_mut() { + populate_value(item, inner_type_data, entity_id); + } + } + TypeData::Nested((_, mapping)) => { + let mut obj = IndexMap::new(); + for (i, item) in inner.iter_mut().enumerate() { + populate_value( + item, + &mapping[&Name::new(format!("_{}", i))], + entity_id, + ); + obj.insert(Name::new(format!("_{}", i)), item.clone()); + } + *value = Value::Object(obj); + } + _ => {} + }, + _ => {} + } + } + + populate_value(&mut array_value, type_data, entity_id); + value_mapping.insert(Name::new(field_name), array_value); + } + } + TypeData::Nested((_, nested_mapping)) => { + let nested_values = build_value_mapping( + row, + nested_mapping, + &column_name, + is_internal, + snake_case, + entity_id, + )?; + value_mapping.insert(Name::new(field_name), Value::Object(nested_values)); + } + } + } + + Ok(value_mapping) } + let value_mapping = build_value_mapping(row, types, "", is_internal, snake_case, &entity_id)?; Ok(value_mapping) } @@ -208,14 +267,37 @@ fn fetch_value( row: &SqliteRow, field_name: &str, type_name: &str, - is_external: bool, + is_internal: bool, + snake_case: bool, ) -> sqlx::Result { - let column_name = if is_external { - format!("external_{}", field_name) + let mut column_name = if is_internal { + format!("internal_{}", field_name) + } else if snake_case { + field_name.to_case(Case::Snake) } else { - field_name.to_string().to_case(Case::Snake) + field_name.to_string() }; + // Strip _0, _1, etc. from tuple field names + // to get the actual SQL column name which is 0, 1 etc.. + column_name = column_name + .split('.') + .map(|part| { + if part.starts_with('_') && part[1..].parse::().is_ok() { + part[1..].to_string() + } else { + part.to_string() + } + }) + .collect::>() + .join("."); + + // for enum options, remove the ".option" suffix to get the variant + // through the enum itself field name + if type_name == "Enum" && column_name.ends_with(".option") { + column_name = column_name.trim_end_matches(".option").to_string(); + } + match Primitive::from_str(type_name) { // fetch boolean Ok(Primitive::Bool(_)) => { diff --git a/crates/torii/graphql/src/schema.rs b/crates/torii/graphql/src/schema.rs index 79ec29e15d..adee496350 100644 --- a/crates/torii/graphql/src/schema.rs +++ b/crates/torii/graphql/src/schema.rs @@ -1,7 +1,8 @@ use anyhow::Result; use async_graphql::dynamic::{Object, Scalar, Schema, Subscription, Union}; +use dojo_types::schema::Ty; use sqlx::SqlitePool; -use torii_core::types::Model; +use torii_sqlite::types::Model; use super::object::connection::page_info::PageInfoObject; use super::object::entity::EntityObject; @@ -22,7 +23,7 @@ use crate::object::metadata::MetadataObject; use crate::object::model::ModelObject; use crate::object::transaction::TransactionObject; use crate::object::ObjectVariant; -use crate::query::type_mapping_query; +use crate::query::build_type_mapping; // The graphql schema is built dynamically at runtime, this is because we won't know the schema of // the models until runtime. There are however, predefined objects such as entities and @@ -139,7 +140,9 @@ async fn build_objects(pool: &SqlitePool) -> Result<(Vec, Vec Result<(Vec, Vec, pool: &Pool, - external_url: Option, ) -> (SocketAddr, impl Future + 'static) { let schema = build_schema(pool).await.unwrap(); - let mut conn = pool.acquire().await.unwrap(); - let num_models = count_rows(&mut conn, MODEL_TABLE, &None, &None).await.unwrap(); - - let routes = graphql_filter(schema, external_url, num_models == 0); + let routes = graphql_filter(schema); warp::serve(routes).bind_with_graceful_shutdown(([127, 0, 0, 1], 0), async move { shutdown_rx.recv().await.ok(); }) } -fn graphql_filter( - schema: Schema, - external_url: Option, - is_empty: bool, -) -> impl Filter + Clone { +fn graphql_filter(schema: Schema) -> impl Filter + Clone { let graphql_post = async_graphql_warp::graphql(schema.clone()).and_then( move |(schema, request): (Schema, Request)| async move { - if is_empty { - return Ok::<_, Rejection>(empty_response()); - } - // Execute query let response = schema.execute(request).await; // Return result @@ -48,39 +32,15 @@ fn graphql_filter( }, ); - let subscription_endpoint = if let Some(external_url) = external_url { - let mut websocket_url = external_url.clone(); - websocket_url.set_path("/graphql/ws"); - - let websocket_scheme = match websocket_url.scheme() { - "http" => "ws", - "https" => "wss", - _ => panic!("Invalid URL scheme"), // URL validated on input so this never hits - }; - - let _ = websocket_url.set_scheme(websocket_scheme); - websocket_url.to_string() - } else { - "/graphql/ws".to_string() - }; - let playground_filter = warp::path("graphql").map(move || { warp::reply::html( GraphiQLSource::build() - .endpoint("/graphql") - .subscription_endpoint(subscription_endpoint.as_str()) - .finish(), + .subscription_endpoint("/ws") + // we patch the generated source to use the current URL instead of the origin + // for hosted services like SLOT + .finish().replace("new URL(endpoint, window.location.origin);", "new URL(window.location.href.trimEnd('/') + endpoint)"), ) }); graphql_subscription(schema).or(graphql_post).or(playground_filter) } - -fn empty_response() -> warp::reply::Json { - let empty_response = json!({ - "errors": [{ - "message": "World does not have any indexed data yet." - }] - }); - warp::reply::json(&empty_response) -} diff --git a/crates/torii/graphql/src/tests/metadata_test.rs b/crates/torii/graphql/src/tests/metadata_test.rs index b9cfab8a93..552e10c277 100644 --- a/crates/torii/graphql/src/tests/metadata_test.rs +++ b/crates/torii/graphql/src/tests/metadata_test.rs @@ -8,10 +8,10 @@ mod tests { use starknet::providers::jsonrpc::HttpTransport; use starknet::providers::JsonRpcClient; use tokio::sync::broadcast; - use torii_core::executor::Executor; - use torii_core::sql::cache::ModelCache; - use torii_core::sql::Sql; - use torii_core::types::{Contract, ContractType}; + use torii_sqlite::cache::ModelCache; + use torii_sqlite::executor::Executor; + use torii_sqlite::types::{Contract, ContractType}; + use torii_sqlite::Sql; use url::Url; use crate::schema::build_schema; diff --git a/crates/torii/graphql/src/tests/mod.rs b/crates/torii/graphql/src/tests/mod.rs index 06d7afa747..92e23ad3e8 100644 --- a/crates/torii/graphql/src/tests/mod.rs +++ b/crates/torii/graphql/src/tests/mod.rs @@ -27,11 +27,11 @@ use starknet::providers::jsonrpc::HttpTransport; use starknet::providers::{JsonRpcClient, Provider}; use tokio::sync::broadcast; use tokio_stream::StreamExt; -use torii_core::engine::{Engine, EngineConfig, Processors}; -use torii_core::executor::Executor; -use torii_core::sql::cache::ModelCache; -use torii_core::sql::Sql; -use torii_core::types::{Contract, ContractType}; +use torii_indexer::engine::{Engine, EngineConfig, Processors}; +use torii_sqlite::cache::ModelCache; +use torii_sqlite::executor::Executor; +use torii_sqlite::types::{Contract, ContractType}; +use torii_sqlite::Sql; mod entities_test; mod events_test; diff --git a/crates/torii/graphql/src/tests/subscription_test.rs b/crates/torii/graphql/src/tests/subscription_test.rs index ca332a4cf6..50cfb23bb4 100644 --- a/crates/torii/graphql/src/tests/subscription_test.rs +++ b/crates/torii/graphql/src/tests/subscription_test.rs @@ -17,11 +17,11 @@ mod tests { use starknet::providers::JsonRpcClient; use starknet_crypto::{poseidon_hash_many, Felt}; use tokio::sync::{broadcast, mpsc}; - use torii_core::executor::Executor; - use torii_core::sql::cache::ModelCache; - use torii_core::sql::utils::felts_to_sql_string; - use torii_core::sql::Sql; - use torii_core::types::{Contract, ContractType}; + use torii_sqlite::cache::ModelCache; + use torii_sqlite::executor::Executor; + use torii_sqlite::types::{Contract, ContractType}; + use torii_sqlite::utils::felts_to_sql_string; + use torii_sqlite::Sql; use url::Url; use crate::tests::{model_fixtures, run_graphql_subscription}; diff --git a/crates/torii/graphql/src/types.rs b/crates/torii/graphql/src/types.rs index 14256dd911..11bc5bb378 100644 --- a/crates/torii/graphql/src/types.rs +++ b/crates/torii/graphql/src/types.rs @@ -42,6 +42,13 @@ impl TypeData { matches!(self, TypeData::List(_)) } + pub fn inner(&self) -> Option<&TypeData> { + match self { + TypeData::List(inner) => Some(inner), + _ => None, + } + } + // pub fn is_enum(&self) -> bool { // matches!(self, TypeData::Enum(_)) // } diff --git a/crates/torii/grpc/Cargo.toml b/crates/torii/grpc/Cargo.toml index e585af72f2..fc88ccd8ee 100644 --- a/crates/torii/grpc/Cargo.toml +++ b/crates/torii/grpc/Cargo.toml @@ -14,7 +14,7 @@ rayon.workspace = true starknet.workspace = true starknet-crypto.workspace = true thiserror.workspace = true -torii-core = { path = "../core", optional = true } +torii-sqlite = { path = "../sqlite", optional = true } crypto-bigint.workspace = true serde.workspace = true @@ -38,6 +38,7 @@ katana-runner.workspace = true scarb.workspace = true tempfile.workspace = true sozo-scarbext.workspace = true +torii-indexer.workspace = true [target.'cfg(target_arch = "wasm32")'.dependencies] tonic-web-wasm-client.workspace = true @@ -61,4 +62,4 @@ wasm-tonic-build.workspace = true [features] client = [ ] -server = [ "dep:torii-core" ] # this feature can't be build on wasm32 +server = [ "dep:torii-sqlite" ] # this feature can't be build on wasm32 diff --git a/crates/torii/grpc/proto/types.proto b/crates/torii/grpc/proto/types.proto index d474065f63..2b5545e497 100644 --- a/crates/torii/grpc/proto/types.proto +++ b/crates/torii/grpc/proto/types.proto @@ -74,6 +74,9 @@ message Query { uint32 limit = 2; uint32 offset = 3; bool dont_include_hashed_keys = 4; + repeated OrderBy order_by = 5; + repeated string entity_models = 6; + uint64 entity_updated_after = 7; } message EventQuery { @@ -117,9 +120,14 @@ message MemberValue { oneof value_type { Primitive primitive = 1; string string = 2; + MemberValueList list = 3; } } +message MemberValueList { + repeated MemberValue values = 1; +} + message MemberClause { string model = 2; string member = 3; @@ -149,6 +157,8 @@ enum ComparisonOperator { GTE = 3; LT = 4; LTE = 5; + IN = 6; + NOT_IN = 7; } message Token { @@ -164,4 +174,15 @@ message TokenBalance { string account_address = 2; string contract_address = 3; string token_id = 4; +} + +message OrderBy { + string model = 1; + string member = 2; + OrderDirection direction = 3; +} + +enum OrderDirection { + ASC = 0; + DESC = 1; } \ No newline at end of file diff --git a/crates/torii/grpc/proto/world.proto b/crates/torii/grpc/proto/world.proto index 2c7e7b1270..57e4ef76db 100644 --- a/crates/torii/grpc/proto/world.proto +++ b/crates/torii/grpc/proto/world.proto @@ -34,6 +34,12 @@ service World { // Update entity subscription rpc UpdateEventMessagesSubscription (UpdateEventMessagesSubscriptionRequest) returns (google.protobuf.Empty); + // Subscribe to token balance updates. + rpc SubscribeTokenBalances (RetrieveTokenBalancesRequest) returns (stream SubscribeTokenBalancesResponse); + + // Update token balance subscription + rpc UpdateTokenBalancesSubscription (UpdateTokenBalancesSubscriptionRequest) returns (google.protobuf.Empty); + // Retrieve entities rpc RetrieveEventMessages (RetrieveEventMessagesRequest) returns (RetrieveEntitiesResponse); @@ -50,6 +56,24 @@ service World { rpc RetrieveTokenBalances (RetrieveTokenBalancesRequest) returns (RetrieveTokenBalancesResponse); } +// A request to update a token balance subscription +message UpdateTokenBalancesSubscriptionRequest { + // The subscription ID + uint64 subscription_id = 1; + // The list of contract addresses to subscribe to + repeated bytes contract_addresses = 2; + // The list of account addresses to subscribe to + repeated bytes account_addresses = 3; +} + +// A response containing token balances +message SubscribeTokenBalancesResponse { + // The subscription ID + uint64 subscription_id = 1; + // The token balance + types.TokenBalance balance = 2; +} + // A request to retrieve tokens message RetrieveTokensRequest { // The list of contract addresses to retrieve tokens for diff --git a/crates/torii/grpc/src/client.rs b/crates/torii/grpc/src/client.rs index f24d5f5f7e..53d34f77ff 100644 --- a/crates/torii/grpc/src/client.rs +++ b/crates/torii/grpc/src/client.rs @@ -16,11 +16,14 @@ use crate::proto::world::{ SubscribeEntitiesRequest, SubscribeEntityResponse, SubscribeEventMessagesRequest, SubscribeEventsRequest, SubscribeEventsResponse, SubscribeIndexerRequest, SubscribeIndexerResponse, SubscribeModelsRequest, SubscribeModelsResponse, - UpdateEntitiesSubscriptionRequest, UpdateEventMessagesSubscriptionRequest, + SubscribeTokenBalancesResponse, UpdateEntitiesSubscriptionRequest, + UpdateEventMessagesSubscriptionRequest, UpdateTokenBalancesSubscriptionRequest, WorldMetadataRequest, }; use crate::types::schema::{Entity, SchemaError}; -use crate::types::{EntityKeysClause, Event, EventQuery, IndexerUpdate, ModelKeysClause, Query}; +use crate::types::{ + EntityKeysClause, Event, EventQuery, IndexerUpdate, ModelKeysClause, Query, TokenBalance, +}; #[derive(Debug, thiserror::Error)] pub enum Error { @@ -295,6 +298,76 @@ impl WorldClient { None => empty_state_update(), })))) } + + /// Subscribe to token balances. + pub async fn subscribe_token_balances( + &mut self, + contract_addresses: Vec, + account_addresses: Vec, + ) -> Result { + let request = RetrieveTokenBalancesRequest { + contract_addresses: contract_addresses + .into_iter() + .map(|c| c.to_bytes_be().to_vec()) + .collect(), + account_addresses: account_addresses + .into_iter() + .map(|a| a.to_bytes_be().to_vec()) + .collect(), + }; + let stream = self + .inner + .subscribe_token_balances(request) + .await + .map_err(Error::Grpc) + .map(|res| res.into_inner())?; + Ok(TokenBalanceStreaming(stream.map_ok(Box::new(|res| { + (res.subscription_id, res.balance.unwrap().try_into().expect("must able to serialize")) + })))) + } + + /// Update a token balances subscription. + pub async fn update_token_balances_subscription( + &mut self, + subscription_id: u64, + contract_addresses: Vec, + account_addresses: Vec, + ) -> Result<(), Error> { + let request = UpdateTokenBalancesSubscriptionRequest { + subscription_id, + contract_addresses: contract_addresses + .into_iter() + .map(|c| c.to_bytes_be().to_vec()) + .collect(), + account_addresses: account_addresses + .into_iter() + .map(|a| a.to_bytes_be().to_vec()) + .collect(), + }; + self.inner + .update_token_balances_subscription(request) + .await + .map_err(Error::Grpc) + .map(|res| res.into_inner()) + } +} + +type TokenBalanceMappedStream = MapOk< + tonic::Streaming, + Box (SubscriptionId, TokenBalance) + Send>, +>; + +#[derive(Debug)] +pub struct TokenBalanceStreaming(TokenBalanceMappedStream); + +impl Stream for TokenBalanceStreaming { + type Item = ::Item; + fn poll_next( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + self.0.poll_next_unpin(cx) + } } type ModelDiffMappedStream = MapOk< diff --git a/crates/torii/grpc/src/server/mod.rs b/crates/torii/grpc/src/server/mod.rs index a0dee77df9..c64768ff62 100644 --- a/crates/torii/grpc/src/server/mod.rs +++ b/crates/torii/grpc/src/server/mod.rs @@ -13,6 +13,7 @@ use std::str::FromStr; use std::sync::Arc; use std::time::Duration; +use dojo_types::naming::compute_selector_from_tag; use dojo_types::primitive::{Primitive, PrimitiveError}; use dojo_types::schema::Ty; use dojo_world::contracts::naming::compute_selector_from_names; @@ -26,12 +27,14 @@ use proto::world::{ use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; use sqlx::prelude::FromRow; use sqlx::sqlite::SqliteRow; +use sqlx::types::chrono::{DateTime, Utc}; use sqlx::{Pool, Row, Sqlite}; use starknet::core::types::Felt; use starknet::providers::jsonrpc::HttpTransport; use starknet::providers::JsonRpcClient; use subscriptions::event::EventManager; use subscriptions::indexer::IndexerManager; +use subscriptions::token_balance::TokenBalanceManager; use tokio::net::TcpListener; use tokio::sync::mpsc::{channel, Receiver}; use tokio_stream::wrappers::{ReceiverStream, TcpListenerStream}; @@ -39,11 +42,10 @@ use tonic::codec::CompressionEncoding; use tonic::transport::Server; use tonic::{Request, Response, Status}; use tonic_web::GrpcWebLayer; -use torii_core::error::{Error, ParseError, QueryError}; -use torii_core::model::{build_sql_query, map_row_to_ty}; -use torii_core::sql::cache::ModelCache; -use torii_core::sql::utils::sql_string_to_felts; -use torii_core::types::{Token, TokenBalance}; +use torii_sqlite::cache::ModelCache; +use torii_sqlite::error::{Error, ParseError, QueryError}; +use torii_sqlite::model::{fetch_entities, map_row_to_ty}; +use torii_sqlite::types::{Token, TokenBalance}; use tower_http::cors::{AllowOrigin, CorsLayer}; use self::subscriptions::entity::EntityManager; @@ -58,7 +60,8 @@ use crate::proto::world::{ RetrieveTokenBalancesResponse, RetrieveTokensRequest, RetrieveTokensResponse, SubscribeEntitiesRequest, SubscribeEntityResponse, SubscribeEventMessagesRequest, SubscribeEventsResponse, SubscribeIndexerRequest, SubscribeIndexerResponse, - UpdateEventMessagesSubscriptionRequest, WorldMetadataRequest, WorldMetadataResponse, + SubscribeTokenBalancesResponse, UpdateEventMessagesSubscriptionRequest, + UpdateTokenBalancesSubscriptionRequest, WorldMetadataRequest, WorldMetadataResponse, }; use crate::proto::{self}; use crate::types::schema::SchemaError; @@ -66,11 +69,11 @@ use crate::types::ComparisonOperator; pub(crate) static ENTITIES_TABLE: &str = "entities"; pub(crate) static ENTITIES_MODEL_RELATION_TABLE: &str = "entity_model"; -pub(crate) static ENTITIES_ENTITY_RELATION_COLUMN: &str = "entity_id"; +pub(crate) static ENTITIES_ENTITY_RELATION_COLUMN: &str = "internal_entity_id"; pub(crate) static EVENT_MESSAGES_TABLE: &str = "event_messages"; pub(crate) static EVENT_MESSAGES_MODEL_RELATION_TABLE: &str = "event_model"; -pub(crate) static EVENT_MESSAGES_ENTITY_RELATION_COLUMN: &str = "event_message_id"; +pub(crate) static EVENT_MESSAGES_ENTITY_RELATION_COLUMN: &str = "internal_event_message_id"; pub(crate) static EVENT_MESSAGES_HISTORICAL_TABLE: &str = "event_messages_historical"; @@ -122,6 +125,7 @@ pub struct DojoWorld { event_manager: Arc, state_diff_manager: Arc, indexer_manager: Arc, + token_balance_manager: Arc, } impl DojoWorld { @@ -137,6 +141,7 @@ impl DojoWorld { let event_manager = Arc::new(EventManager::default()); let state_diff_manager = Arc::new(StateDiffManager::default()); let indexer_manager = Arc::new(IndexerManager::default()); + let token_balance_manager = Arc::new(TokenBalanceManager::default()); tokio::task::spawn(subscriptions::model_diff::Service::new_with_block_rcv( block_rx, @@ -155,6 +160,10 @@ impl DojoWorld { tokio::task::spawn(subscriptions::indexer::Service::new(Arc::clone(&indexer_manager))); + tokio::task::spawn(subscriptions::token_balance::Service::new(Arc::clone( + &token_balance_manager, + ))); + Self { pool, world_address, @@ -164,6 +173,7 @@ impl DojoWorld { event_manager, state_diff_manager, indexer_manager, + token_balance_manager, } } } @@ -218,6 +228,7 @@ impl DojoWorld { Ok(proto::types::WorldMetadata { world_address, models: models_metadata }) } + #[allow(clippy::too_many_arguments)] async fn entities_all( &self, table: &str, @@ -226,6 +237,9 @@ impl DojoWorld { limit: u32, offset: u32, dont_include_hashed_keys: bool, + order_by: Option<&str>, + entity_models: Vec, + entity_updated_after: Option, ) -> Result<(Vec, u32), Error> { self.query_by_hashed_keys( table, @@ -235,6 +249,9 @@ impl DojoWorld { Some(limit), Some(offset), dont_include_hashed_keys, + order_by, + entity_models, + entity_updated_after, ) .await } @@ -253,111 +270,19 @@ impl DojoWorld { row_events.iter().map(map_row_to_event).collect() } - async fn fetch_entities( - &self, - table: &str, - entity_relation_column: &str, - entities: Vec<(String, String)>, - dont_include_hashed_keys: bool, - ) -> Result, Error> { - // Group entities by their model combinations - let mut model_groups: HashMap> = HashMap::new(); - for (entity_id, models_str) in entities { - model_groups.entry(models_str).or_default().push(entity_id); - } - - let mut all_entities = Vec::new(); - - let mut tx = self.pool.begin().await?; - - // Create a temporary table to store entity IDs due to them potentially exceeding - // SQLite's parameters limit which is 999 - sqlx::query( - "CREATE TEMPORARY TABLE temp_entity_ids (id TEXT PRIMARY KEY, model_group TEXT)", - ) - .execute(&mut *tx) - .await?; - - // Insert all entity IDs into the temporary table - for (model_ids, entity_ids) in &model_groups { - for chunk in entity_ids.chunks(999) { - let placeholders = chunk.iter().map(|_| "(?, ?)").collect::>().join(","); - let query = format!( - "INSERT INTO temp_entity_ids (id, model_group) VALUES {}", - placeholders - ); - let mut query = sqlx::query(&query); - for id in chunk { - query = query.bind(id).bind(model_ids); - } - query.execute(&mut *tx).await?; - } - } - - for (models_str, _) in model_groups { - let model_ids = - models_str.split(',').map(|id| Felt::from_str(id).unwrap()).collect::>(); - let schemas = - self.model_cache.models(&model_ids).await?.into_iter().map(|m| m.schema).collect(); - - let (entity_query, arrays_queries, _) = build_sql_query( - &schemas, - table, - entity_relation_column, - Some(&format!( - "[{table}].id IN (SELECT id FROM temp_entity_ids WHERE model_group = ?)" - )), - Some(&format!( - "[{table}].id IN (SELECT id FROM temp_entity_ids WHERE model_group = ?)" - )), - None, - None, - )?; - - let rows = sqlx::query(&entity_query).bind(&models_str).fetch_all(&mut *tx).await?; - - let mut arrays_rows = HashMap::new(); - for (name, array_query) in arrays_queries { - let array_rows = - sqlx::query(&array_query).bind(&models_str).fetch_all(&mut *tx).await?; - arrays_rows.insert(name, array_rows); - } - - let arrays_rows = Arc::new(arrays_rows); - let schemas = Arc::new(schemas); - - let group_entities: Result, Error> = rows - .par_iter() - .map(|row| map_row_to_entity(row, &arrays_rows, &schemas, dont_include_hashed_keys)) - .collect(); - - all_entities.extend(group_entities?); - } - - sqlx::query("DROP TABLE temp_entity_ids").execute(&mut *tx).await?; - - tx.commit().await?; - - Ok(all_entities) - } - async fn fetch_historical_event_messages( &self, query: &str, - keys_pattern: Option<&str>, + bind_values: Vec, limit: Option, offset: Option, ) -> Result, Error> { - let db_entities: Vec<(String, String, String, String)> = if keys_pattern.is_some() { - sqlx::query_as(query) - .bind(keys_pattern.unwrap()) - .bind(limit) - .bind(offset) - .fetch_all(&self.pool) - .await? - } else { - sqlx::query_as(query).bind(limit).bind(offset).fetch_all(&self.pool).await? - }; + let mut query = sqlx::query_as(query); + for value in bind_values { + query = query.bind(value); + } + let db_entities: Vec<(String, String, String, String)> = + query.bind(limit).bind(offset).fetch_all(&self.pool).await?; let mut entities = HashMap::new(); for (id, data, model_id, _) in db_entities { @@ -368,7 +293,8 @@ impl DojoWorld { .model(&Felt::from_str(&model_id).map_err(ParseError::FromStr)?) .await?; let mut schema = model.schema; - schema.deserialize(&mut sql_string_to_felts(&data))?; + schema + .from_json_value(serde_json::from_str(&data).map_err(ParseError::FromJsonStr)?)?; let entity = entities .entry(id) @@ -389,81 +315,117 @@ impl DojoWorld { limit: Option, offset: Option, dont_include_hashed_keys: bool, + order_by: Option<&str>, + entity_models: Vec, + entity_updated_after: Option, ) -> Result<(Vec, u32), Error> { - // TODO: use prepared statement for where clause - let filter_ids = match hashed_keys { + let where_clause = match &hashed_keys { Some(hashed_keys) => { - let ids = hashed_keys - .hashed_keys - .iter() - .map(|id| Ok(format!("{table}.id = '{:#x}'", Felt::from_bytes_be_slice(id)))) - .collect::, Error>>()?; - - format!("WHERE {}", ids.join(" OR ")) + let ids = + hashed_keys.hashed_keys.iter().map(|_| "{table}.id = ?").collect::>(); + format!( + "{} {}", + ids.join(" OR "), + if entity_updated_after.is_some() { + format!("AND {table}.updated_at >= ?") + } else { + String::new() + } + ) + } + None => { + if entity_updated_after.is_some() { + format!("{table}.updated_at >= ?") + } else { + String::new() + } } - None => String::new(), }; - // count query that matches filter_ids - let count_query = format!( - r#" - SELECT count(*) - FROM {table} - {filter_ids} - "# - ); - // total count of rows without limit and offset - let total_count: u32 = - sqlx::query_scalar(&count_query).fetch_optional(&self.pool).await?.unwrap_or(0); - if total_count == 0 { - return Ok((Vec::new(), 0)); + let mut bind_values = vec![]; + if let Some(hashed_keys) = hashed_keys { + bind_values = hashed_keys + .hashed_keys + .iter() + .map(|key| format!("{:#x}", Felt::from_bytes_be_slice(key))) + .collect::>(); } - - // Query to get entity IDs and their model IDs - let mut query = if table == EVENT_MESSAGES_HISTORICAL_TABLE { - format!( - r#" - SELECT {table}.id, {table}.data, {table}.model_id, group_concat({model_relation_table}.model_id) as model_ids - FROM {table} - JOIN {model_relation_table} ON {table}.id = {model_relation_table}.entity_id - {filter_ids} - GROUP BY {table}.event_id - ORDER BY {table}.event_id DESC - "# - ) - } else { - format!( - r#" - SELECT {table}.id, group_concat({model_relation_table}.model_id) as model_ids - FROM {table} - JOIN {model_relation_table} ON {table}.id = {model_relation_table}.entity_id - {filter_ids} - GROUP BY {table}.id - ORDER BY {table}.event_id DESC - "# - ) - }; - - if limit.is_some() { - query += " LIMIT ?" + if let Some(entity_updated_after) = entity_updated_after.clone() { + bind_values.push(entity_updated_after); } - if offset.is_some() { - query += " OFFSET ?" - } + let entity_models = + entity_models.iter().map(|model| compute_selector_from_tag(model)).collect::>(); + let schemas = self + .model_cache + .models(&entity_models) + .await? + .iter() + .map(|m| m.schema.clone()) + .collect::>(); + + let having_clause = entity_models + .iter() + .map(|model| format!("INSTR(model_ids, '{:#x}') > 0", model)) + .collect::>() + .join(" OR "); if table == EVENT_MESSAGES_HISTORICAL_TABLE { - let entities = - self.fetch_historical_event_messages(&query, None, limit, offset).await?; + let count_query = format!( + r#" + SELECT COUNT(*) FROM {table} + JOIN {model_relation_table} ON {table}.id = {model_relation_table}.entity_id + WHERE {where_clause} + GROUP BY {table}.event_id + "# + ); + let mut total_count = sqlx::query_scalar(&count_query); + for value in &bind_values { + total_count = total_count.bind(value); + } + let total_count = total_count.fetch_one(&self.pool).await?; + if total_count == 0 { + return Ok((Vec::new(), 0)); + } + + let entities = self.fetch_historical_event_messages( + &format!( + r#" + SELECT {table}.id, {table}.data, {table}.model_id, group_concat({model_relation_table}.model_id) as model_ids + FROM {table} + JOIN {model_relation_table} ON {table}.id = {model_relation_table}.entity_id + WHERE {where_clause} + GROUP BY {table}.event_id + ORDER BY {table}.event_id DESC + "# + ), + bind_values, + limit, + offset + ).await?; return Ok((entities, total_count)); } - let db_entities: Vec<(String, String)> = - sqlx::query_as(&query).bind(limit).bind(offset).fetch_all(&self.pool).await?; + let (rows, total_count) = fetch_entities( + &self.pool, + &schemas, + table, + model_relation_table, + entity_relation_column, + if !where_clause.is_empty() { Some(&where_clause) } else { None }, + if !having_clause.is_empty() { Some(&having_clause) } else { None }, + order_by, + limit, + offset, + bind_values, + ) + .await?; + + let entities = rows + .par_iter() + .map(|row| map_row_to_entity(row, &schemas, dont_include_hashed_keys)) + .collect::, Error>>()?; - let entities = self - .fetch_entities(table, entity_relation_column, db_entities, dont_include_hashed_keys) - .await?; Ok((entities, total_count)) } @@ -477,129 +439,98 @@ impl DojoWorld { limit: Option, offset: Option, dont_include_hashed_keys: bool, + order_by: Option<&str>, + entity_models: Vec, + entity_updated_after: Option, ) -> Result<(Vec, u32), Error> { let keys_pattern = build_keys_pattern(keys_clause)?; - // total count of rows that matches keys_pattern without limit and offset - let count_query = format!( - r#" - SELECT count(*) - FROM {table} - {} - "#, - if !keys_clause.models.is_empty() { - // split the model names to namespace and model - let model_ids = keys_clause - .models - .iter() - .map(|model| { - model - .split_once('-') - .ok_or(QueryError::InvalidNamespacedModel(model.clone())) - }) - .collect::, _>>()?; - // get the model selector from namespace and model and format - let model_ids_str = model_ids - .iter() - .map(|(namespace, model)| { - format!("'{:#x}'", compute_selector_from_names(namespace, model)) - }) - .collect::>() - .join(","); - format!( - r#" - JOIN {model_relation_table} ON {table}.id = {model_relation_table}.entity_id - WHERE {model_relation_table}.model_id IN ({}) - AND {table}.keys REGEXP ? - "#, - model_ids_str - ) + let where_clause = format!( + "{table}.keys REGEXP ? {}", + if entity_updated_after.is_some() { + format!("AND {table}.updated_at >= ?") } else { - format!( - r#" - WHERE {table}.keys REGEXP ? - "# - ) + String::new() } ); - let total_count = sqlx::query_scalar(&count_query) - .bind(&keys_pattern) - .fetch_optional(&self.pool) - .await? - .unwrap_or(0); - if total_count == 0 { - return Ok((Vec::new(), 0)); + let mut bind_values = vec![keys_pattern]; + if let Some(entity_updated_after) = entity_updated_after.clone() { + bind_values.push(entity_updated_after); } - let mut models_query = if table == EVENT_MESSAGES_HISTORICAL_TABLE { - format!( + let entity_models = + entity_models.iter().map(|model| compute_selector_from_tag(model)).collect::>(); + let schemas = self + .model_cache + .models(&entity_models) + .await? + .iter() + .map(|m| m.schema.clone()) + .collect::>(); + + let having_clause = entity_models + .iter() + .map(|model| format!("INSTR(model_ids, '{:#x}') > 0", model)) + .collect::>() + .join(" OR "); + + if table == EVENT_MESSAGES_HISTORICAL_TABLE { + let count_query = format!( r#" - SELECT {table}.id, {table}.data, {table}.model_id, group_concat({model_relation_table}.model_id) as model_ids - FROM {table} + SELECT COUNT(*) FROM {table} JOIN {model_relation_table} ON {table}.id = {model_relation_table}.entity_id - WHERE {table}.keys REGEXP ? + WHERE {where_clause} GROUP BY {table}.event_id "# - ) - } else { - format!( - r#" - SELECT {table}.id, group_concat({model_relation_table}.model_id) as model_ids - FROM {table} - JOIN {model_relation_table} ON {table}.id = {model_relation_table}.entity_id - WHERE {table}.keys REGEXP ? - GROUP BY {table}.id - "# - ) - }; - - if !keys_clause.models.is_empty() { - // filter by models - models_query += &format!( - "HAVING {}", - keys_clause - .models - .iter() - .map(|model| { - let (namespace, name) = model - .split_once('-') - .ok_or(QueryError::InvalidNamespacedModel(model.clone()))?; - let model_id = compute_selector_from_names(namespace, name); - Ok(format!("INSTR(model_ids, '{:#x}') > 0", model_id)) - }) - .collect::, Error>>()? - .join(" OR ") - .as_str() ); - } - - models_query += &format!(" ORDER BY {table}.event_id DESC"); - - if limit.is_some() { - models_query += " LIMIT ?"; - } - if offset.is_some() { - models_query += " OFFSET ?"; - } + let mut total_count = sqlx::query_scalar(&count_query); + for value in &bind_values { + total_count = total_count.bind(value); + } + let total_count = total_count.fetch_one(&self.pool).await?; + if total_count == 0 { + return Ok((Vec::new(), 0)); + } - if table == EVENT_MESSAGES_HISTORICAL_TABLE { - let entities = self - .fetch_historical_event_messages(&models_query, Some(&keys_pattern), limit, offset) - .await?; + let entities = self.fetch_historical_event_messages( + &format!( + r#" + SELECT {table}.id, {table}.data, {table}.model_id, group_concat({model_relation_table}.model_id) as model_ids + FROM {table} + JOIN {model_relation_table} ON {table}.id = {model_relation_table}.entity_id + WHERE {where_clause} + GROUP BY {table}.event_id + ORDER BY {table}.event_id DESC + "# + ), + bind_values, + limit, + offset + ).await?; return Ok((entities, total_count)); } - let db_entities: Vec<(String, String)> = sqlx::query_as(&models_query) - .bind(&keys_pattern) - .bind(limit) - .bind(offset) - .fetch_all(&self.pool) - .await?; + let (rows, total_count) = fetch_entities( + &self.pool, + &schemas, + table, + model_relation_table, + entity_relation_column, + Some(&where_clause), + if !having_clause.is_empty() { Some(&having_clause) } else { None }, + order_by, + limit, + offset, + bind_values, + ) + .await?; + + let entities = rows + .par_iter() + .map(|row| map_row_to_entity(row, &schemas, dont_include_hashed_keys)) + .collect::, Error>>()?; - let entities = self - .fetch_entities(table, entity_relation_column, db_entities, dont_include_hashed_keys) - .await?; Ok((entities, total_count)) } @@ -640,19 +571,41 @@ impl DojoWorld { limit: Option, offset: Option, dont_include_hashed_keys: bool, + order_by: Option<&str>, + entity_models: Vec, + entity_updated_after: Option, ) -> Result<(Vec, u32), Error> { + let entity_models = + entity_models.iter().map(|model| compute_selector_from_tag(model)).collect::>(); let comparison_operator = ComparisonOperator::from_repr(member_clause.operator as usize) .expect("invalid comparison operator"); - let comparison_value = - match member_clause.value.ok_or(QueryError::MissingParam("value".into()))?.value_type { - Some(ValueType::String(value)) => value, + fn prepare_comparison( + value: &proto::types::MemberValue, + bind_values: &mut Vec, + ) -> Result { + match &value.value_type { + Some(ValueType::String(value)) => { + bind_values.push(value.to_string()); + Ok("?".to_string()) + } Some(ValueType::Primitive(value)) => { - let primitive: Primitive = value.try_into()?; - primitive.to_sql_value() + let primitive: Primitive = (value.clone()).try_into()?; + bind_values.push(primitive.to_sql_value()); + Ok("?".to_string()) } - None => return Err(QueryError::MissingParam("value_type".into()).into()), - }; + Some(ValueType::List(values)) => Ok(format!( + "({})", + values + .values + .iter() + .map(|v| prepare_comparison(v, bind_values)) + .collect::, Error>>()? + .join(", ") + )), + None => Err(QueryError::MissingParam("value_type".into()).into()), + } + } let (namespace, model) = member_clause .model @@ -680,53 +633,59 @@ impl DojoWorld { let model_ids = models_str .split(',') - .map(Felt::from_str) - .collect::, _>>() - .map_err(ParseError::FromStr)?; - let schemas = - self.model_cache.models(&model_ids).await?.into_iter().map(|m| m.schema).collect(); - - let model = member_clause.model.clone(); - let parts: Vec<&str> = member_clause.member.split('.').collect(); - let (table_name, column_name) = if parts.len() > 1 { - let nested_table = parts[..parts.len() - 1].join("$"); - (format!("{model}${nested_table}"), format!("external_{}", parts.last().unwrap())) - } else { - (model, format!("external_{}", member_clause.member)) - }; - let (entity_query, arrays_queries, count_query) = build_sql_query( + .filter_map(|id| { + let model_id = Felt::from_str(id).unwrap(); + if entity_models.is_empty() || entity_models.contains(&model_id) { + Some(model_id) + } else { + None + } + }) + .collect::>(); + let schemas = self + .model_cache + .models(&model_ids) + .await? + .into_iter() + .map(|m| m.schema) + .collect::>(); + + // Use the member name directly as the column name since it's already flattened + let mut bind_values = Vec::new(); + let value = prepare_comparison( + &member_clause.value.clone().ok_or(QueryError::MissingParam("value".into()))?, + &mut bind_values, + )?; + let mut where_clause = format!( + "[{}].[{}] {comparison_operator} {value}", + member_clause.model, member_clause.member + ); + if entity_updated_after.is_some() { + where_clause += &format!(" AND {table}.updated_at >= ?"); + bind_values.push(entity_updated_after.unwrap()); + } + + let (rows, total_count) = fetch_entities( + &self.pool, &schemas, table, + model_relation_table, entity_relation_column, - Some(&format!("[{table_name}].{column_name} {comparison_operator} ?")), + Some(&where_clause), None, + order_by, limit, offset, - )?; - - let total_count = sqlx::query_scalar(&count_query) - .bind(comparison_value.clone()) - .fetch_optional(&self.pool) - .await? - .unwrap_or(0); - let db_entities = sqlx::query(&entity_query) - .bind(comparison_value.clone()) - .bind(limit) - .bind(offset) - .fetch_all(&self.pool) - .await?; - let mut arrays_rows = HashMap::new(); - for (name, query) in arrays_queries { - let rows = - sqlx::query(&query).bind(comparison_value.clone()).fetch_all(&self.pool).await?; - arrays_rows.insert(name, rows); - } + bind_values, + ) + .await?; - let entities_collection: Result, Error> = db_entities + let entities = rows .par_iter() - .map(|row| map_row_to_entity(row, &arrays_rows, &schemas, dont_include_hashed_keys)) - .collect(); - Ok((entities_collection?, total_count)) + .map(|row| map_row_to_entity(row, &schemas, dont_include_hashed_keys)) + .collect::, Error>>()?; + + Ok((entities, total_count)) } #[allow(clippy::too_many_arguments)] @@ -739,56 +698,49 @@ impl DojoWorld { limit: Option, offset: Option, dont_include_hashed_keys: bool, + order_by: Option<&str>, + entity_models: Vec, + entity_updated_after: Option, ) -> Result<(Vec, u32), Error> { - let (where_clause, having_clause, join_clause, bind_values) = - build_composite_clause(table, model_relation_table, &composite)?; - - let count_query = format!( - r#" - SELECT COUNT(DISTINCT [{table}].id) - FROM [{table}] - JOIN {model_relation_table} ON [{table}].id = {model_relation_table}.entity_id - {join_clause} - {where_clause} - {having_clause} - "# - ); - - let mut count_query = sqlx::query_scalar::<_, u32>(&count_query); - for value in &bind_values { - count_query = count_query.bind(value); - } - - let total_count = count_query.fetch_optional(&self.pool).await?.unwrap_or(0); - if total_count == 0 { - return Ok((Vec::new(), 0)); - } + let (where_clause, bind_values) = + build_composite_clause(table, &composite, entity_updated_after)?; + + let entity_models = + entity_models.iter().map(|model| compute_selector_from_tag(model)).collect::>(); + let schemas = self + .model_cache + .models(&entity_models) + .await? + .into_iter() + .map(|m| m.schema) + .collect::>(); - let query = format!( - r#" - SELECT [{table}].id, group_concat({model_relation_table}.model_id) as model_ids - FROM [{table}] - JOIN {model_relation_table} ON [{table}].id = {model_relation_table}.entity_id - {join_clause} - {where_clause} - GROUP BY [{table}].id - {having_clause} - ORDER BY [{table}].event_id DESC - LIMIT ? OFFSET ? - "# - ); + let having_clause = entity_models + .iter() + .map(|model| format!("INSTR(model_ids, '{:#x}') > 0", model)) + .collect::>() + .join(" OR "); - let mut db_query = sqlx::query_as(&query); - for value in &bind_values { - db_query = db_query.bind(value); - } - db_query = db_query.bind(limit.unwrap_or(u32::MAX)).bind(offset.unwrap_or(0)); + let (rows, total_count) = fetch_entities( + &self.pool, + &schemas, + table, + model_relation_table, + entity_relation_column, + if where_clause.is_empty() { None } else { Some(&where_clause) }, + if having_clause.is_empty() { None } else { Some(&having_clause) }, + order_by, + limit, + offset, + bind_values, + ) + .await?; - let db_entities: Vec<(String, String)> = db_query.fetch_all(&self.pool).await?; + let entities = rows + .par_iter() + .map(|row| map_row_to_entity(row, &schemas, dont_include_hashed_keys)) + .collect::, Error>>()?; - let entities = self - .fetch_entities(table, entity_relation_column, db_entities, dont_include_hashed_keys) - .await?; Ok((entities, total_count)) } @@ -821,20 +773,17 @@ impl DojoWorld { let query = if contract_addresses.is_empty() { "SELECT * FROM tokens".to_string() } else { - format!( - "SELECT * FROM tokens WHERE contract_address IN ({})", - contract_addresses - .iter() - .map(|address| format!("{:#x}", address)) - .collect::>() - .join(", ") - ) + let placeholders = vec!["?"; contract_addresses.len()].join(", "); + format!("SELECT * FROM tokens WHERE contract_address IN ({})", placeholders) }; - let tokens: Vec = sqlx::query_as(&query) - .fetch_all(&self.pool) - .await - .map_err(|e| Status::internal(e.to_string()))?; + let mut query = sqlx::query_as(&query); + for address in &contract_addresses { + query = query.bind(format!("{:#x}", address)); + } + + let tokens: Vec = + query.fetch_all(&self.pool).await.map_err(|e| Status::internal(e.to_string()))?; let tokens = tokens.iter().map(|token| token.clone().into()).collect(); Ok(RetrieveTokensResponse { tokens }) @@ -846,42 +795,46 @@ impl DojoWorld { contract_addresses: Vec, ) -> Result { let mut query = "SELECT * FROM token_balances".to_string(); - + let mut bind_values = Vec::new(); let mut conditions = Vec::new(); + if !account_addresses.is_empty() { - conditions.push(format!( - "account_address IN ({})", - account_addresses - .iter() - .map(|address| format!("{:#x}", address)) - .collect::>() - .join(", ") - )); + let placeholders = vec!["?"; account_addresses.len()].join(", "); + conditions.push(format!("account_address IN ({})", placeholders)); + bind_values.extend(account_addresses.iter().map(|addr| format!("{:#x}", addr))); } + if !contract_addresses.is_empty() { - conditions.push(format!( - "contract_address IN ({})", - contract_addresses - .iter() - .map(|address| format!("{:#x}", address)) - .collect::>() - .join(", ") - )); + let placeholders = vec!["?"; contract_addresses.len()].join(", "); + conditions.push(format!("contract_address IN ({})", placeholders)); + bind_values.extend(contract_addresses.iter().map(|addr| format!("{:#x}", addr))); } if !conditions.is_empty() { query += &format!(" WHERE {}", conditions.join(" AND ")); } - let balances: Vec = sqlx::query_as(&query) - .fetch_all(&self.pool) - .await - .map_err(|e| Status::internal(e.to_string()))?; + let mut query = sqlx::query_as(&query); + for value in bind_values { + query = query.bind(value); + } + + let balances: Vec = + query.fetch_all(&self.pool).await.map_err(|e| Status::internal(e.to_string()))?; let balances = balances.iter().map(|balance| balance.clone().into()).collect(); Ok(RetrieveTokenBalancesResponse { balances }) } + async fn subscribe_token_balances( + &self, + contract_addresses: Vec, + account_addresses: Vec, + ) -> Result>, Error> + { + self.token_balance_manager.add_subscriber(contract_addresses, account_addresses).await + } + async fn subscribe_indexer( &self, contract_address: Felt, @@ -932,6 +885,43 @@ impl DojoWorld { entity_relation_column: &str, query: proto::types::Query, ) -> Result { + let order_by = query + .order_by + .iter() + .map(|order_by| { + format!( + "[{}].[{}] {}", + order_by.model, + order_by.member, + match order_by.direction { + 0 => "ASC", + 1 => "DESC", + _ => unreachable!(), + } + ) + }) + .collect::>() + .join(", "); + + let order_by = if order_by.is_empty() { None } else { Some(order_by.as_str()) }; + + let entity_updated_after = match query.entity_updated_after { + 0 => None, + _ => Some( + // This conversion would include a `UTC` suffix, which is not valid for the SQL + // query when comparing the timestamp with equality. + // To have `>=` working, we need to remove the `UTC` suffix. + DateTime::::from_timestamp(query.entity_updated_after as i64, 0) + .ok_or_else(|| { + Error::from(QueryError::InvalidTimestamp(query.entity_updated_after)) + })? + .to_string() + .replace("UTC", "") + .trim() + .to_string(), + ), + }; + let (entities, total_count) = match query.clause { None => { self.entities_all( @@ -941,6 +931,9 @@ impl DojoWorld { query.limit, query.offset, query.dont_include_hashed_keys, + order_by, + query.entity_models, + entity_updated_after, ) .await? } @@ -962,6 +955,9 @@ impl DojoWorld { Some(query.limit), Some(query.offset), query.dont_include_hashed_keys, + order_by, + query.entity_models, + entity_updated_after, ) .await? } @@ -974,6 +970,9 @@ impl DojoWorld { Some(query.limit), Some(query.offset), query.dont_include_hashed_keys, + order_by, + query.entity_models, + entity_updated_after, ) .await? } @@ -986,6 +985,9 @@ impl DojoWorld { Some(query.limit), Some(query.offset), query.dont_include_hashed_keys, + order_by, + query.entity_models, + entity_updated_after, ) .await? } @@ -998,6 +1000,9 @@ impl DojoWorld { Some(query.limit), Some(query.offset), query.dont_include_hashed_keys, + order_by, + query.entity_models, + entity_updated_after, ) .await? } @@ -1058,16 +1063,22 @@ fn map_row_to_event(row: &(String, String, String)) -> Result>, schemas: &[Ty], dont_include_hashed_keys: bool, ) -> Result { let hashed_keys = Felt::from_str(&row.get::("id")).map_err(ParseError::FromStr)?; + let model_ids = row + .get::("model_ids") + .split(',') + .map(|id| Felt::from_str(id).map_err(ParseError::FromStr)) + .collect::, _>>()?; + let models = schemas .iter() + .filter(|schema| model_ids.contains(&compute_selector_from_tag(&schema.name()))) .map(|schema| { let mut ty = schema.clone(); - map_row_to_ty("", &schema.name(), &mut ty, row, arrays_rows)?; + map_row_to_ty("", &schema.name(), &mut ty, row)?; Ok(ty.as_struct().unwrap().clone().into()) }) .collect::, Error>>()?; @@ -1103,7 +1114,7 @@ fn build_keys_pattern(clause: &proto::types::KeysClause) -> Result Result Result<(String, String, String, Vec), Error> { + entity_updated_after: Option, +) -> Result<(String, Vec), Error> { let is_or = composite.operator == LogicalOperator::Or as i32; let mut where_clauses = Vec::new(); - let mut join_clauses = Vec::new(); - let mut having_clauses = Vec::new(); let mut bind_values = Vec::new(); - // HashMap to track the number of joins per model - let mut model_counters: HashMap = HashMap::new(); - for clause in &composite.clauses { match clause.clause_type.as_ref().unwrap() { ClauseType::HashedKeys(hashed_keys) => { @@ -1137,92 +1143,96 @@ fn build_composite_clause( }) .collect::>() .join(", "); - where_clauses.push(format!("{table}.id IN ({})", ids)); + where_clauses.push(format!("({table}.id IN ({}))", ids)); } ClauseType::Keys(keys) => { let keys_pattern = build_keys_pattern(keys)?; bind_values.push(keys_pattern); - where_clauses.push(format!("{table}.keys REGEXP ?")); + where_clauses.push(format!("({table}.keys REGEXP ?)")); + + // Add model checks for specified models + + // NOTE: disabled since we are now using the top level entity models + + // for model in &keys.models { + // let (namespace, model_name) = model + // .split_once('-') + // .ok_or(QueryError::InvalidNamespacedModel(model.clone()))?; + // let model_id = compute_selector_from_names(namespace, model_name); + + // having_clauses.push(format!("INSTR(model_ids, '{:#x}') > 0", model_id)); + // } } ClauseType::Member(member) => { let comparison_operator = ComparisonOperator::from_repr(member.operator as usize) .expect("invalid comparison operator"); - let value = member.value.clone(); - let comparison_value = - match value.ok_or(QueryError::MissingParam("value".into()))?.value_type { - Some(ValueType::String(value)) => value, + let value = member.value.clone().ok_or(QueryError::MissingParam("value".into()))?; + fn prepare_comparison( + value: &proto::types::MemberValue, + bind_values: &mut Vec, + ) -> Result { + match &value.value_type { + Some(ValueType::String(value)) => { + bind_values.push(value.to_string()); + Ok("?".to_string()) + } Some(ValueType::Primitive(value)) => { - let primitive: Primitive = value.try_into()?; - primitive.to_sql_value() + let primitive: Primitive = (value.clone()).try_into()?; + bind_values.push(primitive.to_sql_value()); + Ok("?".to_string()) } - None => return Err(QueryError::MissingParam("value_type".into()).into()), - }; - bind_values.push(comparison_value); + Some(ValueType::List(values)) => Ok(format!( + "({})", + values + .values + .iter() + .map(|v| prepare_comparison(v, bind_values)) + .collect::, Error>>()? + .join(", ") + )), + None => Err(QueryError::MissingParam("value_type".into()).into()), + } + } + let value = prepare_comparison(&value, &mut bind_values)?; let model = member.model.clone(); - let parts: Vec<&str> = member.member.split('.').collect(); - let (table_name, column_name) = if parts.len() > 1 { - let nested_table = parts[..parts.len() - 1].join("$"); - ( - format!("[{model}${nested_table}]"), - format!("external_{}", parts.last().unwrap()), - ) - } else { - (format!("[{model}]"), format!("external_{}", member.member)) - }; - - let (namespace, model_name) = member - .model - .split_once('-') - .ok_or(QueryError::InvalidNamespacedModel(member.model.clone()))?; - let model_id = compute_selector_from_names(namespace, model_name); - - // Generate a unique alias for each model - let counter = model_counters.entry(model.clone()).or_insert(0); - *counter += 1; - let alias = - if *counter == 1 { model.clone() } else { format!("{model}_{}", *counter - 1) }; - - join_clauses.push(format!( - "LEFT JOIN {table_name} AS [{alias}] ON [{table}].id = [{alias}].entity_id" - )); - where_clauses.push(format!("[{alias}].{column_name} {comparison_operator} ?")); - having_clauses.push(format!( - "INSTR(group_concat({model_relation_table}.model_id), '{:#x}') > 0", - model_id - )); + + // Use the column name directly since it's already flattened + where_clauses + .push(format!("([{model}].[{}] {comparison_operator} {value})", member.member)); } - ClauseType::Composite(nested_composite) => { - let (nested_where, nested_having, nested_join, nested_values) = - build_composite_clause(table, model_relation_table, nested_composite)?; - where_clauses.push(format!("({})", nested_where.trim_start_matches("WHERE "))); - if !nested_having.is_empty() { - having_clauses.push(nested_having.trim_start_matches("HAVING ").to_string()); + ClauseType::Composite(nested) => { + // Handle nested composite by recursively building the clause + let (nested_where, nested_values) = + build_composite_clause(table, nested, entity_updated_after.clone())?; + + if !nested_where.is_empty() { + where_clauses.push(nested_where); } - join_clauses.extend( - nested_join - .split_whitespace() - .filter(|&s| s.starts_with("LEFT")) - .map(String::from), - ); bind_values.extend(nested_values); } } } - let join_clause = join_clauses.join(" "); let where_clause = if !where_clauses.is_empty() { - format!("WHERE {}", where_clauses.join(if is_or { " OR " } else { " AND " })) - } else { - String::new() - }; - let having_clause = if !having_clauses.is_empty() { - format!("HAVING {}", having_clauses.join(if is_or { " OR " } else { " AND " })) + format!( + "{} {}", + where_clauses.join(if is_or { " OR " } else { " AND " }), + if let Some(entity_updated_after) = entity_updated_after.clone() { + bind_values.push(entity_updated_after); + format!("AND {table}.updated_at >= ?") + } else { + String::new() + } + ) + } else if let Some(entity_updated_after) = entity_updated_after.clone() { + bind_values.push(entity_updated_after); + format!("{table}.updated_at >= ?") } else { String::new() }; - Ok((where_clause, having_clause, join_clause, bind_values)) + Ok((where_clause, bind_values)) } type ServiceResult = Result, Status>; @@ -1236,6 +1246,8 @@ type SubscribeIndexerResponseStream = Pin> + Send>>; type RetrieveEntitiesStreamingResponseStream = Pin> + Send>>; +type SubscribeTokenBalancesResponseStream = + Pin> + Send>>; #[tonic::async_trait] impl proto::world::world_server::World for DojoWorld { @@ -1245,6 +1257,7 @@ impl proto::world::world_server::World for DojoWorld { type SubscribeEventsStream = SubscribeEventsResponseStream; type SubscribeIndexerStream = SubscribeIndexerResponseStream; type RetrieveEntitiesStreamingStream = RetrieveEntitiesStreamingResponseStream; + type SubscribeTokenBalancesStream = SubscribeTokenBalancesResponseStream; async fn world_metadata( &self, @@ -1347,6 +1360,52 @@ impl proto::world::world_server::World for DojoWorld { Ok(Response::new(())) } + async fn subscribe_token_balances( + &self, + request: Request, + ) -> ServiceResult { + let RetrieveTokenBalancesRequest { contract_addresses, account_addresses } = + request.into_inner(); + let contract_addresses = contract_addresses + .iter() + .map(|address| Felt::from_bytes_be_slice(address)) + .collect::>(); + let account_addresses = account_addresses + .iter() + .map(|address| Felt::from_bytes_be_slice(address)) + .collect::>(); + + let rx = self + .subscribe_token_balances(contract_addresses, account_addresses) + .await + .map_err(|e| Status::internal(e.to_string()))?; + Ok(Response::new(Box::pin(ReceiverStream::new(rx)) as Self::SubscribeTokenBalancesStream)) + } + + async fn update_token_balances_subscription( + &self, + request: Request, + ) -> ServiceResult<()> { + let UpdateTokenBalancesSubscriptionRequest { + subscription_id, + contract_addresses, + account_addresses, + } = request.into_inner(); + let contract_addresses = contract_addresses + .iter() + .map(|address| Felt::from_bytes_be_slice(address)) + .collect::>(); + let account_addresses = account_addresses + .iter() + .map(|address| Felt::from_bytes_be_slice(address)) + .collect::>(); + + self.token_balance_manager + .update_subscriber(subscription_id, contract_addresses, account_addresses) + .await; + Ok(Response::new(())) + } + async fn retrieve_entities( &self, request: Request, diff --git a/crates/torii/grpc/src/server/subscriptions/entity.rs b/crates/torii/grpc/src/server/subscriptions/entity.rs index f8793b5109..d014f3b4dd 100644 --- a/crates/torii/grpc/src/server/subscriptions/entity.rs +++ b/crates/torii/grpc/src/server/subscriptions/entity.rs @@ -13,10 +13,10 @@ use tokio::sync::mpsc::{ channel, unbounded_channel, Receiver, Sender, UnboundedReceiver, UnboundedSender, }; use tokio::sync::RwLock; -use torii_core::constants::SQL_FELT_DELIMITER; -use torii_core::error::{Error, ParseError}; -use torii_core::simple_broker::SimpleBroker; -use torii_core::types::OptimisticEntity; +use torii_sqlite::constants::SQL_FELT_DELIMITER; +use torii_sqlite::error::{Error, ParseError}; +use torii_sqlite::simple_broker::SimpleBroker; +use torii_sqlite::types::OptimisticEntity; use tracing::{error, trace}; use super::match_entity_keys; @@ -114,6 +114,7 @@ impl Service { ) -> Result<(), Error> { let mut closed_stream = Vec::new(); let hashed = Felt::from_str(&entity.id).map_err(ParseError::FromStr)?; + // sometimes for some reason keys isxx empty. investigate the issue let keys = entity .keys .trim_end_matches(SQL_FELT_DELIMITER) diff --git a/crates/torii/grpc/src/server/subscriptions/error.rs b/crates/torii/grpc/src/server/subscriptions/error.rs index c901113f98..70c9c3fe38 100644 --- a/crates/torii/grpc/src/server/subscriptions/error.rs +++ b/crates/torii/grpc/src/server/subscriptions/error.rs @@ -1,5 +1,5 @@ use starknet::providers::ProviderError; -use torii_core::error::ParseError; +use torii_sqlite::error::ParseError; #[derive(Debug, thiserror::Error)] pub enum SubscriptionError { diff --git a/crates/torii/grpc/src/server/subscriptions/event.rs b/crates/torii/grpc/src/server/subscriptions/event.rs index 65a7cade52..c615b1d732 100644 --- a/crates/torii/grpc/src/server/subscriptions/event.rs +++ b/crates/torii/grpc/src/server/subscriptions/event.rs @@ -13,10 +13,10 @@ use tokio::sync::mpsc::{ channel, unbounded_channel, Receiver, Sender, UnboundedReceiver, UnboundedSender, }; use tokio::sync::RwLock; -use torii_core::constants::SQL_FELT_DELIMITER; -use torii_core::error::{Error, ParseError}; -use torii_core::simple_broker::SimpleBroker; -use torii_core::types::Event; +use torii_sqlite::constants::SQL_FELT_DELIMITER; +use torii_sqlite::error::{Error, ParseError}; +use torii_sqlite::simple_broker::SimpleBroker; +use torii_sqlite::types::Event; use tracing::{error, trace}; use super::match_keys; diff --git a/crates/torii/grpc/src/server/subscriptions/event_message.rs b/crates/torii/grpc/src/server/subscriptions/event_message.rs index db1141cee8..fd5419d095 100644 --- a/crates/torii/grpc/src/server/subscriptions/event_message.rs +++ b/crates/torii/grpc/src/server/subscriptions/event_message.rs @@ -13,10 +13,10 @@ use tokio::sync::mpsc::{ channel, unbounded_channel, Receiver, Sender, UnboundedReceiver, UnboundedSender, }; use tokio::sync::RwLock; -use torii_core::constants::SQL_FELT_DELIMITER; -use torii_core::error::{Error, ParseError}; -use torii_core::simple_broker::SimpleBroker; -use torii_core::types::OptimisticEventMessage; +use torii_sqlite::constants::SQL_FELT_DELIMITER; +use torii_sqlite::error::{Error, ParseError}; +use torii_sqlite::simple_broker::SimpleBroker; +use torii_sqlite::types::OptimisticEventMessage; use tracing::{error, trace}; use super::match_entity_keys; diff --git a/crates/torii/grpc/src/server/subscriptions/indexer.rs b/crates/torii/grpc/src/server/subscriptions/indexer.rs index 11271ce970..b342ffbc57 100644 --- a/crates/torii/grpc/src/server/subscriptions/indexer.rs +++ b/crates/torii/grpc/src/server/subscriptions/indexer.rs @@ -13,9 +13,9 @@ use tokio::sync::mpsc::{ channel, unbounded_channel, Receiver, Sender, UnboundedReceiver, UnboundedSender, }; use tokio::sync::RwLock; -use torii_core::error::{Error, ParseError}; -use torii_core::simple_broker::SimpleBroker; -use torii_core::types::ContractCursor as ContractUpdated; +use torii_sqlite::error::{Error, ParseError}; +use torii_sqlite::simple_broker::SimpleBroker; +use torii_sqlite::types::ContractCursor as ContractUpdated; use tracing::{error, trace}; use crate::proto; diff --git a/crates/torii/grpc/src/server/subscriptions/mod.rs b/crates/torii/grpc/src/server/subscriptions/mod.rs index b58810d611..caaa38736e 100644 --- a/crates/torii/grpc/src/server/subscriptions/mod.rs +++ b/crates/torii/grpc/src/server/subscriptions/mod.rs @@ -9,6 +9,7 @@ pub mod event; pub mod event_message; pub mod indexer; pub mod model_diff; +pub mod token_balance; pub(crate) fn match_entity_keys( id: Felt, diff --git a/crates/torii/grpc/src/server/subscriptions/model_diff.rs b/crates/torii/grpc/src/server/subscriptions/model_diff.rs index ba9ef6b49c..7ad3eaab3f 100644 --- a/crates/torii/grpc/src/server/subscriptions/model_diff.rs +++ b/crates/torii/grpc/src/server/subscriptions/model_diff.rs @@ -16,7 +16,7 @@ use starknet::providers::Provider; use starknet_crypto::poseidon_hash_many; use tokio::sync::mpsc::{channel, Receiver, Sender}; use tokio::sync::RwLock; -use torii_core::error::Error; +use torii_sqlite::error::Error; use tracing::{debug, error, trace}; use super::error::SubscriptionError; diff --git a/crates/torii/grpc/src/server/subscriptions/token_balance.rs b/crates/torii/grpc/src/server/subscriptions/token_balance.rs new file mode 100644 index 0000000000..e883d6fb0b --- /dev/null +++ b/crates/torii/grpc/src/server/subscriptions/token_balance.rs @@ -0,0 +1,193 @@ +use std::collections::{HashMap, HashSet}; +use std::future::Future; +use std::pin::Pin; +use std::str::FromStr; +use std::sync::Arc; +use std::task::{Context, Poll}; + +use futures::{Stream, StreamExt}; +use rand::Rng; +use starknet_crypto::Felt; +use tokio::sync::mpsc::{ + channel, unbounded_channel, Receiver, Sender, UnboundedReceiver, UnboundedSender, +}; +use tokio::sync::RwLock; +use torii_sqlite::error::{Error, ParseError}; +use torii_sqlite::simple_broker::SimpleBroker; +use torii_sqlite::types::TokenBalance; +use tracing::{error, trace}; + +use crate::proto; +use crate::proto::world::SubscribeTokenBalancesResponse; + +pub(crate) const LOG_TARGET: &str = "torii::grpc::server::subscriptions::balance"; + +#[derive(Debug)] +pub struct TokenBalanceSubscriber { + /// Contract addresses that the subscriber is interested in + /// If empty, subscriber receives updates for all contracts + pub contract_addresses: HashSet, + /// Account addresses that the subscriber is interested in + /// If empty, subscriber receives updates for all accounts + pub account_addresses: HashSet, + /// The channel to send the response back to the subscriber. + pub sender: Sender>, +} + +#[derive(Debug, Default)] +pub struct TokenBalanceManager { + subscribers: RwLock>, +} + +impl TokenBalanceManager { + pub async fn add_subscriber( + &self, + contract_addresses: Vec, + account_addresses: Vec, + ) -> Result>, Error> { + let subscription_id = rand::thread_rng().gen::(); + let (sender, receiver) = channel(1); + + // Send initial empty response + let _ = sender + .send(Ok(SubscribeTokenBalancesResponse { subscription_id, balance: None })) + .await; + + self.subscribers.write().await.insert( + subscription_id, + TokenBalanceSubscriber { + contract_addresses: contract_addresses.into_iter().collect(), + account_addresses: account_addresses.into_iter().collect(), + sender, + }, + ); + + Ok(receiver) + } + + pub async fn update_subscriber( + &self, + id: u64, + contract_addresses: Vec, + account_addresses: Vec, + ) { + let sender = { + let subscribers = self.subscribers.read().await; + if let Some(subscriber) = subscribers.get(&id) { + subscriber.sender.clone() + } else { + return; // Subscriber not found, exit early + } + }; + + self.subscribers.write().await.insert( + id, + TokenBalanceSubscriber { + contract_addresses: contract_addresses.into_iter().collect(), + account_addresses: account_addresses.into_iter().collect(), + sender, + }, + ); + } + + pub(super) async fn remove_subscriber(&self, id: u64) { + self.subscribers.write().await.remove(&id); + } +} + +#[must_use = "Service does nothing unless polled"] +#[allow(missing_debug_implementations)] +pub struct Service { + simple_broker: Pin + Send>>, + balance_sender: UnboundedSender, +} + +impl Service { + pub fn new(subs_manager: Arc) -> Self { + let (balance_sender, balance_receiver) = unbounded_channel(); + let service = Self { + simple_broker: Box::pin(SimpleBroker::::subscribe()), + balance_sender, + }; + + tokio::spawn(Self::publish_updates(subs_manager, balance_receiver)); + + service + } + + async fn publish_updates( + subs: Arc, + mut balance_receiver: UnboundedReceiver, + ) { + while let Some(balance) = balance_receiver.recv().await { + if let Err(e) = Self::process_balance_update(&subs, &balance).await { + error!(target = LOG_TARGET, error = %e, "Processing balance update."); + } + } + } + + async fn process_balance_update( + subs: &Arc, + balance: &TokenBalance, + ) -> Result<(), Error> { + let mut closed_stream = Vec::new(); + + for (idx, sub) in subs.subscribers.read().await.iter() { + let contract_address = + Felt::from_str(&balance.contract_address).map_err(ParseError::FromStr)?; + let account_address = + Felt::from_str(&balance.account_address).map_err(ParseError::FromStr)?; + + // Skip if contract address filter doesn't match + if !sub.contract_addresses.is_empty() + && !sub.contract_addresses.contains(&contract_address) + { + continue; + } + + // Skip if account address filter doesn't match + if !sub.account_addresses.is_empty() + && !sub.account_addresses.contains(&account_address) + { + continue; + } + + let resp = SubscribeTokenBalancesResponse { + subscription_id: *idx, + balance: Some(proto::types::TokenBalance { + contract_address: balance.contract_address.clone(), + account_address: balance.account_address.clone(), + token_id: balance.token_id.clone(), + balance: balance.balance.clone(), + }), + }; + + if sub.sender.send(Ok(resp)).await.is_err() { + closed_stream.push(*idx); + } + } + + for id in closed_stream { + trace!(target = LOG_TARGET, id = %id, "Closing balance stream."); + subs.remove_subscriber(id).await + } + + Ok(()) + } +} + +impl Future for Service { + type Output = (); + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.get_mut(); + + while let Poll::Ready(Some(balance)) = this.simple_broker.poll_next_unpin(cx) { + if let Err(e) = this.balance_sender.send(balance) { + error!(target = LOG_TARGET, error = %e, "Sending balance update to processor."); + } + } + + Poll::Pending + } +} diff --git a/crates/torii/grpc/src/server/tests/entities_test.rs b/crates/torii/grpc/src/server/tests/entities_test.rs index 031779da58..37f7674cec 100644 --- a/crates/torii/grpc/src/server/tests/entities_test.rs +++ b/crates/torii/grpc/src/server/tests/entities_test.rs @@ -22,11 +22,11 @@ use starknet::providers::{JsonRpcClient, Provider}; use starknet_crypto::poseidon_hash_many; use tempfile::NamedTempFile; use tokio::sync::broadcast; -use torii_core::engine::{Engine, EngineConfig, Processors}; -use torii_core::executor::Executor; -use torii_core::sql::cache::ModelCache; -use torii_core::sql::Sql; -use torii_core::types::{Contract, ContractType}; +use torii_indexer::engine::{Engine, EngineConfig, Processors}; +use torii_sqlite::cache::ModelCache; +use torii_sqlite::executor::Executor; +use torii_sqlite::types::{Contract, ContractType}; +use torii_sqlite::Sql; use crate::proto::types::KeysClause; use crate::server::DojoWorld; @@ -132,7 +132,7 @@ async fn test_entities_queries(sequencer: &RunnerCtx) { .query_by_keys( "entities", "entity_model", - "entity_id", + "internal_entity_id", &KeysClause { keys: vec![account.address().to_bytes_be().to_vec()], pattern_matching: 0, @@ -141,6 +141,9 @@ async fn test_entities_queries(sequencer: &RunnerCtx) { Some(1), None, false, + None, + vec!["ns-Moves".to_string(), "ns-Position".to_string()], + None, ) .await .unwrap() diff --git a/crates/torii/grpc/src/types/mod.rs b/crates/torii/grpc/src/types/mod.rs index 0b1c18430c..6e088a7ae2 100644 --- a/crates/torii/grpc/src/types/mod.rs +++ b/crates/torii/grpc/src/types/mod.rs @@ -79,12 +79,40 @@ impl From for IndexerUpdate { } } +#[derive(Debug, Serialize, Deserialize, PartialEq, Hash, Eq, Clone)] +pub struct OrderBy { + pub model: String, + pub member: String, + pub direction: OrderDirection, +} + +impl From for proto::types::OrderBy { + fn from(value: OrderBy) -> Self { + Self { model: value.model, member: value.member, direction: value.direction as i32 } + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Hash, Eq, Clone)] +pub enum OrderDirection { + Asc, + Desc, +} + #[derive(Debug, Serialize, Deserialize, PartialEq, Hash, Eq, Clone)] pub struct Query { pub clause: Option, pub limit: u32, pub offset: u32, + /// Whether or not to include the hashed keys (entity id) of the entities. + /// This is useful for large queries compressed with GZIP to reduce the size of the response. pub dont_include_hashed_keys: bool, + pub order_by: Vec, + /// If the array is not empty, only the given models are retrieved. + /// All entities that don't have a model in the array are excluded. + pub entity_models: Vec, + /// The internal updated at timestamp in seconds (unix timestamp) from which entities are + /// retrieved (inclusive). Use 0 to retrieve all entities. + pub entity_updated_after: u64, } #[derive(Debug, Serialize, Deserialize, PartialEq, Hash, Eq, Clone)] @@ -123,6 +151,7 @@ pub enum PatternMatching { pub enum MemberValue { Primitive(Primitive), String(String), + List(Vec), } #[derive(Debug, Serialize, Deserialize, PartialEq, Hash, Eq, Clone)] @@ -159,6 +188,8 @@ pub enum ComparisonOperator { Gte, Lt, Lte, + In, + NotIn, } impl fmt::Display for ComparisonOperator { @@ -170,6 +201,8 @@ impl fmt::Display for ComparisonOperator { ComparisonOperator::Lte => write!(f, "<="), ComparisonOperator::Neq => write!(f, "!="), ComparisonOperator::Eq => write!(f, "="), + ComparisonOperator::In => write!(f, "IN"), + ComparisonOperator::NotIn => write!(f, "NOT IN"), } } } @@ -183,6 +216,8 @@ impl From for ComparisonOperator { proto::types::ComparisonOperator::Lt => ComparisonOperator::Lt, proto::types::ComparisonOperator::Lte => ComparisonOperator::Lte, proto::types::ComparisonOperator::Neq => ComparisonOperator::Neq, + proto::types::ComparisonOperator::In => ComparisonOperator::In, + proto::types::ComparisonOperator::NotIn => ComparisonOperator::NotIn, } } } @@ -248,6 +283,9 @@ impl From for proto::types::Query { limit: value.limit, offset: value.offset, dont_include_hashed_keys: value.dont_include_hashed_keys, + order_by: value.order_by.into_iter().map(|o| o.into()).collect(), + entity_models: value.entity_models, + entity_updated_after: value.entity_updated_after, } } } @@ -381,6 +419,14 @@ impl From for member_value::ValueType { member_value::ValueType::Primitive(primitive.into()) } MemberValue::String(string) => member_value::ValueType::String(string), + MemberValue::List(list) => { + member_value::ValueType::List(proto::types::MemberValueList { + values: list + .into_iter() + .map(|v| proto::types::MemberValue { value_type: Some(v.into()) }) + .collect(), + }) + } } } } diff --git a/crates/torii/core/Cargo.toml b/crates/torii/indexer/Cargo.toml similarity index 90% rename from crates/torii/core/Cargo.toml rename to crates/torii/indexer/Cargo.toml index 09b25d8cca..2a2e1ad22c 100644 --- a/crates/torii/core/Cargo.toml +++ b/crates/torii/indexer/Cargo.toml @@ -1,8 +1,8 @@ [package] -description = "Torii core implementation." +description = "Torii indexer implementation." edition.workspace = true license-file.workspace = true -name = "torii-core" +name = "torii-indexer" repository.workspace = true version.workspace = true @@ -18,6 +18,7 @@ chrono.workspace = true crypto-bigint.workspace = true data-url.workspace = true dojo-types.workspace = true +dojo-utils.workspace = true dojo-world.workspace = true futures-channel = "0.3.0" futures-util.workspace = true @@ -28,7 +29,6 @@ reqwest.workspace = true serde.workspace = true serde_json.workspace = true slab = "0.4.2" -sqlx.workspace = true starknet-crypto.workspace = true starknet.workspace = true thiserror.workspace = true @@ -37,6 +37,7 @@ tokio = { version = "1.32.0", features = [ "macros", "sync" ], default-features ipfs-api-backend-hyper.workspace = true tokio-util.workspace = true tracing.workspace = true +torii-sqlite.workspace = true [dev-dependencies] dojo-test-utils.workspace = true @@ -45,3 +46,4 @@ katana-runner.workspace = true scarb.workspace = true sozo-scarbext.workspace = true tempfile.workspace = true +sqlx.workspace = true diff --git a/crates/torii/indexer/src/constants.rs b/crates/torii/indexer/src/constants.rs new file mode 100644 index 0000000000..9bdbfc9a40 --- /dev/null +++ b/crates/torii/indexer/src/constants.rs @@ -0,0 +1 @@ +pub(crate) const LOG_TARGET: &str = "torii::engine"; diff --git a/crates/torii/core/src/engine.rs b/crates/torii/indexer/src/engine.rs similarity index 95% rename from crates/torii/core/src/engine.rs rename to crates/torii/indexer/src/engine.rs index 86730665f3..1210c5ec9d 100644 --- a/crates/torii/core/src/engine.rs +++ b/crates/torii/indexer/src/engine.rs @@ -6,6 +6,7 @@ use std::time::Duration; use anyhow::Result; use bitflags::bitflags; +use dojo_utils::provider as provider_utils; use dojo_world::contracts::world::WorldContractReader; use futures_util::future::{join_all, try_join_all}; use hashlink::LinkedHashMap; @@ -22,6 +23,8 @@ use tokio::sync::mpsc::Sender as BoundedSender; use tokio::sync::Semaphore; use tokio::task::JoinSet; use tokio::time::{sleep, Instant}; +use torii_sqlite::types::{Contract, ContractType}; +use torii_sqlite::{Cursors, Sql}; use tracing::{debug, error, info, trace, warn}; use crate::constants::LOG_TARGET; @@ -43,8 +46,6 @@ use crate::processors::upgrade_model::UpgradeModelProcessor; use crate::processors::{ BlockProcessor, EventProcessor, EventProcessorConfig, TransactionProcessor, }; -use crate::sql::{Cursors, Sql}; -use crate::types::{Contract, ContractType}; type EventProcessorMap

= HashMap>>>; @@ -147,6 +148,7 @@ pub struct EngineConfig { pub max_concurrent_tasks: usize, pub flags: IndexingFlags, pub event_processor_config: EventProcessorConfig, + pub world_block: u64, } impl Default for EngineConfig { @@ -158,6 +160,7 @@ impl Default for EngineConfig { max_concurrent_tasks: 100, flags: IndexingFlags::empty(), event_processor_config: EventProcessorConfig::default(), + world_block: 0, } } } @@ -169,6 +172,17 @@ pub enum FetchDataResult { None, } +impl FetchDataResult { + pub fn block_id(&self) -> Option { + match self { + FetchDataResult::Range(range) => Some(BlockId::Number(range.latest_block_number)), + FetchDataResult::Pending(_pending) => Some(BlockId::Tag(BlockTag::Pending)), + // we dont require block_id when result is none, we return None + FetchDataResult::None => None, + } + } +} + #[derive(Debug)] pub struct FetchRangeResult { // (block_number, transaction_hash) -> events @@ -241,6 +255,11 @@ impl Engine

{ } pub async fn start(&mut self) -> Result<()> { + if let Err(e) = provider_utils::health_check_provider(self.provider.clone()).await { + error!(target: LOG_TARGET,"Provider health check failed during engine start"); + return Err(e); + } + let mut backoff_delay = Duration::from_secs(1); let max_backoff_delay = Duration::from_secs(60); @@ -263,11 +282,16 @@ impl Engine

{ info!(target: LOG_TARGET, "Syncing reestablished."); } + let block_id = fetch_result.block_id(); match self.process(fetch_result).await { Ok(_) => { - self.db.flush().await?; - self.db.apply_cache_diff().await?; - self.db.execute().await?; + // Its only `None` when `FetchDataResult::None` in which case + // we don't need to flush or apply cache diff + if let Some(block_id) = block_id { + self.db.flush().await?; + self.db.apply_cache_diff(block_id).await?; + self.db.execute().await?; + } }, Err(e) => { error!(target: LOG_TARGET, error = %e, "Processing fetched data."); @@ -301,7 +325,7 @@ impl Engine

{ pub async fn fetch_data(&mut self, cursors: &Cursors) -> Result { let latest_block = self.provider.block_hash_and_number().await?; - let from = cursors.head.unwrap_or(0); + let from = cursors.head.unwrap_or(self.config.world_block); let total_remaining_blocks = latest_block.block_number - from; let blocks_to_process = total_remaining_blocks.min(self.config.blocks_chunk_size); let to = from + blocks_to_process; @@ -835,8 +859,10 @@ impl Engine

{ let task_identifier = match processor.event_key().as_str() { "StoreSetRecord" | "StoreUpdateRecord" | "StoreUpdateMember" | "StoreDelRecord" => { let mut hasher = DefaultHasher::new(); - event.keys[0].hash(&mut hasher); + // model selector event.keys[1].hash(&mut hasher); + // entity id + event.keys[2].hash(&mut hasher); hasher.finish() } _ => 0, diff --git a/crates/torii/indexer/src/lib.rs b/crates/torii/indexer/src/lib.rs new file mode 100644 index 0000000000..7191c5480f --- /dev/null +++ b/crates/torii/indexer/src/lib.rs @@ -0,0 +1,10 @@ +mod constants; + +#[cfg(test)] +#[path = "test.rs"] +mod test; + +pub mod engine; +pub mod processors; + +pub use engine::Engine; diff --git a/crates/torii/core/src/processors/erc20_legacy_transfer.rs b/crates/torii/indexer/src/processors/erc20_legacy_transfer.rs similarity index 91% rename from crates/torii/core/src/processors/erc20_legacy_transfer.rs rename to crates/torii/indexer/src/processors/erc20_legacy_transfer.rs index 4ed17416bc..3b207d466b 100644 --- a/crates/torii/core/src/processors/erc20_legacy_transfer.rs +++ b/crates/torii/indexer/src/processors/erc20_legacy_transfer.rs @@ -4,12 +4,12 @@ use cainome::cairo_serde::{CairoSerde, U256 as U256Cainome}; use dojo_world::contracts::world::WorldContractReader; use starknet::core::types::{Event, U256}; use starknet::providers::Provider; +use torii_sqlite::Sql; use tracing::debug; use super::{EventProcessor, EventProcessorConfig}; -use crate::sql::Sql; -pub(crate) const LOG_TARGET: &str = "torii_core::processors::erc20_legacy_transfer"; +pub(crate) const LOG_TARGET: &str = "torii_indexer::processors::erc20_legacy_transfer"; #[derive(Default, Debug)] pub struct Erc20LegacyTransferProcessor; @@ -38,7 +38,7 @@ where &self, world: &WorldContractReader

, db: &mut Sql, - _block_number: u64, + block_number: u64, block_timestamp: u64, event_id: &str, event: &Event, @@ -59,6 +59,7 @@ where world.provider(), block_timestamp, event_id, + block_number, ) .await?; debug!(target: LOG_TARGET,from = ?from, to = ?to, value = ?value, "Legacy ERC20 Transfer"); diff --git a/crates/torii/core/src/processors/erc20_transfer.rs b/crates/torii/indexer/src/processors/erc20_transfer.rs similarity index 92% rename from crates/torii/core/src/processors/erc20_transfer.rs rename to crates/torii/indexer/src/processors/erc20_transfer.rs index 64f50d13a2..a0643abd41 100644 --- a/crates/torii/core/src/processors/erc20_transfer.rs +++ b/crates/torii/indexer/src/processors/erc20_transfer.rs @@ -4,12 +4,12 @@ use cainome::cairo_serde::{CairoSerde, U256 as U256Cainome}; use dojo_world::contracts::world::WorldContractReader; use starknet::core::types::{Event, U256}; use starknet::providers::Provider; +use torii_sqlite::Sql; use tracing::debug; use super::{EventProcessor, EventProcessorConfig}; -use crate::sql::Sql; -pub(crate) const LOG_TARGET: &str = "torii_core::processors::erc20_transfer"; +pub(crate) const LOG_TARGET: &str = "torii_indexer::processors::erc20_transfer"; #[derive(Default, Debug)] pub struct Erc20TransferProcessor; @@ -38,7 +38,7 @@ where &self, world: &WorldContractReader

, db: &mut Sql, - _block_number: u64, + block_number: u64, block_timestamp: u64, event_id: &str, event: &Event, @@ -59,6 +59,7 @@ where world.provider(), block_timestamp, event_id, + block_number, ) .await?; debug!(target: LOG_TARGET,from = ?from, to = ?to, value = ?value, "ERC20 Transfer"); diff --git a/crates/torii/core/src/processors/erc721_legacy_transfer.rs b/crates/torii/indexer/src/processors/erc721_legacy_transfer.rs similarity index 81% rename from crates/torii/core/src/processors/erc721_legacy_transfer.rs rename to crates/torii/indexer/src/processors/erc721_legacy_transfer.rs index ebbf4d6b53..df6b2a88de 100644 --- a/crates/torii/core/src/processors/erc721_legacy_transfer.rs +++ b/crates/torii/indexer/src/processors/erc721_legacy_transfer.rs @@ -4,12 +4,12 @@ use cainome::cairo_serde::{CairoSerde, U256 as U256Cainome}; use dojo_world::contracts::world::WorldContractReader; use starknet::core::types::{Event, U256}; use starknet::providers::Provider; +use torii_sqlite::Sql; use tracing::debug; use super::{EventProcessor, EventProcessorConfig}; -use crate::sql::Sql; -pub(crate) const LOG_TARGET: &str = "torii_core::processors::erc721_legacy_transfer"; +pub(crate) const LOG_TARGET: &str = "torii_indexer::processors::erc721_legacy_transfer"; #[derive(Default, Debug)] pub struct Erc721LegacyTransferProcessor; @@ -38,7 +38,7 @@ where &self, _world: &WorldContractReader

, db: &mut Sql, - _block_number: u64, + block_number: u64, block_timestamp: u64, event_id: &str, event: &Event, @@ -51,8 +51,16 @@ where let token_id = U256Cainome::cairo_deserialize(&event.data, 2)?; let token_id = U256::from_words(token_id.low, token_id.high); - db.handle_erc721_transfer(token_address, from, to, token_id, block_timestamp, event_id) - .await?; + db.handle_erc721_transfer( + token_address, + from, + to, + token_id, + block_timestamp, + event_id, + block_number, + ) + .await?; debug!(target: LOG_TARGET, from = ?from, to = ?to, token_id = ?token_id, "ERC721 Transfer"); Ok(()) diff --git a/crates/torii/core/src/processors/erc721_transfer.rs b/crates/torii/indexer/src/processors/erc721_transfer.rs similarity index 82% rename from crates/torii/core/src/processors/erc721_transfer.rs rename to crates/torii/indexer/src/processors/erc721_transfer.rs index a0f56479a9..faf124360b 100644 --- a/crates/torii/core/src/processors/erc721_transfer.rs +++ b/crates/torii/indexer/src/processors/erc721_transfer.rs @@ -4,12 +4,12 @@ use cainome::cairo_serde::{CairoSerde, U256 as U256Cainome}; use dojo_world::contracts::world::WorldContractReader; use starknet::core::types::{Event, U256}; use starknet::providers::Provider; +use torii_sqlite::Sql; use tracing::debug; use super::{EventProcessor, EventProcessorConfig}; -use crate::sql::Sql; -pub(crate) const LOG_TARGET: &str = "torii_core::processors::erc721_transfer"; +pub(crate) const LOG_TARGET: &str = "torii_indexer::processors::erc721_transfer"; #[derive(Default, Debug)] pub struct Erc721TransferProcessor; @@ -38,7 +38,7 @@ where &self, _world: &WorldContractReader

, db: &mut Sql, - _block_number: u64, + block_number: u64, block_timestamp: u64, event_id: &str, event: &Event, @@ -51,8 +51,16 @@ where let token_id = U256Cainome::cairo_deserialize(&event.keys, 3)?; let token_id = U256::from_words(token_id.low, token_id.high); - db.handle_erc721_transfer(token_address, from, to, token_id, block_timestamp, event_id) - .await?; + db.handle_erc721_transfer( + token_address, + from, + to, + token_id, + block_timestamp, + event_id, + block_number, + ) + .await?; debug!(target: LOG_TARGET, from = ?from, to = ?to, token_id = ?token_id, "ERC721 Transfer"); Ok(()) diff --git a/crates/torii/core/src/processors/event_message.rs b/crates/torii/indexer/src/processors/event_message.rs similarity index 95% rename from crates/torii/core/src/processors/event_message.rs rename to crates/torii/indexer/src/processors/event_message.rs index 9d36368d3b..4495665bed 100644 --- a/crates/torii/core/src/processors/event_message.rs +++ b/crates/torii/indexer/src/processors/event_message.rs @@ -5,12 +5,12 @@ use dojo_world::contracts::naming::get_tag; use dojo_world::contracts::world::WorldContractReader; use starknet::core::types::{Event, Felt}; use starknet::providers::Provider; +use torii_sqlite::Sql; use tracing::info; use super::{EventProcessor, EventProcessorConfig}; -use crate::sql::Sql; -pub(crate) const LOG_TARGET: &str = "torii_core::processors::event_message"; +pub(crate) const LOG_TARGET: &str = "torii_indexer::processors::event_message"; #[derive(Default, Debug)] pub struct EventMessageProcessor; diff --git a/crates/torii/core/src/processors/metadata_update.rs b/crates/torii/indexer/src/processors/metadata_update.rs similarity index 94% rename from crates/torii/core/src/processors/metadata_update.rs rename to crates/torii/indexer/src/processors/metadata_update.rs index f9cff569f8..ec2c4b493d 100644 --- a/crates/torii/core/src/processors/metadata_update.rs +++ b/crates/torii/indexer/src/processors/metadata_update.rs @@ -9,14 +9,14 @@ use dojo_world::contracts::world::WorldContractReader; use dojo_world::uri::Uri; use starknet::core::types::{Event, Felt}; use starknet::providers::Provider; +use torii_sqlite::constants::IPFS_CLIENT_MAX_RETRY; +use torii_sqlite::utils::fetch_content_from_ipfs; +use torii_sqlite::Sql; use tracing::{error, info}; use super::{EventProcessor, EventProcessorConfig}; -use crate::constants::IPFS_CLIENT_MAX_RETRY; -use crate::sql::Sql; -use crate::utils::fetch_content_from_ipfs; -pub(crate) const LOG_TARGET: &str = "torii_core::processors::metadata_update"; +pub(crate) const LOG_TARGET: &str = "torii_indexer::processors::metadata_update"; #[derive(Default, Debug)] pub struct MetadataUpdateProcessor; diff --git a/crates/torii/core/src/processors/mod.rs b/crates/torii/indexer/src/processors/mod.rs similarity index 96% rename from crates/torii/core/src/processors/mod.rs rename to crates/torii/indexer/src/processors/mod.rs index 4d4c5c637d..abe358b6e7 100644 --- a/crates/torii/core/src/processors/mod.rs +++ b/crates/torii/indexer/src/processors/mod.rs @@ -5,8 +5,7 @@ use async_trait::async_trait; use dojo_world::contracts::world::WorldContractReader; use starknet::core::types::{Event, Felt, Transaction}; use starknet::providers::Provider; - -use crate::sql::Sql; +use torii_sqlite::Sql; pub mod erc20_legacy_transfer; pub mod erc20_transfer; @@ -25,9 +24,6 @@ pub mod store_update_record; pub mod upgrade_event; pub mod upgrade_model; -const MODEL_INDEX: usize = 0; -const ENTITY_ID_INDEX: usize = 1; - #[derive(Clone, Debug, Default)] pub struct EventProcessorConfig { pub historical_events: HashSet, diff --git a/crates/torii/core/src/processors/raw_event.rs b/crates/torii/indexer/src/processors/raw_event.rs similarity index 97% rename from crates/torii/core/src/processors/raw_event.rs rename to crates/torii/indexer/src/processors/raw_event.rs index ff496ef74b..c30a918fa2 100644 --- a/crates/torii/core/src/processors/raw_event.rs +++ b/crates/torii/indexer/src/processors/raw_event.rs @@ -3,9 +3,9 @@ use async_trait::async_trait; use dojo_world::contracts::world::WorldContractReader; use starknet::core::types::Event; use starknet::providers::Provider; +use torii_sqlite::Sql; use super::{EventProcessor, EventProcessorConfig}; -use crate::sql::Sql; #[derive(Default, Debug)] pub struct RawEventProcessor; diff --git a/crates/torii/core/src/processors/register_event.rs b/crates/torii/indexer/src/processors/register_event.rs similarity index 96% rename from crates/torii/core/src/processors/register_event.rs rename to crates/torii/indexer/src/processors/register_event.rs index be9919200c..e9c94f296a 100644 --- a/crates/torii/core/src/processors/register_event.rs +++ b/crates/torii/indexer/src/processors/register_event.rs @@ -5,12 +5,12 @@ use dojo_world::contracts::model::ModelReader; use dojo_world::contracts::world::WorldContractReader; use starknet::core::types::Event; use starknet::providers::Provider; +use torii_sqlite::Sql; use tracing::{debug, info}; use super::{EventProcessor, EventProcessorConfig}; -use crate::sql::Sql; -pub(crate) const LOG_TARGET: &str = "torii_core::processors::register_event"; +pub(crate) const LOG_TARGET: &str = "torii_indexer::processors::register_event"; #[derive(Default, Debug)] pub struct RegisterEventProcessor; diff --git a/crates/torii/core/src/processors/register_model.rs b/crates/torii/indexer/src/processors/register_model.rs similarity index 96% rename from crates/torii/core/src/processors/register_model.rs rename to crates/torii/indexer/src/processors/register_model.rs index ed0f710f2e..d630feff88 100644 --- a/crates/torii/core/src/processors/register_model.rs +++ b/crates/torii/indexer/src/processors/register_model.rs @@ -5,12 +5,12 @@ use dojo_world::contracts::model::ModelReader; use dojo_world::contracts::world::WorldContractReader; use starknet::core::types::Event; use starknet::providers::Provider; +use torii_sqlite::Sql; use tracing::{debug, info}; use super::{EventProcessor, EventProcessorConfig}; -use crate::sql::Sql; -pub(crate) const LOG_TARGET: &str = "torii_core::processors::register_model"; +pub(crate) const LOG_TARGET: &str = "torii_indexer::processors::register_model"; #[derive(Default, Debug)] pub struct RegisterModelProcessor; diff --git a/crates/torii/core/src/processors/store_del_record.rs b/crates/torii/indexer/src/processors/store_del_record.rs similarity index 95% rename from crates/torii/core/src/processors/store_del_record.rs rename to crates/torii/indexer/src/processors/store_del_record.rs index f6b34db24a..e109f069d7 100644 --- a/crates/torii/core/src/processors/store_del_record.rs +++ b/crates/torii/indexer/src/processors/store_del_record.rs @@ -4,12 +4,12 @@ use dojo_world::contracts::abigen::world::Event as WorldEvent; use dojo_world::contracts::world::WorldContractReader; use starknet::core::types::Event; use starknet::providers::Provider; +use torii_sqlite::Sql; use tracing::{debug, info}; use super::{EventProcessor, EventProcessorConfig}; -use crate::sql::Sql; -pub(crate) const LOG_TARGET: &str = "torii_core::processors::store_del_record"; +pub(crate) const LOG_TARGET: &str = "torii_indexer::processors::store_del_record"; #[derive(Default, Debug)] pub struct StoreDelRecordProcessor; diff --git a/crates/torii/core/src/processors/store_set_record.rs b/crates/torii/indexer/src/processors/store_set_record.rs similarity index 94% rename from crates/torii/core/src/processors/store_set_record.rs rename to crates/torii/indexer/src/processors/store_set_record.rs index cc602fd868..c564cb3968 100644 --- a/crates/torii/core/src/processors/store_set_record.rs +++ b/crates/torii/indexer/src/processors/store_set_record.rs @@ -4,13 +4,13 @@ use dojo_world::contracts::abigen::world::Event as WorldEvent; use dojo_world::contracts::world::WorldContractReader; use starknet::core::types::Event; use starknet::providers::Provider; +use torii_sqlite::utils::felts_to_sql_string; +use torii_sqlite::Sql; use tracing::{debug, info}; use super::{EventProcessor, EventProcessorConfig}; -use crate::sql::utils::felts_to_sql_string; -use crate::sql::Sql; -pub(crate) const LOG_TARGET: &str = "torii_core::processors::store_set_record"; +pub(crate) const LOG_TARGET: &str = "torii_indexer::processors::store_set_record"; #[derive(Default, Debug)] pub struct StoreSetRecordProcessor; diff --git a/crates/torii/core/src/processors/store_transaction.rs b/crates/torii/indexer/src/processors/store_transaction.rs similarity index 97% rename from crates/torii/core/src/processors/store_transaction.rs rename to crates/torii/indexer/src/processors/store_transaction.rs index 101fb88093..3eb52725b8 100644 --- a/crates/torii/core/src/processors/store_transaction.rs +++ b/crates/torii/indexer/src/processors/store_transaction.rs @@ -2,9 +2,9 @@ use anyhow::{Error, Ok, Result}; use async_trait::async_trait; use starknet::core::types::{Felt, Transaction}; use starknet::providers::Provider; +use torii_sqlite::Sql; use super::TransactionProcessor; -use crate::sql::Sql; #[derive(Default, Debug)] pub struct StoreTransactionProcessor; diff --git a/crates/torii/core/src/processors/store_update_member.rs b/crates/torii/indexer/src/processors/store_update_member.rs similarity index 56% rename from crates/torii/core/src/processors/store_update_member.rs rename to crates/torii/indexer/src/processors/store_update_member.rs index 2dd28735ff..776d61d9cb 100644 --- a/crates/torii/core/src/processors/store_update_member.rs +++ b/crates/torii/indexer/src/processors/store_update_member.rs @@ -1,21 +1,17 @@ use anyhow::{Context, Error, Result}; use async_trait::async_trait; use dojo_types::schema::{Struct, Ty}; -use dojo_world::contracts::naming; +use dojo_world::contracts::abigen::world::Event as WorldEvent; use dojo_world::contracts::world::WorldContractReader; -use num_traits::ToPrimitive; use starknet::core::types::Event; use starknet::core::utils::get_selector_from_name; use starknet::providers::Provider; -use tracing::{info, warn}; +use torii_sqlite::Sql; +use tracing::info; use super::{EventProcessor, EventProcessorConfig}; -use crate::processors::{ENTITY_ID_INDEX, MODEL_INDEX}; -use crate::sql::Sql; -pub(crate) const LOG_TARGET: &str = "torii_core::processors::store_update_member"; - -const MEMBER_INDEX: usize = 2; +pub(crate) const LOG_TARGET: &str = "torii_indexer::processors::store_update_member"; #[derive(Default, Debug)] pub struct StoreUpdateMemberProcessor; @@ -29,16 +25,7 @@ where "StoreUpdateMember".to_string() } - fn validate(&self, event: &Event) -> bool { - if event.keys.len() > 1 { - info!( - target: LOG_TARGET, - event_key = %>::event_key(self), - invalid_keys = %>::event_keys_as_string(self, event), - "Invalid event keys." - ); - return false; - } + fn validate(&self, _event: &Event) -> bool { true } @@ -52,13 +39,27 @@ where event: &Event, _config: &EventProcessorConfig, ) -> Result<(), Error> { - let model_id = event.data[MODEL_INDEX]; - let entity_id = event.data[ENTITY_ID_INDEX]; - let member_selector = event.data[MEMBER_INDEX]; + // Torii version is coupled to the world version, so we can expect the event to be well + // formed. + let event = match WorldEvent::try_from(event).unwrap_or_else(|_| { + panic!( + "Expected {} event to be well formed.", + >::event_key(self) + ) + }) { + WorldEvent::StoreUpdateMember(e) => e, + _ => { + unreachable!() + } + }; + + let model_selector = event.selector; + let entity_id = event.entity_id; + let member_selector = event.member_selector; // If the model does not exist, silently ignore it. // This can happen if only specific namespaces are indexed. - let model = match db.model(model_id).await { + let model = match db.model(model_selector).await { Ok(m) => m, Err(e) => { if e.to_string().contains("no rows") { @@ -90,30 +91,12 @@ where "Store update member.", ); - let values_start = MEMBER_INDEX + 1; - let values_end: usize = - values_start + event.data[values_start].to_usize().context("invalid usize")?; - - // Skip the length to only get the values as they will be deserialized. - let mut values = event.data[values_start + 1..=values_end].to_vec(); - - let tag = naming::get_tag(&model.namespace, &model.name); - - if !db.does_entity_exist(tag.clone(), entity_id).await? { - warn!( - target: LOG_TARGET, - tag, - entity_id = format!("{:#x}", entity_id), - "Entity not found, must be set before updating a member.", - ); - - return Ok(()); - } - + let mut values = event.values.to_vec(); member.ty.deserialize(&mut values)?; - let wrapped_ty = Ty::Struct(Struct { name: schema.name(), children: vec![member] }); - db.set_entity(wrapped_ty, event_id, block_timestamp, entity_id, model_id, None).await?; + let wrapped_ty = Ty::Struct(Struct { name: schema.name(), children: vec![member] }); + db.set_entity(wrapped_ty, event_id, block_timestamp, entity_id, model_selector, None) + .await?; Ok(()) } } diff --git a/crates/torii/core/src/processors/store_update_record.rs b/crates/torii/indexer/src/processors/store_update_record.rs similarity index 96% rename from crates/torii/core/src/processors/store_update_record.rs rename to crates/torii/indexer/src/processors/store_update_record.rs index f94920e411..b92344849d 100644 --- a/crates/torii/core/src/processors/store_update_record.rs +++ b/crates/torii/indexer/src/processors/store_update_record.rs @@ -5,12 +5,12 @@ use dojo_world::contracts::abigen::world::Event as WorldEvent; use dojo_world::contracts::world::WorldContractReader; use starknet::core::types::Event; use starknet::providers::Provider; +use torii_sqlite::Sql; use tracing::{debug, info}; use super::{EventProcessor, EventProcessorConfig}; -use crate::sql::Sql; -pub(crate) const LOG_TARGET: &str = "torii_core::processors::store_update_record"; +pub(crate) const LOG_TARGET: &str = "torii_indexer::processors::store_update_record"; #[derive(Default, Debug)] pub struct StoreUpdateRecordProcessor; diff --git a/crates/torii/core/src/processors/upgrade_event.rs b/crates/torii/indexer/src/processors/upgrade_event.rs similarity index 97% rename from crates/torii/core/src/processors/upgrade_event.rs rename to crates/torii/indexer/src/processors/upgrade_event.rs index da329501af..ba7966d63b 100644 --- a/crates/torii/core/src/processors/upgrade_event.rs +++ b/crates/torii/indexer/src/processors/upgrade_event.rs @@ -5,12 +5,12 @@ use dojo_world::contracts::model::ModelReader; use dojo_world::contracts::world::WorldContractReader; use starknet::core::types::Event; use starknet::providers::Provider; +use torii_sqlite::Sql; use tracing::{debug, info}; use super::{EventProcessor, EventProcessorConfig}; -use crate::sql::Sql; -pub(crate) const LOG_TARGET: &str = "torii_core::processors::upgrade_event"; +pub(crate) const LOG_TARGET: &str = "torii_indexer::processors::upgrade_event"; #[derive(Default, Debug)] pub struct UpgradeEventProcessor; diff --git a/crates/torii/core/src/processors/upgrade_model.rs b/crates/torii/indexer/src/processors/upgrade_model.rs similarity index 97% rename from crates/torii/core/src/processors/upgrade_model.rs rename to crates/torii/indexer/src/processors/upgrade_model.rs index 0b7e36820d..40717df30d 100644 --- a/crates/torii/core/src/processors/upgrade_model.rs +++ b/crates/torii/indexer/src/processors/upgrade_model.rs @@ -5,12 +5,12 @@ use dojo_world::contracts::model::ModelReader; use dojo_world::contracts::world::WorldContractReader; use starknet::core::types::Event; use starknet::providers::Provider; +use torii_sqlite::Sql; use tracing::{debug, info}; use super::{EventProcessor, EventProcessorConfig}; -use crate::sql::Sql; -pub(crate) const LOG_TARGET: &str = "torii_core::processors::upgrade_model"; +pub(crate) const LOG_TARGET: &str = "torii_indexer::processors::upgrade_model"; #[derive(Default, Debug)] pub struct UpgradeModelProcessor; diff --git a/crates/torii/core/src/sql/test.rs b/crates/torii/indexer/src/test.rs similarity index 77% rename from crates/torii/core/src/sql/test.rs rename to crates/torii/indexer/src/test.rs index 7d79f3ba4a..ccfc77608b 100644 --- a/crates/torii/core/src/sql/test.rs +++ b/crates/torii/indexer/src/test.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use std::str::FromStr; use std::sync::Arc; -use cainome::cairo_serde::ContractAddress; +use cainome::cairo_serde::{ByteArray, CairoSerde, ContractAddress}; use dojo_test_utils::compiler::CompilerTestSetup; use dojo_test_utils::migration::copy_spawn_and_move_db; use dojo_utils::{TransactionExt, TransactionWaiter, TxnConfig}; @@ -20,12 +20,12 @@ use starknet::providers::{JsonRpcClient, Provider}; use starknet_crypto::poseidon_hash_many; use tempfile::NamedTempFile; use tokio::sync::broadcast; +use torii_sqlite::cache::ModelCache; +use torii_sqlite::executor::Executor; +use torii_sqlite::types::{Contract, ContractType}; +use torii_sqlite::Sql; use crate::engine::{Engine, EngineConfig, Processors}; -use crate::executor::Executor; -use crate::sql::cache::ModelCache; -use crate::sql::Sql; -use crate::types::{Contract, ContractType}; pub async fn bootstrap_engine

( world: WorldContractReader

, @@ -390,6 +390,115 @@ async fn test_update_with_set_record(sequencer: &RunnerCtx) { let _ = bootstrap_engine(world_reader, db.clone(), Arc::clone(&provider)).await.unwrap(); } +#[ignore = "This test is being flaky and need to find why. Sometimes it fails, sometimes it passes."] +#[tokio::test(flavor = "multi_thread")] +#[katana_runner::test(accounts = 10, db_dir = copy_spawn_and_move_db().as_str())] +async fn test_load_from_remote_update(sequencer: &RunnerCtx) { + let setup = CompilerTestSetup::from_examples("../../dojo/core", "../../../examples/"); + let config = setup.build_test_config("spawn-and-move", Profile::DEV); + + let ws = scarb::ops::read_workspace(config.manifest_path(), &config).unwrap(); + + let account = sequencer.account(0); + let provider = Arc::new(JsonRpcClient::new(HttpTransport::new(sequencer.url()))); + + let world_local = ws.load_world_local().unwrap(); + let world_address = world_local.deterministic_world_address().unwrap(); + let actions_address = world_local + .get_contract_address_local(compute_selector_from_names("ns", "actions")) + .unwrap(); + + let world = WorldContract::new(world_address, &account); + + let res = world + .grant_writer(&compute_bytearray_hash("ns"), &ContractAddress(actions_address)) + .send_with_cfg(&TxnConfig::init_wait()) + .await + .unwrap(); + + TransactionWaiter::new(res.transaction_hash, &provider).await.unwrap(); + + // spawn + let res = account + .execute_v1(vec![Call { + to: actions_address, + selector: get_selector_from_name("spawn").unwrap(), + calldata: vec![], + }]) + .send_with_cfg(&TxnConfig::init_wait()) + .await + .unwrap(); + + TransactionWaiter::new(res.transaction_hash, &provider).await.unwrap(); + + // Set player config. + let res = account + .execute_v1(vec![Call { + to: actions_address, + selector: get_selector_from_name("set_player_config").unwrap(), + // Empty ByteArray. + calldata: vec![Felt::ZERO, Felt::ZERO, Felt::ZERO], + }]) + .send_with_cfg(&TxnConfig::init_wait()) + .await + .unwrap(); + + TransactionWaiter::new(res.transaction_hash, &provider).await.unwrap(); + + let name = ByteArray::from_string("mimi").unwrap(); + let res = account + .execute_v1(vec![Call { + to: actions_address, + selector: get_selector_from_name("update_player_config_name").unwrap(), + calldata: ByteArray::cairo_serialize(&name), + }]) + .send_with_cfg(&TxnConfig::init_wait()) + .await + .unwrap(); + + TransactionWaiter::new(res.transaction_hash, &provider).await.unwrap(); + + let world_reader = WorldContractReader::new(world_address, Arc::clone(&provider)); + + let tempfile = NamedTempFile::new().unwrap(); + let path = tempfile.path().to_string_lossy(); + let options = SqliteConnectOptions::from_str(&path).unwrap().create_if_missing(true); + let pool = SqlitePoolOptions::new().connect_with(options).await.unwrap(); + sqlx::migrate!("../migrations").run(&pool).await.unwrap(); + + let (shutdown_tx, _) = broadcast::channel(1); + let (mut executor, sender) = + Executor::new(pool.clone(), shutdown_tx.clone(), Arc::clone(&provider), 100).await.unwrap(); + tokio::spawn(async move { + executor.run().await.unwrap(); + }); + + let model_cache = Arc::new(ModelCache::new(pool.clone())); + let db = Sql::new( + pool.clone(), + sender.clone(), + &[Contract { address: world_reader.address, r#type: ContractType::WORLD }], + model_cache.clone(), + ) + .await + .unwrap(); + + let _ = bootstrap_engine(world_reader, db.clone(), Arc::clone(&provider)).await.unwrap(); + + let name: String = sqlx::query_scalar( + format!( + "SELECT name FROM [ns-PlayerConfig] WHERE internal_id = '{:#x}'", + poseidon_hash_many(&[account.address()]) + ) + .as_str(), + ) + .fetch_one(&pool) + .await + .unwrap(); + + assert_eq!(name, "mimi"); +} + /// Count the number of rows in a table. /// /// # Arguments diff --git a/crates/torii/libp2p/Cargo.toml b/crates/torii/libp2p/Cargo.toml index 05cf945115..83f75e1f83 100644 --- a/crates/torii/libp2p/Cargo.toml +++ b/crates/torii/libp2p/Cargo.toml @@ -5,43 +5,49 @@ name = "torii-relay" repository.workspace = true version.workspace = true +[features] +client = [ ] +server = [ "dep:sqlx", "dep:torii-sqlite", "dep:dojo-types", "dep:dojo-world", "dep:starknet-crypto", "dep:chrono", "dep:libp2p-webrtc", "dep:rand" ] + # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] futures.workspace = true -rand.workspace = true serde.workspace = true # preserve order anyhow.workspace = true -cainome.workspace = true -chrono.workspace = true -crypto-bigint.workspace = true -dojo-types.workspace = true -dojo-world.workspace = true -indexmap.workspace = true serde_json.workspace = true -starknet-crypto.workspace = true starknet.workspace = true thiserror.workspace = true +torii-typed-data.workspace = true tracing.workspace = true +sqlx = { workspace = true, optional = true } +torii-sqlite = { workspace = true, optional = true } +dojo-types = { workspace = true, optional = true } +dojo-world = { workspace = true, optional = true } +rand = { workspace = true, optional = true } +starknet-crypto = { workspace = true, optional = true } +chrono = { workspace = true, optional = true } +libp2p-webrtc = { git = "https://github.com/libp2p/rust-libp2p", features = [ "pem", "tokio" ], rev = "cdc9638", optional = true } [dev-dependencies] +indexmap.workspace = true katana-runner.workspace = true tempfile.workspace = true tokio.workspace = true tracing-subscriber.workspace = true +[target.'cfg(target_arch = "wasm32")'.dev-dependencies] +tracing-wasm = "0.2.1" +wasm-bindgen-futures = "0.4.40" +wasm-bindgen-test = "0.3.40" +wasm-timer = "0.2.5" + + [target.'cfg(not(target_arch = "wasm32"))'.dependencies] libp2p = { git = "https://github.com/libp2p/rust-libp2p", features = [ "dns", "ed25519", "gossipsub", "identify", "macros", "noise", "ping", "quic", "relay", "tcp", "tokio", "websocket", "yamux" ], rev = "cdc9638" } -libp2p-webrtc = { git = "https://github.com/libp2p/rust-libp2p", features = [ "pem", "tokio" ], rev = "cdc9638" } -sqlx.workspace = true -torii-core.workspace = true [target.'cfg(target_arch = "wasm32")'.dependencies] libp2p = { git = "https://github.com/libp2p/rust-libp2p", features = [ "ed25519", "gossipsub", "identify", "macros", "noise", "ping", "tcp", "wasm-bindgen", "yamux" ], rev = "cdc9638" } libp2p-webrtc-websys = { git = "https://github.com/libp2p/rust-libp2p", rev = "cdc9638" } libp2p-websocket-websys = { git = "https://github.com/libp2p/rust-libp2p", rev = "cdc9638" } -tracing-wasm = "0.2.1" -wasm-bindgen-futures = "0.4.40" -wasm-bindgen-test = "0.3.40" -wasm-timer = "0.2.5" diff --git a/crates/torii/libp2p/src/client/mod.rs b/crates/torii/libp2p/src/client/mod.rs index f438d23072..eb5e66bb18 100644 --- a/crates/torii/libp2p/src/client/mod.rs +++ b/crates/torii/libp2p/src/client/mod.rs @@ -17,7 +17,7 @@ use tracing::info; pub mod events; use crate::client::events::ClientEvent; use crate::constants; -use crate::errors::Error; +use crate::error::Error; use crate::types::Message; pub(crate) const LOG_TARGET: &str = "torii::relay::client"; diff --git a/crates/torii/libp2p/src/errors.rs b/crates/torii/libp2p/src/error.rs similarity index 91% rename from crates/torii/libp2p/src/errors.rs rename to crates/torii/libp2p/src/error.rs index ec615b88d7..a8234c3cce 100644 --- a/crates/torii/libp2p/src/errors.rs +++ b/crates/torii/libp2p/src/error.rs @@ -6,6 +6,7 @@ use libp2p::gossipsub::{PublishError, SubscriptionError}; use libp2p::noise; use starknet::providers::ProviderError; use thiserror::Error; +use torii_typed_data::error::Error as TypedDataError; #[derive(Error, Debug)] pub enum Error { @@ -41,12 +42,15 @@ pub enum Error { #[error("Failed to read certificate: {0}")] ReadCertificateError(anyhow::Error), - #[error("Invalid message provided: {0}")] - InvalidMessageError(String), - #[error("Invalid type provided: {0}")] InvalidTypeError(String), #[error(transparent)] ProviderError(#[from] ProviderError), + + #[error(transparent)] + TypedDataError(#[from] TypedDataError), + + #[error("Invalid message provided: {0}")] + InvalidMessageError(String), } diff --git a/crates/torii/libp2p/src/lib.rs b/crates/torii/libp2p/src/lib.rs index 9d08de68c6..ec95bd8724 100644 --- a/crates/torii/libp2p/src/lib.rs +++ b/crates/torii/libp2p/src/lib.rs @@ -1,10 +1,14 @@ #![warn(unused_crate_dependencies)] -pub mod client; mod constants; -pub mod errors; -#[cfg(not(target_arch = "wasm32"))] +pub mod error; + +#[cfg(feature = "client")] +pub mod client; +#[cfg(feature = "server")] pub mod server; -mod tests; -pub mod typed_data; + +#[cfg(test)] +mod test; + pub mod types; diff --git a/crates/torii/libp2p/src/server/mod.rs b/crates/torii/libp2p/src/server/mod.rs index 85b81ade16..13bb63382f 100644 --- a/crates/torii/libp2p/src/server/mod.rs +++ b/crates/torii/libp2p/src/server/mod.rs @@ -25,20 +25,21 @@ use starknet::core::types::{BlockId, BlockTag, Felt, FunctionCall}; use starknet::core::utils::get_selector_from_name; use starknet::providers::Provider; use starknet_crypto::poseidon_hash_many; -use torii_core::executor::QueryMessage; -use torii_core::sql::utils::felts_to_sql_string; -use torii_core::sql::Sql; +use torii_sqlite::executor::QueryMessage; +use torii_sqlite::utils::felts_to_sql_string; +use torii_sqlite::Sql; use tracing::{info, warn}; use webrtc::tokio::Certificate; use crate::constants; -use crate::errors::Error; +use crate::error::Error; mod events; +use torii_typed_data::typed_data::{parse_value_to_ty, PrimitiveType, TypedData}; + use crate::server::events::ServerEvent; -use crate::typed_data::{encode_type, parse_value_to_ty, PrimitiveType, TypedData}; -use crate::types::{Message, Signature}; +use crate::types::Message; pub(crate) const LOG_TARGET: &str = "torii::relay::server"; @@ -128,7 +129,7 @@ impl Relay

{ relay: relay::Behaviour::new(key.public().to_peer_id(), Default::default()), ping: ping::Behaviour::new(ping::Config::new()), identify: identify::Behaviour::new(identify::Config::new( - format!("/torii-relay/{}", env!("CARGO_PKG_VERSION")), + format!("/{}/{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")), key.public(), )), gossipsub: gossipsub::Behaviour::new( @@ -253,7 +254,7 @@ impl Relay

{ // select only identity field, if doesn't exist, empty string let query = format!( - "SELECT external_identity FROM [{}] WHERE id = ?", + "SELECT identity FROM [{}] WHERE internal_id = ?", ty.name() ); let entity_identity: Option = match sqlx::query_scalar(&query) @@ -414,51 +415,24 @@ async fn validate_signature( provider: &P, entity_identity: Felt, message: &TypedData, - signature: &Signature, + signature: &[Felt], ) -> Result { let message_hash = message.encode(entity_identity)?; - match signature { - Signature::Account(signature) => { - let mut calldata = vec![message_hash, Felt::from(signature.len())]; - calldata.extend(signature); - provider - .call( - FunctionCall { - contract_address: entity_identity, - entry_point_selector: get_selector_from_name("is_valid_signature").unwrap(), - calldata, - }, - BlockId::Tag(BlockTag::Pending), - ) - .await - .map_err(Error::ProviderError) - .map(|res| res[0] != Felt::ZERO) - } - Signature::Session(signature) => { - let mut calldata = vec![ - Felt::ONE, - get_selector_from_name(&encode_type(&message.primary_type, &message.types)?) - .map_err(|e| Error::InvalidMessageError(e.to_string()))?, - message_hash, - signature.len().into(), - ]; - calldata.extend(signature); - provider - .call( - FunctionCall { - contract_address: entity_identity, - entry_point_selector: get_selector_from_name("is_session_sigature_valid") - .unwrap(), - calldata, - }, - BlockId::Tag(BlockTag::Pending), - ) - .await - .map_err(Error::ProviderError) - .map(|res| res[0] != Felt::ZERO) - } - } + let mut calldata = vec![message_hash, Felt::from(signature.len())]; + calldata.extend(signature); + provider + .call( + FunctionCall { + contract_address: entity_identity, + entry_point_selector: get_selector_from_name("is_valid_signature").unwrap(), + calldata, + }, + BlockId::Tag(BlockTag::Pending), + ) + .await + .map_err(Error::ProviderError) + .map(|res| res[0] != Felt::ZERO) } fn ty_keys(ty: &Ty) -> Result, Error> { diff --git a/crates/torii/libp2p/src/test.rs b/crates/torii/libp2p/src/test.rs new file mode 100644 index 0000000000..35c6a85965 --- /dev/null +++ b/crates/torii/libp2p/src/test.rs @@ -0,0 +1,241 @@ +use std::error::Error; + +use crate::client::RelayClient; + +#[cfg(target_arch = "wasm32")] +wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); +use dojo_types::primitive::Primitive; +use katana_runner::KatanaRunner; +#[cfg(target_arch = "wasm32")] +use wasm_bindgen_test::*; + +// This tests subscribing to a topic and receiving a message +#[cfg(not(target_arch = "wasm32"))] +#[tokio::test] +async fn test_client_messaging() -> Result<(), Box> { + use std::sync::Arc; + use std::time::Duration; + + use dojo_types::schema::{Member, Struct, Ty}; + use dojo_world::contracts::abigen::model::Layout; + use indexmap::IndexMap; + use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions}; + use starknet::providers::jsonrpc::HttpTransport; + use starknet::providers::JsonRpcClient; + use starknet::signers::SigningKey; + use starknet_crypto::Felt; + use tempfile::NamedTempFile; + use tokio::select; + use tokio::sync::broadcast; + use tokio::time::sleep; + use torii_sqlite::cache::ModelCache; + use torii_sqlite::executor::Executor; + use torii_sqlite::types::{Contract, ContractType}; + use torii_sqlite::Sql; + use torii_typed_data::typed_data::{Domain, Field, SimpleField, TypedData}; + + use crate::server::Relay; + use crate::types::Message; + + let _ = tracing_subscriber::fmt() + .with_env_filter("torii::relay::client=debug,torii::relay::server=debug") + .try_init(); + + // Database + let tempfile = NamedTempFile::new().unwrap(); + let path = tempfile.path().to_string_lossy(); + let options = ::from_str(&path) + .unwrap() + .create_if_missing(true); + let pool = SqlitePoolOptions::new() + .min_connections(1) + .idle_timeout(None) + .max_lifetime(None) + .connect_with(options) + .await + .unwrap(); + sqlx::migrate!("../migrations").run(&pool).await.unwrap(); + + let sequencer = KatanaRunner::new().expect("Failed to create Katana sequencer"); + + let provider = Arc::new(JsonRpcClient::new(HttpTransport::new(sequencer.url()))); + + let account = sequencer.account_data(0); + + let (shutdown_tx, _) = broadcast::channel(1); + let (mut executor, sender) = + Executor::new(pool.clone(), shutdown_tx.clone(), Arc::clone(&provider), 100).await.unwrap(); + tokio::spawn(async move { + executor.run().await.unwrap(); + }); + + let model_cache = Arc::new(ModelCache::new(pool.clone())); + let mut db = Sql::new( + pool.clone(), + sender, + &[Contract { address: Felt::ZERO, r#type: ContractType::WORLD }], + model_cache, + ) + .await + .unwrap(); + + // Register the model of our Message + db.register_model( + "types_test", + &Ty::Struct(Struct { + name: "Message".to_string(), + children: vec![ + Member { + name: "identity".to_string(), + ty: Ty::Primitive(Primitive::ContractAddress(None)), + key: true, + }, + Member { + name: "message".to_string(), + ty: Ty::ByteArray("".to_string()), + key: false, + }, + ], + }), + Layout::Fixed(vec![]), + Felt::ZERO, + Felt::ZERO, + 0, + 0, + 0, + None, + ) + .await + .unwrap(); + db.execute().await.unwrap(); + + // Initialize the relay server + let mut relay_server = Relay::new(db, provider, 9900, 9901, 9902, None, None)?; + tokio::spawn(async move { + relay_server.run().await; + }); + + // Initialize the first client (listener) + let client = RelayClient::new("/ip4/127.0.0.1/tcp/9900".to_string())?; + tokio::spawn(async move { + client.event_loop.lock().await.run().await; + }); + + let mut typed_data = TypedData::new( + IndexMap::from_iter(vec![ + ( + "types_test-Message".to_string(), + vec![ + Field::SimpleType(SimpleField { + name: "identity".to_string(), + r#type: "ContractAddress".to_string(), + }), + Field::SimpleType(SimpleField { + name: "message".to_string(), + r#type: "string".to_string(), + }), + ], + ), + ( + "StarknetDomain".to_string(), + vec![ + Field::SimpleType(SimpleField { + name: "name".to_string(), + r#type: "shortstring".to_string(), + }), + Field::SimpleType(SimpleField { + name: "version".to_string(), + r#type: "shortstring".to_string(), + }), + Field::SimpleType(SimpleField { + name: "chainId".to_string(), + r#type: "shortstring".to_string(), + }), + Field::SimpleType(SimpleField { + name: "revision".to_string(), + r#type: "shortstring".to_string(), + }), + ], + ), + ]), + "types_test-Message", + Domain::new("types_test-Message", "1", "0x0", Some("1")), + IndexMap::new(), + ); + typed_data.message.insert( + "identity".to_string(), + torii_typed_data::typed_data::PrimitiveType::String(account.address.to_string()), + ); + + typed_data.message.insert( + "message".to_string(), + torii_typed_data::typed_data::PrimitiveType::String("mimi".to_string()), + ); + + let message_hash = typed_data.encode(account.address).unwrap(); + let signature = + SigningKey::from_secret_scalar(account.private_key.clone().unwrap().secret_scalar()) + .sign(&message_hash) + .unwrap(); + + client + .command_sender + .publish(Message { message: typed_data, signature: vec![signature.r, signature.s] }) + .await?; + + sleep(std::time::Duration::from_secs(2)).await; + + loop { + select! { + entity = sqlx::query("SELECT * FROM entities").fetch_one(&pool) => if entity.is_ok() { + println!("Test OK: Received message within 5 seconds."); + return Ok(()); + }, + _ = sleep(Duration::from_secs(5)) => { + println!("Test Failed: Did not receive message within 5 seconds."); + return Err("Timeout reached without receiving a message".into()); + } + } + } +} + +#[cfg(target_arch = "wasm32")] +#[wasm_bindgen_test] +async fn test_client_connection_wasm() -> Result<(), Box> { + use futures::future::{select, Either}; + use wasm_bindgen_futures::spawn_local; + + tracing_wasm::set_as_global_default(); + + let _ = tracing_subscriber::fmt().with_env_filter("torii_libp2p=debug").try_init(); + // Initialize the first client (listener) + // Make sure the cert hash is correct - corresponding to the cert in the relay server + let mut client = RelayClient::new( + "/ip4/127.0.0.1/udp/9091/webrtc-direct/certhash/\ + uEiCAoeHQh49fCHDolECesXO0CPR7fpz0sv0PWVaIahzT4g" + .to_string(), + )?; + + spawn_local(async move { + client.event_loop.lock().await.run().await; + }); + + client.command_sender.subscribe("mawmaw".to_string()).await?; + client.command_sender.wait_for_relay().await?; + client.command_sender.publish("mawmaw".to_string(), "mimi".as_bytes().to_vec()).await?; + + let timeout = wasm_timer::Delay::new(std::time::Duration::from_secs(2)); + let mut message_future = client.message_receiver.lock().await; + let message_future = message_future.next(); + + match select(message_future, timeout).await { + Either::Left((Some(_message), _)) => { + println!("Test OK: Received message within 5 seconds."); + Ok(()) + } + _ => { + println!("Test Failed: Did not receive message within 5 seconds."); + Err("Timeout reached without receiving a message".into()) + } + } +} diff --git a/crates/torii/libp2p/src/tests.rs b/crates/torii/libp2p/src/tests.rs deleted file mode 100644 index 5dc6ae6bf0..0000000000 --- a/crates/torii/libp2p/src/tests.rs +++ /dev/null @@ -1,758 +0,0 @@ -#[cfg(test)] -mod test { - use std::error::Error; - - use crate::client::RelayClient; - use crate::typed_data::{ - map_ty_to_primitive, parse_value_to_ty, Domain, PrimitiveType, TypedData, - }; - - #[cfg(target_arch = "wasm32")] - wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); - use crypto_bigint::U256; - use dojo_types::primitive::Primitive; - use dojo_types::schema::{Enum, EnumOption, Member, Struct, Ty}; - use katana_runner::KatanaRunner; - use serde_json::Number; - use starknet::core::types::Felt; - #[cfg(target_arch = "wasm32")] - use wasm_bindgen_test::*; - - #[test] - fn test_parse_primitive_to_ty() { - // primitives - let mut ty = Ty::Primitive(Primitive::U8(None)); - let value = PrimitiveType::Number(Number::from(1u64)); - parse_value_to_ty(&value, &mut ty).unwrap(); - assert_eq!(ty, Ty::Primitive(Primitive::U8(Some(1)))); - - let mut ty = Ty::Primitive(Primitive::U16(None)); - let value = PrimitiveType::Number(Number::from(1u64)); - parse_value_to_ty(&value, &mut ty).unwrap(); - assert_eq!(ty, Ty::Primitive(Primitive::U16(Some(1)))); - - let mut ty = Ty::Primitive(Primitive::U32(None)); - let value = PrimitiveType::Number(Number::from(1u64)); - parse_value_to_ty(&value, &mut ty).unwrap(); - assert_eq!(ty, Ty::Primitive(Primitive::U32(Some(1)))); - - let mut ty = Ty::Primitive(Primitive::USize(None)); - let value = PrimitiveType::Number(Number::from(1u64)); - parse_value_to_ty(&value, &mut ty).unwrap(); - assert_eq!(ty, Ty::Primitive(Primitive::USize(Some(1)))); - - let mut ty = Ty::Primitive(Primitive::U64(None)); - let value = PrimitiveType::Number(Number::from(1u64)); - parse_value_to_ty(&value, &mut ty).unwrap(); - assert_eq!(ty, Ty::Primitive(Primitive::U64(Some(1)))); - - let mut ty = Ty::Primitive(Primitive::U128(None)); - let value = PrimitiveType::String("1".to_string()); - parse_value_to_ty(&value, &mut ty).unwrap(); - assert_eq!(ty, Ty::Primitive(Primitive::U128(Some(1)))); - - // test u256 with low high - let mut ty = Ty::Primitive(Primitive::U256(None)); - let value = PrimitiveType::Object( - vec![ - ("low".to_string(), PrimitiveType::String("1".to_string())), - ("high".to_string(), PrimitiveType::String("0".to_string())), - ] - .into_iter() - .collect(), - ); - parse_value_to_ty(&value, &mut ty).unwrap(); - assert_eq!(ty, Ty::Primitive(Primitive::U256(Some(U256::ONE)))); - - let mut ty = Ty::Primitive(Primitive::Felt252(None)); - let value = PrimitiveType::String("1".to_string()); - parse_value_to_ty(&value, &mut ty).unwrap(); - assert_eq!(ty, Ty::Primitive(Primitive::Felt252(Some(Felt::ONE)))); - - let mut ty = Ty::Primitive(Primitive::ClassHash(None)); - let value = PrimitiveType::String("1".to_string()); - parse_value_to_ty(&value, &mut ty).unwrap(); - assert_eq!(ty, Ty::Primitive(Primitive::ClassHash(Some(Felt::ONE)))); - - let mut ty = Ty::Primitive(Primitive::ContractAddress(None)); - let value = PrimitiveType::String("1".to_string()); - parse_value_to_ty(&value, &mut ty).unwrap(); - assert_eq!(ty, Ty::Primitive(Primitive::ContractAddress(Some(Felt::ONE)))); - - let mut ty = Ty::Primitive(Primitive::Bool(None)); - let value = PrimitiveType::Bool(true); - parse_value_to_ty(&value, &mut ty).unwrap(); - assert_eq!(ty, Ty::Primitive(Primitive::Bool(Some(true)))); - - // bytearray - let mut ty = Ty::ByteArray("".to_string()); - let value = PrimitiveType::String("mimi".to_string()); - parse_value_to_ty(&value, &mut ty).unwrap(); - assert_eq!(ty, Ty::ByteArray("mimi".to_string())); - } - - #[test] - fn test_map_ty_to_primitive() { - let ty = Ty::Primitive(Primitive::U8(Some(1))); - let value = PrimitiveType::Number(Number::from(1u64)); - assert_eq!(value, map_ty_to_primitive(&ty).unwrap()); - - let ty = Ty::Primitive(Primitive::U16(Some(1))); - let value = PrimitiveType::Number(Number::from(1u64)); - assert_eq!(value, map_ty_to_primitive(&ty).unwrap()); - - let ty = Ty::Primitive(Primitive::U32(Some(1))); - let value = PrimitiveType::Number(Number::from(1u64)); - assert_eq!(value, map_ty_to_primitive(&ty).unwrap()); - - let ty = Ty::Primitive(Primitive::USize(Some(1))); - let value = PrimitiveType::Number(Number::from(1u64)); - assert_eq!(value, map_ty_to_primitive(&ty).unwrap()); - - let ty = Ty::Primitive(Primitive::U64(Some(1))); - let value = PrimitiveType::String("1".to_string()); - assert_eq!(value, map_ty_to_primitive(&ty).unwrap()); - - let ty = Ty::Primitive(Primitive::U128(Some(1))); - let value = PrimitiveType::String("1".to_string()); - assert_eq!(value, map_ty_to_primitive(&ty).unwrap()); - - let ty = Ty::Primitive(Primitive::U256(Some(U256::ONE))); - let value = PrimitiveType::Object( - vec![ - ("low".to_string(), PrimitiveType::String("1".to_string())), - ("high".to_string(), PrimitiveType::String("0".to_string())), - ] - .into_iter() - .collect(), - ); - assert_eq!(value, map_ty_to_primitive(&ty).unwrap()); - - let ty = Ty::Primitive(Primitive::Felt252(Some(Felt::ONE))); - let value = PrimitiveType::String("1".to_string()); - assert_eq!(value, map_ty_to_primitive(&ty).unwrap()); - - let ty = Ty::Primitive(Primitive::ClassHash(Some(Felt::ONE))); - let value = PrimitiveType::String("1".to_string()); - assert_eq!(value, map_ty_to_primitive(&ty).unwrap()); - - let ty = Ty::Primitive(Primitive::ContractAddress(Some(Felt::ONE))); - let value = PrimitiveType::String("1".to_string()); - assert_eq!(value, map_ty_to_primitive(&ty).unwrap()); - - let ty = Ty::Primitive(Primitive::Bool(Some(true))); - let value = PrimitiveType::Bool(true); - assert_eq!(value, map_ty_to_primitive(&ty).unwrap()); - - let ty = Ty::ByteArray("mimi".to_string()); - let value = PrimitiveType::String("mimi".to_string()); - assert_eq!(value, map_ty_to_primitive(&ty).unwrap()); - } - - #[test] - fn test_parse_complex_to_ty() { - let mut ty = Ty::Struct(Struct { - name: "PlayerConfig".to_string(), - children: vec![ - Member { - name: "player".to_string(), - ty: Ty::Primitive(Primitive::ContractAddress(None)), - key: true, - }, - Member { name: "name".to_string(), ty: Ty::ByteArray("".to_string()), key: false }, - Member { - name: "items".to_string(), - // array of PlayerItem struct - ty: Ty::Array(vec![Ty::Struct(Struct { - name: "PlayerItem".to_string(), - children: vec![ - Member { - name: "item_id".to_string(), - ty: Ty::Primitive(Primitive::U32(None)), - key: false, - }, - Member { - name: "quantity".to_string(), - ty: Ty::Primitive(Primitive::U32(None)), - key: false, - }, - ], - })]), - key: false, - }, - // a favorite_item field with enum type Option - Member { - name: "favorite_item".to_string(), - ty: Ty::Enum(Enum { - name: "Option".to_string(), - option: None, - options: vec![ - EnumOption { name: "None".to_string(), ty: Ty::Tuple(vec![]) }, - EnumOption { - name: "Some".to_string(), - ty: Ty::Struct(Struct { - name: "PlayerItem".to_string(), - children: vec![ - Member { - name: "item_id".to_string(), - ty: Ty::Primitive(Primitive::U32(None)), - key: false, - }, - Member { - name: "quantity".to_string(), - ty: Ty::Primitive(Primitive::U32(None)), - key: false, - }, - ], - }), - }, - ], - }), - key: false, - }, - ], - }); - - let value = PrimitiveType::Object( - vec![ - ("player".to_string(), PrimitiveType::String("1".to_string())), - ("name".to_string(), PrimitiveType::String("mimi".to_string())), - ( - "items".to_string(), - PrimitiveType::Array(vec![PrimitiveType::Object( - vec![ - ("item_id".to_string(), PrimitiveType::String("1".to_string())), - ("quantity".to_string(), PrimitiveType::Number(Number::from(1u64))), - ] - .into_iter() - .collect(), - )]), - ), - ( - "favorite_item".to_string(), - PrimitiveType::Object( - vec![( - "Some".to_string(), - PrimitiveType::Object( - vec![ - ("item_id".to_string(), PrimitiveType::String("1".to_string())), - ( - "quantity".to_string(), - PrimitiveType::Number(Number::from(1u64)), - ), - ] - .into_iter() - .collect(), - ), - )] - .into_iter() - .collect(), - ), - ), - ] - .into_iter() - .collect(), - ); - - parse_value_to_ty(&value, &mut ty).unwrap(); - - assert_eq!( - ty, - Ty::Struct(Struct { - name: "PlayerConfig".to_string(), - children: vec![ - Member { - name: "player".to_string(), - ty: Ty::Primitive(Primitive::ContractAddress(Some(Felt::ONE))), - key: true, - }, - Member { - name: "name".to_string(), - ty: Ty::ByteArray("mimi".to_string()), - key: false, - }, - Member { - name: "items".to_string(), - ty: Ty::Array(vec![Ty::Struct(Struct { - name: "PlayerItem".to_string(), - children: vec![ - Member { - name: "item_id".to_string(), - ty: Ty::Primitive(Primitive::U32(Some(1))), - key: false, - }, - Member { - name: "quantity".to_string(), - ty: Ty::Primitive(Primitive::U32(Some(1))), - key: false, - }, - ], - })]), - key: false, - }, - Member { - name: "favorite_item".to_string(), - ty: Ty::Enum(Enum { - name: "Option".to_string(), - option: Some(1_u8), - options: vec![ - EnumOption { name: "None".to_string(), ty: Ty::Tuple(vec![]) }, - EnumOption { - name: "Some".to_string(), - ty: Ty::Struct(Struct { - name: "PlayerItem".to_string(), - children: vec![ - Member { - name: "item_id".to_string(), - ty: Ty::Primitive(Primitive::U32(Some(1))), - key: false, - }, - Member { - name: "quantity".to_string(), - ty: Ty::Primitive(Primitive::U32(Some(1))), - key: false, - }, - ], - }), - }, - ] - }), - key: false, - }, - ], - }) - ); - } - - #[test] - fn test_map_ty_to_complex() { - let ty = Ty::Struct(Struct { - name: "PlayerConfig".to_string(), - children: vec![ - Member { - name: "player".to_string(), - ty: Ty::Primitive(Primitive::ContractAddress(Some(Felt::ONE))), - key: true, - }, - Member { - name: "name".to_string(), - ty: Ty::ByteArray("mimi".to_string()), - key: false, - }, - Member { - name: "items".to_string(), - ty: Ty::Array(vec![Ty::Struct(Struct { - name: "PlayerItem".to_string(), - children: vec![ - Member { - name: "item_id".to_string(), - ty: Ty::Primitive(Primitive::U32(Some(1))), - key: false, - }, - Member { - name: "quantity".to_string(), - ty: Ty::Primitive(Primitive::U32(Some(1))), - key: false, - }, - ], - })]), - key: false, - }, - Member { - name: "favorite_item".to_string(), - ty: Ty::Enum(Enum { - name: "Option".to_string(), - option: Some(1_u8), - options: vec![ - EnumOption { name: "None".to_string(), ty: Ty::Tuple(vec![]) }, - EnumOption { - name: "Some".to_string(), - ty: Ty::Struct(Struct { - name: "PlayerItem".to_string(), - children: vec![ - Member { - name: "item_id".to_string(), - ty: Ty::Primitive(Primitive::U32(Some(1))), - key: false, - }, - Member { - name: "quantity".to_string(), - ty: Ty::Primitive(Primitive::U32(Some(1))), - key: false, - }, - ], - }), - }, - ], - }), - key: false, - }, - ], - }); - - let value = PrimitiveType::Object( - vec![ - ("player".to_string(), PrimitiveType::String("1".to_string())), - ("name".to_string(), PrimitiveType::String("mimi".to_string())), - ( - "items".to_string(), - PrimitiveType::Array(vec![PrimitiveType::Object( - vec![ - ("item_id".to_string(), PrimitiveType::Number(Number::from(1u64))), - ("quantity".to_string(), PrimitiveType::Number(Number::from(1u64))), - ] - .into_iter() - .collect(), - )]), - ), - ( - "favorite_item".to_string(), - PrimitiveType::Object( - vec![( - "Some".to_string(), - PrimitiveType::Object( - vec![ - ( - "item_id".to_string(), - PrimitiveType::Number(Number::from(1u64)), - ), - ( - "quantity".to_string(), - PrimitiveType::Number(Number::from(1u64)), - ), - ] - .into_iter() - .collect(), - ), - )] - .into_iter() - .collect(), - ), - ), - ] - .into_iter() - .collect(), - ); - - assert_eq!(value, map_ty_to_primitive(&ty).unwrap()); - } - - #[test] - fn test_model_to_typed_data() { - let ty = Ty::Struct(Struct { - name: "PlayerConfig".to_string(), - children: vec![ - Member { - name: "player".to_string(), - ty: Ty::Primitive(Primitive::ContractAddress(Some(Felt::ONE))), - key: true, - }, - Member { - name: "name".to_string(), - ty: Ty::ByteArray("mimi".to_string()), - key: false, - }, - Member { - name: "items".to_string(), - // array of PlayerItem struct - ty: Ty::Array(vec![Ty::Struct(Struct { - name: "PlayerItem".to_string(), - children: vec![ - Member { - name: "item_id".to_string(), - ty: Ty::Primitive(Primitive::U32(Some(1))), - key: false, - }, - Member { - name: "quantity".to_string(), - ty: Ty::Primitive(Primitive::U32(Some(1))), - key: false, - }, - ], - })]), - key: false, - }, - // a favorite_item field with enum type Option - Member { - name: "favorite_item".to_string(), - ty: Ty::Enum(Enum { - name: "Option".to_string(), - option: Some(1), - options: vec![ - EnumOption { name: "None".to_string(), ty: Ty::Tuple(vec![]) }, - EnumOption { - name: "Some".to_string(), - ty: Ty::Struct(Struct { - name: "PlayerItem".to_string(), - children: vec![ - Member { - name: "item_id".to_string(), - ty: Ty::Primitive(Primitive::U32(Some(69))), - key: false, - }, - Member { - name: "quantity".to_string(), - ty: Ty::Primitive(Primitive::U32(Some(42))), - key: false, - }, - ], - }), - }, - ], - }), - key: false, - }, - ], - }); - - let typed_data = - TypedData::from_model(ty, Domain::new("Test", "1", "Test", Some("1"))).unwrap(); - - let path = "mocks/model_PlayerConfig.json"; - let file = std::fs::File::open(path).unwrap(); - let reader = std::io::BufReader::new(file); - - let file_typed_data: TypedData = serde_json::from_reader(reader).unwrap(); - - assert_eq!( - typed_data.encode(Felt::ZERO).unwrap(), - file_typed_data.encode(Felt::ZERO).unwrap() - ); - } - - // This tests subscribing to a topic and receiving a message - #[cfg(not(target_arch = "wasm32"))] - #[tokio::test] - async fn test_client_messaging() -> Result<(), Box> { - use std::sync::Arc; - use std::time::Duration; - - use dojo_types::schema::{Member, Struct, Ty}; - use dojo_world::contracts::abigen::model::Layout; - use indexmap::IndexMap; - use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions}; - use starknet::providers::jsonrpc::HttpTransport; - use starknet::providers::JsonRpcClient; - use starknet::signers::SigningKey; - use starknet_crypto::Felt; - use tempfile::NamedTempFile; - use tokio::select; - use tokio::sync::broadcast; - use tokio::time::sleep; - use torii_core::executor::Executor; - use torii_core::sql::cache::ModelCache; - use torii_core::sql::Sql; - use torii_core::types::{Contract, ContractType}; - - use crate::server::Relay; - use crate::typed_data::{Domain, Field, SimpleField, TypedData}; - use crate::types::{Message, Signature}; - - let _ = tracing_subscriber::fmt() - .with_env_filter("torii::relay::client=debug,torii::relay::server=debug") - .try_init(); - - // Database - let tempfile = NamedTempFile::new().unwrap(); - let path = tempfile.path().to_string_lossy(); - let options = ::from_str(&path) - .unwrap() - .create_if_missing(true); - let pool = SqlitePoolOptions::new() - .min_connections(1) - .idle_timeout(None) - .max_lifetime(None) - .connect_with(options) - .await - .unwrap(); - sqlx::migrate!("../migrations").run(&pool).await.unwrap(); - - let sequencer = KatanaRunner::new().expect("Failed to create Katana sequencer"); - - let provider = Arc::new(JsonRpcClient::new(HttpTransport::new(sequencer.url()))); - - let account = sequencer.account_data(0); - - let (shutdown_tx, _) = broadcast::channel(1); - let (mut executor, sender) = - Executor::new(pool.clone(), shutdown_tx.clone(), Arc::clone(&provider), 100) - .await - .unwrap(); - tokio::spawn(async move { - executor.run().await.unwrap(); - }); - - let model_cache = Arc::new(ModelCache::new(pool.clone())); - let mut db = Sql::new( - pool.clone(), - sender, - &[Contract { address: Felt::ZERO, r#type: ContractType::WORLD }], - model_cache, - ) - .await - .unwrap(); - - // Register the model of our Message - db.register_model( - "types_test", - &Ty::Struct(Struct { - name: "Message".to_string(), - children: vec![ - Member { - name: "identity".to_string(), - ty: Ty::Primitive(Primitive::ContractAddress(None)), - key: true, - }, - Member { - name: "message".to_string(), - ty: Ty::ByteArray("".to_string()), - key: false, - }, - ], - }), - Layout::Fixed(vec![]), - Felt::ZERO, - Felt::ZERO, - 0, - 0, - 0, - None, - ) - .await - .unwrap(); - db.execute().await.unwrap(); - - // Initialize the relay server - let mut relay_server = Relay::new(db, provider, 9900, 9901, 9902, None, None)?; - tokio::spawn(async move { - relay_server.run().await; - }); - - // Initialize the first client (listener) - let client = RelayClient::new("/ip4/127.0.0.1/tcp/9900".to_string())?; - tokio::spawn(async move { - client.event_loop.lock().await.run().await; - }); - - let mut typed_data = TypedData::new( - IndexMap::from_iter(vec![ - ( - "types_test-Message".to_string(), - vec![ - Field::SimpleType(SimpleField { - name: "identity".to_string(), - r#type: "ContractAddress".to_string(), - }), - Field::SimpleType(SimpleField { - name: "message".to_string(), - r#type: "string".to_string(), - }), - ], - ), - ( - "StarknetDomain".to_string(), - vec![ - Field::SimpleType(SimpleField { - name: "name".to_string(), - r#type: "shortstring".to_string(), - }), - Field::SimpleType(SimpleField { - name: "version".to_string(), - r#type: "shortstring".to_string(), - }), - Field::SimpleType(SimpleField { - name: "chainId".to_string(), - r#type: "shortstring".to_string(), - }), - Field::SimpleType(SimpleField { - name: "revision".to_string(), - r#type: "shortstring".to_string(), - }), - ], - ), - ]), - "types_test-Message", - Domain::new("types_test-Message", "1", "0x0", Some("1")), - IndexMap::new(), - ); - typed_data.message.insert( - "identity".to_string(), - crate::typed_data::PrimitiveType::String(account.address.to_string()), - ); - - typed_data.message.insert( - "message".to_string(), - crate::typed_data::PrimitiveType::String("mimi".to_string()), - ); - - let message_hash = typed_data.encode(account.address).unwrap(); - let signature = - SigningKey::from_secret_scalar(account.private_key.clone().unwrap().secret_scalar()) - .sign(&message_hash) - .unwrap(); - - client - .command_sender - .publish(Message { - message: typed_data, - signature: Signature::Account(vec![signature.r, signature.s]), - }) - .await?; - - sleep(std::time::Duration::from_secs(2)).await; - - loop { - select! { - entity = sqlx::query("SELECT * FROM entities").fetch_one(&pool) => if entity.is_ok() { - println!("Test OK: Received message within 5 seconds."); - return Ok(()); - }, - _ = sleep(Duration::from_secs(5)) => { - println!("Test Failed: Did not receive message within 5 seconds."); - return Err("Timeout reached without receiving a message".into()); - } - } - } - } - - #[cfg(target_arch = "wasm32")] - #[wasm_bindgen_test] - async fn test_client_connection_wasm() -> Result<(), Box> { - use futures::future::{select, Either}; - use wasm_bindgen_futures::spawn_local; - - tracing_wasm::set_as_global_default(); - - let _ = tracing_subscriber::fmt().with_env_filter("torii_libp2p=debug").try_init(); - // Initialize the first client (listener) - // Make sure the cert hash is correct - corresponding to the cert in the relay server - let mut client = RelayClient::new( - "/ip4/127.0.0.1/udp/9091/webrtc-direct/certhash/\ - uEiCAoeHQh49fCHDolECesXO0CPR7fpz0sv0PWVaIahzT4g" - .to_string(), - )?; - - spawn_local(async move { - client.event_loop.lock().await.run().await; - }); - - client.command_sender.subscribe("mawmaw".to_string()).await?; - client.command_sender.wait_for_relay().await?; - client.command_sender.publish("mawmaw".to_string(), "mimi".as_bytes().to_vec()).await?; - - let timeout = wasm_timer::Delay::new(std::time::Duration::from_secs(2)); - let mut message_future = client.message_receiver.lock().await; - let message_future = message_future.next(); - - match select(message_future, timeout).await { - Either::Left((Some(_message), _)) => { - println!("Test OK: Received message within 5 seconds."); - Ok(()) - } - _ => { - println!("Test Failed: Did not receive message within 5 seconds."); - Err("Timeout reached without receiving a message".into()) - } - } - } -} diff --git a/crates/torii/libp2p/src/types.rs b/crates/torii/libp2p/src/types.rs index f4c3bfce15..e8d52c2402 100644 --- a/crates/torii/libp2p/src/types.rs +++ b/crates/torii/libp2p/src/types.rs @@ -1,16 +1,9 @@ use serde::{Deserialize, Serialize}; use starknet::core::types::Felt; - -use crate::typed_data::TypedData; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum Signature { - Account(Vec), - Session(Vec), -} +use torii_typed_data::TypedData; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Message { pub message: TypedData, - pub signature: Signature, + pub signature: Vec, } diff --git a/crates/torii/migrations/20241126064130_model_schema.sql b/crates/torii/migrations/20241126064130_model_schema.sql new file mode 100644 index 0000000000..50965b49b5 --- /dev/null +++ b/crates/torii/migrations/20241126064130_model_schema.sql @@ -0,0 +1,3 @@ +-- Adds a new schema column to the models table. +-- The schema is the JSON serialized Ty of the model. +ALTER TABLE models ADD COLUMN schema BLOB NOT NULL; diff --git a/crates/torii/migrations/20241126064421_delete_model_members.sql b/crates/torii/migrations/20241126064421_delete_model_members.sql new file mode 100644 index 0000000000..3c3052d9ba --- /dev/null +++ b/crates/torii/migrations/20241126064421_delete_model_members.sql @@ -0,0 +1,4 @@ +-- Deletes the model_members table. Which is no longer needed since we store the schema in the models table. +PRAGMA foreign_keys = OFF; +DROP TABLE model_members; +PRAGMA foreign_keys = ON; diff --git a/crates/torii/migrations/20241219104134_indices.sql b/crates/torii/migrations/20241219104134_indices.sql new file mode 100644 index 0000000000..7a7e93de72 --- /dev/null +++ b/crates/torii/migrations/20241219104134_indices.sql @@ -0,0 +1,9 @@ +-- Add index on the updated_at column of the entities table. As we are now allowing querying entities by their updated_at +-- we need to ensure that the updated_at column is indexed. +CREATE INDEX idx_entities_updated_at ON entities (updated_at); + +-- Add indices on the event_messages_historical table. +CREATE INDEX idx_event_messages_historical_id ON event_messages_historical (id); +CREATE INDEX idx_event_messages_historical_keys ON event_messages_historical (keys); +CREATE INDEX idx_event_messages_historical_event_id ON event_messages_historical (event_id); +CREATE INDEX idx_event_messages_historical_executed_at ON event_messages_historical (executed_at); \ No newline at end of file diff --git a/crates/torii/runner/Cargo.toml b/crates/torii/runner/Cargo.toml new file mode 100644 index 0000000000..4eefa45d4e --- /dev/null +++ b/crates/torii/runner/Cargo.toml @@ -0,0 +1,63 @@ +[package] +edition.workspace = true +license-file.workspace = true +name = "torii-runner" +repository.workspace = true +version.workspace = true + + + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow.workspace = true +async-trait.workspace = true +base64.workspace = true +camino.workspace = true +chrono.workspace = true +ctrlc = { version = "3.4", features = [ "termination" ] } +dojo-metrics.workspace = true +dojo-types.workspace = true +dojo-utils.workspace = true +dojo-world.workspace = true +either = "1.9.0" +futures.workspace = true +http-body = "0.4.5" +http.workspace = true +hyper-reverse-proxy = { git = "https://github.com/tarrencev/hyper-reverse-proxy" } +hyper.workspace = true +indexmap.workspace = true +lazy_static.workspace = true +serde.workspace = true +serde_json.workspace = true +sqlx.workspace = true +starknet-crypto.workspace = true +starknet.workspace = true +tokio-stream = "0.1.11" +tokio-util = "0.7.7" +tokio.workspace = true +toml.workspace = true +torii-cli.workspace = true +torii-indexer.workspace = true +torii-sqlite.workspace = true +torii-graphql.workspace = true +torii-grpc = { workspace = true, features = [ "server" ] } +torii-relay = { workspace = true, features = [ "server" ] } +torii-server.workspace = true +tower.workspace = true + +tempfile.workspace = true +tower-http.workspace = true +tracing-subscriber.workspace = true +tracing.workspace = true +url.workspace = true +webbrowser = "0.8" + +[dev-dependencies] +assert_matches.workspace = true +camino.workspace = true + +[features] +default = [ "jemalloc", "sqlite" ] +jemalloc = [ "dojo-metrics/jemalloc" ] +sqlite = [ "sqlx/sqlite" ] diff --git a/crates/torii/runner/src/lib.rs b/crates/torii/runner/src/lib.rs new file mode 100644 index 0000000000..f45e73cd2b --- /dev/null +++ b/crates/torii/runner/src/lib.rs @@ -0,0 +1,314 @@ +//! Torii binary executable. +//! +//! ## Feature Flags +//! +//! - `jemalloc`: Uses [jemallocator](https://github.com/tikv/jemallocator) as the global allocator. +//! This is **not recommended on Windows**. See [here](https://rust-lang.github.io/rfcs/1974-global-allocators.html#jemalloc) +//! for more info. +//! - `jemalloc-prof`: Enables [jemallocator's](https://github.com/tikv/jemallocator) heap profiling +//! and leak detection functionality. See [jemalloc's opt.prof](https://jemalloc.net/jemalloc.3.html#opt.prof) +//! documentation for usage details. This is **not recommended on Windows**. See [here](https://rust-lang.github.io/rfcs/1974-global-allocators.html#jemalloc) +//! for more info. + +use std::cmp; +use std::net::SocketAddr; +use std::str::FromStr; +use std::sync::Arc; +use std::time::Duration; + +use camino::Utf8PathBuf; +use dojo_metrics::exporters::prometheus::PrometheusRecorder; +use dojo_world::contracts::world::WorldContractReader; +use sqlx::sqlite::{ + SqliteAutoVacuum, SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions, SqliteSynchronous, +}; +use sqlx::SqlitePool; +use starknet::providers::jsonrpc::HttpTransport; +use starknet::providers::JsonRpcClient; +use tempfile::{NamedTempFile, TempDir}; +use tokio::sync::broadcast; +use tokio::sync::broadcast::Sender; +use tokio_stream::StreamExt; +use torii_cli::ToriiArgs; +use torii_indexer::engine::{Engine, EngineConfig, IndexingFlags, Processors}; +use torii_indexer::processors::store_transaction::StoreTransactionProcessor; +use torii_indexer::processors::EventProcessorConfig; +use torii_server::proxy::Proxy; +use torii_sqlite::cache::ModelCache; +use torii_sqlite::executor::Executor; +use torii_sqlite::simple_broker::SimpleBroker; +use torii_sqlite::types::{Contract, ContractType, Model}; +use torii_sqlite::Sql; +use tracing::{error, info}; +use tracing_subscriber::{fmt, EnvFilter}; +use url::form_urlencoded; + +pub(crate) const LOG_TARGET: &str = "torii:runner"; + +#[derive(Debug, Clone)] +pub struct Runner { + args: ToriiArgs, +} + +impl Runner { + pub fn new(args: ToriiArgs) -> Self { + Self { args } + } + + pub async fn run(mut self) -> anyhow::Result<()> { + let world_address = if let Some(world_address) = self.args.world_address { + world_address + } else { + return Err(anyhow::anyhow!("Please specify a world address.")); + }; + + self.args + .indexing + .contracts + .push(Contract { address: world_address, r#type: ContractType::WORLD }); + + let filter_layer = EnvFilter::try_from_default_env() + .unwrap_or_else(|_| EnvFilter::new("info,hyper_reverse_proxy=off")); + + let subscriber = fmt::Subscriber::builder().with_env_filter(filter_layer).finish(); + + // Set the global subscriber + tracing::subscriber::set_global_default(subscriber) + .expect("Failed to set the global tracing subscriber"); + + // Setup cancellation for graceful shutdown + let (shutdown_tx, _) = broadcast::channel(1); + + let shutdown_tx_clone = shutdown_tx.clone(); + ctrlc::set_handler(move || { + let _ = shutdown_tx_clone.send(()); + }) + .expect("Error setting Ctrl-C handler"); + + let tempfile = NamedTempFile::new()?; + let database_path = if let Some(db_dir) = self.args.db_dir { + // Create the directory if it doesn't exist + std::fs::create_dir_all(&db_dir)?; + // Set the database file path inside the directory + db_dir.join("torii.db") + } else { + tempfile.path().to_path_buf() + }; + + let mut options = SqliteConnectOptions::from_str(&database_path.to_string_lossy())? + .create_if_missing(true) + .with_regexp(); + + // Performance settings + options = options.auto_vacuum(SqliteAutoVacuum::None); + options = options.journal_mode(SqliteJournalMode::Wal); + options = options.synchronous(SqliteSynchronous::Normal); + + let pool = + SqlitePoolOptions::new().min_connections(1).connect_with(options.clone()).await?; + + let readonly_options = options.read_only(true); + let readonly_pool = SqlitePoolOptions::new() + .min_connections(1) + .max_connections(100) + .connect_with(readonly_options) + .await?; + + // Set the number of threads based on CPU count + let cpu_count = std::thread::available_parallelism().unwrap().get(); + let thread_count = cmp::min(cpu_count, 8); + sqlx::query(&format!("PRAGMA threads = {};", thread_count)).execute(&pool).await?; + + sqlx::migrate!("../migrations").run(&pool).await?; + + let provider: Arc<_> = JsonRpcClient::new(HttpTransport::new(self.args.rpc)).into(); + + // Get world address + let world = WorldContractReader::new(world_address, provider.clone()); + + let (mut executor, sender) = Executor::new( + pool.clone(), + shutdown_tx.clone(), + provider.clone(), + self.args.indexing.max_concurrent_tasks, + ) + .await?; + let executor_handle = tokio::spawn(async move { executor.run().await }); + + let model_cache = Arc::new(ModelCache::new(readonly_pool.clone())); + let db = Sql::new( + pool.clone(), + sender.clone(), + &self.args.indexing.contracts, + model_cache.clone(), + ) + .await?; + + let processors = Processors { + transaction: vec![Box::new(StoreTransactionProcessor)], + ..Processors::default() + }; + + let (block_tx, block_rx) = tokio::sync::mpsc::channel(100); + + let mut flags = IndexingFlags::empty(); + if self.args.indexing.transactions { + flags.insert(IndexingFlags::TRANSACTIONS); + } + if self.args.events.raw { + flags.insert(IndexingFlags::RAW_EVENTS); + } + if self.args.indexing.pending { + flags.insert(IndexingFlags::PENDING_BLOCKS); + } + + let mut engine: Engine>> = Engine::new( + world, + db.clone(), + provider.clone(), + processors, + EngineConfig { + max_concurrent_tasks: self.args.indexing.max_concurrent_tasks, + blocks_chunk_size: self.args.indexing.blocks_chunk_size, + events_chunk_size: self.args.indexing.events_chunk_size, + polling_interval: Duration::from_millis(self.args.indexing.polling_interval), + flags, + event_processor_config: EventProcessorConfig { + historical_events: self.args.events.historical.into_iter().collect(), + namespaces: self.args.indexing.namespaces.into_iter().collect(), + }, + world_block: self.args.indexing.world_block, + }, + shutdown_tx.clone(), + Some(block_tx), + &self.args.indexing.contracts, + ); + + let shutdown_rx = shutdown_tx.subscribe(); + let (grpc_addr, grpc_server) = torii_grpc::server::new( + shutdown_rx, + &readonly_pool, + block_rx, + world_address, + Arc::clone(&provider), + model_cache, + ) + .await?; + + let temp_dir = TempDir::new()?; + let artifacts_path = self + .args + .artifacts_path + .unwrap_or_else(|| Utf8PathBuf::from(temp_dir.path().to_str().unwrap())); + + tokio::fs::create_dir_all(&artifacts_path).await?; + let absolute_path = artifacts_path.canonicalize_utf8()?; + + let (artifacts_addr, artifacts_server) = torii_server::artifacts::new( + shutdown_tx.subscribe(), + &absolute_path, + readonly_pool.clone(), + ) + .await?; + + let mut libp2p_relay_server = torii_relay::server::Relay::new( + db, + provider.clone(), + self.args.relay.port, + self.args.relay.webrtc_port, + self.args.relay.websocket_port, + self.args.relay.local_key_path, + self.args.relay.cert_path, + ) + .expect("Failed to start libp2p relay server"); + + let addr = SocketAddr::new(self.args.server.http_addr, self.args.server.http_port); + + let proxy_server = Arc::new(Proxy::new( + addr, + self.args.server.http_cors_origins.filter(|cors_origins| !cors_origins.is_empty()), + Some(grpc_addr), + None, + Some(artifacts_addr), + Arc::new(readonly_pool.clone()), + )); + + let graphql_server = spawn_rebuilding_graphql_server( + shutdown_tx.clone(), + readonly_pool.into(), + proxy_server.clone(), + ); + + let gql_endpoint = format!("{addr}/graphql"); + let encoded: String = form_urlencoded::byte_serialize( + gql_endpoint.replace("0.0.0.0", "localhost").as_bytes(), + ) + .collect(); + let explorer_url = format!("https://worlds.dev/torii?url={}", encoded); + info!(target: LOG_TARGET, endpoint = %addr, "Starting torii endpoint."); + info!(target: LOG_TARGET, endpoint = %gql_endpoint, "Serving Graphql playground."); + info!(target: LOG_TARGET, url = %explorer_url, "Serving World Explorer."); + info!(target: LOG_TARGET, path = %artifacts_path, "Serving ERC artifacts at path"); + + if self.args.explorer { + if let Err(e) = webbrowser::open(&explorer_url) { + error!(target: LOG_TARGET, error = %e, "Opening World Explorer in the browser."); + } + } + + if self.args.metrics.metrics { + let addr = + SocketAddr::new(self.args.metrics.metrics_addr, self.args.metrics.metrics_port); + info!(target: LOG_TARGET, %addr, "Starting metrics endpoint."); + let prometheus_handle = PrometheusRecorder::install("torii")?; + let server = dojo_metrics::Server::new(prometheus_handle).with_process_metrics(); + tokio::spawn(server.start(addr)); + } + + let engine_handle = tokio::spawn(async move { engine.start().await }); + let proxy_server_handle = + tokio::spawn(async move { proxy_server.start(shutdown_tx.subscribe()).await }); + let graphql_server_handle = tokio::spawn(graphql_server); + let grpc_server_handle = tokio::spawn(grpc_server); + let libp2p_relay_server_handle = + tokio::spawn(async move { libp2p_relay_server.run().await }); + let artifacts_server_handle = tokio::spawn(artifacts_server); + + tokio::select! { + res = engine_handle => res??, + res = executor_handle => res??, + res = proxy_server_handle => res??, + res = graphql_server_handle => res?, + res = grpc_server_handle => res??, + res = libp2p_relay_server_handle => res?, + res = artifacts_server_handle => res?, + _ = dojo_utils::signal::wait_signals() => {}, + }; + + Ok(()) + } +} + +async fn spawn_rebuilding_graphql_server( + shutdown_tx: Sender<()>, + pool: Arc, + proxy_server: Arc, +) { + let mut broker = SimpleBroker::::subscribe(); + + loop { + let shutdown_rx = shutdown_tx.subscribe(); + let (new_addr, new_server) = torii_graphql::server::new(shutdown_rx, &pool).await; + + tokio::spawn(new_server); + + proxy_server.set_graphql_addr(new_addr).await; + + // Break the loop if there are no more events + if broker.next().await.is_none() { + break; + } else { + tokio::time::sleep(Duration::from_secs(1)).await; + } + } +} diff --git a/crates/torii/server/Cargo.toml b/crates/torii/server/Cargo.toml index 6aade5f7f0..9d01849f4f 100644 --- a/crates/torii/server/Cargo.toml +++ b/crates/torii/server/Cargo.toml @@ -24,9 +24,13 @@ serde_json.workspace = true sqlx.workspace = true tokio-util = "0.7.7" tokio.workspace = true -torii-core.workspace = true +torii-sqlite.workspace = true tower-http.workspace = true tower.workspace = true tracing.workspace = true warp.workspace = true form_urlencoded = "1.2.1" +async-trait = "0.1.83" +tokio-tungstenite = "0.20.0" +hyper-tungstenite = "0.11.1" +futures-util.workspace = true diff --git a/crates/torii/server/src/artifacts.rs b/crates/torii/server/src/artifacts.rs index c0d17687a5..30e3a55135 100644 --- a/crates/torii/server/src/artifacts.rs +++ b/crates/torii/server/src/artifacts.rs @@ -15,8 +15,8 @@ use tokio::fs; use tokio::fs::File; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::sync::broadcast::Receiver; -use torii_core::constants::{IPFS_CLIENT_MAX_RETRY, TOKENS_TABLE}; -use torii_core::utils::fetch_content_from_ipfs; +use torii_sqlite::constants::{IPFS_CLIENT_MAX_RETRY, TOKENS_TABLE}; +use torii_sqlite::utils::fetch_content_from_ipfs; use tracing::{debug, error, trace}; use warp::http::Response; use warp::path::Tail; diff --git a/crates/torii/server/src/handlers/graphql.rs b/crates/torii/server/src/handlers/graphql.rs new file mode 100644 index 0000000000..c51e7cccd7 --- /dev/null +++ b/crates/torii/server/src/handlers/graphql.rs @@ -0,0 +1,46 @@ +use std::net::{IpAddr, SocketAddr}; + +use http::StatusCode; +use hyper::{Body, Request, Response}; +use tracing::error; + +use super::Handler; + +pub(crate) const LOG_TARGET: &str = "torii::server::handlers::graphql"; + +pub struct GraphQLHandler { + client_ip: IpAddr, + graphql_addr: Option, +} + +impl GraphQLHandler { + pub fn new(client_ip: IpAddr, graphql_addr: Option) -> Self { + Self { client_ip, graphql_addr } + } +} + +#[async_trait::async_trait] +impl Handler for GraphQLHandler { + fn should_handle(&self, req: &Request) -> bool { + req.uri().path().starts_with("/graphql") + } + + async fn handle(&self, req: Request) -> Response { + if let Some(addr) = self.graphql_addr { + let graphql_addr = format!("http://{}", addr); + match crate::proxy::GRAPHQL_PROXY_CLIENT.call(self.client_ip, &graphql_addr, req).await + { + Ok(response) => response, + Err(_error) => { + error!(target: LOG_TARGET, "GraphQL proxy error: {:?}", _error); + Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(Body::empty()) + .unwrap() + } + } + } else { + Response::builder().status(StatusCode::NOT_FOUND).body(Body::empty()).unwrap() + } + } +} diff --git a/crates/torii/server/src/handlers/grpc.rs b/crates/torii/server/src/handlers/grpc.rs new file mode 100644 index 0000000000..befa3e56a4 --- /dev/null +++ b/crates/torii/server/src/handlers/grpc.rs @@ -0,0 +1,49 @@ +use std::net::{IpAddr, SocketAddr}; + +use http::header::CONTENT_TYPE; +use hyper::{Body, Request, Response, StatusCode}; +use tracing::error; + +use super::Handler; + +pub(crate) const LOG_TARGET: &str = "torii::server::handlers::grpc"; + +pub struct GrpcHandler { + client_ip: IpAddr, + grpc_addr: Option, +} + +impl GrpcHandler { + pub fn new(client_ip: IpAddr, grpc_addr: Option) -> Self { + Self { client_ip, grpc_addr } + } +} + +#[async_trait::async_trait] +impl Handler for GrpcHandler { + fn should_handle(&self, req: &Request) -> bool { + req.headers() + .get(CONTENT_TYPE) + .and_then(|ct| ct.to_str().ok()) + .map(|ct| ct.starts_with("application/grpc")) + .unwrap_or(false) + } + + async fn handle(&self, req: Request) -> Response { + if let Some(grpc_addr) = self.grpc_addr { + let grpc_addr = format!("http://{}", grpc_addr); + match crate::proxy::GRPC_PROXY_CLIENT.call(self.client_ip, &grpc_addr, req).await { + Ok(response) => response, + Err(_error) => { + error!(target: LOG_TARGET, "{:?}", _error); + Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(Body::empty()) + .unwrap() + } + } + } else { + Response::builder().status(StatusCode::NOT_FOUND).body(Body::empty()).unwrap() + } + } +} diff --git a/crates/torii/server/src/handlers/mcp.rs b/crates/torii/server/src/handlers/mcp.rs new file mode 100644 index 0000000000..22c9e4a64b --- /dev/null +++ b/crates/torii/server/src/handlers/mcp.rs @@ -0,0 +1,416 @@ +use std::sync::Arc; + +use futures_util::{SinkExt, StreamExt}; +use hyper::{Body, Request, Response, StatusCode}; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; +use sqlx::{Row, SqlitePool}; +use tokio_tungstenite::tungstenite::Message; + +use super::sql::map_row_to_json; +use super::Handler; + +const JSONRPC_VERSION: &str = "2.0"; +const MCP_VERSION: &str = "2024-11-05"; + +#[derive(Debug, Deserialize)] +#[serde(untagged)] +enum JsonRpcMessage { + Request(JsonRpcRequest), + Notification(JsonRpcNotification), +} + +#[derive(Debug, Deserialize)] +struct JsonRpcRequest { + jsonrpc: String, + id: Value, + method: String, + params: Option, +} + +#[derive(Debug, Deserialize)] +struct JsonRpcNotification { + _jsonrpc: String, + _method: String, + _params: Option, +} + +#[derive(Debug, Serialize)] +struct JsonRpcResponse { + jsonrpc: String, + id: Value, + result: Option, + #[serde(skip_serializing_if = "Option::is_none")] + error: Option, +} + +#[derive(Debug, Serialize)] +struct JsonRpcError { + code: i32, + message: String, + #[serde(skip_serializing_if = "Option::is_none")] + data: Option, +} + +#[derive(Debug, Serialize)] +struct Implementation { + name: String, + version: String, +} + +#[derive(Debug, Serialize)] +struct ServerCapabilities { + tools: ToolCapabilities, + resources: ResourceCapabilities, +} + +#[derive(Debug, Serialize)] +struct ToolCapabilities { + list_changed: bool, +} + +#[derive(Debug, Serialize)] +struct ResourceCapabilities { + subscribe: bool, + list_changed: bool, +} + +#[derive(Clone)] +pub struct McpHandler { + pool: Arc, +} + +impl McpHandler { + pub fn new(pool: Arc) -> Self { + Self { pool } + } + + async fn handle_request(&self, request: JsonRpcRequest) -> JsonRpcResponse { + if request.jsonrpc != JSONRPC_VERSION { + return JsonRpcResponse::invalid_request(request.id); + } + + match request.method.as_str() { + "initialize" => self.handle_initialize(request.id), + "tools/list" => self.handle_tools_list(request.id), + "tools/call" => self.handle_tools_call(request).await, + _ => JsonRpcResponse::method_not_found(request.id), + } + } + + fn handle_initialize(&self, id: Value) -> JsonRpcResponse { + JsonRpcResponse::ok( + id, + json!({ + "protocolVersion": MCP_VERSION, + "serverInfo": Implementation { + name: "torii-mcp".to_string(), + version: env!("CARGO_PKG_VERSION").to_string(), + }, + "capabilities": ServerCapabilities { + tools: ToolCapabilities { + list_changed: true, + }, + resources: ResourceCapabilities { + subscribe: true, + list_changed: true, + }, + }, + "instructions": include_str!("../../static/mcp-instructions.txt") + }), + ) + } + + fn handle_tools_list(&self, id: Value) -> JsonRpcResponse { + JsonRpcResponse::ok( + id, + json!({ + "tools": [ + { + "name": "query", + "description": "Execute a SQL query on the database", + "inputSchema": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "SQL query to execute" + } + }, + "required": ["query"] + } + }, + { + "name": "schema", + "description": "Retrieve the database schema including tables, columns, and their types", + "inputSchema": { + "type": "object", + "properties": { + "table": { + "type": "string", + "description": "Optional table name to get schema for. If omitted, returns schema for all tables." + } + } + } + } + ] + }), + ) + } + + async fn handle_tools_call(&self, request: JsonRpcRequest) -> JsonRpcResponse { + let Some(params) = &request.params else { + return JsonRpcResponse::invalid_params(request.id, "Missing params"); + }; + + let Some(tool_name) = params.get("name").and_then(Value::as_str) else { + return JsonRpcResponse::invalid_params(request.id, "Missing tool name"); + }; + + match tool_name { + "query" => self.handle_query_tool(request).await, + "schema" => self.handle_schema_tool(request).await, + _ => JsonRpcResponse::method_not_found(request.id), + } + } + + async fn handle_websocket_connection( + &self, + ws_stream: tokio_tungstenite::WebSocketStream, + ) { + let (mut write, mut read) = ws_stream.split(); + + while let Some(msg) = read.next().await { + if let Ok(Message::Text(text)) = msg { + let response = match serde_json::from_str::(&text) { + Ok(JsonRpcMessage::Request(request)) => self.handle_request(request).await, + Ok(JsonRpcMessage::Notification(_notification)) => { + // Handle notifications if needed + continue; + } + Err(e) => JsonRpcResponse::parse_error(Value::Null, &e.to_string()), + }; + + if let Err(e) = + write.send(Message::Text(serde_json::to_string(&response).unwrap())).await + { + eprintln!("Error sending message: {}", e); + break; + } + } + } + } + + async fn handle_schema_tool(&self, request: JsonRpcRequest) -> JsonRpcResponse { + let table_filter = request + .params + .as_ref() + .and_then(|p| p.get("arguments")) + .and_then(|args| args.get("table")) + .and_then(Value::as_str); + + let schema_query = match table_filter { + Some(_table) => "SELECT + m.name as table_name, + p.* + FROM sqlite_master m + JOIN pragma_table_info(m.name) p + WHERE m.type = 'table' + AND m.name = ? + ORDER BY m.name, p.cid" + .to_string(), + None => "SELECT + m.name as table_name, + p.* + FROM sqlite_master m + JOIN pragma_table_info(m.name) p + WHERE m.type = 'table' + ORDER BY m.name, p.cid" + .to_string(), + }; + + let rows = match table_filter { + Some(table) => sqlx::query(&schema_query).bind(table).fetch_all(&*self.pool).await, + None => sqlx::query(&schema_query).fetch_all(&*self.pool).await, + }; + + match rows { + Ok(rows) => { + let mut schema = serde_json::Map::new(); + + for row in rows { + let table_name: String = row.try_get("table_name").unwrap(); + let column_name: String = row.try_get("name").unwrap(); + let column_type: String = row.try_get("type").unwrap(); + let not_null: bool = row.try_get::("notnull").unwrap(); + let pk: bool = row.try_get::("pk").unwrap(); + let default_value: Option = row.try_get("dflt_value").unwrap(); + + let table_entry = schema.entry(table_name).or_insert_with(|| { + json!({ + "columns": serde_json::Map::new() + }) + }); + + if let Some(columns) = + table_entry.get_mut("columns").and_then(|v| v.as_object_mut()) + { + columns.insert( + column_name, + json!({ + "type": column_type, + "nullable": !not_null, + "primary_key": pk, + "default": default_value + }), + ); + } + } + + JsonRpcResponse { + jsonrpc: JSONRPC_VERSION.to_string(), + id: request.id, + result: Some(json!({ + "content": [{ + "type": "text", + "text": serde_json::to_string_pretty(&schema).unwrap() + }] + })), + error: None, + } + } + Err(e) => JsonRpcResponse { + jsonrpc: JSONRPC_VERSION.to_string(), + id: request.id, + result: None, + error: Some(JsonRpcError { + code: -32603, + message: "Database error".to_string(), + data: Some(json!({ "details": e.to_string() })), + }), + }, + } + } + + async fn handle_query_tool(&self, request: JsonRpcRequest) -> JsonRpcResponse { + if let Some(params) = request.params { + if let Some(query) = params.get("arguments").and_then(Value::as_str) { + match sqlx::query(query).fetch_all(&*self.pool).await { + Ok(rows) => { + // Convert rows to JSON using shared mapping function + let result = rows.iter().map(map_row_to_json).collect::>(); + + JsonRpcResponse { + jsonrpc: JSONRPC_VERSION.to_string(), + id: request.id, + result: Some(json!({ + "content": [{ + "type": "text", + "text": serde_json::to_string(&result).unwrap() + }] + })), + error: None, + } + } + Err(e) => JsonRpcResponse { + jsonrpc: JSONRPC_VERSION.to_string(), + id: request.id, + result: None, + error: Some(JsonRpcError { + code: -32603, + message: "Database error".to_string(), + data: Some(json!({ "details": e.to_string() })), + }), + }, + } + } else { + JsonRpcResponse { + jsonrpc: JSONRPC_VERSION.to_string(), + id: request.id, + result: None, + error: Some(JsonRpcError { + code: -32602, + message: "Invalid params".to_string(), + data: Some(json!({ "details": "Missing query parameter" })), + }), + } + } + } else { + JsonRpcResponse { + jsonrpc: JSONRPC_VERSION.to_string(), + id: request.id, + result: None, + error: Some(JsonRpcError { + code: -32602, + message: "Invalid params".to_string(), + data: None, + }), + } + } + } +} + +impl JsonRpcResponse { + fn ok(id: Value, result: Value) -> Self { + Self { jsonrpc: JSONRPC_VERSION.to_string(), id, result: Some(result), error: None } + } + + fn error(id: Value, code: i32, message: &str, data: Option) -> Self { + Self { + jsonrpc: JSONRPC_VERSION.to_string(), + id, + result: None, + error: Some(JsonRpcError { code, message: message.to_string(), data }), + } + } + + fn invalid_request(id: Value) -> Self { + Self::error(id, -32600, "Invalid Request", None) + } + + fn method_not_found(id: Value) -> Self { + Self::error(id, -32601, "Method not found", None) + } + + fn invalid_params(id: Value, details: &str) -> Self { + Self::error(id, -32602, "Invalid params", Some(json!({ "details": details }))) + } + + fn parse_error(id: Value, details: &str) -> Self { + Self::error(id, -32700, "Parse error", Some(json!({ "details": details }))) + } +} + +#[async_trait::async_trait] +impl Handler for McpHandler { + fn should_handle(&self, req: &Request) -> bool { + req.uri().path().starts_with("/mcp") + && req + .headers() + .get("upgrade") + .and_then(|h| h.to_str().ok()) + .map(|h| h.eq_ignore_ascii_case("websocket")) + .unwrap_or(false) + } + + async fn handle(&self, req: Request) -> Response { + if hyper_tungstenite::is_upgrade_request(&req) { + let (response, websocket) = hyper_tungstenite::upgrade(req, None) + .expect("Failed to upgrade WebSocket connection"); + + let this = self.clone(); + tokio::spawn(async move { + if let Ok(ws_stream) = websocket.await { + this.handle_websocket_connection(ws_stream).await; + } + }); + + response + } else { + Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from("Not a WebSocket upgrade request")) + .unwrap() + } + } +} diff --git a/crates/torii/server/src/handlers/mod.rs b/crates/torii/server/src/handlers/mod.rs new file mode 100644 index 0000000000..d40ece8ad0 --- /dev/null +++ b/crates/torii/server/src/handlers/mod.rs @@ -0,0 +1,16 @@ +pub mod graphql; +pub mod grpc; +pub mod mcp; +pub mod sql; +pub mod static_files; + +use hyper::{Body, Request, Response}; + +#[async_trait::async_trait] +pub trait Handler: Send + Sync { + // Check if this handler should handle the given request + fn should_handle(&self, req: &Request) -> bool; + + // Handle the request + async fn handle(&self, req: Request) -> Response; +} diff --git a/crates/torii/server/src/handlers/sql.rs b/crates/torii/server/src/handlers/sql.rs new file mode 100644 index 0000000000..563786f495 --- /dev/null +++ b/crates/torii/server/src/handlers/sql.rs @@ -0,0 +1,158 @@ +use std::sync::Arc; + +use base64::engine::general_purpose::STANDARD; +use base64::Engine; +use http::header::CONTENT_TYPE; +use hyper::{Body, Method, Request, Response, StatusCode}; +use include_str; +use sqlx::{Column, Row, SqlitePool, TypeInfo}; + +use super::Handler; + +pub struct SqlHandler { + pool: Arc, +} + +impl SqlHandler { + pub fn new(pool: Arc) -> Self { + Self { pool } + } + + pub async fn execute_query(&self, query: String) -> Response { + match sqlx::query(&query).fetch_all(&*self.pool).await { + Ok(rows) => { + let result: Vec<_> = rows.iter().map(map_row_to_json).collect(); + let json = match serde_json::to_string(&result) { + Ok(json) => json, + Err(e) => { + return Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(Body::from(format!("Failed to serialize result: {:?}", e))) + .unwrap(); + } + }; + + Response::builder() + .status(StatusCode::OK) + .header(CONTENT_TYPE, "application/json") + .body(Body::from(json)) + .unwrap() + } + Err(e) => Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(format!("Query error: {:?}", e))) + .unwrap(), + } + } + + async fn extract_query(&self, req: Request) -> Result> { + match *req.method() { + Method::GET => { + // Get the query from the query params + let params = req.uri().query().unwrap_or_default(); + form_urlencoded::parse(params.as_bytes()) + .find(|(key, _)| key == "q" || key == "query") + .map(|(_, value)| value.to_string()) + .ok_or( + Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from("Missing 'q' or 'query' parameter.")) + .unwrap(), + ) + } + Method::POST => { + // Get the query from request body + let body_bytes = hyper::body::to_bytes(req.into_body()).await.map_err(|_| { + Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from("Failed to read query from request body")) + .unwrap() + })?; + String::from_utf8(body_bytes.to_vec()).map_err(|_| { + Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from("Invalid query")) + .unwrap() + }) + } + _ => Err(Response::builder() + .status(StatusCode::METHOD_NOT_ALLOWED) + .body(Body::from("Only GET and POST methods are allowed")) + .unwrap()), + } + } + + async fn serve_playground(&self) -> Response { + let html = include_str!("../../static/sql-playground.html"); + + Response::builder() + .status(StatusCode::OK) + .header(CONTENT_TYPE, "text/html") + .header("Access-Control-Allow-Origin", "*") + .body(Body::from(html)) + .unwrap() + } + + async fn handle_request(&self, req: Request) -> Response { + if req.method() == Method::GET && req.uri().query().unwrap_or_default().is_empty() { + self.serve_playground().await + } else { + match self.extract_query(req).await { + Ok(query) => self.execute_query(query).await, + Err(_) => self.serve_playground().await, + } + } + } +} + +#[async_trait::async_trait] +impl Handler for SqlHandler { + fn should_handle(&self, req: &Request) -> bool { + req.uri().path().starts_with("/sql") + } + + async fn handle(&self, req: Request) -> Response { + self.handle_request(req).await + } +} + +pub fn map_row_to_json(row: &sqlx::sqlite::SqliteRow) -> serde_json::Value { + let mut obj = serde_json::Map::new(); + for (i, column) in row.columns().iter().enumerate() { + let value: serde_json::Value = match column.type_info().name() { + "TEXT" => row + .get::, _>(i) + .map_or(serde_json::Value::Null, serde_json::Value::String), + "INTEGER" => row + .get::, _>(i) + .map_or(serde_json::Value::Null, |n| serde_json::Value::Number(n.into())), + "REAL" => row.get::, _>(i).map_or(serde_json::Value::Null, |f| { + serde_json::Number::from_f64(f) + .map_or(serde_json::Value::Null, serde_json::Value::Number) + }), + "BLOB" => row.get::>, _>(i).map_or(serde_json::Value::Null, |bytes| { + serde_json::Value::String(STANDARD.encode(bytes)) + }), + _ => { + // Try different types in order + if let Ok(val) = row.try_get::(i) { + serde_json::Value::Number(val.into()) + } else if let Ok(val) = row.try_get::(i) { + serde_json::json!(val) + } else if let Ok(val) = row.try_get::(i) { + serde_json::Value::Bool(val) + } else if let Ok(val) = row.try_get::(i) { + serde_json::Value::String(val) + } else { + // Handle or fallback to BLOB as base64 + let val = row.get::>, _>(i); + val.map_or(serde_json::Value::Null, |bytes| { + serde_json::Value::String(STANDARD.encode(bytes)) + }) + } + } + }; + obj.insert(column.name().to_string(), value); + } + serde_json::Value::Object(obj) +} diff --git a/crates/torii/server/src/handlers/static_files.rs b/crates/torii/server/src/handlers/static_files.rs new file mode 100644 index 0000000000..631b032e11 --- /dev/null +++ b/crates/torii/server/src/handlers/static_files.rs @@ -0,0 +1,47 @@ +use std::net::{IpAddr, SocketAddr}; + +use hyper::{Body, Request, Response, StatusCode}; +use tracing::error; + +use super::Handler; + +pub(crate) const LOG_TARGET: &str = "torii::server::handlers::static"; + +pub struct StaticHandler { + client_ip: IpAddr, + artifacts_addr: Option, +} + +impl StaticHandler { + pub fn new(client_ip: IpAddr, artifacts_addr: Option) -> Self { + Self { client_ip, artifacts_addr } + } +} + +#[async_trait::async_trait] +impl Handler for StaticHandler { + fn should_handle(&self, req: &Request) -> bool { + req.uri().path().starts_with("/static") + } + + async fn handle(&self, req: Request) -> Response { + if let Some(artifacts_addr) = self.artifacts_addr { + let artifacts_addr = format!("http://{}", artifacts_addr); + match crate::proxy::GRAPHQL_PROXY_CLIENT + .call(self.client_ip, &artifacts_addr, req) + .await + { + Ok(response) => response, + Err(_error) => { + error!(target: LOG_TARGET, "{:?}", _error); + Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(Body::empty()) + .unwrap() + } + } + } else { + Response::builder().status(StatusCode::NOT_FOUND).body(Body::empty()).unwrap() + } + } +} diff --git a/crates/torii/server/src/lib.rs b/crates/torii/server/src/lib.rs index 621f66d155..9fe086e1fb 100644 --- a/crates/torii/server/src/lib.rs +++ b/crates/torii/server/src/lib.rs @@ -1,2 +1,3 @@ pub mod artifacts; +pub(crate) mod handlers; pub mod proxy; diff --git a/crates/torii/server/src/proxy.rs b/crates/torii/server/src/proxy.rs index 2808db2295..bf1c57ae12 100644 --- a/crates/torii/server/src/proxy.rs +++ b/crates/torii/server/src/proxy.rs @@ -3,8 +3,6 @@ use std::net::{IpAddr, SocketAddr}; use std::sync::Arc; use std::time::Duration; -use base64::engine::general_purpose::STANDARD; -use base64::Engine; use http::header::CONTENT_TYPE; use http::{HeaderName, Method}; use hyper::client::connect::dns::GaiResolver; @@ -14,13 +12,17 @@ use hyper::service::make_service_fn; use hyper::{Body, Client, Request, Response, Server, StatusCode}; use hyper_reverse_proxy::ReverseProxy; use serde_json::json; -use sqlx::{Column, Row, SqlitePool, TypeInfo}; +use sqlx::SqlitePool; use tokio::sync::RwLock; use tower::ServiceBuilder; use tower_http::cors::{AllowOrigin, CorsLayer}; -use tracing::error; -pub(crate) const LOG_TARGET: &str = "torii::server::proxy"; +use crate::handlers::graphql::GraphQLHandler; +use crate::handlers::grpc::GrpcHandler; +use crate::handlers::mcp::McpHandler; +use crate::handlers::sql::SqlHandler; +use crate::handlers::static_files::StaticHandler; +use crate::handlers::Handler; const DEFAULT_ALLOW_HEADERS: [&str; 13] = [ "accept", @@ -42,14 +44,14 @@ const DEFAULT_EXPOSED_HEADERS: [&str; 4] = const DEFAULT_MAX_AGE: Duration = Duration::from_secs(24 * 60 * 60); lazy_static::lazy_static! { - static ref GRAPHQL_PROXY_CLIENT: ReverseProxy> = { + pub(crate) static ref GRAPHQL_PROXY_CLIENT: ReverseProxy> = { ReverseProxy::new( Client::builder() .build_http(), ) }; - static ref GRPC_PROXY_CLIENT: ReverseProxy> = { + pub(crate) static ref GRPC_PROXY_CLIENT: ReverseProxy> = { ReverseProxy::new( Client::builder() .http2_only(true) @@ -170,158 +172,29 @@ async fn handle( pool: Arc, req: Request, ) -> Result, Infallible> { - if req.uri().path().starts_with("/static") { - if let Some(artifacts_addr) = artifacts_addr { - let artifacts_addr = format!("http://{}", artifacts_addr); - - return match GRAPHQL_PROXY_CLIENT.call(client_ip, &artifacts_addr, req).await { - Ok(response) => Ok(response), - Err(_error) => { - error!(target: LOG_TARGET, "Artifacts proxy error: {:?}", _error); - Ok(Response::builder() - .status(StatusCode::INTERNAL_SERVER_ERROR) - .body(Body::empty()) - .unwrap()) - } - }; - } else { - return Ok(Response::builder() - .status(StatusCode::NOT_FOUND) - .body(Body::empty()) - .unwrap()); - } - } - - if req.uri().path().starts_with("/graphql") { - if let Some(graphql_addr) = graphql_addr { - let graphql_addr = format!("http://{}", graphql_addr); - return match GRAPHQL_PROXY_CLIENT.call(client_ip, &graphql_addr, req).await { - Ok(response) => Ok(response), - Err(_error) => { - error!(target: LOG_TARGET, "GraphQL proxy error: {:?}", _error); - Ok(Response::builder() - .status(StatusCode::INTERNAL_SERVER_ERROR) - .body(Body::empty()) - .unwrap()) - } - }; - } else { - return Ok(Response::builder() - .status(StatusCode::NOT_FOUND) - .body(Body::empty()) - .unwrap()); + let handlers: Vec> = vec![ + Box::new(SqlHandler::new(pool.clone())), + Box::new(GraphQLHandler::new(client_ip, graphql_addr)), + Box::new(GrpcHandler::new(client_ip, grpc_addr)), + Box::new(StaticHandler::new(client_ip, artifacts_addr)), + Box::new(McpHandler::new(pool.clone())), + ]; + + for handler in handlers { + if handler.should_handle(&req) { + return Ok(handler.handle(req).await); } } - if let Some(content_type) = req.headers().get(CONTENT_TYPE) { - if content_type.to_str().unwrap().starts_with("application/grpc") { - if let Some(grpc_addr) = grpc_addr { - let grpc_addr = format!("http://{}", grpc_addr); - return match GRPC_PROXY_CLIENT.call(client_ip, &grpc_addr, req).await { - Ok(response) => Ok(response), - Err(_error) => { - error!(target: LOG_TARGET, "GRPC proxy error: {:?}", _error); - Ok(Response::builder() - .status(StatusCode::INTERNAL_SERVER_ERROR) - .body(Body::empty()) - .unwrap()) - } - }; - } else { - return Ok(Response::builder() - .status(StatusCode::NOT_FOUND) - .body(Body::empty()) - .unwrap()); - } - } - } - - if req.uri().path().starts_with("/sql") { - let query = if req.method() == Method::GET { - // Get the query from URL parameters - let params = req.uri().query().unwrap_or_default(); - form_urlencoded::parse(params.as_bytes()) - .find(|(key, _)| key == "q") - .map(|(_, value)| value.to_string()) - .unwrap_or_default() - } else if req.method() == Method::POST { - // Get the query from request body - let body_bytes = hyper::body::to_bytes(req.into_body()).await.unwrap_or_default(); - String::from_utf8(body_bytes.to_vec()).unwrap_or_default() - } else { - return Ok(Response::builder() - .status(StatusCode::METHOD_NOT_ALLOWED) - .body(Body::from("Only GET and POST methods are allowed")) - .unwrap()); - }; - - // Execute the query - return match sqlx::query(&query).fetch_all(&*pool).await { - Ok(rows) => { - let result: Vec<_> = rows - .iter() - .map(|row| { - let mut obj = serde_json::Map::new(); - for (i, column) in row.columns().iter().enumerate() { - let value: serde_json::Value = match column.type_info().name() { - "TEXT" => row - .get::, _>(i) - .map_or(serde_json::Value::Null, serde_json::Value::String), - // for operators like count(*) the type info is NULL - // so we default to a number - "INTEGER" | "NULL" => row - .get::, _>(i) - .map_or(serde_json::Value::Null, |n| { - serde_json::Value::Number(n.into()) - }), - "REAL" => row.get::, _>(i).map_or( - serde_json::Value::Null, - |f| { - serde_json::Number::from_f64(f).map_or( - serde_json::Value::Null, - serde_json::Value::Number, - ) - }, - ), - "BLOB" => row - .get::>, _>(i) - .map_or(serde_json::Value::Null, |bytes| { - serde_json::Value::String(STANDARD.encode(bytes)) - }), - _ => row - .get::, _>(i) - .map_or(serde_json::Value::Null, serde_json::Value::String), - }; - obj.insert(column.name().to_string(), value); - } - serde_json::Value::Object(obj) - }) - .collect(); - - let json = serde_json::to_string(&result).unwrap(); - - Ok(Response::builder() - .status(StatusCode::OK) - .header(CONTENT_TYPE, "application/json") - .body(Body::from(json)) - .unwrap()) - } - Err(e) => Ok(Response::builder() - .status(StatusCode::BAD_REQUEST) - .body(Body::from(format!("Query error: {:?}", e))) - .unwrap()), - }; - } - + // Default response if no handler matches let json = json!({ "service": "torii", "success": true }); - let body = Body::from(json.to_string()); - let response = Response::builder() + + Ok(Response::builder() .status(StatusCode::OK) .header(CONTENT_TYPE, "application/json") - .body(body) - .unwrap(); - Ok(response) + .body(Body::from(json.to_string())) + .unwrap()) } diff --git a/crates/torii/server/static/mcp-instructions.txt b/crates/torii/server/static/mcp-instructions.txt new file mode 100644 index 0000000000..db1fdfd7bf --- /dev/null +++ b/crates/torii/server/static/mcp-instructions.txt @@ -0,0 +1,64 @@ +Torii - Dojo Game Indexer for Starknet + +Torii is a specialized indexer designed for Dojo games running on Starknet. It indexes and tracks Entity Component System (ECS) data, providing a comprehensive view of game state and history. + +Database Structure: +- entities: Tracks all game entities and their current state +- dynamic model tables: Stores model data data associated with entities +- models: Contains model definitions from the game +- events: Records all game events and state changes +- transactions: Stores all blockchain transactions affecting the game +- event messages: event messages follow same structure as entities but are events + + +You should always retrieve the schema if unsure about how to query the database. With the schema you can then associate entities with models and know +what to query. + +Key Features: +1. Entity Tracking + - Query entities by type, component, or state + - Track entity history and state changes + - Aggregate entity statistics + +2. Component Analysis + - Retrieve component data for specific entities + - Query entities with specific component combinations + - Track component value changes over time + +3. Event History + - Access chronological game events + - Filter events by type, entity, or time range + - Analyze event patterns and frequencies + +4. Transaction Records + - Query game-related transactions + - Track transaction status and effects + - Link transactions to entity changes + +Available Tools: +1. 'query': Execute custom SQL queries for complex data analysis +2. 'schema': Retrieve database schema information to understand table structures and query data / entities efficiently + +Common Query Patterns: +1. Entity Lookup: + SELECT * FROM entities WHERE entity_id = X + +2. Component State: + SELECT * from [ns-Position] where internal_entity_id = X + +3. Event History: + SELECT * FROM events + WHERE entity_id = X + ORDER BY block_number DESC + +4. State Changes: + SELECT * FROM transactions + WHERE affected_entity_id = X + ORDER BY block_number DESC + +The database is optimized for querying game state and history, allowing clients to: +- Retrieve current game state +- Track entity lifecycle +- Analyze game events +- Monitor state changes +- Generate game statistics \ No newline at end of file diff --git a/crates/torii/server/static/sql-playground.html b/crates/torii/server/static/sql-playground.html new file mode 100644 index 0000000000..2e06ac4a6b --- /dev/null +++ b/crates/torii/server/static/sql-playground.html @@ -0,0 +1,925 @@ + + + + + + Torii SQL Playground + + + + + + + +

+ + + diff --git a/crates/torii/sqlite/Cargo.toml b/crates/torii/sqlite/Cargo.toml new file mode 100644 index 0000000000..8ae31daa35 --- /dev/null +++ b/crates/torii/sqlite/Cargo.toml @@ -0,0 +1,48 @@ +[package] +description = "Torii SQLite implementation." +edition.workspace = true +license-file.workspace = true +name = "torii-sqlite" +repository.workspace = true +version.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow.workspace = true +async-trait.workspace = true +base64.workspace = true +bitflags = "2.6.0" +cainome.workspace = true +chrono.workspace = true +crypto-bigint.workspace = true +data-url.workspace = true +dojo-types.workspace = true +dojo-utils.workspace = true +dojo-world.workspace = true +futures-channel = "0.3.0" +futures-util.workspace = true +hashlink.workspace = true +once_cell.workspace = true +reqwest.workspace = true +serde.workspace = true +serde_json.workspace = true +slab = "0.4.2" +sqlx.workspace = true +starknet-crypto.workspace = true +starknet.workspace = true +thiserror.workspace = true +tokio = { version = "1.32.0", features = [ "macros", "sync" ], default-features = true } +# tokio-stream = "0.1.11" +ipfs-api-backend-hyper.workspace = true +tokio-util.workspace = true +tracing.workspace = true + +[dev-dependencies] +dojo-test-utils.workspace = true +dojo-utils.workspace = true +katana-runner.workspace = true +scarb.workspace = true +sozo-scarbext.workspace = true +tempfile.workspace = true +torii-indexer.workspace = true diff --git a/crates/torii/core/src/sql/cache.rs b/crates/torii/sqlite/src/cache.rs similarity index 78% rename from crates/torii/core/src/sql/cache.rs rename to crates/torii/sqlite/src/cache.rs index 76ea4a0574..bbfad566db 100644 --- a/crates/torii/core/src/sql/cache.rs +++ b/crates/torii/sqlite/src/cache.rs @@ -7,10 +7,9 @@ use starknet_crypto::Felt; use tokio::sync::RwLock; use crate::constants::TOKEN_BALANCE_TABLE; -use crate::error::{Error, ParseError, QueryError}; -use crate::model::{parse_sql_model_members, SqlModelMember}; -use crate::sql::utils::I256; +use crate::error::{Error, ParseError}; use crate::types::ContractType; +use crate::utils::I256; #[derive(Debug, Clone)] pub struct Model { @@ -42,6 +41,10 @@ impl ModelCache { } pub async fn models(&self, selectors: &[Felt]) -> Result, Error> { + if selectors.is_empty() { + return Ok(self.model_cache.read().await.values().cloned().collect()); + } + let mut schemas = Vec::with_capacity(selectors.len()); for selector in selectors { schemas.push(self.model(selector).await?); @@ -62,19 +65,18 @@ impl ModelCache { } async fn update_model(&self, selector: &Felt) -> Result { - let formatted_selector = format!("{:#x}", selector); - - let (namespace, name, class_hash, contract_address, packed_size, unpacked_size, layout): ( - String, - String, - String, - String, - u32, - u32, - String, - ) = sqlx::query_as( + let ( + namespace, + name, + class_hash, + contract_address, + packed_size, + unpacked_size, + layout, + schema, + ): (String, String, String, String, u32, u32, String, String) = sqlx::query_as( "SELECT namespace, name, class_hash, contract_address, packed_size, unpacked_size, \ - layout FROM models WHERE id = ?", + layout, schema FROM models WHERE id = ?", ) .bind(format!("{:#x}", selector)) .fetch_one(&self.pool) @@ -84,20 +86,8 @@ impl ModelCache { let contract_address = Felt::from_hex(&contract_address).map_err(ParseError::FromStr)?; let layout = serde_json::from_str(&layout).map_err(ParseError::FromJsonStr)?; + let schema = serde_json::from_str(&schema).map_err(ParseError::FromJsonStr)?; - let model_members: Vec = sqlx::query_as( - "SELECT id, model_idx, member_idx, name, type, type_enum, enum_options, key FROM \ - model_members WHERE model_id = ? ORDER BY model_idx ASC, member_idx ASC", - ) - .bind(formatted_selector) - .fetch_all(&self.pool) - .await?; - - if model_members.is_empty() { - return Err(QueryError::ModelNotFound(name.clone()).into()); - } - - let schema = parse_sql_model_members(&namespace, &name, &model_members); let mut cache = self.model_cache.write().await; let model = Model { diff --git a/crates/torii/core/src/constants.rs b/crates/torii/sqlite/src/constants.rs similarity index 90% rename from crates/torii/core/src/constants.rs rename to crates/torii/sqlite/src/constants.rs index ce46d77d31..be9e7140b2 100644 --- a/crates/torii/core/src/constants.rs +++ b/crates/torii/sqlite/src/constants.rs @@ -1,9 +1,9 @@ +pub const QUERY_QUEUE_BATCH_SIZE: usize = 1000; pub const TOKEN_BALANCE_TABLE: &str = "token_balances"; pub const TOKEN_TRANSFER_TABLE: &str = "token_transfers"; pub const TOKENS_TABLE: &str = "tokens"; - -pub(crate) const LOG_TARGET: &str = "torii_core::engine"; -pub const QUERY_QUEUE_BATCH_SIZE: usize = 1000; +pub const WORLD_CONTRACT_TYPE: &str = "WORLD"; +pub const SQL_FELT_DELIMITER: &str = "/"; pub const IPFS_URL: &str = "https://ipfs.io/ipfs/"; pub const IPFS_CLIENT_MAX_RETRY: u8 = 3; @@ -11,6 +11,3 @@ pub const IPFS_CLIENT_MAX_RETRY: u8 = 3; pub const IPFS_CLIENT_URL: &str = "https://ipfs.infura.io:5001"; pub const IPFS_CLIENT_USERNAME: &str = "2EBrzr7ZASQZKH32sl2xWauXPSA"; pub const IPFS_CLIENT_PASSWORD: &str = "12290b883db9138a8ae3363b6739d220"; - -pub const WORLD_CONTRACT_TYPE: &str = "WORLD"; -pub const SQL_FELT_DELIMITER: &str = "/"; diff --git a/crates/torii/core/src/sql/erc.rs b/crates/torii/sqlite/src/erc.rs similarity index 94% rename from crates/torii/core/src/sql/erc.rs rename to crates/torii/sqlite/src/erc.rs index cf94e005f0..f11f7988c1 100644 --- a/crates/torii/core/src/sql/erc.rs +++ b/crates/torii/sqlite/src/erc.rs @@ -14,9 +14,11 @@ use crate::executor::{ ApplyBalanceDiffQuery, Argument, QueryMessage, QueryType, RegisterErc20TokenQuery, RegisterErc721TokenQuery, }; -use crate::sql::utils::{felt_and_u256_to_sql_string, felt_to_sql_string, felts_to_sql_string}; use crate::types::ContractType; -use crate::utils::utc_dt_string_from_timestamp; +use crate::utils::{ + felt_and_u256_to_sql_string, felt_to_sql_string, felts_to_sql_string, + utc_dt_string_from_timestamp, +}; impl Sql { #[allow(clippy::too_many_arguments)] @@ -29,6 +31,7 @@ impl Sql { provider: &P, block_timestamp: u64, event_id: &str, + block_number: u64, ) -> Result<()> { // contract_address let token_id = felt_to_sql_string(&contract_address); @@ -66,10 +69,11 @@ impl Sql { self.local_cache.erc_cache.entry((ContractType::ERC20, to_balance_id)).or_default(); *to_balance += I256::from(amount); } + let block_id = BlockId::Number(block_number); if self.local_cache.erc_cache.len() >= 100000 { self.flush().await.with_context(|| "Failed to flush in handle_erc20_transfer")?; - self.apply_cache_diff().await?; + self.apply_cache_diff(block_id).await?; } Ok(()) @@ -84,6 +88,7 @@ impl Sql { token_id: U256, block_timestamp: u64, event_id: &str, + block_number: u64, ) -> Result<()> { // contract_address:id let actual_token_id = token_id; @@ -127,10 +132,11 @@ impl Sql { .or_default(); *to_balance += I256::from(1u8); } + let block_id = BlockId::Number(block_number); if self.local_cache.erc_cache.len() >= 100000 { self.flush().await.with_context(|| "Failed to flush in handle_erc721_transfer")?; - self.apply_cache_diff().await?; + self.apply_cache_diff(block_id).await?; } Ok(()) @@ -272,7 +278,7 @@ impl Sql { Ok(()) } - pub async fn apply_cache_diff(&mut self) -> Result<()> { + pub async fn apply_cache_diff(&mut self, block_id: BlockId) -> Result<()> { if !self.local_cache.erc_cache.is_empty() { self.executor.send(QueryMessage::new( "".to_string(), @@ -282,6 +288,7 @@ impl Sql { &mut self.local_cache.erc_cache, HashMap::with_capacity(64), ), + block_id, }), ))?; } diff --git a/crates/torii/core/src/error.rs b/crates/torii/sqlite/src/error.rs similarity index 93% rename from crates/torii/core/src/error.rs rename to crates/torii/sqlite/src/error.rs index d2e595c874..0a8c497a76 100644 --- a/crates/torii/core/src/error.rs +++ b/crates/torii/sqlite/src/error.rs @@ -51,4 +51,6 @@ pub enum QueryError { SqliteJoinLimit, #[error("Invalid namespaced model: {0}")] InvalidNamespacedModel(String), + #[error("Invalid timestamp: {0}. Expected valid number of seconds since unix epoch.")] + InvalidTimestamp(u64), } diff --git a/crates/torii/core/src/executor/erc.rs b/crates/torii/sqlite/src/executor/erc.rs similarity index 74% rename from crates/torii/core/src/executor/erc.rs rename to crates/torii/sqlite/src/executor/erc.rs index afa39c89c0..c91cdce8a6 100644 --- a/crates/torii/core/src/executor/erc.rs +++ b/crates/torii/sqlite/src/executor/erc.rs @@ -10,13 +10,16 @@ use starknet::core::types::{BlockId, BlockTag, FunctionCall, U256}; use starknet::core::utils::{get_selector_from_name, parse_cairo_short_string}; use starknet::providers::Provider; use starknet_crypto::Felt; -use tracing::{debug, trace}; +use tracing::{debug, trace, warn}; use super::{ApplyBalanceDiffQuery, Executor}; use crate::constants::{IPFS_CLIENT_MAX_RETRY, SQL_FELT_DELIMITER, TOKEN_BALANCE_TABLE}; -use crate::sql::utils::{felt_to_sql_string, sql_string_to_u256, u256_to_sql_string, I256}; -use crate::types::ContractType; -use crate::utils::fetch_content_from_ipfs; +use crate::executor::LOG_TARGET; +use crate::simple_broker::SimpleBroker; +use crate::types::{ContractType, TokenBalance}; +use crate::utils::{ + felt_to_sql_string, fetch_content_from_ipfs, sql_string_to_u256, u256_to_sql_string, I256, +}; #[derive(Debug, Clone)] pub struct RegisterErc721TokenQuery { @@ -46,6 +49,7 @@ impl<'c, P: Provider + Sync + Send + 'static> Executor<'c, P> { pub async fn apply_balance_diff( &mut self, apply_balance_diff: ApplyBalanceDiffQuery, + provider: Arc

, ) -> Result<()> { let erc_cache = apply_balance_diff.erc_cache; for ((contract_type, id_str), balance) in erc_cache.iter() { @@ -66,6 +70,8 @@ impl<'c, P: Provider + Sync + Send + 'static> Executor<'c, P> { contract_address, token_id, balance, + Arc::clone(&provider), + apply_balance_diff.block_id, ) .await .with_context(|| "Failed to apply balance diff in apply_cache_diff")?; @@ -83,6 +89,8 @@ impl<'c, P: Provider + Sync + Send + 'static> Executor<'c, P> { contract_address, token_id, balance, + Arc::clone(&provider), + apply_balance_diff.block_id, ) .await .with_context(|| "Failed to apply balance diff in apply_cache_diff")?; @@ -93,6 +101,7 @@ impl<'c, P: Provider + Sync + Send + 'static> Executor<'c, P> { Ok(()) } + #[allow(clippy::too_many_arguments)] pub async fn apply_balance_diff_helper( &mut self, id: &str, @@ -100,6 +109,8 @@ impl<'c, P: Provider + Sync + Send + 'static> Executor<'c, P> { contract_address: &str, token_id: &str, balance_diff: &I256, + provider: Arc

, + block_id: BlockId, ) -> Result<()> { let tx = &mut self.transaction; let balance: Option<(String,)> = @@ -116,26 +127,55 @@ impl<'c, P: Provider + Sync + Send + 'static> Executor<'c, P> { if balance_diff.is_negative { if balance < balance_diff.value { - dbg!(&balance_diff, balance, id); + // HACK: ideally we should never hit this case. But ETH on starknet mainnet didn't + // emit transfer events properly so they are broken. For those cases + // we manually fetch the balance of the address using RPC + + let current_balance = provider + .call( + FunctionCall { + contract_address: Felt::from_str(contract_address).unwrap(), + entry_point_selector: get_selector_from_name("balanceOf").unwrap(), + calldata: vec![Felt::from_str(account_address).unwrap()], + }, + block_id, + ) + .await + .with_context(|| format!("Failed to fetch balance for id: {}", id))?; + + let current_balance = + cainome::cairo_serde::U256::cairo_deserialize(¤t_balance, 0).unwrap(); + + warn!( + target: LOG_TARGET, + id = id, + "Invalid transfer event detected, overriding balance by querying RPC directly" + ); + // override the balance from onchain data + balance = U256::from_words(current_balance.low, current_balance.high); + } else { + balance -= balance_diff.value; } - balance -= balance_diff.value; } else { balance += balance_diff.value; } // write the new balance to the database - sqlx::query(&format!( + let token_balance: TokenBalance = sqlx::query_as(&format!( "INSERT OR REPLACE INTO {TOKEN_BALANCE_TABLE} (id, contract_address, account_address, \ - token_id, balance) VALUES (?, ?, ?, ?, ?)", + token_id, balance) VALUES (?, ?, ?, ?, ?) RETURNING *", )) .bind(id) .bind(contract_address) .bind(account_address) .bind(token_id) .bind(u256_to_sql_string(&balance)) - .execute(&mut **tx) + .fetch_one(&mut **tx) .await?; + debug!(target: LOG_TARGET, token_balance = ?token_balance, "Applied balance diff"); + SimpleBroker::publish(token_balance); + Ok(()) } @@ -176,7 +216,14 @@ impl<'c, P: Provider + Sync + Send + 'static> Executor<'c, P> { { token_uri } else { - return Err(anyhow::anyhow!("Failed to fetch token_uri")); + warn!( + contract_address = format!("{:#x}", register_erc721_token.contract_address), + token_id = %register_erc721_token.actual_token_id, + "Error fetching token URI, empty metadata will be used instead.", + ); + + // Ignoring the token URI if the contract can't return it. + ByteArray::cairo_serialize(&"".try_into().unwrap()) }; let token_uri = if let Ok(byte_array) = ByteArray::cairo_deserialize(&token_uri, 0) { @@ -192,13 +239,28 @@ impl<'c, P: Provider + Sync + Send + 'static> Executor<'c, P> { return Err(anyhow::anyhow!("token_uri is neither ByteArray nor Array")); }; - let metadata = Self::fetch_metadata(&token_uri).await.with_context(|| { - format!( - "Failed to fetch metadata for token_id: {}", - register_erc721_token.actual_token_id - ) - })?; - let metadata = serde_json::to_string(&metadata).context("Failed to serialize metadata")?; + let metadata = if token_uri.is_empty() { + "".to_string() + } else { + let metadata = Self::fetch_metadata(&token_uri).await.with_context(|| { + format!( + "Failed to fetch metadata for token_id: {}", + register_erc721_token.actual_token_id + ) + }); + + if let Ok(metadata) = metadata { + serde_json::to_string(&metadata).context("Failed to serialize metadata")? + } else { + warn!( + contract_address = format!("{:#x}", register_erc721_token.contract_address), + token_id = %register_erc721_token.actual_token_id, + "Error fetching metadata, empty metadata will be used instead.", + ); + "".to_string() + } + }; + Ok(RegisterErc721TokenMetadata { query: register_erc721_token, metadata, name, symbol }) } diff --git a/crates/torii/core/src/executor/mod.rs b/crates/torii/sqlite/src/executor/mod.rs similarity index 98% rename from crates/torii/core/src/executor/mod.rs rename to crates/torii/sqlite/src/executor/mod.rs index 18c76f6d64..303e7291a1 100644 --- a/crates/torii/core/src/executor/mod.rs +++ b/crates/torii/sqlite/src/executor/mod.rs @@ -20,17 +20,17 @@ use tracing::{debug, error}; use crate::constants::TOKENS_TABLE; use crate::simple_broker::SimpleBroker; -use crate::sql::utils::{felt_to_sql_string, I256}; use crate::types::{ ContractCursor, ContractType, Entity as EntityUpdated, Event as EventEmitted, EventMessage as EventMessageUpdated, Model as ModelRegistered, OptimisticEntity, OptimisticEventMessage, }; +use crate::utils::{felt_to_sql_string, I256}; pub mod erc; pub use erc::{RegisterErc20TokenQuery, RegisterErc721TokenMetadata, RegisterErc721TokenQuery}; -pub(crate) const LOG_TARGET: &str = "torii_core::executor"; +pub(crate) const LOG_TARGET: &str = "torii::sqlite::executor"; #[derive(Debug, Clone)] pub enum Argument { @@ -61,6 +61,7 @@ pub struct DeleteEntityQuery { #[derive(Debug, Clone)] pub struct ApplyBalanceDiffQuery { pub erc_cache: HashMap<(ContractType, String), I256>, + pub block_id: BlockId, } #[derive(Debug, Clone)] @@ -548,14 +549,7 @@ impl<'c, P: Provider + Sync + Send + 'static> Executor<'c, P> { if em_query.is_historical { event_counter += 1; - let data = em_query - .ty - .serialize()? - .iter() - .map(|felt| format!("{:#x}", felt)) - .collect::>() - .join("/"); - + let data = serde_json::to_string(&em_query.ty.to_json_value()?)?; sqlx::query( "INSERT INTO event_messages_historical (id, keys, event_id, data, \ model_id, executed_at) VALUES (?, ?, ?, ?, ?, ?) RETURNING *", @@ -613,14 +607,14 @@ impl<'c, P: Provider + Sync + Send + 'static> Executor<'c, P> { QueryType::ApplyBalanceDiff(apply_balance_diff) => { debug!(target: LOG_TARGET, "Applying balance diff."); let instant = Instant::now(); - self.apply_balance_diff(apply_balance_diff).await?; + self.apply_balance_diff(apply_balance_diff, self.provider.clone()).await?; debug!(target: LOG_TARGET, duration = ?instant.elapsed(), "Applied balance diff."); } QueryType::RegisterErc721Token(register_erc721_token) => { let semaphore = self.semaphore.clone(); let provider = self.provider.clone(); let res = sqlx::query_as::<_, (String, String)>(&format!( - "SELECT name, symbol FROM {TOKENS_TABLE} WHERE contract_address = ?" + "SELECT name, symbol FROM {TOKENS_TABLE} WHERE contract_address = ? LIMIT 1" )) .bind(felt_to_sql_string(®ister_erc721_token.contract_address)) .fetch_one(&mut **tx) @@ -763,10 +757,12 @@ impl<'c, P: Provider + Sync + Send + 'static> Executor<'c, P> { } } QueryType::Other => { - query.execute(&mut **tx).await.with_context(|| { - format!( - "Failed to execute query: {:?}, args: {:?}", - query_message.statement, query_message.arguments + query.execute(&mut **tx).await.map_err(|e| { + anyhow::anyhow!( + "Failed to execute query: {:?}, args: {:?}, error: {:?}", + query_message.statement, + query_message.arguments, + e ) })?; } diff --git a/crates/torii/sqlite/src/lib.rs b/crates/torii/sqlite/src/lib.rs new file mode 100644 index 0000000000..7666108620 --- /dev/null +++ b/crates/torii/sqlite/src/lib.rs @@ -0,0 +1,957 @@ +use std::collections::HashMap; +use std::convert::TryInto; +use std::str::FromStr; +use std::sync::Arc; + +use anyhow::{anyhow, Context, Result}; +use dojo_types::naming::get_tag; +use dojo_types::schema::{Struct, Ty}; +use dojo_world::config::WorldMetadata; +use dojo_world::contracts::abigen::model::Layout; +use dojo_world::contracts::naming::compute_selector_from_names; +use sqlx::pool::PoolConnection; +use sqlx::{Pool, Sqlite}; +use starknet::core::types::{Event, Felt, InvokeTransaction, Transaction}; +use starknet_crypto::poseidon_hash_many; +use tokio::sync::mpsc::UnboundedSender; +use utils::felts_to_sql_string; + +use crate::constants::SQL_FELT_DELIMITER; +use crate::executor::{ + Argument, DeleteEntityQuery, EventMessageQuery, QueryMessage, QueryType, ResetCursorsQuery, + SetHeadQuery, UpdateCursorsQuery, +}; +use crate::types::Contract; +use crate::utils::utc_dt_string_from_timestamp; + +type IsEventMessage = bool; +type IsStoreUpdate = bool; + +pub mod cache; +pub mod constants; +pub mod erc; +pub mod error; +pub mod executor; +pub mod model; +pub mod simple_broker; +pub mod types; +pub mod utils; + +use cache::{LocalCache, Model, ModelCache}; + +#[derive(Debug, Clone)] +pub struct Sql { + pub pool: Pool, + pub executor: UnboundedSender, + model_cache: Arc, + // when SQL struct is cloned a empty local_cache is created + local_cache: LocalCache, +} + +#[derive(Debug, Clone)] +pub struct Cursors { + pub cursor_map: HashMap, + pub last_pending_block_tx: Option, + pub head: Option, +} + +impl Sql { + pub async fn new( + pool: Pool, + executor: UnboundedSender, + contracts: &[Contract], + model_cache: Arc, + ) -> Result { + for contract in contracts { + executor.send(QueryMessage::other( + "INSERT OR IGNORE INTO contracts (id, contract_address, contract_type) VALUES (?, \ + ?, ?)" + .to_string(), + vec![ + Argument::FieldElement(contract.address), + Argument::FieldElement(contract.address), + Argument::String(contract.r#type.to_string()), + ], + ))?; + } + + let local_cache = LocalCache::new(pool.clone()).await; + let db = Self { pool: pool.clone(), executor, model_cache, local_cache }; + + db.execute().await?; + + Ok(db) + } + + pub async fn head(&self, contract: Felt) -> Result<(u64, Option, Option)> { + let indexer_query = + sqlx::query_as::<_, (Option, Option, Option, String)>( + "SELECT head, last_pending_block_contract_tx, last_pending_block_tx, \ + contract_type FROM contracts WHERE id = ?", + ) + .bind(format!("{:#x}", contract)); + + let indexer: (Option, Option, Option, String) = indexer_query + .fetch_one(&self.pool) + .await + .with_context(|| format!("Failed to fetch head for contract: {:#x}", contract))?; + Ok(( + indexer + .0 + .map(|h| h.try_into().map_err(|_| anyhow!("Head value {} doesn't fit in u64", h))) + .transpose()? + .unwrap_or(0), + indexer.1.map(|f| Felt::from_str(&f)).transpose()?, + indexer.2.map(|f| Felt::from_str(&f)).transpose()?, + )) + } + + pub async fn set_head( + &mut self, + head: u64, + last_block_timestamp: u64, + world_txns_count: u64, + contract_address: Felt, + ) -> Result<()> { + let head_arg = Argument::Int( + head.try_into().map_err(|_| anyhow!("Head value {} doesn't fit in i64", head))?, + ); + let last_block_timestamp_arg = + Argument::Int(last_block_timestamp.try_into().map_err(|_| { + anyhow!("Last block timestamp value {} doesn't fit in i64", last_block_timestamp) + })?); + let id = Argument::FieldElement(contract_address); + + self.executor.send(QueryMessage::new( + "UPDATE contracts SET head = ?, last_block_timestamp = ? WHERE id = ?".to_string(), + vec![head_arg, last_block_timestamp_arg, id], + QueryType::SetHead(SetHeadQuery { + head, + last_block_timestamp, + txns_count: world_txns_count, + contract_address, + }), + ))?; + + Ok(()) + } + + pub fn set_last_pending_block_contract_tx( + &mut self, + contract: Felt, + last_pending_block_contract_tx: Option, + ) -> Result<()> { + let last_pending_block_contract_tx = if let Some(f) = last_pending_block_contract_tx { + Argument::String(format!("{:#x}", f)) + } else { + Argument::Null + }; + + let id = Argument::FieldElement(contract); + + self.executor.send(QueryMessage::other( + "UPDATE contracts SET last_pending_block_contract_tx = ? WHERE id = ?".to_string(), + vec![last_pending_block_contract_tx, id], + ))?; + + Ok(()) + } + + pub fn set_last_pending_block_tx(&mut self, last_pending_block_tx: Option) -> Result<()> { + let last_pending_block_tx = if let Some(f) = last_pending_block_tx { + Argument::String(format!("{:#x}", f)) + } else { + Argument::Null + }; + + self.executor.send(QueryMessage::other( + "UPDATE contracts SET last_pending_block_tx = ? WHERE 1=1".to_string(), + vec![last_pending_block_tx], + ))?; + + Ok(()) + } + + pub async fn cursors(&self) -> Result { + let mut conn: PoolConnection = self.pool.acquire().await?; + let cursors = sqlx::query_as::<_, (String, String)>( + "SELECT contract_address, last_pending_block_contract_tx FROM contracts WHERE \ + last_pending_block_contract_tx IS NOT NULL", + ) + .fetch_all(&mut *conn) + .await?; + + let (head, last_pending_block_tx) = sqlx::query_as::<_, (Option, Option)>( + "SELECT head, last_pending_block_tx FROM contracts WHERE 1=1", + ) + .fetch_one(&mut *conn) + .await?; + + let head = head.map(|h| h.try_into().expect("doesn't fit in u64")); + let last_pending_block_tx = + last_pending_block_tx.map(|t| Felt::from_str(&t).expect("its a valid felt")); + Ok(Cursors { + cursor_map: cursors + .into_iter() + .map(|(c, t)| { + ( + Felt::from_str(&c).expect("its a valid felt"), + Felt::from_str(&t).expect("its a valid felt"), + ) + }) + .collect(), + last_pending_block_tx, + head, + }) + } + + // For a given contract address, sets head to the passed value and sets + // last_pending_block_contract_tx and last_pending_block_tx to null + pub fn reset_cursors( + &mut self, + head: u64, + cursor_map: HashMap, + last_block_timestamp: u64, + ) -> Result<()> { + self.executor.send(QueryMessage::new( + "".to_string(), + vec![], + QueryType::ResetCursors(ResetCursorsQuery { + cursor_map, + last_block_timestamp, + last_block_number: head, + }), + ))?; + + Ok(()) + } + + pub fn update_cursors( + &mut self, + head: u64, + last_pending_block_tx: Option, + cursor_map: HashMap, + pending_block_timestamp: u64, + ) -> Result<()> { + self.executor.send(QueryMessage::new( + "".to_string(), + vec![], + QueryType::UpdateCursors(UpdateCursorsQuery { + cursor_map, + last_pending_block_tx, + last_block_number: head, + pending_block_timestamp, + }), + ))?; + Ok(()) + } + + #[allow(clippy::too_many_arguments)] + pub async fn register_model( + &mut self, + namespace: &str, + model: &Ty, + layout: Layout, + class_hash: Felt, + contract_address: Felt, + packed_size: u32, + unpacked_size: u32, + block_timestamp: u64, + upgrade_diff: Option<&Ty>, + ) -> Result<()> { + let selector = compute_selector_from_names(namespace, &model.name()); + let namespaced_name = get_tag(namespace, &model.name()); + let namespaced_schema = Ty::Struct(Struct { + name: namespaced_name.clone(), + children: model.as_struct().unwrap().children.clone(), + }); + + let insert_models = + "INSERT INTO models (id, namespace, name, class_hash, contract_address, layout, \ + schema, packed_size, unpacked_size, executed_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, \ + ?) ON CONFLICT(id) DO UPDATE SET contract_address=EXCLUDED.contract_address, \ + class_hash=EXCLUDED.class_hash, layout=EXCLUDED.layout, schema=EXCLUDED.schema, \ + packed_size=EXCLUDED.packed_size, unpacked_size=EXCLUDED.unpacked_size, \ + executed_at=EXCLUDED.executed_at RETURNING *"; + let arguments = vec![ + Argument::String(format!("{:#x}", selector)), + Argument::String(namespace.to_string()), + Argument::String(model.name().to_string()), + Argument::String(format!("{class_hash:#x}")), + Argument::String(format!("{contract_address:#x}")), + Argument::String(serde_json::to_string(&layout)?), + Argument::String(serde_json::to_string(&namespaced_schema)?), + Argument::Int(packed_size as i64), + Argument::Int(unpacked_size as i64), + Argument::String(utc_dt_string_from_timestamp(block_timestamp)), + ]; + self.executor.send(QueryMessage::new( + insert_models.to_string(), + arguments, + QueryType::RegisterModel, + ))?; + + self.build_model_query(vec![namespaced_name.clone()], model, upgrade_diff)?; + + // we set the model in the cache directly + // because entities might be using it before the query queue is processed + self.model_cache + .set( + selector, + Model { + namespace: namespace.to_string(), + name: model.name().to_string(), + selector, + class_hash, + contract_address, + packed_size, + unpacked_size, + layout, + schema: namespaced_schema, + }, + ) + .await; + + Ok(()) + } + + pub async fn set_entity( + &mut self, + entity: Ty, + event_id: &str, + block_timestamp: u64, + entity_id: Felt, + model_id: Felt, + keys_str: Option<&str>, + ) -> Result<()> { + let namespaced_name = entity.name(); + + let entity_id = format!("{:#x}", entity_id); + let model_id = format!("{:#x}", model_id); + + let insert_entities = if keys_str.is_some() { + "INSERT INTO entities (id, event_id, executed_at, keys) VALUES (?, ?, ?, ?) ON \ + CONFLICT(id) DO UPDATE SET updated_at=CURRENT_TIMESTAMP, \ + executed_at=EXCLUDED.executed_at, event_id=EXCLUDED.event_id, keys=EXCLUDED.keys \ + RETURNING *" + } else { + "INSERT INTO entities (id, event_id, executed_at) VALUES (?, ?, ?) ON CONFLICT(id) DO \ + UPDATE SET updated_at=CURRENT_TIMESTAMP, executed_at=EXCLUDED.executed_at, \ + event_id=EXCLUDED.event_id RETURNING *" + }; + + let mut arguments = vec![ + Argument::String(entity_id.clone()), + Argument::String(event_id.to_string()), + Argument::String(utc_dt_string_from_timestamp(block_timestamp)), + ]; + + if let Some(keys) = keys_str { + arguments.push(Argument::String(keys.to_string())); + } + + self.executor.send(QueryMessage::new( + insert_entities.to_string(), + arguments, + QueryType::SetEntity(entity.clone()), + ))?; + + self.executor.send(QueryMessage::other( + "INSERT INTO entity_model (entity_id, model_id) VALUES (?, ?) ON CONFLICT(entity_id, \ + model_id) DO NOTHING" + .to_string(), + vec![Argument::String(entity_id.clone()), Argument::String(model_id.clone())], + ))?; + + self.set_entity_model( + &namespaced_name, + event_id, + (&entity_id, false), + (&entity, keys_str.is_none()), + block_timestamp, + )?; + + Ok(()) + } + + pub async fn set_event_message( + &mut self, + entity: Ty, + event_id: &str, + block_timestamp: u64, + is_historical: bool, + ) -> Result<()> { + let keys = if let Ty::Struct(s) = &entity { + let mut keys = Vec::new(); + for m in s.keys() { + keys.extend(m.serialize()?); + } + keys + } else { + return Err(anyhow!("Entity is not a struct")); + }; + + let namespaced_name = entity.name(); + let (model_namespace, model_name) = namespaced_name.split_once('-').unwrap(); + + let entity_id = format!("{:#x}", poseidon_hash_many(&keys)); + let model_id = format!("{:#x}", compute_selector_from_names(model_namespace, model_name)); + + let keys_str = felts_to_sql_string(&keys); + let block_timestamp_str = utc_dt_string_from_timestamp(block_timestamp); + + let insert_entities = "INSERT INTO event_messages (id, keys, event_id, executed_at) \ + VALUES (?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET \ + updated_at=CURRENT_TIMESTAMP, executed_at=EXCLUDED.executed_at, \ + event_id=EXCLUDED.event_id RETURNING *"; + self.executor.send(QueryMessage::new( + insert_entities.to_string(), + vec![ + Argument::String(entity_id.clone()), + Argument::String(keys_str.clone()), + Argument::String(event_id.to_string()), + Argument::String(block_timestamp_str.clone()), + ], + QueryType::EventMessage(EventMessageQuery { + entity_id: entity_id.clone(), + model_id: model_id.clone(), + keys_str: keys_str.clone(), + event_id: event_id.to_string(), + block_timestamp: block_timestamp_str.clone(), + ty: entity.clone(), + is_historical, + }), + ))?; + + self.set_entity_model( + &namespaced_name, + event_id, + (&entity_id, true), + (&entity, false), + block_timestamp, + )?; + + Ok(()) + } + + pub async fn delete_entity( + &mut self, + entity_id: Felt, + model_id: Felt, + entity: Ty, + event_id: &str, + block_timestamp: u64, + ) -> Result<()> { + let entity_id = format!("{:#x}", entity_id); + let model_table = entity.name(); + + self.executor.send(QueryMessage::new( + format!( + "DELETE FROM [{model_table}] WHERE internal_id = ?; DELETE FROM entity_model \ + WHERE entity_id = ? AND model_id = ?" + ) + .to_string(), + vec![Argument::String(entity_id.clone()), Argument::String(format!("{:#x}", model_id))], + QueryType::DeleteEntity(DeleteEntityQuery { + entity_id: entity_id.clone(), + event_id: event_id.to_string(), + block_timestamp: utc_dt_string_from_timestamp(block_timestamp), + ty: entity.clone(), + }), + ))?; + + Ok(()) + } + + pub fn set_metadata(&mut self, resource: &Felt, uri: &str, block_timestamp: u64) -> Result<()> { + let resource = Argument::FieldElement(*resource); + let uri = Argument::String(uri.to_string()); + let executed_at = Argument::String(utc_dt_string_from_timestamp(block_timestamp)); + + self.executor.send(QueryMessage::other( + "INSERT INTO metadata (id, uri, executed_at) VALUES (?, ?, ?) ON CONFLICT(id) DO \ + UPDATE SET id=excluded.id, executed_at=excluded.executed_at, \ + updated_at=CURRENT_TIMESTAMP" + .to_string(), + vec![resource, uri, executed_at], + ))?; + + Ok(()) + } + + pub fn update_metadata( + &mut self, + resource: &Felt, + uri: &str, + metadata: &WorldMetadata, + icon_img: &Option, + cover_img: &Option, + ) -> Result<()> { + let json = serde_json::to_string(metadata).unwrap(); // safe unwrap + + let mut update = vec!["uri=?", "json=?", "updated_at=CURRENT_TIMESTAMP"]; + let mut arguments = vec![Argument::String(uri.to_string()), Argument::String(json)]; + + if let Some(icon) = icon_img { + update.push("icon_img=?"); + arguments.push(Argument::String(icon.clone())); + } + + if let Some(cover) = cover_img { + update.push("cover_img=?"); + arguments.push(Argument::String(cover.clone())); + } + + let statement = format!("UPDATE metadata SET {} WHERE id = ?", update.join(",")); + arguments.push(Argument::FieldElement(*resource)); + + self.executor.send(QueryMessage::other(statement, arguments))?; + + Ok(()) + } + + pub async fn model(&self, selector: Felt) -> Result { + self.model_cache.model(&selector).await.map_err(|e| e.into()) + } + + pub async fn entities(&self, model: String) -> Result>> { + let query = sqlx::query_as::<_, (i32, String, String)>("SELECT * FROM ?").bind(model); + let mut conn: PoolConnection = self.pool.acquire().await?; + let mut rows = query.fetch_all(&mut *conn).await?; + Ok(rows.drain(..).map(|row| serde_json::from_str(&row.2).unwrap()).collect()) + } + + pub fn store_transaction( + &mut self, + transaction: &Transaction, + transaction_id: &str, + block_timestamp: u64, + ) -> Result<()> { + let id = Argument::String(transaction_id.to_string()); + + let transaction_type = match transaction { + Transaction::Invoke(_) => "INVOKE", + Transaction::L1Handler(_) => "L1_HANDLER", + _ => return Ok(()), + }; + + let (transaction_hash, sender_address, calldata, max_fee, signature, nonce) = + match transaction { + Transaction::Invoke(InvokeTransaction::V3(invoke_v3_transaction)) => ( + Argument::FieldElement(invoke_v3_transaction.transaction_hash), + Argument::FieldElement(invoke_v3_transaction.sender_address), + Argument::String(felts_to_sql_string(&invoke_v3_transaction.calldata)), + Argument::FieldElement(Felt::ZERO), // has no max_fee + Argument::String(felts_to_sql_string(&invoke_v3_transaction.signature)), + Argument::FieldElement(invoke_v3_transaction.nonce), + ), + Transaction::Invoke(InvokeTransaction::V1(invoke_v1_transaction)) => ( + Argument::FieldElement(invoke_v1_transaction.transaction_hash), + Argument::FieldElement(invoke_v1_transaction.sender_address), + Argument::String(felts_to_sql_string(&invoke_v1_transaction.calldata)), + Argument::FieldElement(invoke_v1_transaction.max_fee), + Argument::String(felts_to_sql_string(&invoke_v1_transaction.signature)), + Argument::FieldElement(invoke_v1_transaction.nonce), + ), + Transaction::L1Handler(l1_handler_transaction) => ( + Argument::FieldElement(l1_handler_transaction.transaction_hash), + Argument::FieldElement(l1_handler_transaction.contract_address), + Argument::String(felts_to_sql_string(&l1_handler_transaction.calldata)), + Argument::FieldElement(Felt::ZERO), // has no max_fee + Argument::String("".to_string()), // has no signature + Argument::FieldElement((l1_handler_transaction.nonce).into()), + ), + _ => return Ok(()), + }; + + self.executor.send(QueryMessage::other( + "INSERT OR IGNORE INTO transactions (id, transaction_hash, sender_address, calldata, \ + max_fee, signature, nonce, transaction_type, executed_at) VALUES (?, ?, ?, ?, ?, ?, \ + ?, ?, ?)" + .to_string(), + vec![ + id, + transaction_hash, + sender_address, + calldata, + max_fee, + signature, + nonce, + Argument::String(transaction_type.to_string()), + Argument::String(utc_dt_string_from_timestamp(block_timestamp)), + ], + ))?; + + Ok(()) + } + + pub fn store_event( + &mut self, + event_id: &str, + event: &Event, + transaction_hash: Felt, + block_timestamp: u64, + ) -> Result<()> { + let id = Argument::String(event_id.to_string()); + let keys = Argument::String(felts_to_sql_string(&event.keys)); + let data = Argument::String(felts_to_sql_string(&event.data)); + let hash = Argument::FieldElement(transaction_hash); + let executed_at = Argument::String(utc_dt_string_from_timestamp(block_timestamp)); + + self.executor.send(QueryMessage::new( + "INSERT OR IGNORE INTO events (id, keys, data, transaction_hash, executed_at) VALUES \ + (?, ?, ?, ?, ?) RETURNING *" + .to_string(), + vec![id, keys, data, hash, executed_at], + QueryType::StoreEvent, + ))?; + + Ok(()) + } + + fn set_entity_model( + &mut self, + model_name: &str, + event_id: &str, + entity_id: (&str, IsEventMessage), + entity: (&Ty, IsStoreUpdate), + block_timestamp: u64, + ) -> Result<()> { + let (entity_id, is_event_message) = entity_id; + let (entity, is_store_update) = entity; + + let mut columns = vec![ + "internal_id".to_string(), + "internal_event_id".to_string(), + "internal_executed_at".to_string(), + "internal_updated_at".to_string(), + if is_event_message { + "internal_event_message_id".to_string() + } else { + "internal_entity_id".to_string() + }, + ]; + + let mut arguments = vec![ + Argument::String(if is_event_message { + "event:".to_string() + entity_id + } else { + entity_id.to_string() + }), + Argument::String(event_id.to_string()), + Argument::String(utc_dt_string_from_timestamp(block_timestamp)), + Argument::String(chrono::Utc::now().to_rfc3339()), + Argument::String(entity_id.to_string()), + ]; + + fn collect_members( + prefix: &str, + ty: &Ty, + columns: &mut Vec, + arguments: &mut Vec, + ) -> Result<()> { + match ty { + Ty::Struct(s) => { + for member in &s.children { + let column_name = if prefix.is_empty() { + member.name.clone() + } else { + format!("{}.{}", prefix, member.name) + }; + collect_members(&column_name, &member.ty, columns, arguments)?; + } + } + Ty::Enum(e) => { + columns.push(format!("\"{}\"", prefix)); + arguments.push(Argument::String(e.to_sql_value())); + + if let Some(option_idx) = e.option { + let option = &e.options[option_idx as usize]; + if let Ty::Tuple(t) = &option.ty { + if t.is_empty() { + return Ok(()); + } + } + let variant_path = format!("{}.{}", prefix, option.name); + collect_members(&variant_path, &option.ty, columns, arguments)?; + } + } + Ty::Tuple(t) => { + for (idx, member) in t.iter().enumerate() { + let column_name = if prefix.is_empty() { + format!("{}", idx) + } else { + format!("{}.{}", prefix, idx) + }; + collect_members(&column_name, member, columns, arguments)?; + } + } + Ty::Array(array) => { + columns.push(format!("\"{}\"", prefix)); + let values = + array.iter().map(|v| v.to_json_value()).collect::, _>>()?; + arguments.push(Argument::String(serde_json::to_string(&values)?)); + } + Ty::Primitive(ty) => { + columns.push(format!("\"{}\"", prefix)); + arguments.push(Argument::String(ty.to_sql_value())); + } + Ty::ByteArray(b) => { + columns.push(format!("\"{}\"", prefix)); + arguments.push(Argument::String(b.clone())); + } + } + Ok(()) + } + + // Collect all columns and arguments recursively + collect_members("", entity, &mut columns, &mut arguments)?; + + // Build the final query + let placeholders: Vec<&str> = arguments.iter().map(|_| "?").collect(); + let statement = if is_store_update { + arguments.push(Argument::String(if is_event_message { + "event:".to_string() + entity_id + } else { + entity_id.to_string() + })); + + format!( + "UPDATE [{}] SET {} WHERE internal_id = ?", + model_name, + columns + .iter() + .zip(placeholders.iter()) + .map(|(column, placeholder)| format!("{} = {}", column, placeholder)) + .collect::>() + .join(", ") + ) + } else { + format!( + "INSERT OR REPLACE INTO [{}] ({}) VALUES ({})", + model_name, + columns.join(","), + placeholders.join(",") + ) + }; + + // Execute the single query + self.executor.send(QueryMessage::other(statement, arguments))?; + + Ok(()) + } + + #[allow(clippy::too_many_arguments)] + fn build_model_query( + &mut self, + path: Vec, + model: &Ty, + upgrade_diff: Option<&Ty>, + ) -> Result<()> { + let table_id = path[0].clone(); // Use only the root path component + let mut columns = Vec::new(); + let mut indices = Vec::new(); + let mut alter_table_queries = Vec::new(); + + // Start building the create table query with internal columns + let mut create_table_query = format!( + "CREATE TABLE IF NOT EXISTS [{table_id}] (internal_id TEXT NOT NULL PRIMARY KEY, \ + internal_event_id TEXT NOT NULL, internal_entity_id TEXT, internal_event_message_id \ + TEXT, " + ); + + indices.push(format!( + "CREATE INDEX IF NOT EXISTS [idx_{table_id}_internal_entity_id] ON [{table_id}] \ + ([internal_entity_id]);" + )); + indices.push(format!( + "CREATE INDEX IF NOT EXISTS [idx_{table_id}_internal_event_message_id] ON \ + [{table_id}] ([internal_event_message_id]);" + )); + + // Recursively add columns for all nested type + add_columns_recursive( + &path, + model, + &mut columns, + &mut alter_table_queries, + &mut indices, + &table_id, + upgrade_diff, + )?; + + // Add all columns to the create table query + for column in columns { + create_table_query.push_str(&format!("{}, ", column)); + } + + // Add internal timestamps + create_table_query.push_str("internal_executed_at DATETIME NOT NULL, "); + create_table_query + .push_str("internal_created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, "); + create_table_query + .push_str("internal_updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, "); + + // Add foreign key constraints + create_table_query.push_str("FOREIGN KEY (internal_entity_id) REFERENCES entities(id), "); + create_table_query + .push_str("FOREIGN KEY (internal_event_message_id) REFERENCES event_messages(id));"); + + // Execute the queries + if upgrade_diff.is_some() { + for alter_query in alter_table_queries { + self.executor.send(QueryMessage::other(alter_query, vec![]))?; + } + } else { + self.executor.send(QueryMessage::other(create_table_query, vec![]))?; + } + + // Create indices + for index_query in indices { + self.executor.send(QueryMessage::other(index_query, vec![]))?; + } + + Ok(()) + } + + pub async fn execute(&self) -> Result<()> { + let (execute, recv) = QueryMessage::execute_recv(); + self.executor.send(execute)?; + recv.await? + } + + pub async fn flush(&self) -> Result<()> { + let (flush, recv) = QueryMessage::flush_recv(); + self.executor.send(flush)?; + recv.await? + } + + pub async fn rollback(&self) -> Result<()> { + let (rollback, recv) = QueryMessage::rollback_recv(); + self.executor.send(rollback)?; + recv.await? + } +} + +fn add_columns_recursive( + path: &[String], + ty: &Ty, + columns: &mut Vec, + alter_table_queries: &mut Vec, + indices: &mut Vec, + table_id: &str, + upgrade_diff: Option<&Ty>, +) -> Result<()> { + let column_prefix = if path.len() > 1 { path[1..].join(".") } else { String::new() }; + + let mut add_column = |name: &str, sql_type: &str| { + if upgrade_diff.is_some() { + alter_table_queries + .push(format!("ALTER TABLE [{table_id}] ADD COLUMN [{name}] {sql_type}")); + } else { + columns.push(format!("[{name}] {sql_type}")); + } + indices.push(format!( + "CREATE INDEX IF NOT EXISTS [idx_{table_id}_{name}] ON [{table_id}] ([{name}]);" + )); + }; + + match ty { + Ty::Struct(s) => { + for member in &s.children { + if let Some(upgrade_diff) = upgrade_diff { + if !upgrade_diff + .as_struct() + .unwrap() + .children + .iter() + .any(|m| m.name == member.name) + { + continue; + } + } + + let mut new_path = path.to_vec(); + new_path.push(member.name.clone()); + + add_columns_recursive( + &new_path, + &member.ty, + columns, + alter_table_queries, + indices, + table_id, + None, + )?; + } + } + Ty::Tuple(tuple) => { + for (idx, member) in tuple.iter().enumerate() { + let mut new_path = path.to_vec(); + new_path.push(idx.to_string()); + + add_columns_recursive( + &new_path, + member, + columns, + alter_table_queries, + indices, + table_id, + None, + )?; + } + } + Ty::Array(_) => { + let column_name = + if column_prefix.is_empty() { "value".to_string() } else { column_prefix }; + + add_column(&column_name, "TEXT"); + } + Ty::Enum(e) => { + // The variant of the enum + let column_name = + if column_prefix.is_empty() { "option".to_string() } else { column_prefix }; + + let all_options = + e.options.iter().map(|c| format!("'{}'", c.name)).collect::>().join(", "); + + let sql_type = format!("TEXT CHECK([{column_name}] IN ({all_options}))"); + add_column(&column_name, &sql_type); + + for child in &e.options { + if let Ty::Tuple(tuple) = &child.ty { + if tuple.is_empty() { + continue; + } + } + + let mut new_path = path.to_vec(); + new_path.push(child.name.clone()); + + add_columns_recursive( + &new_path, + &child.ty, + columns, + alter_table_queries, + indices, + table_id, + None, + )?; + } + } + Ty::ByteArray(_) => { + let column_name = + if column_prefix.is_empty() { "value".to_string() } else { column_prefix }; + + add_column(&column_name, "TEXT"); + } + Ty::Primitive(p) => { + let column_name = + if column_prefix.is_empty() { "value".to_string() } else { column_prefix }; + + add_column(&column_name, p.to_sql_type().as_ref()); + } + } + + Ok(()) +} diff --git a/crates/torii/sqlite/src/model.rs b/crates/torii/sqlite/src/model.rs new file mode 100644 index 0000000000..76c70a8599 --- /dev/null +++ b/crates/torii/sqlite/src/model.rs @@ -0,0 +1,690 @@ +use std::str::FromStr; + +use async_trait::async_trait; +use crypto_bigint::U256; +use dojo_types::primitive::{Primitive, PrimitiveError}; +use dojo_types::schema::Ty; +use dojo_world::contracts::abigen::model::Layout; +use dojo_world::contracts::model::ModelReader; +use serde_json::Value as JsonValue; +use sqlx::sqlite::SqliteRow; +use sqlx::{Pool, Row, Sqlite}; +use starknet::core::types::Felt; + +use super::error::{self, Error}; +use crate::error::ParseError; + +#[derive(Debug)] +pub struct ModelSQLReader { + /// Namespace of the model + namespace: String, + /// The name of the model + name: String, + /// The selector of the model + selector: Felt, + /// The class hash of the model + class_hash: Felt, + /// The contract address of the model + contract_address: Felt, + pool: Pool, + packed_size: u32, + unpacked_size: u32, + layout: Layout, +} + +impl ModelSQLReader { + pub async fn new(selector: Felt, pool: Pool) -> Result { + let (namespace, name, class_hash, contract_address, packed_size, unpacked_size, layout): ( + String, + String, + String, + String, + u32, + u32, + String, + ) = sqlx::query_as( + "SELECT namespace, name, class_hash, contract_address, packed_size, unpacked_size, \ + layout FROM models WHERE id = ?", + ) + .bind(format!("{:#x}", selector)) + .fetch_one(&pool) + .await?; + + let class_hash = Felt::from_hex(&class_hash).map_err(error::ParseError::FromStr)?; + let contract_address = + Felt::from_hex(&contract_address).map_err(error::ParseError::FromStr)?; + + let layout = serde_json::from_str(&layout).map_err(error::ParseError::FromJsonStr)?; + + Ok(Self { + namespace, + name, + selector, + class_hash, + contract_address, + pool, + packed_size, + unpacked_size, + layout, + }) + } +} + +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +impl ModelReader for ModelSQLReader { + fn namespace(&self) -> &str { + &self.namespace + } + + fn name(&self) -> &str { + &self.name + } + + fn selector(&self) -> Felt { + self.selector + } + + fn class_hash(&self) -> Felt { + self.class_hash + } + + fn contract_address(&self) -> Felt { + self.contract_address + } + + async fn schema(&self) -> Result { + let schema: String = sqlx::query_scalar("SELECT schema FROM models WHERE id = ?") + .bind(format!("{:#x}", self.selector)) + .fetch_one(&self.pool) + .await?; + + Ok(serde_json::from_str(&schema).map_err(error::ParseError::FromJsonStr)?) + } + + async fn packed_size(&self) -> Result { + Ok(self.packed_size) + } + + async fn unpacked_size(&self) -> Result { + Ok(self.unpacked_size) + } + + async fn layout(&self) -> Result { + Ok(self.layout.clone()) + } +} + +/// Creates a query that fetches all models and their nested data. +#[allow(clippy::too_many_arguments)] +pub fn build_sql_query( + schemas: &Vec, + table_name: &str, + model_relation_table: &str, + entity_relation_column: &str, + where_clause: Option<&str>, + having_clause: Option<&str>, + order_by: Option<&str>, + limit: Option, + offset: Option, +) -> Result<(String, String), Error> { + fn collect_columns(table_prefix: &str, path: &str, ty: &Ty, selections: &mut Vec) { + match ty { + Ty::Struct(s) => { + for child in &s.children { + let new_path = if path.is_empty() { + child.name.clone() + } else { + format!("{}.{}", path, child.name) + }; + collect_columns(table_prefix, &new_path, &child.ty, selections); + } + } + Ty::Tuple(t) => { + for (i, child) in t.iter().enumerate() { + let new_path = + if path.is_empty() { format!("{}", i) } else { format!("{}.{}", path, i) }; + collect_columns(table_prefix, &new_path, child, selections); + } + } + Ty::Enum(e) => { + // Add the enum variant column with table prefix and alias + selections.push(format!("[{table_prefix}].[{path}] as \"{table_prefix}.{path}\"",)); + + // Add columns for each variant's value (if not empty tuple) + for option in &e.options { + if let Ty::Tuple(t) = &option.ty { + if t.is_empty() { + continue; + } + } + let variant_path = format!("{}.{}", path, option.name); + collect_columns(table_prefix, &variant_path, &option.ty, selections); + } + } + Ty::Array(_) | Ty::Primitive(_) | Ty::ByteArray(_) => { + selections.push(format!("[{table_prefix}].[{path}] as \"{table_prefix}.{path}\"",)); + } + } + } + + let mut selections = Vec::new(); + let mut joins = Vec::new(); + + // Add base table columns + selections.push(format!("{}.id", table_name)); + selections.push(format!("{}.keys", table_name)); + selections.push(format!("group_concat({model_relation_table}.model_id) as model_ids")); + + // Process each model schema + for model in schemas { + let model_table = model.name(); + joins.push(format!( + "LEFT JOIN [{model_table}] ON {table_name}.id = \ + [{model_table}].{entity_relation_column}", + )); + + // Collect columns with table prefix + collect_columns(&model_table, "", model, &mut selections); + } + + joins.push(format!( + "JOIN {model_relation_table} ON {table_name}.id = {model_relation_table}.entity_id" + )); + + let selections_clause = selections.join(", "); + let joins_clause = joins.join(" "); + + let mut query = format!("SELECT {} FROM [{}] {}", selections_clause, table_name, joins_clause); + + // Include model_ids in the subquery and put WHERE before GROUP BY + let mut count_query = format!( + "SELECT COUNT(*) FROM (SELECT {}.id, group_concat({}.model_id) as model_ids FROM [{}] {}", + table_name, model_relation_table, table_name, joins_clause + ); + + if let Some(where_clause) = where_clause { + query += &format!(" WHERE {}", where_clause); + count_query += &format!(" WHERE {}", where_clause); + } + + query += &format!(" GROUP BY {table_name}.id"); + count_query += &format!(" GROUP BY {table_name}.id"); + + if let Some(having_clause) = having_clause { + query += &format!(" HAVING {}", having_clause); + count_query += &format!(" HAVING {}", having_clause); + } + + // Close the subquery + count_query += ") AS filtered_entities"; + + // Use custom order by if provided, otherwise default to event_id DESC + if let Some(order_clause) = order_by { + query += &format!(" ORDER BY {}", order_clause); + } else { + query += &format!(" ORDER BY {}.event_id DESC", table_name); + } + + if let Some(limit) = limit { + query += &format!(" LIMIT {}", limit); + } + + if let Some(offset) = offset { + query += &format!(" OFFSET {}", offset); + } + + Ok((query, count_query)) +} + +/// Populate the values of a Ty (schema) from SQLite row. +pub fn map_row_to_ty( + path: &str, + name: &str, + ty: &mut Ty, + // the row that contains non dynamic data for Ty + row: &SqliteRow, +) -> Result<(), Error> { + let column_name = if path.is_empty() { name } else { &format!("{}.{}", path, name) }; + + match ty { + Ty::Primitive(primitive) => { + match &primitive { + Primitive::I8(_) => { + let value = row.try_get::(column_name)?; + primitive.set_i8(Some(value))?; + } + Primitive::I16(_) => { + let value = row.try_get::(column_name)?; + primitive.set_i16(Some(value))?; + } + Primitive::I32(_) => { + let value = row.try_get::(column_name)?; + primitive.set_i32(Some(value))?; + } + Primitive::I64(_) => { + let value = row.try_get::(column_name)?; + primitive.set_i64(Some(value))?; + } + Primitive::I128(_) => { + let value = row.try_get::(column_name)?; + let hex_str = value.trim_start_matches("0x"); + + primitive.set_i128(Some( + u128::from_str_radix(hex_str, 16).map_err(ParseError::ParseIntError)? + as i128, + ))?; + } + Primitive::U8(_) => { + let value = row.try_get::(column_name)?; + primitive.set_u8(Some(value))?; + } + Primitive::U16(_) => { + let value = row.try_get::(column_name)?; + primitive.set_u16(Some(value))?; + } + Primitive::U32(_) => { + let value = row.try_get::(column_name)?; + primitive.set_u32(Some(value))?; + } + Primitive::U64(_) => { + let value = row.try_get::(column_name)?; + let hex_str = value.trim_start_matches("0x"); + + if !hex_str.is_empty() { + primitive.set_u64(Some( + u64::from_str_radix(hex_str, 16).map_err(ParseError::ParseIntError)?, + ))?; + } + } + Primitive::U128(_) => { + let value = row.try_get::(column_name)?; + let hex_str = value.trim_start_matches("0x"); + + if !hex_str.is_empty() { + primitive.set_u128(Some( + u128::from_str_radix(hex_str, 16).map_err(ParseError::ParseIntError)?, + ))?; + } + } + Primitive::U256(_) => { + let value = row.try_get::(column_name)?; + let hex_str = value.trim_start_matches("0x"); + + if !hex_str.is_empty() { + primitive.set_u256(Some(U256::from_be_hex(hex_str)))?; + } + } + Primitive::USize(_) => { + let value = row.try_get::(column_name)?; + primitive.set_usize(Some(value))?; + } + Primitive::Bool(_) => { + let value = row.try_get::(column_name)?; + primitive.set_bool(Some(value))?; + } + Primitive::Felt252(_) => { + let value = row.try_get::(column_name)?; + if !value.is_empty() { + primitive.set_felt252(Some( + Felt::from_str(&value).map_err(ParseError::FromStr)?, + ))?; + } + } + Primitive::ClassHash(_) => { + let value = row.try_get::(column_name)?; + if !value.is_empty() { + primitive.set_class_hash(Some( + Felt::from_str(&value).map_err(ParseError::FromStr)?, + ))?; + } + } + Primitive::ContractAddress(_) => { + let value = row.try_get::(column_name)?; + if !value.is_empty() { + primitive.set_contract_address(Some( + Felt::from_str(&value).map_err(ParseError::FromStr)?, + ))?; + } + } + }; + } + Ty::Enum(enum_ty) => { + let option_name = row.try_get::(column_name)?; + if !option_name.is_empty() { + enum_ty.set_option(&option_name)?; + } + + for option in &mut enum_ty.options { + if option.name != option_name { + continue; + } + + map_row_to_ty(column_name, &option.name, &mut option.ty, row)?; + } + } + Ty::Struct(struct_ty) => { + for member in &mut struct_ty.children { + map_row_to_ty(column_name, &member.name, &mut member.ty, row)?; + } + } + Ty::Tuple(ty) => { + for (i, member) in ty.iter_mut().enumerate() { + map_row_to_ty(column_name, &i.to_string(), member, row)?; + } + } + Ty::Array(ty) => { + let schema = ty[0].clone(); + let serialized_array = row.try_get::(column_name)?; + if serialized_array.is_empty() { + *ty = vec![]; + return Ok(()); + } + + let values: Vec = + serde_json::from_str(&serialized_array).map_err(ParseError::FromJsonStr)?; + *ty = values + .iter() + .map(|v| { + let mut ty = schema.clone(); + ty.from_json_value(v.clone())?; + Result::<_, PrimitiveError>::Ok(ty) + }) + .collect::, _>>()?; + } + Ty::ByteArray(bytearray) => { + let value = row.try_get::(column_name)?; + *bytearray = value; + } + }; + + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +pub async fn fetch_entities( + pool: &Pool, + schemas: &[Ty], + table_name: &str, + model_relation_table: &str, + entity_relation_column: &str, + where_clause: Option<&str>, + having_clause: Option<&str>, + order_by: Option<&str>, + limit: Option, + offset: Option, + bind_values: Vec, +) -> Result<(Vec, u32), Error> { + // Helper function to collect columns (existing implementation) + fn collect_columns(table_prefix: &str, path: &str, ty: &Ty, selections: &mut Vec) { + match ty { + Ty::Struct(s) => { + for child in &s.children { + let new_path = if path.is_empty() { + child.name.clone() + } else { + format!("{}.{}", path, child.name) + }; + collect_columns(table_prefix, &new_path, &child.ty, selections); + } + } + Ty::Tuple(t) => { + for (i, child) in t.iter().enumerate() { + let new_path = + if path.is_empty() { format!("{}", i) } else { format!("{}.{}", path, i) }; + collect_columns(table_prefix, &new_path, child, selections); + } + } + Ty::Enum(e) => { + // Add the enum variant column with table prefix and alias + selections.push(format!("[{table_prefix}].[{path}] as \"{table_prefix}.{path}\"",)); + + // Add columns for each variant's value (if not empty tuple) + for option in &e.options { + if let Ty::Tuple(t) = &option.ty { + if t.is_empty() { + continue; + } + } + let variant_path = format!("{}.{}", path, option.name); + collect_columns(table_prefix, &variant_path, &option.ty, selections); + } + } + Ty::Array(_) | Ty::Primitive(_) | Ty::ByteArray(_) => { + selections.push(format!("[{table_prefix}].[{path}] as \"{table_prefix}.{path}\"",)); + } + } + } + + const MAX_JOINS: usize = 64; + let schema_chunks = schemas.chunks(MAX_JOINS); + let mut total_count = 0; + let mut all_rows = Vec::new(); + + for chunk in schema_chunks { + let mut selections = Vec::new(); + let mut joins = Vec::new(); + + // Add base table columns + selections.push(format!("{}.id", table_name)); + selections.push(format!("{}.keys", table_name)); + selections.push(format!("group_concat({model_relation_table}.model_id) as model_ids")); + + // Process each model schema in the chunk + for model in chunk { + let model_table = model.name(); + joins.push(format!( + "LEFT JOIN [{model_table}] ON {table_name}.id = \ + [{model_table}].{entity_relation_column}" + )); + collect_columns(&model_table, "", model, &mut selections); + } + + joins.push(format!( + "JOIN {model_relation_table} ON {table_name}.id = {model_relation_table}.entity_id" + )); + + let selections_clause = selections.join(", "); + let joins_clause = joins.join(" "); + + // Build count query + let count_query = format!( + "SELECT COUNT(*) FROM (SELECT {}.id, group_concat({}.model_id) as model_ids FROM [{}] \ + {} {} GROUP BY {}.id {})", + table_name, + model_relation_table, + table_name, + joins_clause, + where_clause.map_or(String::new(), |w| format!(" WHERE {}", w)), + table_name, + having_clause.map_or(String::new(), |h| format!(" HAVING {}", h)) + ); + + // Execute count query + let mut count_stmt = sqlx::query_scalar(&count_query); + for value in &bind_values { + count_stmt = count_stmt.bind(value); + } + let chunk_count: u32 = count_stmt.fetch_one(pool).await?; + total_count += chunk_count; + + if chunk_count > 0 { + // Build main query + let mut query = + format!("SELECT {} FROM [{}] {}", selections_clause, table_name, joins_clause); + + if let Some(where_clause) = where_clause { + query += &format!(" WHERE {}", where_clause); + } + + query += &format!(" GROUP BY {table_name}.id"); + + if let Some(having_clause) = having_clause { + query += &format!(" HAVING {}", having_clause); + } + + if let Some(order_clause) = order_by { + query += &format!(" ORDER BY {}", order_clause); + } else { + query += &format!(" ORDER BY {}.event_id DESC", table_name); + } + + if let Some(limit) = limit { + query += &format!(" LIMIT {}", limit); + } + + if let Some(offset) = offset { + query += &format!(" OFFSET {}", offset); + } + + // Execute main query + let mut stmt = sqlx::query(&query); + for value in &bind_values { + stmt = stmt.bind(value); + } + let chunk_rows = stmt.fetch_all(pool).await?; + all_rows.extend(chunk_rows); + } + } + + Ok((all_rows, total_count)) +} + +#[cfg(test)] +mod tests { + use dojo_types::schema::{Enum, EnumOption, Member, Struct, Ty}; + + use super::build_sql_query; + + #[test] + fn struct_ty_to_query() { + let position = Ty::Struct(Struct { + name: "Test-Position".into(), + children: vec![ + dojo_types::schema::Member { + name: "player".into(), + key: true, + ty: Ty::Primitive("ContractAddress".parse().unwrap()), + }, + dojo_types::schema::Member { + name: "vec".into(), + key: false, + ty: Ty::Struct(Struct { + name: "Vec2".into(), + children: vec![ + Member { + name: "x".into(), + key: false, + ty: Ty::Primitive("u32".parse().unwrap()), + }, + Member { + name: "y".into(), + key: false, + ty: Ty::Primitive("u32".parse().unwrap()), + }, + ], + }), + }, + dojo_types::schema::Member { + name: "test_everything".into(), + key: false, + ty: Ty::Array(vec![Ty::Struct(Struct { + name: "TestEverything".into(), + children: vec![Member { + name: "data".into(), + key: false, + ty: Ty::Tuple(vec![ + Ty::Array(vec![Ty::Primitive("u32".parse().unwrap())]), + Ty::Array(vec![Ty::Array(vec![Ty::Tuple(vec![ + Ty::Primitive("u32".parse().unwrap()), + Ty::Struct(Struct { + name: "Vec2".into(), + children: vec![ + Member { + name: "x".into(), + key: false, + ty: Ty::Primitive("u32".parse().unwrap()), + }, + Member { + name: "y".into(), + key: false, + ty: Ty::Primitive("u32".parse().unwrap()), + }, + ], + }), + ])])]), + ]), + }], + })]), + }, + ], + }); + + let player_config = Ty::Struct(Struct { + name: "Test-PlayerConfig".into(), + children: vec![ + dojo_types::schema::Member { + name: "favorite_item".into(), + key: false, + ty: Ty::Enum(Enum { + name: "Option".into(), + option: None, + options: vec![ + EnumOption { name: "None".into(), ty: Ty::Tuple(vec![]) }, + EnumOption { + name: "Some".into(), + ty: Ty::Primitive("u32".parse().unwrap()), + }, + ], + }), + }, + dojo_types::schema::Member { + name: "items".into(), + key: false, + ty: Ty::Array(vec![Ty::Struct(Struct { + name: "PlayerItem".into(), + children: vec![ + Member { + name: "item_id".into(), + key: false, + ty: Ty::Primitive("u32".parse().unwrap()), + }, + Member { + name: "quantity".into(), + key: false, + ty: Ty::Primitive("u32".parse().unwrap()), + }, + ], + })]), + }, + ], + }); + + let query = build_sql_query( + &vec![position, player_config], + "entities", + "entity_model", + "internal_entity_id", + None, + None, + None, + None, + None, + ) + .unwrap(); + + let expected_query = + "SELECT entities.id, entities.keys, group_concat(entity_model.model_id) as model_ids, \ + [Test-Position].[player] as \"Test-Position.player\", [Test-Position].[vec.x] as \ + \"Test-Position.vec.x\", [Test-Position].[vec.y] as \"Test-Position.vec.y\", \ + [Test-Position].[test_everything] as \"Test-Position.test_everything\", \ + [Test-PlayerConfig].[favorite_item] as \"Test-PlayerConfig.favorite_item\", \ + [Test-PlayerConfig].[favorite_item.Some] as \ + \"Test-PlayerConfig.favorite_item.Some\", [Test-PlayerConfig].[items] as \ + \"Test-PlayerConfig.items\" FROM [entities] LEFT JOIN [Test-Position] ON entities.id \ + = [Test-Position].internal_entity_id LEFT JOIN [Test-PlayerConfig] ON entities.id = \ + [Test-PlayerConfig].internal_entity_id JOIN entity_model ON entities.id = \ + entity_model.entity_id GROUP BY entities.id ORDER BY entities.event_id DESC"; + assert_eq!(query.0, expected_query); + } +} diff --git a/crates/torii/core/src/simple_broker.rs b/crates/torii/sqlite/src/simple_broker.rs similarity index 100% rename from crates/torii/core/src/simple_broker.rs rename to crates/torii/sqlite/src/simple_broker.rs diff --git a/crates/torii/core/src/types.rs b/crates/torii/sqlite/src/types.rs similarity index 99% rename from crates/torii/core/src/types.rs rename to crates/torii/sqlite/src/types.rs index fef378f162..d56d33cb50 100644 --- a/crates/torii/core/src/types.rs +++ b/crates/torii/sqlite/src/types.rs @@ -107,6 +107,8 @@ pub struct Model { pub class_hash: String, pub contract_address: String, pub transaction_hash: String, + pub layout: String, + pub schema: String, pub executed_at: DateTime, pub created_at: DateTime, } diff --git a/crates/torii/core/src/sql/utils.rs b/crates/torii/sqlite/src/utils.rs similarity index 79% rename from crates/torii/core/src/sql/utils.rs rename to crates/torii/sqlite/src/utils.rs index d5a6c5cd75..1d16e1a27e 100644 --- a/crates/torii/core/src/sql/utils.rs +++ b/crates/torii/sqlite/src/utils.rs @@ -1,11 +1,31 @@ use std::cmp::Ordering; use std::ops::{Add, AddAssign, Sub, SubAssign}; use std::str::FromStr; +use std::time::Duration; +use anyhow::Result; +use chrono::{DateTime, Utc}; +use futures_util::TryStreamExt; +use ipfs_api_backend_hyper::{IpfsApi, IpfsClient, TryFromUri}; use starknet::core::types::U256; use starknet_crypto::Felt; +use tokio_util::bytes::Bytes; +use tracing::info; + +use crate::constants::{ + IPFS_CLIENT_MAX_RETRY, IPFS_CLIENT_PASSWORD, IPFS_CLIENT_URL, IPFS_CLIENT_USERNAME, + SQL_FELT_DELIMITER, +}; + +pub fn must_utc_datetime_from_timestamp(timestamp: u64) -> DateTime { + let naive_dt = DateTime::from_timestamp(timestamp as i64, 0) + .expect("Failed to convert timestamp to NaiveDateTime"); + naive_dt.to_utc() +} -use crate::constants::SQL_FELT_DELIMITER; +pub fn utc_dt_string_from_timestamp(timestamp: u64) -> String { + must_utc_datetime_from_timestamp(timestamp).to_rfc3339() +} pub fn felts_to_sql_string(felts: &[Felt]) -> String { felts.iter().map(|k| format!("{:#x}", k)).collect::>().join(SQL_FELT_DELIMITER) @@ -33,6 +53,32 @@ pub fn sql_string_to_felts(sql_string: &str) -> Vec { sql_string.split(SQL_FELT_DELIMITER).map(|felt| Felt::from_str(felt).unwrap()).collect() } +pub async fn fetch_content_from_ipfs(cid: &str, mut retries: u8) -> Result { + let client = IpfsClient::from_str(IPFS_CLIENT_URL)? + .with_credentials(IPFS_CLIENT_USERNAME, IPFS_CLIENT_PASSWORD); + while retries > 0 { + let response = client.cat(cid).map_ok(|chunk| chunk.to_vec()).try_concat().await; + match response { + Ok(stream) => return Ok(Bytes::from(stream)), + Err(e) => { + retries -= 1; + if retries > 0 { + info!( + error = %e, + "Fetch uri." + ); + tokio::time::sleep(Duration::from_secs(3)).await; + } + } + } + } + + Err(anyhow::anyhow!(format!( + "Failed to pull data from IPFS after {} attempts, cid: {}", + IPFS_CLIENT_MAX_RETRY, cid + ))) +} + // type used to do calculation on inmemory balances #[derive(Debug, Clone, Copy)] pub struct I256 { @@ -114,8 +160,37 @@ impl SubAssign for I256 { #[cfg(test)] mod tests { + use chrono::{DateTime, NaiveDate, NaiveTime, Utc}; + use super::*; + #[test] + fn test_must_utc_datetime_from_timestamp() { + let timestamp = 1633027200; + let expected_date = NaiveDate::from_ymd_opt(2021, 9, 30).unwrap(); + let expected_time = NaiveTime::from_hms_opt(18, 40, 0).unwrap(); + let expected = + DateTime::::from_naive_utc_and_offset(expected_date.and_time(expected_time), Utc); + let out = must_utc_datetime_from_timestamp(timestamp); + assert_eq!(out, expected, "Failed to convert timestamp to DateTime"); + } + + #[test] + #[should_panic(expected = "Failed to convert timestamp to NaiveDateTime")] + fn test_must_utc_datetime_from_timestamp_incorrect_timestamp() { + let timestamp = i64::MAX as u64 + 1; + let _result = must_utc_datetime_from_timestamp(timestamp); + } + + #[test] + fn test_utc_dt_string_from_timestamp() { + let timestamp = 1633027200; + let expected = "2021-09-30T18:40:00+00:00"; + let out = utc_dt_string_from_timestamp(timestamp); + println!("{}", out); + assert_eq!(out, expected, "Failed to convert timestamp to String"); + } + #[test] fn test_add_zero_false_and_zero_false() { // 0,false + 0,false == 0,false diff --git a/crates/torii/typed-data/Cargo.toml b/crates/torii/typed-data/Cargo.toml new file mode 100644 index 0000000000..5694354d1a --- /dev/null +++ b/crates/torii/typed-data/Cargo.toml @@ -0,0 +1,20 @@ +[package] +edition.workspace = true +license-file.workspace = true +name = "torii-typed-data" +repository.workspace = true +version.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +serde.workspace = true +# preserve order +cainome.workspace = true +crypto-bigint.workspace = true +dojo-types.workspace = true +indexmap.workspace = true +serde_json.workspace = true +starknet-crypto.workspace = true +starknet.workspace = true +thiserror.workspace = true diff --git a/crates/torii/libp2p/mocks/example_baseTypes.json b/crates/torii/typed-data/mocks/example_baseTypes.json similarity index 100% rename from crates/torii/libp2p/mocks/example_baseTypes.json rename to crates/torii/typed-data/mocks/example_baseTypes.json diff --git a/crates/torii/libp2p/mocks/example_enum.json b/crates/torii/typed-data/mocks/example_enum.json similarity index 100% rename from crates/torii/libp2p/mocks/example_enum.json rename to crates/torii/typed-data/mocks/example_enum.json diff --git a/crates/torii/libp2p/mocks/example_presetTypes.json b/crates/torii/typed-data/mocks/example_presetTypes.json similarity index 100% rename from crates/torii/libp2p/mocks/example_presetTypes.json rename to crates/torii/typed-data/mocks/example_presetTypes.json diff --git a/crates/torii/libp2p/mocks/mail_StructArray.json b/crates/torii/typed-data/mocks/mail_StructArray.json similarity index 100% rename from crates/torii/libp2p/mocks/mail_StructArray.json rename to crates/torii/typed-data/mocks/mail_StructArray.json diff --git a/crates/torii/libp2p/mocks/model_PlayerConfig.json b/crates/torii/typed-data/mocks/model_PlayerConfig.json similarity index 100% rename from crates/torii/libp2p/mocks/model_PlayerConfig.json rename to crates/torii/typed-data/mocks/model_PlayerConfig.json diff --git a/crates/torii/typed-data/src/error.rs b/crates/torii/typed-data/src/error.rs new file mode 100644 index 0000000000..28a09c2986 --- /dev/null +++ b/crates/torii/typed-data/src/error.rs @@ -0,0 +1,37 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error("Invalid type: {0}")] + InvalidType(String), + + #[error("Type not found: {0}")] + TypeNotFound(String), + + #[error("Invalid enum: {0}")] + InvalidEnum(String), + + #[error("Invalid field: {0}")] + InvalidField(String), + + #[error("Invalid value: {0}")] + InvalidValue(String), + + #[error("Invalid domain: {0}")] + InvalidDomain(String), + + #[error("Invalid message: {0}")] + InvalidMessage(String), + + #[error("Serialization error: {0}")] + SerializationError(#[from] serde_json::Error), + + #[error("Crypto error: {0}")] + CryptoError(String), + + #[error("Parse error: {0}")] + ParseError(String), + + #[error("Field not found: {0}")] + FieldNotFound(String), +} diff --git a/crates/torii/typed-data/src/lib.rs b/crates/torii/typed-data/src/lib.rs new file mode 100644 index 0000000000..bf289fda18 --- /dev/null +++ b/crates/torii/typed-data/src/lib.rs @@ -0,0 +1,9 @@ +#![warn(unused_crate_dependencies)] + +#[cfg(test)] +mod test; + +pub mod error; +pub mod typed_data; + +pub use typed_data::TypedData; diff --git a/crates/torii/typed-data/src/test.rs b/crates/torii/typed-data/src/test.rs new file mode 100644 index 0000000000..1dc71203c3 --- /dev/null +++ b/crates/torii/typed-data/src/test.rs @@ -0,0 +1,489 @@ +use crypto_bigint::U256; +use dojo_types::primitive::Primitive; +use dojo_types::schema::{Enum, EnumOption, Member, Struct, Ty}; +use serde_json::Number; +use starknet_crypto::Felt; + +use crate::typed_data::{map_ty_to_primitive, parse_value_to_ty, Domain, PrimitiveType, TypedData}; + +#[test] +fn test_parse_primitive_to_ty() { + // primitives + let mut ty = Ty::Primitive(Primitive::U8(None)); + let value = PrimitiveType::Number(Number::from(1u64)); + parse_value_to_ty(&value, &mut ty).unwrap(); + assert_eq!(ty, Ty::Primitive(Primitive::U8(Some(1)))); + + let mut ty = Ty::Primitive(Primitive::U16(None)); + let value = PrimitiveType::Number(Number::from(1u64)); + parse_value_to_ty(&value, &mut ty).unwrap(); + assert_eq!(ty, Ty::Primitive(Primitive::U16(Some(1)))); + + let mut ty = Ty::Primitive(Primitive::U32(None)); + let value = PrimitiveType::Number(Number::from(1u64)); + parse_value_to_ty(&value, &mut ty).unwrap(); + assert_eq!(ty, Ty::Primitive(Primitive::U32(Some(1)))); + + let mut ty = Ty::Primitive(Primitive::USize(None)); + let value = PrimitiveType::Number(Number::from(1u64)); + parse_value_to_ty(&value, &mut ty).unwrap(); + assert_eq!(ty, Ty::Primitive(Primitive::USize(Some(1)))); + + let mut ty = Ty::Primitive(Primitive::U64(None)); + let value = PrimitiveType::Number(Number::from(1u64)); + parse_value_to_ty(&value, &mut ty).unwrap(); + assert_eq!(ty, Ty::Primitive(Primitive::U64(Some(1)))); + + let mut ty = Ty::Primitive(Primitive::U128(None)); + let value = PrimitiveType::String("1".to_string()); + parse_value_to_ty(&value, &mut ty).unwrap(); + assert_eq!(ty, Ty::Primitive(Primitive::U128(Some(1)))); + + // test u256 with low high + let mut ty = Ty::Primitive(Primitive::U256(None)); + let value = PrimitiveType::Object( + vec![ + ("low".to_string(), PrimitiveType::String("1".to_string())), + ("high".to_string(), PrimitiveType::String("0".to_string())), + ] + .into_iter() + .collect(), + ); + parse_value_to_ty(&value, &mut ty).unwrap(); + assert_eq!(ty, Ty::Primitive(Primitive::U256(Some(U256::ONE)))); + + let mut ty = Ty::Primitive(Primitive::Felt252(None)); + let value = PrimitiveType::String("1".to_string()); + parse_value_to_ty(&value, &mut ty).unwrap(); + assert_eq!(ty, Ty::Primitive(Primitive::Felt252(Some(Felt::ONE)))); + + let mut ty = Ty::Primitive(Primitive::ClassHash(None)); + let value = PrimitiveType::String("1".to_string()); + parse_value_to_ty(&value, &mut ty).unwrap(); + assert_eq!(ty, Ty::Primitive(Primitive::ClassHash(Some(Felt::ONE)))); + + let mut ty = Ty::Primitive(Primitive::ContractAddress(None)); + let value = PrimitiveType::String("1".to_string()); + parse_value_to_ty(&value, &mut ty).unwrap(); + assert_eq!(ty, Ty::Primitive(Primitive::ContractAddress(Some(Felt::ONE)))); + + let mut ty = Ty::Primitive(Primitive::Bool(None)); + let value = PrimitiveType::Bool(true); + parse_value_to_ty(&value, &mut ty).unwrap(); + assert_eq!(ty, Ty::Primitive(Primitive::Bool(Some(true)))); + + // bytearray + let mut ty = Ty::ByteArray("".to_string()); + let value = PrimitiveType::String("mimi".to_string()); + parse_value_to_ty(&value, &mut ty).unwrap(); + assert_eq!(ty, Ty::ByteArray("mimi".to_string())); +} + +#[test] +fn test_map_ty_to_primitive() { + let ty = Ty::Primitive(Primitive::U8(Some(1))); + let value = PrimitiveType::Number(Number::from(1u64)); + assert_eq!(value, map_ty_to_primitive(&ty).unwrap()); + + let ty = Ty::Primitive(Primitive::U16(Some(1))); + let value = PrimitiveType::Number(Number::from(1u64)); + assert_eq!(value, map_ty_to_primitive(&ty).unwrap()); + + let ty = Ty::Primitive(Primitive::U32(Some(1))); + let value = PrimitiveType::Number(Number::from(1u64)); + assert_eq!(value, map_ty_to_primitive(&ty).unwrap()); + + let ty = Ty::Primitive(Primitive::USize(Some(1))); + let value = PrimitiveType::Number(Number::from(1u64)); + assert_eq!(value, map_ty_to_primitive(&ty).unwrap()); + + let ty = Ty::Primitive(Primitive::U64(Some(1))); + let value = PrimitiveType::String("1".to_string()); + assert_eq!(value, map_ty_to_primitive(&ty).unwrap()); + + let ty = Ty::Primitive(Primitive::U128(Some(1))); + let value = PrimitiveType::String("1".to_string()); + assert_eq!(value, map_ty_to_primitive(&ty).unwrap()); + + let ty = Ty::Primitive(Primitive::U256(Some(U256::ONE))); + let value = PrimitiveType::Object( + vec![ + ("low".to_string(), PrimitiveType::String("1".to_string())), + ("high".to_string(), PrimitiveType::String("0".to_string())), + ] + .into_iter() + .collect(), + ); + assert_eq!(value, map_ty_to_primitive(&ty).unwrap()); + + let ty = Ty::Primitive(Primitive::Felt252(Some(Felt::ONE))); + let value = PrimitiveType::String("1".to_string()); + assert_eq!(value, map_ty_to_primitive(&ty).unwrap()); + + let ty = Ty::Primitive(Primitive::ClassHash(Some(Felt::ONE))); + let value = PrimitiveType::String("1".to_string()); + assert_eq!(value, map_ty_to_primitive(&ty).unwrap()); + + let ty = Ty::Primitive(Primitive::ContractAddress(Some(Felt::ONE))); + let value = PrimitiveType::String("1".to_string()); + assert_eq!(value, map_ty_to_primitive(&ty).unwrap()); + + let ty = Ty::Primitive(Primitive::Bool(Some(true))); + let value = PrimitiveType::Bool(true); + assert_eq!(value, map_ty_to_primitive(&ty).unwrap()); + + let ty = Ty::ByteArray("mimi".to_string()); + let value = PrimitiveType::String("mimi".to_string()); + assert_eq!(value, map_ty_to_primitive(&ty).unwrap()); +} + +#[test] +fn test_parse_complex_to_ty() { + let mut ty = Ty::Struct(Struct { + name: "PlayerConfig".to_string(), + children: vec![ + Member { + name: "player".to_string(), + ty: Ty::Primitive(Primitive::ContractAddress(None)), + key: true, + }, + Member { name: "name".to_string(), ty: Ty::ByteArray("".to_string()), key: false }, + Member { + name: "items".to_string(), + // array of PlayerItem struct + ty: Ty::Array(vec![Ty::Struct(Struct { + name: "PlayerItem".to_string(), + children: vec![ + Member { + name: "item_id".to_string(), + ty: Ty::Primitive(Primitive::U32(None)), + key: false, + }, + Member { + name: "quantity".to_string(), + ty: Ty::Primitive(Primitive::U32(None)), + key: false, + }, + ], + })]), + key: false, + }, + // a favorite_item field with enum type Option + Member { + name: "favorite_item".to_string(), + ty: Ty::Enum(Enum { + name: "Option".to_string(), + option: None, + options: vec![ + EnumOption { name: "None".to_string(), ty: Ty::Tuple(vec![]) }, + EnumOption { + name: "Some".to_string(), + ty: Ty::Struct(Struct { + name: "PlayerItem".to_string(), + children: vec![ + Member { + name: "item_id".to_string(), + ty: Ty::Primitive(Primitive::U32(None)), + key: false, + }, + Member { + name: "quantity".to_string(), + ty: Ty::Primitive(Primitive::U32(None)), + key: false, + }, + ], + }), + }, + ], + }), + key: false, + }, + ], + }); + + let value = PrimitiveType::Object( + vec![ + ("player".to_string(), PrimitiveType::String("1".to_string())), + ("name".to_string(), PrimitiveType::String("mimi".to_string())), + ( + "items".to_string(), + PrimitiveType::Array(vec![PrimitiveType::Object( + vec![ + ("item_id".to_string(), PrimitiveType::String("1".to_string())), + ("quantity".to_string(), PrimitiveType::Number(Number::from(1u64))), + ] + .into_iter() + .collect(), + )]), + ), + ( + "favorite_item".to_string(), + PrimitiveType::Object( + vec![( + "Some".to_string(), + PrimitiveType::Object( + vec![ + ("item_id".to_string(), PrimitiveType::String("1".to_string())), + ("quantity".to_string(), PrimitiveType::Number(Number::from(1u64))), + ] + .into_iter() + .collect(), + ), + )] + .into_iter() + .collect(), + ), + ), + ] + .into_iter() + .collect(), + ); + + parse_value_to_ty(&value, &mut ty).unwrap(); + + assert_eq!( + ty, + Ty::Struct(Struct { + name: "PlayerConfig".to_string(), + children: vec![ + Member { + name: "player".to_string(), + ty: Ty::Primitive(Primitive::ContractAddress(Some(Felt::ONE))), + key: true, + }, + Member { + name: "name".to_string(), + ty: Ty::ByteArray("mimi".to_string()), + key: false, + }, + Member { + name: "items".to_string(), + ty: Ty::Array(vec![Ty::Struct(Struct { + name: "PlayerItem".to_string(), + children: vec![ + Member { + name: "item_id".to_string(), + ty: Ty::Primitive(Primitive::U32(Some(1))), + key: false, + }, + Member { + name: "quantity".to_string(), + ty: Ty::Primitive(Primitive::U32(Some(1))), + key: false, + }, + ], + })]), + key: false, + }, + Member { + name: "favorite_item".to_string(), + ty: Ty::Enum(Enum { + name: "Option".to_string(), + option: Some(1_u8), + options: vec![ + EnumOption { name: "None".to_string(), ty: Ty::Tuple(vec![]) }, + EnumOption { + name: "Some".to_string(), + ty: Ty::Struct(Struct { + name: "PlayerItem".to_string(), + children: vec![ + Member { + name: "item_id".to_string(), + ty: Ty::Primitive(Primitive::U32(Some(1))), + key: false, + }, + Member { + name: "quantity".to_string(), + ty: Ty::Primitive(Primitive::U32(Some(1))), + key: false, + }, + ], + }), + }, + ] + }), + key: false, + }, + ], + }) + ); +} + +#[test] +fn test_map_ty_to_complex() { + let ty = Ty::Struct(Struct { + name: "PlayerConfig".to_string(), + children: vec![ + Member { + name: "player".to_string(), + ty: Ty::Primitive(Primitive::ContractAddress(Some(Felt::ONE))), + key: true, + }, + Member { name: "name".to_string(), ty: Ty::ByteArray("mimi".to_string()), key: false }, + Member { + name: "items".to_string(), + ty: Ty::Array(vec![Ty::Struct(Struct { + name: "PlayerItem".to_string(), + children: vec![ + Member { + name: "item_id".to_string(), + ty: Ty::Primitive(Primitive::U32(Some(1))), + key: false, + }, + Member { + name: "quantity".to_string(), + ty: Ty::Primitive(Primitive::U32(Some(1))), + key: false, + }, + ], + })]), + key: false, + }, + Member { + name: "favorite_item".to_string(), + ty: Ty::Enum(Enum { + name: "Option".to_string(), + option: Some(1_u8), + options: vec![ + EnumOption { name: "None".to_string(), ty: Ty::Tuple(vec![]) }, + EnumOption { + name: "Some".to_string(), + ty: Ty::Struct(Struct { + name: "PlayerItem".to_string(), + children: vec![ + Member { + name: "item_id".to_string(), + ty: Ty::Primitive(Primitive::U32(Some(1))), + key: false, + }, + Member { + name: "quantity".to_string(), + ty: Ty::Primitive(Primitive::U32(Some(1))), + key: false, + }, + ], + }), + }, + ], + }), + key: false, + }, + ], + }); + + let value = PrimitiveType::Object( + vec![ + ("player".to_string(), PrimitiveType::String("1".to_string())), + ("name".to_string(), PrimitiveType::String("mimi".to_string())), + ( + "items".to_string(), + PrimitiveType::Array(vec![PrimitiveType::Object( + vec![ + ("item_id".to_string(), PrimitiveType::Number(Number::from(1u64))), + ("quantity".to_string(), PrimitiveType::Number(Number::from(1u64))), + ] + .into_iter() + .collect(), + )]), + ), + ( + "favorite_item".to_string(), + PrimitiveType::Object( + vec![( + "Some".to_string(), + PrimitiveType::Object( + vec![ + ("item_id".to_string(), PrimitiveType::Number(Number::from(1u64))), + ("quantity".to_string(), PrimitiveType::Number(Number::from(1u64))), + ] + .into_iter() + .collect(), + ), + )] + .into_iter() + .collect(), + ), + ), + ] + .into_iter() + .collect(), + ); + + assert_eq!(value, map_ty_to_primitive(&ty).unwrap()); +} + +#[test] +fn test_model_to_typed_data() { + let ty = Ty::Struct(Struct { + name: "PlayerConfig".to_string(), + children: vec![ + Member { + name: "player".to_string(), + ty: Ty::Primitive(Primitive::ContractAddress(Some(Felt::ONE))), + key: true, + }, + Member { name: "name".to_string(), ty: Ty::ByteArray("mimi".to_string()), key: false }, + Member { + name: "items".to_string(), + // array of PlayerItem struct + ty: Ty::Array(vec![Ty::Struct(Struct { + name: "PlayerItem".to_string(), + children: vec![ + Member { + name: "item_id".to_string(), + ty: Ty::Primitive(Primitive::U32(Some(1))), + key: false, + }, + Member { + name: "quantity".to_string(), + ty: Ty::Primitive(Primitive::U32(Some(1))), + key: false, + }, + ], + })]), + key: false, + }, + // a favorite_item field with enum type Option + Member { + name: "favorite_item".to_string(), + ty: Ty::Enum(Enum { + name: "Option".to_string(), + option: Some(1), + options: vec![ + EnumOption { name: "None".to_string(), ty: Ty::Tuple(vec![]) }, + EnumOption { + name: "Some".to_string(), + ty: Ty::Struct(Struct { + name: "PlayerItem".to_string(), + children: vec![ + Member { + name: "item_id".to_string(), + ty: Ty::Primitive(Primitive::U32(Some(69))), + key: false, + }, + Member { + name: "quantity".to_string(), + ty: Ty::Primitive(Primitive::U32(Some(42))), + key: false, + }, + ], + }), + }, + ], + }), + key: false, + }, + ], + }); + + let typed_data = + TypedData::from_model(ty, Domain::new("Test", "1", "Test", Some("1"))).unwrap(); + + let path = "mocks/model_PlayerConfig.json"; + let file = std::fs::File::open(path).unwrap(); + let reader = std::io::BufReader::new(file); + + let file_typed_data: TypedData = serde_json::from_reader(reader).unwrap(); + + assert_eq!(typed_data.encode(Felt::ZERO).unwrap(), file_typed_data.encode(Felt::ZERO).unwrap()); +} diff --git a/crates/torii/libp2p/src/typed_data.rs b/crates/torii/typed-data/src/typed_data.rs similarity index 89% rename from crates/torii/libp2p/src/typed_data.rs rename to crates/torii/typed-data/src/typed_data.rs index b15ee88188..ab8feb97ec 100644 --- a/crates/torii/libp2p/src/typed_data.rs +++ b/crates/torii/typed-data/src/typed_data.rs @@ -11,7 +11,7 @@ use starknet::core::types::Felt; use starknet::core::utils::{cairo_short_string_to_felt, get_selector_from_name}; use starknet_crypto::poseidon_hash_many; -use crate::errors::Error; +use crate::error::Error; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct SimpleField { @@ -93,11 +93,7 @@ fn get_preset_types() -> IndexMap> { // Looks up both the types hashmap as well as the preset types // Returns the fields and the hashmap of types fn get_fields(name: &str, types: &IndexMap>) -> Result, Error> { - if let Some(fields) = types.get(name) { - return Ok(fields.clone()); - } - - Err(Error::InvalidMessageError(format!("Type {} not found", name))) + types.get(name).cloned().ok_or_else(|| Error::TypeNotFound(name.to_string())) } fn get_dependencies( @@ -234,17 +230,14 @@ pub(crate) fn get_value_type( } } - Err(Error::InvalidMessageError(format!("Field {} not found in types", name))) + Err(Error::FieldNotFound(name.to_string())) } fn get_hex(value: &str) -> Result { - if let Ok(felt) = Felt::from_str(value) { - Ok(felt) - } else { - // assume its a short string and encode + Felt::from_str(value).or_else(|_| { cairo_short_string_to_felt(value) - .map_err(|e| Error::InvalidMessageError(format!("Invalid shortstring for felt: {}", e))) - } + .map_err(|e| Error::ParseError(format!("Invalid shortstring for felt: {}", e))) + }) } impl PrimitiveType { @@ -263,8 +256,9 @@ impl PrimitiveType { if ctx.base_type == "enum" { let (variant_name, value) = obj.first().ok_or_else(|| { - Error::InvalidMessageError("Enum value must be populated".to_string()) + Error::InvalidEnum("Enum value must be populated".to_string()) })?; + let variant_type = get_value_type(variant_name, types)?; // variant index @@ -282,9 +276,7 @@ impl PrimitiveType { .split(',') .nth(idx) .ok_or_else(|| { - Error::InvalidMessageError( - "Invalid enum variant type".to_string(), - ) + Error::InvalidEnum("Invalid enum variant type".to_string()) })?; let field_hash = @@ -306,10 +298,7 @@ impl PrimitiveType { let type_hash = encode_type(r#type, if ctx.is_preset { preset_types } else { types })?; hashes.push(get_selector_from_name(&type_hash).map_err(|e| { - Error::InvalidMessageError(format!( - "Invalid type {} for selector: {}", - r#type, e - )) + Error::ParseError(format!("Invalid type {} for selector: {}", r#type, e)) })?); for (field_name, value) in obj { @@ -339,9 +328,7 @@ impl PrimitiveType { .collect::>(); if inner_types.len() != array.len() { - return Err(Error::InvalidMessageError( - "Tuple length mismatch".to_string(), - )); + return Err(Error::InvalidValue("Tuple length mismatch".to_string())); } let mut hashes = Vec::new(); @@ -371,7 +358,7 @@ impl PrimitiveType { "string" => { // split the string into short strings and encode let byte_array = ByteArray::from_string(string).map_err(|e| { - Error::InvalidMessageError(format!("Invalid string for bytearray: {}", e)) + Error::ParseError(format!("Invalid string for bytearray: {}", e)) })?; let mut hashes = vec![Felt::from(byte_array.data.len())]; @@ -386,19 +373,18 @@ impl PrimitiveType { Ok(poseidon_hash_many(hashes.as_slice())) } "selector" => get_selector_from_name(string) - .map_err(|e| Error::InvalidMessageError(format!("Invalid selector: {}", e))), + .map_err(|e| Error::ParseError(format!("Invalid selector: {}", e))), "felt" => get_hex(string), "ContractAddress" => get_hex(string), "ClassHash" => get_hex(string), "timestamp" => get_hex(string), "u128" => get_hex(string), "i128" => get_hex(string), - _ => Err(Error::InvalidMessageError(format!("Invalid type {} for string", r#type))), + _ => Err(Error::InvalidType(format!("Invalid type {} for string", r#type))), }, PrimitiveType::Number(number) => { - let felt = Felt::from_str(&number.to_string()).map_err(|_| { - Error::InvalidMessageError(format!("Invalid number {}", number)) - })?; + let felt = Felt::from_str(&number.to_string()) + .map_err(|_| Error::ParseError(format!("Invalid number {}", number)))?; Ok(felt) } } @@ -425,16 +411,19 @@ impl Domain { } pub fn encode(&self, types: &IndexMap>) -> Result { - let mut object = IndexMap::new(); + if self.revision.as_deref().unwrap_or("1") != "1" { + return Err(Error::InvalidDomain("Legacy revision 0 is not supported".to_string())); + } + let mut object = IndexMap::new(); object.insert("name".to_string(), PrimitiveType::String(self.name.clone())); object.insert("version".to_string(), PrimitiveType::String(self.version.clone())); object.insert("chainId".to_string(), PrimitiveType::String(self.chain_id.clone())); + if let Some(revision) = &self.revision { object.insert("revision".to_string(), PrimitiveType::String(revision.clone())); } - // we dont need to pass our preset types here. domain should never use a preset type PrimitiveType::Object(object).encode( "StarknetDomain", types, @@ -444,6 +433,17 @@ impl Domain { } } +macro_rules! from_str { + ($string:expr, $type:ty) => { + if $string.starts_with("0x") || $string.starts_with("0X") { + <$type>::from_str_radix(&$string[2..], 16) + } else { + <$type>::from_str($string) + } + .map_err(|e| Error::ParseError(format!("Failed to parse number: {}", e))) + }; +} + pub fn parse_value_to_ty(value: &PrimitiveType, ty: &mut Ty) -> Result<(), Error> { match value { PrimitiveType::Object(object) => match ty { @@ -451,7 +451,7 @@ pub fn parse_value_to_ty(value: &PrimitiveType, ty: &mut Ty) -> Result<(), Error for (key, value) in object { let member = struct_.children.iter_mut().find(|member| member.name == *key).ok_or_else( - || Error::InvalidMessageError(format!("Member {} not found", key)), + || Error::FieldNotFound(format!("Member {} not found", key)), )?; parse_value_to_ty(value, &mut member.ty)?; @@ -480,9 +480,9 @@ pub fn parse_value_to_ty(value: &PrimitiveType, ty: &mut Ty) -> Result<(), Error // where the K is the variant name // and the value is the variant value Ty::Enum(enum_) => { - let (option_name, value) = object.first().ok_or_else(|| { - Error::InvalidMessageError("Enum variant not found".to_string()) - })?; + let (option_name, value) = object + .first() + .ok_or_else(|| Error::InvalidEnum("Enum variant not found".to_string()))?; enum_.options.iter_mut().for_each(|option| { if option.name == *option_name { @@ -490,15 +490,12 @@ pub fn parse_value_to_ty(value: &PrimitiveType, ty: &mut Ty) -> Result<(), Error } }); - enum_.set_option(option_name).map_err(|e| { - Error::InvalidMessageError(format!("Failed to set enum option: {}", e)) - })?; + enum_ + .set_option(option_name) + .map_err(|e| Error::InvalidEnum(format!("Failed to set enum option: {}", e)))?; } _ => { - return Err(Error::InvalidMessageError(format!( - "Invalid object type for {}", - ty.name() - ))); + return Err(Error::InvalidType(format!("Invalid object type for {}", ty.name()))); } }, PrimitiveType::Array(values) => match ty { @@ -518,7 +515,7 @@ pub fn parse_value_to_ty(value: &PrimitiveType, ty: &mut Ty) -> Result<(), Error Ty::Tuple(tuple) => { // our array values need to match the length of the tuple if tuple.len() != values.len() { - return Err(Error::InvalidMessageError("Tuple length mismatch".to_string())); + return Err(Error::InvalidValue("Tuple length mismatch".to_string())); } for (i, value) in tuple.iter_mut().enumerate() { @@ -526,10 +523,7 @@ pub fn parse_value_to_ty(value: &PrimitiveType, ty: &mut Ty) -> Result<(), Error } } _ => { - return Err(Error::InvalidMessageError(format!( - "Invalid array type for {}", - ty.name() - ))); + return Err(Error::InvalidType(format!("Invalid array type for {}", ty.name()))); } }, PrimitiveType::Number(number) => match ty { @@ -562,17 +556,14 @@ pub fn parse_value_to_ty(value: &PrimitiveType, ty: &mut Ty) -> Result<(), Error *usize = Some(number.as_u64().unwrap() as u32); } _ => { - return Err(Error::InvalidMessageError(format!( + return Err(Error::InvalidType(format!( "Invalid number type for {}", ty.name() ))); } }, _ => { - return Err(Error::InvalidMessageError(format!( - "Invalid number type for {}", - ty.name() - ))); + return Err(Error::InvalidType(format!("Invalid number type for {}", ty.name()))); } }, PrimitiveType::Bool(boolean) => { @@ -581,37 +572,37 @@ pub fn parse_value_to_ty(value: &PrimitiveType, ty: &mut Ty) -> Result<(), Error PrimitiveType::String(string) => match ty { Ty::Primitive(primitive) => match primitive { Primitive::I8(v) => { - *v = Some(i8::from_str(string).unwrap()); + *v = Some(from_str!(string, i8)?); } Primitive::I16(v) => { - *v = Some(i16::from_str(string).unwrap()); + *v = Some(from_str!(string, i16)?); } Primitive::I32(v) => { - *v = Some(i32::from_str(string).unwrap()); + *v = Some(from_str!(string, i32)?); } Primitive::I64(v) => { - *v = Some(i64::from_str(string).unwrap()); + *v = Some(from_str!(string, i64)?); } Primitive::I128(v) => { - *v = Some(i128::from_str(string).unwrap()); + *v = Some(from_str!(string, i128)?); } Primitive::U8(v) => { - *v = Some(u8::from_str(string).unwrap()); + *v = Some(from_str!(string, u8)?); } Primitive::U16(v) => { - *v = Some(u16::from_str(string).unwrap()); + *v = Some(from_str!(string, u16)?); } Primitive::U32(v) => { - *v = Some(u32::from_str(string).unwrap()); + *v = Some(from_str!(string, u32)?); } Primitive::U64(v) => { - *v = Some(u64::from_str(string).unwrap()); + *v = Some(from_str!(string, u64)?); } Primitive::U128(v) => { - *v = Some(u128::from_str(string).unwrap()); + *v = Some(from_str!(string, u128)?); } Primitive::USize(v) => { - *v = Some(u32::from_str(string).unwrap()); + *v = Some(from_str!(string, u32)?); } Primitive::Felt252(v) => { *v = Some(Felt::from_str(string).unwrap()); @@ -626,17 +617,14 @@ pub fn parse_value_to_ty(value: &PrimitiveType, ty: &mut Ty) -> Result<(), Error *v = Some(bool::from_str(string).unwrap()); } _ => { - return Err(Error::InvalidMessageError("Invalid primitive type".to_string())); + return Err(Error::InvalidType("Invalid primitive type".to_string())); } }, Ty::ByteArray(s) => { s.clone_from(string); } _ => { - return Err(Error::InvalidMessageError(format!( - "Invalid string type for {}", - ty.name() - ))); + return Err(Error::InvalidType(format!("Invalid string type for {}", ty.name()))); } }, } @@ -655,13 +643,12 @@ pub fn map_ty_to_primitive(ty: &Ty) -> Result { } Ty::Enum(enum_) => { let mut object = IndexMap::new(); - let option = enum_ - .option - .ok_or(Error::InvalidMessageError("Enum option not found".to_string()))?; + let option = + enum_.option.ok_or(Error::InvalidEnum("Enum option not found".to_string()))?; let option = enum_ .options .get(option as usize) - .ok_or(Error::InvalidMessageError("Enum option not found".to_string()))?; + .ok_or(Error::InvalidEnum("Enum option not found".to_string()))?; object.insert(option.name.clone(), map_ty_to_primitive(&option.ty)?); Ok(PrimitiveType::Object(object)) } @@ -883,18 +870,11 @@ impl TypedData { pub fn encode(&self, account: Felt) -> Result { let preset_types = get_preset_types(); - if self.domain.revision.clone().unwrap_or("1".to_string()) != "1" { - return Err(Error::InvalidMessageError( - "Legacy revision 0 is not supported".to_string(), - )); - } - - let prefix_message = cairo_short_string_to_felt("StarkNet Message").unwrap(); + let prefix_message = cairo_short_string_to_felt("StarkNet Message") + .map_err(|e| Error::CryptoError(e.to_string()))?; - // encode domain separator let domain_hash = self.domain.encode(&self.types)?; - // encode message let message_hash = PrimitiveType::Object(self.message.clone()).encode( &self.primary_type, &self.types, @@ -902,8 +882,7 @@ impl TypedData { &mut Default::default(), )?; - // return full hash - Ok(poseidon_hash_many(vec![prefix_message, domain_hash, account, message_hash].as_slice())) + Ok(poseidon_hash_many(&[prefix_message, domain_hash, account, message_hash])) } } diff --git a/crates/torii/types-test/Scarb.lock b/crates/torii/types-test/Scarb.lock index 0d453bfdcb..0198ef49a0 100644 --- a/crates/torii/types-test/Scarb.lock +++ b/crates/torii/types-test/Scarb.lock @@ -3,7 +3,7 @@ version = 1 [[package]] name = "dojo" -version = "1.0.1" +version = "1.0.11" dependencies = [ "dojo_plugin", ] @@ -14,7 +14,7 @@ version = "2.8.4" [[package]] name = "types_test" -version = "1.0.0" +version = "1.0.9" dependencies = [ "dojo", ] diff --git a/crates/torii/types-test/Scarb.toml b/crates/torii/types-test/Scarb.toml index bb0c6520e9..f4a8adccaa 100644 --- a/crates/torii/types-test/Scarb.toml +++ b/crates/torii/types-test/Scarb.toml @@ -2,7 +2,7 @@ cairo-version = "=2.8.4" edition = "2024_07" name = "types_test" -version = "1.0.1" +version = "1.0.11" [cairo] sierra-replace-ids = true diff --git a/crates/torii/types-test/manifest_dev.json b/crates/torii/types-test/manifest_dev.json new file mode 100644 index 0000000000..48092ca366 --- /dev/null +++ b/crates/torii/types-test/manifest_dev.json @@ -0,0 +1,1496 @@ +{ + "world": { + "class_hash": "0x45575a88cc5cef1e444c77ce60b7b4c9e73a01cbbe20926d5a4c72a94011410", + "address": "0x1413cc3c0d35059f2807a2d77b8f61084dcac934144f24b4a72cf1d0ef9c7f6", + "seed": "types_test", + "name": "types test", + "entrypoints": [ + "uuid", + "set_metadata", + "register_namespace", + "register_event", + "register_model", + "register_contract", + "init_contract", + "upgrade_event", + "upgrade_model", + "upgrade_contract", + "emit_event", + "emit_events", + "set_entity", + "set_entities", + "delete_entity", + "delete_entities", + "grant_owner", + "revoke_owner", + "grant_writer", + "revoke_writer", + "upgrade" + ], + "abi": [ + { + "type": "impl", + "name": "World", + "interface_name": "dojo::world::iworld::IWorld" + }, + { + "type": "struct", + "name": "core::byte_array::ByteArray", + "members": [ + { + "name": "data", + "type": "core::array::Array::" + }, + { + "name": "pending_word", + "type": "core::felt252" + }, + { + "name": "pending_word_len", + "type": "core::integer::u32" + } + ] + }, + { + "type": "enum", + "name": "dojo::world::resource::Resource", + "variants": [ + { + "name": "Model", + "type": "(core::starknet::contract_address::ContractAddress, core::felt252)" + }, + { + "name": "Event", + "type": "(core::starknet::contract_address::ContractAddress, core::felt252)" + }, + { + "name": "Contract", + "type": "(core::starknet::contract_address::ContractAddress, core::felt252)" + }, + { + "name": "Namespace", + "type": "core::byte_array::ByteArray" + }, + { + "name": "World", + "type": "()" + }, + { + "name": "Unregistered", + "type": "()" + } + ] + }, + { + "type": "struct", + "name": "dojo::model::metadata::ResourceMetadata", + "members": [ + { + "name": "resource_id", + "type": "core::felt252" + }, + { + "name": "metadata_uri", + "type": "core::byte_array::ByteArray" + }, + { + "name": "metadata_hash", + "type": "core::felt252" + } + ] + }, + { + "type": "struct", + "name": "core::array::Span::", + "members": [ + { + "name": "snapshot", + "type": "@core::array::Array::" + } + ] + }, + { + "type": "struct", + "name": "core::array::Span::>", + "members": [ + { + "name": "snapshot", + "type": "@core::array::Array::>" + } + ] + }, + { + "type": "enum", + "name": "dojo::model::definition::ModelIndex", + "variants": [ + { + "name": "Keys", + "type": "core::array::Span::" + }, + { + "name": "Id", + "type": "core::felt252" + }, + { + "name": "MemberId", + "type": "(core::felt252, core::felt252)" + } + ] + }, + { + "type": "struct", + "name": "core::array::Span::", + "members": [ + { + "name": "snapshot", + "type": "@core::array::Array::" + } + ] + }, + { + "type": "struct", + "name": "dojo::meta::layout::FieldLayout", + "members": [ + { + "name": "selector", + "type": "core::felt252" + }, + { + "name": "layout", + "type": "dojo::meta::layout::Layout" + } + ] + }, + { + "type": "struct", + "name": "core::array::Span::", + "members": [ + { + "name": "snapshot", + "type": "@core::array::Array::" + } + ] + }, + { + "type": "struct", + "name": "core::array::Span::", + "members": [ + { + "name": "snapshot", + "type": "@core::array::Array::" + } + ] + }, + { + "type": "enum", + "name": "dojo::meta::layout::Layout", + "variants": [ + { + "name": "Fixed", + "type": "core::array::Span::" + }, + { + "name": "Struct", + "type": "core::array::Span::" + }, + { + "name": "Tuple", + "type": "core::array::Span::" + }, + { + "name": "Array", + "type": "core::array::Span::" + }, + { + "name": "ByteArray", + "type": "()" + }, + { + "name": "Enum", + "type": "core::array::Span::" + } + ] + }, + { + "type": "struct", + "name": "core::array::Span::", + "members": [ + { + "name": "snapshot", + "type": "@core::array::Array::" + } + ] + }, + { + "type": "enum", + "name": "core::bool", + "variants": [ + { + "name": "False", + "type": "()" + }, + { + "name": "True", + "type": "()" + } + ] + }, + { + "type": "interface", + "name": "dojo::world::iworld::IWorld", + "items": [ + { + "type": "function", + "name": "resource", + "inputs": [ + { + "name": "selector", + "type": "core::felt252" + } + ], + "outputs": [ + { + "type": "dojo::world::resource::Resource" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "uuid", + "inputs": [], + "outputs": [ + { + "type": "core::integer::u32" + } + ], + "state_mutability": "external" + }, + { + "type": "function", + "name": "metadata", + "inputs": [ + { + "name": "resource_selector", + "type": "core::felt252" + } + ], + "outputs": [ + { + "type": "dojo::model::metadata::ResourceMetadata" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "set_metadata", + "inputs": [ + { + "name": "metadata", + "type": "dojo::model::metadata::ResourceMetadata" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "register_namespace", + "inputs": [ + { + "name": "namespace", + "type": "core::byte_array::ByteArray" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "register_event", + "inputs": [ + { + "name": "namespace", + "type": "core::byte_array::ByteArray" + }, + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "register_model", + "inputs": [ + { + "name": "namespace", + "type": "core::byte_array::ByteArray" + }, + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "register_contract", + "inputs": [ + { + "name": "salt", + "type": "core::felt252" + }, + { + "name": "namespace", + "type": "core::byte_array::ByteArray" + }, + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash" + } + ], + "outputs": [ + { + "type": "core::starknet::contract_address::ContractAddress" + } + ], + "state_mutability": "external" + }, + { + "type": "function", + "name": "init_contract", + "inputs": [ + { + "name": "selector", + "type": "core::felt252" + }, + { + "name": "init_calldata", + "type": "core::array::Span::" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "upgrade_event", + "inputs": [ + { + "name": "namespace", + "type": "core::byte_array::ByteArray" + }, + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "upgrade_model", + "inputs": [ + { + "name": "namespace", + "type": "core::byte_array::ByteArray" + }, + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "upgrade_contract", + "inputs": [ + { + "name": "namespace", + "type": "core::byte_array::ByteArray" + }, + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash" + } + ], + "outputs": [ + { + "type": "core::starknet::class_hash::ClassHash" + } + ], + "state_mutability": "external" + }, + { + "type": "function", + "name": "emit_event", + "inputs": [ + { + "name": "event_selector", + "type": "core::felt252" + }, + { + "name": "keys", + "type": "core::array::Span::" + }, + { + "name": "values", + "type": "core::array::Span::" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "emit_events", + "inputs": [ + { + "name": "event_selector", + "type": "core::felt252" + }, + { + "name": "keys", + "type": "core::array::Span::>" + }, + { + "name": "values", + "type": "core::array::Span::>" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "entity", + "inputs": [ + { + "name": "model_selector", + "type": "core::felt252" + }, + { + "name": "index", + "type": "dojo::model::definition::ModelIndex" + }, + { + "name": "layout", + "type": "dojo::meta::layout::Layout" + } + ], + "outputs": [ + { + "type": "core::array::Span::" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "entities", + "inputs": [ + { + "name": "model_selector", + "type": "core::felt252" + }, + { + "name": "indexes", + "type": "core::array::Span::" + }, + { + "name": "layout", + "type": "dojo::meta::layout::Layout" + } + ], + "outputs": [ + { + "type": "core::array::Span::>" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "set_entity", + "inputs": [ + { + "name": "model_selector", + "type": "core::felt252" + }, + { + "name": "index", + "type": "dojo::model::definition::ModelIndex" + }, + { + "name": "values", + "type": "core::array::Span::" + }, + { + "name": "layout", + "type": "dojo::meta::layout::Layout" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "set_entities", + "inputs": [ + { + "name": "model_selector", + "type": "core::felt252" + }, + { + "name": "indexes", + "type": "core::array::Span::" + }, + { + "name": "values", + "type": "core::array::Span::>" + }, + { + "name": "layout", + "type": "dojo::meta::layout::Layout" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "delete_entity", + "inputs": [ + { + "name": "model_selector", + "type": "core::felt252" + }, + { + "name": "index", + "type": "dojo::model::definition::ModelIndex" + }, + { + "name": "layout", + "type": "dojo::meta::layout::Layout" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "delete_entities", + "inputs": [ + { + "name": "model_selector", + "type": "core::felt252" + }, + { + "name": "indexes", + "type": "core::array::Span::" + }, + { + "name": "layout", + "type": "dojo::meta::layout::Layout" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "is_owner", + "inputs": [ + { + "name": "resource", + "type": "core::felt252" + }, + { + "name": "address", + "type": "core::starknet::contract_address::ContractAddress" + } + ], + "outputs": [ + { + "type": "core::bool" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "grant_owner", + "inputs": [ + { + "name": "resource", + "type": "core::felt252" + }, + { + "name": "address", + "type": "core::starknet::contract_address::ContractAddress" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "revoke_owner", + "inputs": [ + { + "name": "resource", + "type": "core::felt252" + }, + { + "name": "address", + "type": "core::starknet::contract_address::ContractAddress" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "is_writer", + "inputs": [ + { + "name": "resource", + "type": "core::felt252" + }, + { + "name": "contract", + "type": "core::starknet::contract_address::ContractAddress" + } + ], + "outputs": [ + { + "type": "core::bool" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "grant_writer", + "inputs": [ + { + "name": "resource", + "type": "core::felt252" + }, + { + "name": "contract", + "type": "core::starknet::contract_address::ContractAddress" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "revoke_writer", + "inputs": [ + { + "name": "resource", + "type": "core::felt252" + }, + { + "name": "contract", + "type": "core::starknet::contract_address::ContractAddress" + } + ], + "outputs": [], + "state_mutability": "external" + } + ] + }, + { + "type": "impl", + "name": "UpgradeableWorld", + "interface_name": "dojo::world::iworld::IUpgradeableWorld" + }, + { + "type": "interface", + "name": "dojo::world::iworld::IUpgradeableWorld", + "items": [ + { + "type": "function", + "name": "upgrade", + "inputs": [ + { + "name": "new_class_hash", + "type": "core::starknet::class_hash::ClassHash" + } + ], + "outputs": [], + "state_mutability": "external" + } + ] + }, + { + "type": "constructor", + "name": "constructor", + "inputs": [ + { + "name": "world_class_hash", + "type": "core::starknet::class_hash::ClassHash" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world_contract::world::WorldSpawned", + "kind": "struct", + "members": [ + { + "name": "creator", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "data" + }, + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world_contract::world::WorldUpgraded", + "kind": "struct", + "members": [ + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world_contract::world::NamespaceRegistered", + "kind": "struct", + "members": [ + { + "name": "namespace", + "type": "core::byte_array::ByteArray", + "kind": "key" + }, + { + "name": "hash", + "type": "core::felt252", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world_contract::world::ModelRegistered", + "kind": "struct", + "members": [ + { + "name": "name", + "type": "core::byte_array::ByteArray", + "kind": "key" + }, + { + "name": "namespace", + "type": "core::byte_array::ByteArray", + "kind": "key" + }, + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash", + "kind": "data" + }, + { + "name": "address", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world_contract::world::EventRegistered", + "kind": "struct", + "members": [ + { + "name": "name", + "type": "core::byte_array::ByteArray", + "kind": "key" + }, + { + "name": "namespace", + "type": "core::byte_array::ByteArray", + "kind": "key" + }, + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash", + "kind": "data" + }, + { + "name": "address", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world_contract::world::ContractRegistered", + "kind": "struct", + "members": [ + { + "name": "name", + "type": "core::byte_array::ByteArray", + "kind": "key" + }, + { + "name": "namespace", + "type": "core::byte_array::ByteArray", + "kind": "key" + }, + { + "name": "address", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "data" + }, + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash", + "kind": "data" + }, + { + "name": "salt", + "type": "core::felt252", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world_contract::world::ModelUpgraded", + "kind": "struct", + "members": [ + { + "name": "selector", + "type": "core::felt252", + "kind": "key" + }, + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash", + "kind": "data" + }, + { + "name": "address", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "data" + }, + { + "name": "prev_address", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world_contract::world::EventUpgraded", + "kind": "struct", + "members": [ + { + "name": "selector", + "type": "core::felt252", + "kind": "key" + }, + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash", + "kind": "data" + }, + { + "name": "address", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "data" + }, + { + "name": "prev_address", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world_contract::world::ContractUpgraded", + "kind": "struct", + "members": [ + { + "name": "selector", + "type": "core::felt252", + "kind": "key" + }, + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world_contract::world::ContractInitialized", + "kind": "struct", + "members": [ + { + "name": "selector", + "type": "core::felt252", + "kind": "key" + }, + { + "name": "init_calldata", + "type": "core::array::Span::", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world_contract::world::EventEmitted", + "kind": "struct", + "members": [ + { + "name": "selector", + "type": "core::felt252", + "kind": "key" + }, + { + "name": "system_address", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "key" + }, + { + "name": "keys", + "type": "core::array::Span::", + "kind": "data" + }, + { + "name": "values", + "type": "core::array::Span::", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world_contract::world::MetadataUpdate", + "kind": "struct", + "members": [ + { + "name": "resource", + "type": "core::felt252", + "kind": "key" + }, + { + "name": "uri", + "type": "core::byte_array::ByteArray", + "kind": "data" + }, + { + "name": "hash", + "type": "core::felt252", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world_contract::world::StoreSetRecord", + "kind": "struct", + "members": [ + { + "name": "selector", + "type": "core::felt252", + "kind": "key" + }, + { + "name": "entity_id", + "type": "core::felt252", + "kind": "key" + }, + { + "name": "keys", + "type": "core::array::Span::", + "kind": "data" + }, + { + "name": "values", + "type": "core::array::Span::", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world_contract::world::StoreUpdateRecord", + "kind": "struct", + "members": [ + { + "name": "selector", + "type": "core::felt252", + "kind": "key" + }, + { + "name": "entity_id", + "type": "core::felt252", + "kind": "key" + }, + { + "name": "values", + "type": "core::array::Span::", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world_contract::world::StoreUpdateMember", + "kind": "struct", + "members": [ + { + "name": "selector", + "type": "core::felt252", + "kind": "key" + }, + { + "name": "entity_id", + "type": "core::felt252", + "kind": "key" + }, + { + "name": "member_selector", + "type": "core::felt252", + "kind": "key" + }, + { + "name": "values", + "type": "core::array::Span::", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world_contract::world::StoreDelRecord", + "kind": "struct", + "members": [ + { + "name": "selector", + "type": "core::felt252", + "kind": "key" + }, + { + "name": "entity_id", + "type": "core::felt252", + "kind": "key" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world_contract::world::WriterUpdated", + "kind": "struct", + "members": [ + { + "name": "resource", + "type": "core::felt252", + "kind": "key" + }, + { + "name": "contract", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "key" + }, + { + "name": "value", + "type": "core::bool", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world_contract::world::OwnerUpdated", + "kind": "struct", + "members": [ + { + "name": "resource", + "type": "core::felt252", + "kind": "key" + }, + { + "name": "contract", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "key" + }, + { + "name": "value", + "type": "core::bool", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world_contract::world::Event", + "kind": "enum", + "variants": [ + { + "name": "WorldSpawned", + "type": "dojo::world::world_contract::world::WorldSpawned", + "kind": "nested" + }, + { + "name": "WorldUpgraded", + "type": "dojo::world::world_contract::world::WorldUpgraded", + "kind": "nested" + }, + { + "name": "NamespaceRegistered", + "type": "dojo::world::world_contract::world::NamespaceRegistered", + "kind": "nested" + }, + { + "name": "ModelRegistered", + "type": "dojo::world::world_contract::world::ModelRegistered", + "kind": "nested" + }, + { + "name": "EventRegistered", + "type": "dojo::world::world_contract::world::EventRegistered", + "kind": "nested" + }, + { + "name": "ContractRegistered", + "type": "dojo::world::world_contract::world::ContractRegistered", + "kind": "nested" + }, + { + "name": "ModelUpgraded", + "type": "dojo::world::world_contract::world::ModelUpgraded", + "kind": "nested" + }, + { + "name": "EventUpgraded", + "type": "dojo::world::world_contract::world::EventUpgraded", + "kind": "nested" + }, + { + "name": "ContractUpgraded", + "type": "dojo::world::world_contract::world::ContractUpgraded", + "kind": "nested" + }, + { + "name": "ContractInitialized", + "type": "dojo::world::world_contract::world::ContractInitialized", + "kind": "nested" + }, + { + "name": "EventEmitted", + "type": "dojo::world::world_contract::world::EventEmitted", + "kind": "nested" + }, + { + "name": "MetadataUpdate", + "type": "dojo::world::world_contract::world::MetadataUpdate", + "kind": "nested" + }, + { + "name": "StoreSetRecord", + "type": "dojo::world::world_contract::world::StoreSetRecord", + "kind": "nested" + }, + { + "name": "StoreUpdateRecord", + "type": "dojo::world::world_contract::world::StoreUpdateRecord", + "kind": "nested" + }, + { + "name": "StoreUpdateMember", + "type": "dojo::world::world_contract::world::StoreUpdateMember", + "kind": "nested" + }, + { + "name": "StoreDelRecord", + "type": "dojo::world::world_contract::world::StoreDelRecord", + "kind": "nested" + }, + { + "name": "WriterUpdated", + "type": "dojo::world::world_contract::world::WriterUpdated", + "kind": "nested" + }, + { + "name": "OwnerUpdated", + "type": "dojo::world::world_contract::world::OwnerUpdated", + "kind": "nested" + } + ] + } + ] + }, + "contracts": [ + { + "address": "0x5e3b474c077756ba9378903764ba932bed6c9ffe000165f9beba8769e9e8200", + "class_hash": "0x148bf0bb34a0ed998edfbca7f0799e76c1c5bbb87ca972d684b60136550a02", + "abi": [ + { + "type": "impl", + "name": "records__ContractImpl", + "interface_name": "dojo::contract::interface::IContract" + }, + { + "type": "interface", + "name": "dojo::contract::interface::IContract", + "items": [] + }, + { + "type": "impl", + "name": "records__DeployedContractImpl", + "interface_name": "dojo::meta::interface::IDeployedResource" + }, + { + "type": "struct", + "name": "core::byte_array::ByteArray", + "members": [ + { + "name": "data", + "type": "core::array::Array::" + }, + { + "name": "pending_word", + "type": "core::felt252" + }, + { + "name": "pending_word_len", + "type": "core::integer::u32" + } + ] + }, + { + "type": "interface", + "name": "dojo::meta::interface::IDeployedResource", + "items": [ + { + "type": "function", + "name": "dojo_name", + "inputs": [], + "outputs": [ + { + "type": "core::byte_array::ByteArray" + } + ], + "state_mutability": "view" + } + ] + }, + { + "type": "impl", + "name": "RecordsImpl", + "interface_name": "types_test::contracts::IRecords" + }, + { + "type": "interface", + "name": "types_test::contracts::IRecords", + "items": [ + { + "type": "function", + "name": "create", + "inputs": [ + { + "name": "num_records", + "type": "core::integer::u8" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "delete", + "inputs": [ + { + "name": "record_id", + "type": "core::integer::u32" + } + ], + "outputs": [], + "state_mutability": "external" + } + ] + }, + { + "type": "function", + "name": "dojo_init", + "inputs": [], + "outputs": [], + "state_mutability": "view" + }, + { + "type": "impl", + "name": "WorldProviderImpl", + "interface_name": "dojo::contract::components::world_provider::IWorldProvider" + }, + { + "type": "struct", + "name": "dojo::world::iworld::IWorldDispatcher", + "members": [ + { + "name": "contract_address", + "type": "core::starknet::contract_address::ContractAddress" + } + ] + }, + { + "type": "interface", + "name": "dojo::contract::components::world_provider::IWorldProvider", + "items": [ + { + "type": "function", + "name": "world_dispatcher", + "inputs": [], + "outputs": [ + { + "type": "dojo::world::iworld::IWorldDispatcher" + } + ], + "state_mutability": "view" + } + ] + }, + { + "type": "impl", + "name": "UpgradeableImpl", + "interface_name": "dojo::contract::components::upgradeable::IUpgradeable" + }, + { + "type": "interface", + "name": "dojo::contract::components::upgradeable::IUpgradeable", + "items": [ + { + "type": "function", + "name": "upgrade", + "inputs": [ + { + "name": "new_class_hash", + "type": "core::starknet::class_hash::ClassHash" + } + ], + "outputs": [], + "state_mutability": "external" + } + ] + }, + { + "type": "constructor", + "name": "constructor", + "inputs": [] + }, + { + "type": "event", + "name": "dojo::contract::components::upgradeable::upgradeable_cpt::Upgraded", + "kind": "struct", + "members": [ + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::contract::components::upgradeable::upgradeable_cpt::Event", + "kind": "enum", + "variants": [ + { + "name": "Upgraded", + "type": "dojo::contract::components::upgradeable::upgradeable_cpt::Upgraded", + "kind": "nested" + } + ] + }, + { + "type": "event", + "name": "dojo::contract::components::world_provider::world_provider_cpt::Event", + "kind": "enum", + "variants": [] + }, + { + "type": "event", + "name": "types_test::contracts::records::Event", + "kind": "enum", + "variants": [ + { + "name": "UpgradeableEvent", + "type": "dojo::contract::components::upgradeable::upgradeable_cpt::Event", + "kind": "nested" + }, + { + "name": "WorldProviderEvent", + "type": "dojo::contract::components::world_provider::world_provider_cpt::Event", + "kind": "nested" + } + ] + } + ], + "init_calldata": [], + "tag": "types_test-records", + "selector": "0x11bb34944de6c1b172f8f43624c026a63b2913b825f996c99c85ea3d1f9ac8a", + "systems": [ + "create", + "delete", + "upgrade" + ] + } + ], + "models": [ + { + "members": [], + "class_hash": "0x21569c1c7a59f12d1b1d4c14a4447e21c9783c67327caac8853563bb4cc76d", + "tag": "types_test-Record", + "selector": "0x21aeac4d33b64ae624204430d0c09837f34f654a7fac1cc83270a27a739ec71" + }, + { + "members": [], + "class_hash": "0x55a80dd8db0bc8ead4fc1f4c33c76367dbd63507e71672fe7af7ad8dbeac6af", + "tag": "types_test-RecordSibling", + "selector": "0x298128bd79a29d95f2888d2284ff39e037072dd71dae9313ffee995b12e9c86" + }, + { + "members": [], + "class_hash": "0x28299f3de912dbe3777bb8c3dd7e263a0d7785ac19fda69ca35f7e2392f7854", + "tag": "types_test-Subrecord", + "selector": "0x38871602c0e0b8a9c403655653d0080376ce1706b8b00c63a5f850cd6709689" + } + ], + "events": [ + { + "members": [], + "class_hash": "0x4d455c1ea98a2dcb6af77a01b3b992acfd80478bcb4948a663fdef69ae0b4d0", + "tag": "types_test-RecordLogged", + "selector": "0x26d4239a31cc102ac8fe5df0e0af32e84fa6d8667c699fbb5a41ce39ad713b4" + } + ] +} \ No newline at end of file diff --git a/eternum.toml b/eternum.toml new file mode 100644 index 0000000000..cd136378b6 --- /dev/null +++ b/eternum.toml @@ -0,0 +1,14 @@ +world_address = "0x6a9e4c6f0799160ea8ddc43ff982a5f83d7f633e9732ce42701de1288ff705f" +rpc = "https://api.cartridge.gg/x/starknet/mainnet" +explorer = false + +[indexing] +world_block = 947500 +events_chunk_size = 1024 +blocks_chunk_size = 1024000 +pending = true +polling_interval = 500 +max_concurrent_tasks = 100 +transactions = false +contracts = ["ERC721:0x57675b9c0bd62b096a2e15502a37b290fa766ead21c33eda42993e48a714b80"] +namespaces = [] diff --git a/examples/simple/Scarb.lock b/examples/simple/Scarb.lock index 4067faed93..98c7454077 100644 --- a/examples/simple/Scarb.lock +++ b/examples/simple/Scarb.lock @@ -3,7 +3,7 @@ version = 1 [[package]] name = "dojo" -version = "1.0.0" +version = "1.0.11" dependencies = [ "dojo_plugin", ] diff --git a/examples/simple/Scarb.toml b/examples/simple/Scarb.toml index 088aba4870..388df4c4b1 100644 --- a/examples/simple/Scarb.toml +++ b/examples/simple/Scarb.toml @@ -6,7 +6,7 @@ edition = "2024_07" [[target.starknet-contract]] sierra = true -# casm = true +casm = true build-external-contracts = ["dojo::world::world_contract::world"] [dependencies] diff --git a/examples/simple/manifest_dev.json b/examples/simple/manifest_dev.json index ec085d7fce..4cba9216c2 100644 --- a/examples/simple/manifest_dev.json +++ b/examples/simple/manifest_dev.json @@ -1,7 +1,7 @@ { "world": { - "class_hash": "0x79d9ce84b97bcc2a631996c3100d57966fc2f5b061fb1ec4dfd0040976bcac6", - "address": "0x1b2e50266b9b673eb82e68249a1babde860f6414737b4a36ff7b29411a64666", + "class_hash": "0x45575a88cc5cef1e444c77ce60b7b4c9e73a01cbbe20926d5a4c72a94011410", + "address": "0x64613f376f05242dfcc9fe360fa2ce1fdd6b00b1ce73dae2ea649ea118fd9be", "seed": "simple", "name": "simple", "entrypoints": [ @@ -92,6 +92,10 @@ { "name": "metadata_uri", "type": "core::byte_array::ByteArray" + }, + { + "name": "metadata_hash", + "type": "core::felt252" } ] }, @@ -1002,6 +1006,11 @@ "name": "uri", "type": "core::byte_array::ByteArray", "kind": "data" + }, + { + "name": "hash", + "type": "core::felt252", + "kind": "data" } ] }, @@ -1243,7 +1252,7 @@ }, "contracts": [ { - "address": "0x366a86098f39b0c3e026067c846367e83111c727b88fa036597120d154d44f5", + "address": "0x1958afba5b86ac51f48313047357981a59ac274170e353f79d84eedc89219e3", "class_hash": "0x340e197b0fac61961591acdd872a89b0cb862893272ab72455356f5534baa7e", "abi": [ { @@ -1501,7 +1510,7 @@ ] }, { - "address": "0x53fbb8640694994275d8a1b31ce290ec13e0dd433077775e185a9c31f054008", + "address": "0x5d9f3674dd96f173735f949d303abf107d1761b838a733703e95b31cc479270", "class_hash": "0x2a400df88b0add6c980d281dc96354a5cfc2b886547e9ed621b097e21842ee6", "abi": [ { @@ -1677,7 +1686,7 @@ ] }, { - "address": "0x40c69a07b5a9b64f581176511c8f8eac9008b48cb3e3c543eac2bf7466c57e3", + "address": "0x38b6e133a81a3ef41438752fbcf497ed74f284fe64a70e02652c3fb434d4e8", "class_hash": "0x7cc8d15e576873d544640f7fd124bd430bd19c0f31e203fb069b4fc2f5c0ab9", "abi": [ { @@ -1853,7 +1862,7 @@ ] }, { - "address": "0x1302b12ead2cdce8cb0a47f461ecd9d53629e62e8d38327f6452066298381b5", + "address": "0x47882727700801173a3ff731928c9d41303237e142104bd155f48fac3806814", "class_hash": "0x340e197b0fac61961591acdd872a89b0cb862893272ab72455356f5534baa7e", "abi": [ { diff --git a/examples/spawn-and-move/Scarb.lock b/examples/spawn-and-move/Scarb.lock index a3ce4d9320..910b6cc457 100644 --- a/examples/spawn-and-move/Scarb.lock +++ b/examples/spawn-and-move/Scarb.lock @@ -17,7 +17,7 @@ dependencies = [ [[package]] name = "dojo" -version = "1.0.1" +version = "1.0.11" dependencies = [ "dojo_plugin", ] @@ -31,7 +31,7 @@ dependencies = [ [[package]] name = "dojo_examples" -version = "1.0.0" +version = "1.0.11" dependencies = [ "armory", "bestiary", diff --git a/examples/spawn-and-move/Scarb.toml b/examples/spawn-and-move/Scarb.toml index 346bd6004f..9c673b788e 100644 --- a/examples/spawn-and-move/Scarb.toml +++ b/examples/spawn-and-move/Scarb.toml @@ -1,7 +1,7 @@ [package] cairo-version = "=2.8.4" name = "dojo_examples" -version = "1.0.1" +version = "1.0.11" # Use the prelude with the less imports as possible # from corelib. edition = "2024_07" diff --git a/examples/spawn-and-move/dojo_dev.toml b/examples/spawn-and-move/dojo_dev.toml index 23441d37ae..49e8d08de2 100644 --- a/examples/spawn-and-move/dojo_dev.toml +++ b/examples/spawn-and-move/dojo_dev.toml @@ -3,6 +3,26 @@ description = "example world" name = "example" seed = "dojo_examples" +[[models]] +tag = "ns-Message" +description = "Message sent by a player" + +[[models]] +tag = "ns-Position" +description = "position of a player in the world" + +[[models]] +tag = "ns-Moves" +description = "move of a player in the world" + +[[events]] +tag = "ns-Moved" +description = "when a player has moved" + +[[contracts]] +tag = "ns-actions" +description = "set of actions for a player" + [namespace] default = "ns" @@ -11,10 +31,13 @@ rpc_url = "http://localhost:5050/" # Default account for katana with seed = 0 account_address = "0x2af9427c5a277474c079a1283c880ee8a6f0f8fbf73ce969c08d88befec1bba" private_key = "0x1800000000300000180000000000030000000000003006001800006600" -world_address = "0x70058e3886cb7411e8a77db90ee3dd453ac16b763b30bd99b3c8440fe42056e" +world_address = "0x52ee4d3cba58d1a0462bbfb6813bf5aa1b35078c3b859cded2b727c1d9469ea" +ipfs_config.url = "https://ipfs.infura.io:5001" +ipfs_config.username = "2EBrzr7ZASQZKH32sl2xWauXPSA" +ipfs_config.password = "12290b883db9138a8ae3363b6739d220" [init_call_args] "ns-others" = ["0xff"] [writers] -"ns" = [ "ns-mock_token", "ns-actions", "ns-others" ] \ No newline at end of file +"ns" = [ "ns-mock_token", "ns-actions", "ns-others" ] diff --git a/examples/spawn-and-move/manifest_dev.json b/examples/spawn-and-move/manifest_dev.json index 0e88c840b8..fec6bf6d76 100644 --- a/examples/spawn-and-move/manifest_dev.json +++ b/examples/spawn-and-move/manifest_dev.json @@ -1,7 +1,7 @@ { "world": { - "class_hash": "0x2f92b70bd2b5a40ddef12c55257f245176870b25c7eb0bd7a60cf1f1f2fbf0e", - "address": "0x54d0f13bf3fb5f15a8674c5204aad35e3022af96bcc23bdbd16b7e297ffd399", + "class_hash": "0x45575a88cc5cef1e444c77ce60b7b4c9e73a01cbbe20926d5a4c72a94011410", + "address": "0x52ee4d3cba58d1a0462bbfb6813bf5aa1b35078c3b859cded2b727c1d9469ea", "seed": "dojo_examples", "name": "example", "entrypoints": [ @@ -92,6 +92,10 @@ { "name": "metadata_uri", "type": "core::byte_array::ByteArray" + }, + { + "name": "metadata_hash", + "type": "core::felt252" } ] }, @@ -1002,6 +1006,11 @@ "name": "uri", "type": "core::byte_array::ByteArray", "kind": "data" + }, + { + "name": "hash", + "type": "core::felt252", + "kind": "data" } ] }, @@ -1243,8 +1252,8 @@ }, "contracts": [ { - "address": "0x7d32611d1b96df37b041f0a285bf340227cebc144d46c2f34cedd81e2fd5239", - "class_hash": "0x3449ac13c575e4ed3a0e897bf16929f449f0fadea49b119dd25046f6b347301", + "address": "0x6bfba78b8f4f42da469860a95291c2fad959c91ea747e2062de763ff4e62c4a", + "class_hash": "0x3e65d56b082c47e9e0952503424bcf6956ddd6d5faff40dd9900ea787d89c6", "abi": [ { "type": "impl", @@ -1390,6 +1399,18 @@ "outputs": [], "state_mutability": "external" }, + { + "type": "function", + "name": "update_player_config_name", + "inputs": [ + { + "name": "name", + "type": "core::byte_array::ByteArray" + } + ], + "outputs": [], + "state_mutability": "external" + }, { "type": "function", "name": "get_player_position", @@ -1576,6 +1597,7 @@ "spawn", "move", "set_player_config", + "update_player_config_name", "reset_player_config", "set_player_server_profile", "set_models", @@ -1584,7 +1606,7 @@ ] }, { - "address": "0x74967fafd9ea26b0c14287fdafa5867cf6e2d16d9e6fda3dde4361a7cf75c9d", + "address": "0x50b1497d463d52cbeb5919a35a82360ea6702db2b9c62c2d69c167995f34c08", "class_hash": "0x16e8d864b5b5484bad069133a751f72961f9e5edade4f4f3b3b6076f6394c5b", "abi": [ { @@ -1778,7 +1800,7 @@ ] }, { - "address": "0x7077c3246c125a91b17e8c0a90f45af790ad6feabe65a3df225277d9eb02310", + "address": "0x4b41a2abaeff170f3a04acb0144790a5a812e25e7a735dfef959247cfeb527", "class_hash": "0x43e7cc34a35abdb9baf6804c71526811d8d3cccf58d66be5a095bd79b9be82e", "abi": [ { @@ -1954,7 +1976,7 @@ ] }, { - "address": "0x7e2c18814dd45847ae85d3c8eb40196cc2aa869614efd1ff67edf380c45cb8e", + "address": "0x72a9f501c260b2d13f8988ea172680c5c1fdc085c5b44bdcac8477362ed5290", "class_hash": "0xa1884cbaa79b05287019b513a94f993f0d98ed4d9602370bc2e5aaa04feebc", "abi": [ { @@ -2178,13 +2200,13 @@ "events": [ { "members": [], - "class_hash": "0x3dc79bcf7e88304d1331cdf9e232ff9f78ac19a719e998bd68016eb5ebb3e38", + "class_hash": "0x2999886b1a0ee530eb279f9808632b70750ac93418da8493fb554eebfb7b99a", "tag": "ns-Moved", "selector": "0x6d4c1ac3717ba997500153c52344a2acac5123bbfa0f78d3dcc04cb786826b0" }, { "members": [], - "class_hash": "0x7ddb49b2db32089d1bb14a6f66d91582fffdd09d18325187840de65ae443598", + "class_hash": "0x4771fc691467af7b9b606ae34d5fa9abe762adb856d65d2c508ce66b3941671", "tag": "ns-MyInit", "selector": "0xe1c030210beae7e2153a7b996d7d2ae6a428faf16a61f192f02178718b6f0" } diff --git a/examples/spawn-and-move/src/actions.cairo b/examples/spawn-and-move/src/actions.cairo index 334d0b7a5c..b959e5b06a 100644 --- a/examples/spawn-and-move/src/actions.cairo +++ b/examples/spawn-and-move/src/actions.cairo @@ -5,6 +5,7 @@ pub trait IActions { fn spawn(ref self: T); fn move(ref self: T, direction: Direction); fn set_player_config(ref self: T, name: ByteArray); + fn update_player_config_name(ref self: T, name: ByteArray); fn get_player_position(self: @T) -> Position; fn reset_player_config(ref self: T); fn set_player_server_profile(ref self: T, server_id: u32, name: ByteArray); @@ -22,7 +23,7 @@ pub mod actions { Position, Moves, MovesValue, Direction, Vec2, PlayerConfig, PlayerItem, ServerProfile, }; use dojo_examples::utils::next_position; - use dojo::model::{ModelStorage, ModelValueStorage}; + use dojo::model::{ModelStorage, ModelValueStorage, Model}; use dojo::event::EventStorage; // Features can be used on modules, structs, trait and `use`. Not inside @@ -64,7 +65,7 @@ pub mod actions { player: seed.try_into().unwrap(), name: "hello", items: array![], - favorite_item: Option::None + favorite_item: Option::None, }; let mut world = self.world_default(); @@ -127,10 +128,21 @@ pub mod actions { PlayerItem { item_id: 2, quantity: 50, score: -32 } ]; - let config = PlayerConfig { player, name, items, favorite_item: Option::Some(1), }; + let config = PlayerConfig { player, name, items, favorite_item: Option::Some(1) }; world.write_model(@config); } + fn update_player_config_name(ref self: ContractState, name: ByteArray) { + let mut world = self.world_default(); + let player = get_caller_address(); + + // Don't need to read the model here, we directly overwrite the member "name". + world + .write_member( + Model::::ptr_from_keys(player), selector!("name"), name + ); + } + fn reset_player_config(ref self: ContractState) { let player = get_caller_address(); let mut world = self.world_default(); @@ -204,6 +216,7 @@ pub mod actions { self.world(@"ns") } + /// A gas optimized version of `world_default`, where hash are computed at compile time. fn world_default_from_hash(self: @ContractState) -> dojo::world::WorldStorage { self.world_from_hash(poseidon_hash_string!("ns")) } diff --git a/scripts/cargo_bench.sh b/scripts/cargo_bench.sh index f4652a37d5..ac165fdc19 100644 --- a/scripts/cargo_bench.sh +++ b/scripts/cargo_bench.sh @@ -1,7 +1,7 @@ #!/bin/bash set -euxo pipefail -# Can be run for one intergration test with: `--test TEST_NAME` +# Can be run for one integration test with: `--test TEST_NAME` # prepare contract sozo --manifest-path crates/benches/contracts/Scarb.toml build diff --git a/scripts/rust_fmt.sh b/scripts/rust_fmt.sh index db5636de2e..9b3c2f7358 100755 --- a/scripts/rust_fmt.sh +++ b/scripts/rust_fmt.sh @@ -1,3 +1,11 @@ #!/bin/bash -cargo +nightly-2024-08-28 fmt --check --all -- "$@" + +option="--check" + +if [ "$1" == "--fix" ]; then + option="" + shift +fi + +cargo +nightly-2024-08-28 fmt $option --all -- "$@" diff --git a/scripts/spam_txs.sh b/scripts/spam_txs.sh index d42ac22884..12d976daa8 100644 --- a/scripts/spam_txs.sh +++ b/scripts/spam_txs.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # -# This scripts spams transactions to the spawn-and-move example by targetting +# This scripts spams transactions to the spawn-and-move example by targeting # the set_models function. # # Usage: diff --git a/spawn-and-move-db.tar.gz b/spawn-and-move-db.tar.gz deleted file mode 100644 index 2dfc689e77..0000000000 Binary files a/spawn-and-move-db.tar.gz and /dev/null differ diff --git a/types-test-db.tar.gz b/types-test-db.tar.gz deleted file mode 100644 index a290054180..0000000000 Binary files a/types-test-db.tar.gz and /dev/null differ