From 7b18a3b48bbf832dc8efcdaefcad6636a99e2e95 Mon Sep 17 00:00:00 2001 From: lambda-0x <0xlambda@protonmail.com> Date: Wed, 23 Aug 2023 19:42:44 +0530 Subject: [PATCH 01/77] test: for verifying changes in manifest file (#804) * Prepare 0.2.1 (#817) * add test for verifying changes in manifest file * rework tests * update manifest file after rebase * clean up * fix generated file path * make clippy overlords happy * remove unnecessary changes * use symbolic link instead of copying --------- Co-authored-by: Tarrence van As --- .gitignore | 2 +- Cargo.lock | 8 +- crates/dojo-lang/src/manifest.rs | 4 + crates/dojo-lang/src/manifest_test.rs | 35 + .../src/manifest_test_crate/.gitignore | 1 - .../src/manifest_test_crate/Scarb.toml | 4 - .../src/manifest_test_crate/src/lib.cairo | 1 - .../dojo-lang/src/manifest_test_data/manifest | 1068 +++++++++++++++++ .../manifest_test_data/manifest_test_crate | 1 + 9 files changed, 1113 insertions(+), 11 deletions(-) create mode 100644 crates/dojo-lang/src/manifest_test.rs delete mode 100644 crates/dojo-lang/src/manifest_test_crate/.gitignore delete mode 100644 crates/dojo-lang/src/manifest_test_crate/Scarb.toml delete mode 100644 crates/dojo-lang/src/manifest_test_crate/src/lib.cairo create mode 100644 crates/dojo-lang/src/manifest_test_data/manifest create mode 120000 crates/dojo-lang/src/manifest_test_data/manifest_test_crate diff --git a/.gitignore b/.gitignore index 384b889cc8..1e38a84066 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -/target +**/target workspace .idea/ dojo.iml diff --git a/Cargo.lock b/Cargo.lock index 5797bd58e8..f3c2490b3b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4162,7 +4162,7 @@ dependencies = [ [[package]] name = "matchers" -version = "0.1.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" dependencies = [ @@ -4801,7 +4801,7 @@ checksum = "12cc1b0bf1727a77a54b6654e7b5f1af8604923edc8b81885f8ec92f9e3f0a05" [[package]] name = "pin-utils" -version = "0.1.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" @@ -5264,7 +5264,7 @@ dependencies = [ [[package]] name = "rfc7239" -version = "0.1.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "087317b3cf7eb481f13bd9025d729324b7cd068d6f470e2d76d049e191f5ba47" dependencies = [ @@ -7219,7 +7219,7 @@ dependencies = [ [[package]] name = "valuable" -version = "0.1.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" diff --git a/crates/dojo-lang/src/manifest.rs b/crates/dojo-lang/src/manifest.rs index 69396c432a..65db032846 100644 --- a/crates/dojo-lang/src/manifest.rs +++ b/crates/dojo-lang/src/manifest.rs @@ -187,3 +187,7 @@ impl Manifest { Ok(()) } } + +#[cfg(test)] +#[path = "manifest_test.rs"] +mod test; diff --git a/crates/dojo-lang/src/manifest_test.rs b/crates/dojo-lang/src/manifest_test.rs new file mode 100644 index 0000000000..c29690a79b --- /dev/null +++ b/crates/dojo-lang/src/manifest_test.rs @@ -0,0 +1,35 @@ +use std::path::Path; +use std::{env, fs}; + +use cairo_lang_utils::ordered_hash_map::OrderedHashMap; +use dojo_test_utils::compiler::build_test_config; +use scarb::ops; + +cairo_lang_test_utils::test_file_test!( + manifest_file, + "src/manifest_test_data/", + { + manifest: "manifest", + }, + test_manifest_file +); + +pub fn test_manifest_file( + _inputs: &OrderedHashMap, +) -> OrderedHashMap { + let config = + build_test_config("./src/manifest_test_data/manifest_test_crate/Scarb.toml").unwrap(); + let ws = ops::read_workspace(config.manifest_path(), &config).unwrap(); + + let packages = ws.members().map(|p| p.id).collect(); + ops::compile(packages, &ws).unwrap_or_else(|op| panic!("Error compiling: {op:?}")); + + let target_dir = config.target_dir().path_existent().unwrap(); + + let generated_manifest_path = + Path::new(target_dir).join(config.profile().as_str()).join("manifest.json"); + + let generated_file = fs::read_to_string(generated_manifest_path).unwrap(); + + OrderedHashMap::from([("expected_manifest_file".into(), generated_file)]) +} diff --git a/crates/dojo-lang/src/manifest_test_crate/.gitignore b/crates/dojo-lang/src/manifest_test_crate/.gitignore deleted file mode 100644 index 1de565933b..0000000000 --- a/crates/dojo-lang/src/manifest_test_crate/.gitignore +++ /dev/null @@ -1 +0,0 @@ -target \ No newline at end of file diff --git a/crates/dojo-lang/src/manifest_test_crate/Scarb.toml b/crates/dojo-lang/src/manifest_test_crate/Scarb.toml deleted file mode 100644 index a8d020d57d..0000000000 --- a/crates/dojo-lang/src/manifest_test_crate/Scarb.toml +++ /dev/null @@ -1,4 +0,0 @@ -[package] -cairo_version = "1.1.0" -name = "manifest_test" -version = "0.2.1" diff --git a/crates/dojo-lang/src/manifest_test_crate/src/lib.cairo b/crates/dojo-lang/src/manifest_test_crate/src/lib.cairo deleted file mode 100644 index 8b13789179..0000000000 --- a/crates/dojo-lang/src/manifest_test_crate/src/lib.cairo +++ /dev/null @@ -1 +0,0 @@ - diff --git a/crates/dojo-lang/src/manifest_test_data/manifest b/crates/dojo-lang/src/manifest_test_data/manifest new file mode 100644 index 0000000000..5fe74ce331 --- /dev/null +++ b/crates/dojo-lang/src/manifest_test_data/manifest @@ -0,0 +1,1068 @@ +//! > Test generated manifest file + +//! > test_runner_name +test_manifest_file + +//! > expected_manifest_file +{ + "world": { + "name": "world", + "address": null, + "class_hash": "0x1527b232cbd77c7f021fdc129339d7623edfd9a9c79a1b9add29c9064961497", + "abi": [ + { + "type": "impl", + "name": "World", + "interface_name": "dojo::world::IWorld" + }, + { + "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": "core::bool", + "variants": [ + { + "name": "False", + "type": "()" + }, + { + "name": "True", + "type": "()" + } + ] + }, + { + "type": "interface", + "name": "dojo::world::IWorld", + "items": [ + { + "type": "function", + "name": "component", + "inputs": [ + { + "name": "name", + "type": "core::felt252" + } + ], + "outputs": [ + { + "type": "core::starknet::class_hash::ClassHash" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "register_component", + "inputs": [ + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "system", + "inputs": [ + { + "name": "name", + "type": "core::felt252" + } + ], + "outputs": [ + { + "type": "core::starknet::class_hash::ClassHash" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "register_system", + "inputs": [ + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "uuid", + "inputs": [], + "outputs": [ + { + "type": "core::integer::u32" + } + ], + "state_mutability": "external" + }, + { + "type": "function", + "name": "emit", + "inputs": [ + { + "name": "keys", + "type": "core::array::Array::" + }, + { + "name": "values", + "type": "core::array::Span::" + } + ], + "outputs": [], + "state_mutability": "view" + }, + { + "type": "function", + "name": "execute", + "inputs": [ + { + "name": "system", + "type": "core::felt252" + }, + { + "name": "calldata", + "type": "core::array::Array::" + } + ], + "outputs": [ + { + "type": "core::array::Span::" + } + ], + "state_mutability": "external" + }, + { + "type": "function", + "name": "entity", + "inputs": [ + { + "name": "component", + "type": "core::felt252" + }, + { + "name": "keys", + "type": "core::array::Span::" + }, + { + "name": "offset", + "type": "core::integer::u8" + }, + { + "name": "length", + "type": "core::integer::u32" + } + ], + "outputs": [ + { + "type": "core::array::Span::" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "set_entity", + "inputs": [ + { + "name": "component", + "type": "core::felt252" + }, + { + "name": "keys", + "type": "core::array::Span::" + }, + { + "name": "offset", + "type": "core::integer::u8" + }, + { + "name": "value", + "type": "core::array::Span::" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "entities", + "inputs": [ + { + "name": "component", + "type": "core::felt252" + }, + { + "name": "index", + "type": "core::felt252" + }, + { + "name": "length", + "type": "core::integer::u32" + } + ], + "outputs": [ + { + "type": "(core::array::Span::, core::array::Span::>)" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "set_executor", + "inputs": [ + { + "name": "contract_address", + "type": "core::starknet::contract_address::ContractAddress" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "executor", + "inputs": [], + "outputs": [ + { + "type": "core::starknet::contract_address::ContractAddress" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "delete_entity", + "inputs": [ + { + "name": "component", + "type": "core::felt252" + }, + { + "name": "keys", + "type": "core::array::Span::" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "origin", + "inputs": [], + "outputs": [ + { + "type": "core::starknet::contract_address::ContractAddress" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "caller_system", + "inputs": [], + "outputs": [ + { + "type": "core::felt252" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "is_owner", + "inputs": [ + { + "name": "address", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "target", + "type": "core::felt252" + } + ], + "outputs": [ + { + "type": "core::bool" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "grant_owner", + "inputs": [ + { + "name": "address", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "target", + "type": "core::felt252" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "revoke_owner", + "inputs": [ + { + "name": "address", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "target", + "type": "core::felt252" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "is_writer", + "inputs": [ + { + "name": "component", + "type": "core::felt252" + }, + { + "name": "system", + "type": "core::felt252" + } + ], + "outputs": [ + { + "type": "core::bool" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "grant_writer", + "inputs": [ + { + "name": "component", + "type": "core::felt252" + }, + { + "name": "system", + "type": "core::felt252" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "revoke_writer", + "inputs": [ + { + "name": "component", + "type": "core::felt252" + }, + { + "name": "system", + "type": "core::felt252" + } + ], + "outputs": [], + "state_mutability": "external" + } + ] + }, + { + "type": "constructor", + "name": "constructor", + "inputs": [ + { + "name": "executor", + "type": "core::starknet::contract_address::ContractAddress" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world::WorldSpawned", + "kind": "struct", + "members": [ + { + "name": "address", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "data" + }, + { + "name": "caller", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world::ComponentRegistered", + "kind": "struct", + "members": [ + { + "name": "name", + "type": "core::felt252", + "kind": "data" + }, + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world::SystemRegistered", + "kind": "struct", + "members": [ + { + "name": "name", + "type": "core::felt252", + "kind": "data" + }, + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world::StoreSetRecord", + "kind": "struct", + "members": [ + { + "name": "table", + "type": "core::felt252", + "kind": "data" + }, + { + "name": "keys", + "type": "core::array::Span::", + "kind": "data" + }, + { + "name": "offset", + "type": "core::integer::u8", + "kind": "data" + }, + { + "name": "value", + "type": "core::array::Span::", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world::StoreDelRecord", + "kind": "struct", + "members": [ + { + "name": "table", + "type": "core::felt252", + "kind": "data" + }, + { + "name": "keys", + "type": "core::array::Span::", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "dojo::world::world::Event", + "kind": "enum", + "variants": [ + { + "name": "WorldSpawned", + "type": "dojo::world::world::WorldSpawned", + "kind": "nested" + }, + { + "name": "ComponentRegistered", + "type": "dojo::world::world::ComponentRegistered", + "kind": "nested" + }, + { + "name": "SystemRegistered", + "type": "dojo::world::world::SystemRegistered", + "kind": "nested" + }, + { + "name": "StoreSetRecord", + "type": "dojo::world::world::StoreSetRecord", + "kind": "nested" + }, + { + "name": "StoreDelRecord", + "type": "dojo::world::world::StoreDelRecord", + "kind": "nested" + } + ] + } + ] + }, + "executor": { + "name": "executor", + "address": null, + "class_hash": "0x7b79892389a0c9fe22f74b1d28a9e9185c7b6d2c60cc1df814053e47e9078c2", + "abi": [ + { + "type": "impl", + "name": "Executor", + "interface_name": "dojo::executor::IExecutor" + }, + { + "type": "struct", + "name": "core::array::Span::", + "members": [ + { + "name": "snapshot", + "type": "@core::array::Array::" + } + ] + }, + { + "type": "interface", + "name": "dojo::executor::IExecutor", + "items": [ + { + "type": "function", + "name": "execute", + "inputs": [ + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash" + }, + { + "name": "calldata", + "type": "core::array::Span::" + } + ], + "outputs": [ + { + "type": "core::array::Span::" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "call", + "inputs": [ + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash" + }, + { + "name": "entrypoint", + "type": "core::felt252" + }, + { + "name": "calldata", + "type": "core::array::Span::" + } + ], + "outputs": [ + { + "type": "core::array::Span::" + } + ], + "state_mutability": "view" + } + ] + }, + { + "type": "event", + "name": "dojo::executor::executor::Event", + "kind": "enum", + "variants": [] + } + ] + }, + "systems": [ + { + "name": "spawn", + "inputs": [ + { + "name": "self", + "type": "@dojo_examples::systems::spawn::ContractState" + } + ], + "outputs": [], + "class_hash": "0x73a2c968589ad1bf663ae71cdd7e55125265dbbfece0992ab993e0711fbfb1f", + "dependencies": [], + "abi": [ + { + "type": "function", + "name": "name", + "inputs": [], + "outputs": [ + { + "type": "core::felt252" + } + ], + "state_mutability": "view" + }, + { + "type": "struct", + "name": "dojo::world::IWorldDispatcher", + "members": [ + { + "name": "contract_address", + "type": "core::starknet::contract_address::ContractAddress" + } + ] + }, + { + "type": "struct", + "name": "dojo::world::Context", + "members": [ + { + "name": "world", + "type": "dojo::world::IWorldDispatcher" + }, + { + "name": "origin", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "system", + "type": "core::felt252" + }, + { + "name": "system_class_hash", + "type": "core::starknet::class_hash::ClassHash" + } + ] + }, + { + "type": "function", + "name": "execute", + "inputs": [ + { + "name": "ctx", + "type": "dojo::world::Context" + } + ], + "outputs": [], + "state_mutability": "view" + }, + { + "type": "event", + "name": "dojo_examples::systems::spawn::Event", + "kind": "enum", + "variants": [] + } + ] + }, + { + "name": "move", + "inputs": [ + { + "name": "self", + "type": "@dojo_examples::systems::move::ContractState" + }, + { + "name": "direction", + "type": "dojo_examples::systems::move::Direction" + } + ], + "outputs": [], + "class_hash": "0x5df94ea0e971f9c98c222350f0acdb8fea75759678a7d922ca454d14408430f", + "dependencies": [], + "abi": [ + { + "type": "function", + "name": "name", + "inputs": [], + "outputs": [ + { + "type": "core::felt252" + } + ], + "state_mutability": "view" + }, + { + "type": "enum", + "name": "dojo_examples::systems::move::Direction", + "variants": [ + { + "name": "Left", + "type": "()" + }, + { + "name": "Right", + "type": "()" + }, + { + "name": "Up", + "type": "()" + }, + { + "name": "Down", + "type": "()" + } + ] + }, + { + "type": "struct", + "name": "dojo::world::IWorldDispatcher", + "members": [ + { + "name": "contract_address", + "type": "core::starknet::contract_address::ContractAddress" + } + ] + }, + { + "type": "struct", + "name": "dojo::world::Context", + "members": [ + { + "name": "world", + "type": "dojo::world::IWorldDispatcher" + }, + { + "name": "origin", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "system", + "type": "core::felt252" + }, + { + "name": "system_class_hash", + "type": "core::starknet::class_hash::ClassHash" + } + ] + }, + { + "type": "function", + "name": "execute", + "inputs": [ + { + "name": "direction", + "type": "dojo_examples::systems::move::Direction" + }, + { + "name": "ctx", + "type": "dojo::world::Context" + } + ], + "outputs": [], + "state_mutability": "view" + }, + { + "type": "event", + "name": "dojo_examples::systems::move::Event", + "kind": "enum", + "variants": [] + } + ] + }, + { + "name": "library_call", + "inputs": [ + { + "name": "self", + "type": "@dojo::world::library_call::ContractState" + }, + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash" + }, + { + "name": "entrypoint", + "type": "core::felt252" + }, + { + "name": "calladata", + "type": "core::array::Span::" + } + ], + "outputs": [ + { + "type": "core::array::Span::" + } + ], + "class_hash": "0x5c3f8568adfef908692f02fcfcc80e303c237183fe864f6cff8c34d29d3f130", + "dependencies": [], + "abi": [ + { + "type": "function", + "name": "name", + "inputs": [], + "outputs": [ + { + "type": "core::felt252" + } + ], + "state_mutability": "view" + }, + { + "type": "struct", + "name": "core::array::Span::", + "members": [ + { + "name": "snapshot", + "type": "@core::array::Array::" + } + ] + }, + { + "type": "struct", + "name": "dojo::world::IWorldDispatcher", + "members": [ + { + "name": "contract_address", + "type": "core::starknet::contract_address::ContractAddress" + } + ] + }, + { + "type": "struct", + "name": "dojo::world::Context", + "members": [ + { + "name": "world", + "type": "dojo::world::IWorldDispatcher" + }, + { + "name": "origin", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "system", + "type": "core::felt252" + }, + { + "name": "system_class_hash", + "type": "core::starknet::class_hash::ClassHash" + } + ] + }, + { + "type": "function", + "name": "execute", + "inputs": [ + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash" + }, + { + "name": "entrypoint", + "type": "core::felt252" + }, + { + "name": "calladata", + "type": "core::array::Span::" + }, + { + "name": "_ctx", + "type": "dojo::world::Context" + } + ], + "outputs": [ + { + "type": "core::array::Span::" + } + ], + "state_mutability": "view" + }, + { + "type": "event", + "name": "dojo::world::library_call::Event", + "kind": "enum", + "variants": [] + } + ] + } + ], + "contracts": [], + "components": [ + { + "name": "Moves", + "members": [ + { + "name": "player", + "type": "ContractAddress", + "key": true + }, + { + "name": "remaining", + "type": "u8", + "key": false + } + ], + "class_hash": "0x5baa2a6749eebb5c2206c497a31c760455709e37f42bec5a7418bb98126d284", + "abi": [ + { + "type": "function", + "name": "name", + "inputs": [], + "outputs": [ + { + "type": "core::felt252" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "size", + "inputs": [], + "outputs": [ + { + "type": "core::integer::u32" + } + ], + "state_mutability": "view" + }, + { + "type": "enum", + "name": "core::bool", + "variants": [ + { + "name": "False", + "type": "()" + }, + { + "name": "True", + "type": "()" + } + ] + }, + { + "type": "function", + "name": "schema", + "inputs": [], + "outputs": [ + { + "type": "core::array::Array::<(core::felt252, core::felt252, core::bool)>" + } + ], + "state_mutability": "view" + }, + { + "type": "event", + "name": "dojo_examples::components::moves::Event", + "kind": "enum", + "variants": [] + } + ] + }, + { + "name": "Position", + "members": [ + { + "name": "player", + "type": "ContractAddress", + "key": true + }, + { + "name": "x", + "type": "u32", + "key": false + }, + { + "name": "y", + "type": "u32", + "key": false + } + ], + "class_hash": "0x4973c97a1cd5e141d6dfe05c36517234851118ea703e510fcb72a39a092c228", + "abi": [ + { + "type": "function", + "name": "name", + "inputs": [], + "outputs": [ + { + "type": "core::felt252" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "size", + "inputs": [], + "outputs": [ + { + "type": "core::integer::u32" + } + ], + "state_mutability": "view" + }, + { + "type": "enum", + "name": "core::bool", + "variants": [ + { + "name": "False", + "type": "()" + }, + { + "name": "True", + "type": "()" + } + ] + }, + { + "type": "function", + "name": "schema", + "inputs": [], + "outputs": [ + { + "type": "core::array::Array::<(core::felt252, core::felt252, core::bool)>" + } + ], + "state_mutability": "view" + }, + { + "type": "event", + "name": "dojo_examples::components::position::Event", + "kind": "enum", + "variants": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/crates/dojo-lang/src/manifest_test_data/manifest_test_crate b/crates/dojo-lang/src/manifest_test_data/manifest_test_crate new file mode 120000 index 0000000000..9e819a9d49 --- /dev/null +++ b/crates/dojo-lang/src/manifest_test_data/manifest_test_crate @@ -0,0 +1 @@ +../../../../examples/ecs/ \ No newline at end of file From 4206808c55cf899b7a3576226f94b6060f8b4c0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lancelot=20de=20Ferri=C3=A8re?= Date: Thu, 24 Aug 2023 19:48:47 +0200 Subject: [PATCH 02/77] Make sure system calls go through the world & the executor (#822) --- crates/dojo-core/src/executor.cairo | 8 +++++ crates/dojo-core/src/executor_test.cairo | 34 +++++++++++++++++++ .../dojo-lang/src/manifest_test_data/manifest | 4 +-- 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/crates/dojo-core/src/executor.cairo b/crates/dojo-core/src/executor.cairo index 65cade5c3d..fe862d6136 100644 --- a/crates/dojo-core/src/executor.cairo +++ b/crates/dojo-core/src/executor.cairo @@ -21,6 +21,8 @@ mod executor { const EXECUTE_ENTRYPOINT: felt252 = 0x0240060cdb34fcc260f41eac7474ee1d7c80b7e3607daff9ac67c7ea2ebb1c44; + const WORLD_ADDRESS_OFFSET: u32 = 4; + #[storage] struct Storage {} @@ -39,6 +41,12 @@ mod executor { fn execute( self: @ContractState, class_hash: ClassHash, calldata: Span ) -> Span { + assert( + traits::Into::::into(starknet::get_caller_address()) == *calldata + .at(calldata.len() - WORLD_ADDRESS_OFFSET), + 'Only world caller' + ); starknet::syscalls::library_call_syscall(class_hash, EXECUTE_ENTRYPOINT, calldata) .unwrap_syscall() } diff --git a/crates/dojo-core/src/executor_test.cairo b/crates/dojo-core/src/executor_test.cairo index 94ae06f1b7..732fad11ea 100644 --- a/crates/dojo-core/src/executor_test.cairo +++ b/crates/dojo-core/src/executor_test.cairo @@ -54,5 +54,39 @@ fn test_executor() { ctx.serialize(ref system_calldata); + starknet::testing::set_contract_address(ctx.world.contract_address); + + let res = executor.execute(ctx.system_class_hash, system_calldata.span()); +} + + +#[test] +#[available_gas(40000000)] +#[should_panic] +fn test_executor_bad_caller() { + let constructor_calldata = array::ArrayTrait::new(); + let (executor_address, _) = deploy_syscall( + executor::TEST_CLASS_HASH.try_into().unwrap(), 0, constructor_calldata.span(), false + ) + .unwrap(); + + let executor = IExecutorDispatcher { contract_address: executor_address }; + + let mut system_calldata = ArrayTrait::new(); + system_calldata.append(1); + system_calldata.append(42); + system_calldata.append(53); + + let ctx = Context { + world: IWorldDispatcher { + contract_address: starknet::contract_address_const::<0x1337>() + }, + origin: starknet::contract_address_const::<0x1337>(), + system: 'Bar', + system_class_hash: Bar::TEST_CLASS_HASH.try_into().unwrap(), + }; + + ctx.serialize(ref system_calldata); + let res = executor.execute(ctx.system_class_hash, system_calldata.span()); } diff --git a/crates/dojo-lang/src/manifest_test_data/manifest b/crates/dojo-lang/src/manifest_test_data/manifest index 5fe74ce331..f1aec566ec 100644 --- a/crates/dojo-lang/src/manifest_test_data/manifest +++ b/crates/dojo-lang/src/manifest_test_data/manifest @@ -541,7 +541,7 @@ test_manifest_file "executor": { "name": "executor", "address": null, - "class_hash": "0x7b79892389a0c9fe22f74b1d28a9e9185c7b6d2c60cc1df814053e47e9078c2", + "class_hash": "0x6ac7478eec43bd66aacf829f58dcb4694d0e241dc52b332f64de2b736c24137", "abi": [ { "type": "impl", @@ -1065,4 +1065,4 @@ test_manifest_file ] } ] -} \ No newline at end of file +} From d059a36f938bc35febb7cced03015c432d827c36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lancelot=20de=20Ferri=C3=A8re?= Date: Fri, 25 Aug 2023 17:00:12 +0200 Subject: [PATCH 03/77] Use an explicit constant instead of 0 for the world owner (#828) --- crates/dojo-core/src/world.cairo | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/crates/dojo-core/src/world.cairo b/crates/dojo-core/src/world.cairo index 750c39c050..dd4cda4ed1 100644 --- a/crates/dojo-core/src/world.cairo +++ b/crates/dojo-core/src/world.cairo @@ -65,6 +65,8 @@ mod world { const NAME_ENTRYPOINT: felt252 = 0x0361458367e696363fbcc70777d07ebbd2394e89fd0adcaf147faccd1d294d60; + + const WORLD: felt252 = 0; #[event] #[derive(Drop, starknet::Event)] @@ -128,7 +130,7 @@ mod world { self.executor_dispatcher.write(IExecutorDispatcher { contract_address: executor }); self .owners - .write((0, starknet::get_tx_info().unbox().account_contract_address), bool::True(())); + .write((WORLD, starknet::get_tx_info().unbox().account_contract_address), bool::True(())); EventEmitter::emit( ref self, @@ -165,7 +167,7 @@ mod world { fn grant_owner(ref self: ContractState, address: ContractAddress, target: felt252) { let caller = get_caller_address(); assert( - self.is_owner(caller, target) || self.is_owner(caller, 0), + self.is_owner(caller, target) || self.is_owner(caller, WORLD), 'not owner' ); self.owners.write((target, address), bool::True(())); @@ -182,7 +184,7 @@ mod world { let caller = get_caller_address(); assert( self.is_owner(caller, target) - || self.is_owner(caller, 0), + || self.is_owner(caller, WORLD), 'not owner' ); self.owners.write((target, address), bool::False(())); @@ -214,7 +216,7 @@ mod world { assert( self.is_owner(caller, component) - || self.is_owner(caller, 0), + || self.is_owner(caller, WORLD), 'not owner or writer' ); self.writers.write((component, system), bool::True(())); @@ -233,7 +235,7 @@ mod world { assert( self.is_writer(component, self.caller_system()) || self.is_owner(caller, component) - || self.is_owner(caller, 0), + || self.is_owner(caller, WORLD), 'not owner or writer' ); self.writers.write((component, system), bool::False(())); @@ -482,7 +484,7 @@ mod world { /// * `contract_address` - The contract address of the executor. fn set_executor(ref self: ContractState, contract_address: ContractAddress) { // Only owner can set executor - assert(self.is_owner(get_caller_address(), 0), 'only owner can set executor'); + assert(self.is_owner(get_caller_address(), WORLD), 'only owner can set executor'); self .executor_dispatcher .write(IExecutorDispatcher { contract_address: contract_address }); @@ -530,7 +532,7 @@ mod world { assert( IWorld::is_writer(self, component, self.caller_system()) || IWorld::is_owner(self, get_tx_info().unbox().account_contract_address, component) - || IWorld::is_owner(self, get_tx_info().unbox().account_contract_address, 0), + || IWorld::is_owner(self, get_tx_info().unbox().account_contract_address, WORLD), 'not writer' ); } From ff6d6aa4a88159541430947dfb4dbd6e5bf96880 Mon Sep 17 00:00:00 2001 From: Alex Metelli Date: Sat, 26 Aug 2023 03:01:54 +0800 Subject: [PATCH 04/77] tests and fixes for gda / vrgda (#810) * chore: updated Scarb * added constructo test * refacto to latest OZ std * added erc20 tests * added panic expected msgs * import sorting and crate fmt * tests cleanup * moved interface * removed redundant lib * impl global macro and syntax fix * implemented IERC20 and cleaned imports * removed deprecated cairo_project impl discrete and continous gda added gda test + fixes * gda/vrgda tests + fixes * uncommented * readme * added more tests ad fixes * test cleanup --- crates/dojo-defi/README.md | 173 +++++++++++++++ crates/dojo-defi/cairo_project.toml | 2 - .../constant_product_market/components.cairo | 4 +- .../src/constant_product_market/systems.cairo | 59 +++--- crates/dojo-defi/src/dutch_auction.cairo | 1 + .../dojo-defi/src/dutch_auction/common.cairo | 9 + crates/dojo-defi/src/dutch_auction/gda.cairo | 83 +++++--- .../dojo-defi/src/dutch_auction/vrgda.cairo | 109 ++++++++-- crates/dojo-defi/src/lib.cairo | 1 + crates/dojo-defi/src/tests.cairo | 10 + .../src/tests/continuous_gda_test.cairo | 67 ++++++ .../src/tests/discrete_gda_test.cairo | 79 +++++++ .../src/tests/linear_vrgda_test.cairo | 40 ++++ .../src/tests/logistic_vrgda_test.cairo | 46 ++++ crates/dojo-defi/src/tests/utils.cairo | 25 +++ crates/dojo-erc/src/erc20.cairo | 1 + crates/dojo-erc/src/erc20/erc20.cairo | 197 +++++++++--------- crates/dojo-erc/src/erc20/interface.cairo | 17 ++ crates/dojo-erc/src/erc20/lib.cairo | 3 - crates/dojo-erc/src/erc20/systems.cairo | 35 ++-- crates/dojo-erc/src/tests.cairo | 2 + crates/dojo-erc/src/tests/test_erc20.cairo | 162 ++++++++++++++ .../dojo-erc/src/tests/test_erc20_utils.cairo | 61 ++++++ 23 files changed, 983 insertions(+), 203 deletions(-) create mode 100644 crates/dojo-defi/README.md delete mode 100644 crates/dojo-defi/cairo_project.toml create mode 100644 crates/dojo-defi/src/dutch_auction/common.cairo create mode 100644 crates/dojo-defi/src/tests.cairo create mode 100644 crates/dojo-defi/src/tests/continuous_gda_test.cairo create mode 100644 crates/dojo-defi/src/tests/discrete_gda_test.cairo create mode 100644 crates/dojo-defi/src/tests/linear_vrgda_test.cairo create mode 100644 crates/dojo-defi/src/tests/logistic_vrgda_test.cairo create mode 100644 crates/dojo-defi/src/tests/utils.cairo create mode 100644 crates/dojo-erc/src/erc20/interface.cairo delete mode 100644 crates/dojo-erc/src/erc20/lib.cairo create mode 100644 crates/dojo-erc/src/tests/test_erc20.cairo create mode 100644 crates/dojo-erc/src/tests/test_erc20_utils.cairo diff --git a/crates/dojo-defi/README.md b/crates/dojo-defi/README.md new file mode 100644 index 0000000000..d806f17ce4 --- /dev/null +++ b/crates/dojo-defi/README.md @@ -0,0 +1,173 @@ +# Gradual Dutch Auctions (GDA) + +## Introduction + +Gradual Dutch Auctions (GDA) enable efficient sales of assets without relying on liquid markets. GDAs offer a novel solution for selling both non-fungible tokens (NFTs) and fungible tokens through discrete and continuous mechanisms. + +## Discrete GDA + +### Motivation + +Discrete GDAs are perfect for selling NFTs in integer quantities. They offer an efficient way to conduct bulk purchases through a sequence of Dutch auctions. + +### Mechanism + +The process involves holding virtual Dutch auctions for each token, allowing for efficient clearing of batches. Price decay is exponential, controlled by a decay constant, and the starting price increases by a fixed scale factor. + +### Calculating Batch Purchase Prices + +Calculations can be made efficiently for purchasing a batch of auctions, following a given price function. + +## Continuous GDA + +### Motivation + +Continuous GDAs offer a mechanism for selling fungible tokens, allowing for constant rate emissions over time. + +### Mechanism + +The process works by incrementally making more assets available for sale, splitting sales into an infinite sequence of auctions. Various price functions, including exponential decay, can be applied. + +### Calculating Purchase Prices + +It's possible to compute the purchase price for any quantity of tokens gas-efficiently, using specific mathematical expressions. + +## How to Use + +### Discrete Gradual Dutch Auction + +The `DiscreteGDA` structure represents a Gradual Dutch Auction using discrete time steps. Here's how you can use it: + +#### Creating a Discrete GDA + +```rust +let gda = DiscreteGDA { + sold: Fixed::new_unscaled(0), + initial_price: Fixed::new_unscaled(100, false), + scale_factor: FixedTrait::new_unscaled(11, false) / FixedTrait::new_unscaled(10, false), // 1.1 + decay_constant: FixedTrait::new_unscaled(1, false) / FixedTrait::new_unscaled(2, false), // 0.5, +}; +``` + +#### Calculating the Purchase Price + +You can calculate the purchase price for a specific quantity at a given time using the `purchase_price` method. + +```rust +let time_since_start = FixedTrait::new(2, false); // 2 days since the start, it must be scaled to avoid overflow. +let quantity = FixedTrait::new_unscaled(5, false); // Quantity to purchase +let price = gda.purchase_price(time_since_start, quantity); +``` + +### Continuous Gradual Dutch Auction + +The `ContinuousGDA` structure represents a Gradual Dutch Auction using continuous time steps. + +#### Creating a Continuous GDA + +```rust +let gda = ContinuousGDA { + initial_price: FixedTrait::new_unscaled(1000, false), + emission_rate: FixedTrait::ONE(), + decay_constant: FixedTrait::new_unscaled(1, false) / FixedTrait::new_unscaled(2, false), +}; +``` + +#### Calculating the Purchase Price + +Just like with the discrete version, you can calculate the purchase price for a specific quantity at a given time using the `purchase_price` method. + +```rust +let time_since_last = FixedTrait::new(1, false); // 1 day since the last purchase, it must be scaled to avoid overflow. +let quantity = FixedTrait::new_unscaled(3, false); // Quantity to purchase +let price = gda.purchase_price(time_since_last, quantity); +``` + +--- + +These examples demonstrate how to create instances of the `DiscreteGDA` and `ContinuousGDA` structures, and how to utilize their `purchase_price` methods to calculate the price for purchasing specific quantities at given times. + +You'll need to include the `cubit` crate in your project to work with the `Fixed` type and mathematical operations like `exp` and `pow`. Make sure to follow the respective documentation for additional details and proper integration into your project. + +## Conclusion + +GDAs present a powerful tool for selling both fungible and non-fungible tokens in various contexts. They offer efficient, flexible solutions for asset sales, opening doors to innovative applications beyond traditional markets. + +# Variable Rate GDAs (VRGDAs) + +## Overview + +Variable Rate GDAs (VRGDAs) enable the selling of tokens according to a custom schedule, raising or lowering prices based on the sales pace. VRGDA is a generalization of the GDA mechanism. + +## How to Use + +### Linear Variable Rate Gradual Dutch Auction (LinearVRGDA) + +The `LinearVRGDA` struct represents a linear auction where the price decays based on the target price, decay constant, and per-time-unit rate. + +#### Creating a LinearVRGDA instance + +```rust +const _69_42: u128 = 1280572973596917000000; +const _0_31: u128 = 5718490662849961000; + +let auction = LinearVRGDA { + target_price: FixedTrait::new(_69_42, false), + decay_constant: FixedTrait::new(_0_31, false), + per_time_unit: FixedTrait::new_unscaled(2, false), +}; +``` + +#### Calculating Target Sale Time + +```rust +let target_sale_time = auction.get_target_sale_time(sold_quantity); +``` + +#### Calculating VRGDA Price + +```rust +let price = auction.get_vrgda_price(time_since_start, sold_quantity); +``` + +### Logistic Variable Rate Gradual Dutch Auction (LogisticVRGDA) + +The `LogisticVRGDA` struct represents an auction where the price decays according to a logistic function, based on the target price, decay constant, max sellable quantity, and time scale. + +#### Creating a LogisticVRGDA instance + +```rust +const MAX_SELLABLE: u128 = 6392; +const _0_0023: u128 = 42427511369531970; + +let auction = LogisticVRGDA { + target_price: FixedTrait::new(_69_42, false), + decay_constant: FixedTrait::new(_0_31, false), + max_sellable: FixedTrait::new_unscaled(MAX_SELLABLE, false), + time_scale: FixedTrait::new(_0_0023, false), +}; +``` + +#### Calculating Target Sale Time + +```rust +let target_sale_time = auction.get_target_sale_time(sold_quantity); +``` + +#### Calculating VRGDA Price + +```rust +let price = auction.get_vrgda_price(time_since_start, sold_quantity); +``` + +Make sure to import the required dependencies at the beginning of your Cairo file: + +```rust +use cubit::f128::types::fixed::{Fixed, FixedTrait}; +``` + +These examples show you how to create instances of both `LinearVRGDA` and `LogisticVRGDA` and how to use their methods to calculate the target sale time and VRGDA price. + +## Conclusion + +VRGDAs offer a flexible way to issue NFTs on nearly any schedule, enabling seamless purchases at any time. diff --git a/crates/dojo-defi/cairo_project.toml b/crates/dojo-defi/cairo_project.toml deleted file mode 100644 index cf7ed1d24f..0000000000 --- a/crates/dojo-defi/cairo_project.toml +++ /dev/null @@ -1,2 +0,0 @@ -[crate_roots] -dojo_defi = "src" \ No newline at end of file diff --git a/crates/dojo-defi/src/constant_product_market/components.cairo b/crates/dojo-defi/src/constant_product_market/components.cairo index 587334325a..0b3315462a 100644 --- a/crates/dojo-defi/src/constant_product_market/components.cairo +++ b/crates/dojo-defi/src/constant_product_market/components.cairo @@ -4,7 +4,6 @@ use option::OptionTrait; // Cubit fixed point math library use cubit::types::fixed::{Fixed, FixedInto, FixedTrait, ONE_u128}; - use cubit::test::helpers::assert_precise; const SCALING_FACTOR: u128 = 10000; @@ -251,7 +250,6 @@ fn normalize(quantity: usize, market: @Market) -> (u128, u128, u128) { (quantity, available, *market.cash_amount) } - #[test] #[should_panic(expected: ('not enough liquidity', ))] fn test_not_enough_quantity() { @@ -362,7 +360,6 @@ fn test_market_add_liquidity_insufficient_amount() { let (amount_add, quantity_add, liquidity_add) = market.add_liquidity(SCALING_FACTOR * 1, 20); } - #[test] #[available_gas(1000000)] fn test_market_remove_liquidity() { @@ -417,3 +414,4 @@ fn test_market_remove_liquidity_more_than_available() { let (amount_remove, quantity_remove) = market.remove_liquidity(liquidity_remove); } + diff --git a/crates/dojo-defi/src/constant_product_market/systems.cairo b/crates/dojo-defi/src/constant_product_market/systems.cairo index a08150a83b..798378ea72 100644 --- a/crates/dojo-defi/src/constant_product_market/systems.cairo +++ b/crates/dojo-defi/src/constant_product_market/systems.cairo @@ -8,16 +8,16 @@ mod Buy { let player: felt252 = starknet::get_caller_address().into(); let cash_sk: Query = (partition, (player)).into_partitioned(); - let player_cash = get !(ctx.world, cash_sk, Cash); + let player_cash = get!(ctx.world, cash_sk, Cash); let market_sk: Query = (partition, (item_id)).into_partitioned(); - let market = get !(ctx.world, market_sk, Market); + let market = get!(ctx.world, market_sk, Market); let cost = market.buy(quantity); assert(cost <= player_cash.amount, 'not enough cash'); // update market - set !( + set!( ctx.world, market_sk, (Market { @@ -27,12 +27,12 @@ mod Buy { ); // update player cash - set !(ctx.world, cash_sk, (Cash { amount: player_cash.amount - cost })); + set!(ctx.world, cash_sk, (Cash { amount: player_cash.amount - cost })); // update player item let item_sk: Query = (partition, (player, item_id)).into_partitioned(); - let item = get !(ctx.world, item_sk, Item); - set !(ctx.world, item_sk, (Item { quantity: item.quantity + quantity })); + let item = get!(ctx.world, item_sk, Item); + set!(ctx.world, item_sk, (Item { quantity: item.quantity + quantity })); } } @@ -46,19 +46,19 @@ mod Sell { let player: felt252 = starknet::get_caller_address().into(); let item_sk: Query = (partition, (player, item_id)).into_partitioned(); - let item = get !(ctx.world, item_sk, Item); + let item = get!(ctx.world, item_sk, Item); let player_quantity = item.quantity; assert(player_quantity >= quantity, 'not enough items'); let cash_sk: Query = (partition, (player)).into_partitioned(); - let player_cash = get !(ctx.world, cash_sk, Cash); + let player_cash = get!(ctx.world, cash_sk, Cash); let market_sk: Query = (partition, (item_id)).into_partitioned(); - let market = get !(ctx.world, market_sk, Market); + let market = get!(ctx.world, market_sk, Market); let payout = market.sell(quantity); // update market - set !( + set!( ctx.world, market_sk, (Market { @@ -68,10 +68,10 @@ mod Sell { ); // update player cash - set !(ctx.world, cash_sk, (Cash { amount: player_cash.amount + payout })); + set!(ctx.world, cash_sk, (Cash { amount: player_cash.amount + payout })); // update player item - set !(ctx.world, item_sk, (Item { quantity: player_quantity - quantity })); + set!(ctx.world, item_sk, (Item { quantity: player_quantity - quantity })); } } @@ -89,20 +89,20 @@ mod AddLiquidity { let player: felt252 = starknet::get_caller_address().into(); let item_sk: Query = (partition, (player, item_id)).into_partitioned(); - let item = get !(ctx.world, item_sk, Item); + let item = get!(ctx.world, item_sk, Item); let player_quantity = item.quantity; assert(player_quantity >= quantity, 'not enough items'); let cash_sk: Query = (partition, (player)).into_partitioned(); - let player_cash = get !(ctx.world, cash_sk, Cash); + let player_cash = get!(ctx.world, cash_sk, Cash); assert(amount <= player_cash.amount, 'not enough cash'); let market_sk: Query = (partition, (item_id)).into_partitioned(); - let market = get !(ctx.world, market_sk, Market); + let market = get!(ctx.world, market_sk, Market); let (cost_cash, cost_quantity, liquidity_shares) = market.add_liquidity(amount, quantity); // update market - set !( + set!( ctx.world, market_sk, (Market { @@ -112,15 +112,15 @@ mod AddLiquidity { ); // update player cash - set !(ctx.world, cash_sk, (Cash { amount: player_cash.amount - cost_cash })); + set!(ctx.world, cash_sk, (Cash { amount: player_cash.amount - cost_cash })); // update player item - set !(ctx.world, item_sk, (Item { quantity: player_quantity - cost_quantity })); + set!(ctx.world, item_sk, (Item { quantity: player_quantity - cost_quantity })); // update player liquidity let liquidity_sk: Query = (partition, (player, item_id)).into_partitioned(); - let player_liquidity = get !(ctx.world, liquidity_sk, Liquidity); - set !( + let player_liquidity = get!(ctx.world, liquidity_sk, Liquidity); + set!( ctx.world, liquidity_sk, (Liquidity { shares: player_liquidity.shares + liquidity_shares }) @@ -143,15 +143,15 @@ mod RemoveLiquidity { let player: felt252 = starknet::get_caller_address().into(); let liquidity_sk: Query = (partition, (player, item_id)).into_partitioned(); - let player_liquidity = get !(ctx.world, liquidity_sk, Liquidity); + let player_liquidity = get!(ctx.world, liquidity_sk, Liquidity); assert(player_liquidity.shares >= shares, 'not enough shares'); let market_sk: Query = (partition, (item_id)).into_partitioned(); - let market = get !(ctx.world, market_sk, Market); + let market = get!(ctx.world, market_sk, Market); let (payout_cash, payout_quantity) = market.remove_liquidity(shares); // update market - set !( + set!( ctx.world, market_sk, (Market { @@ -162,18 +162,19 @@ mod RemoveLiquidity { // update player cash let cash_sk: Query = (partition, (player)).into_partitioned(); - let player_cash = get !(ctx.world, cash_sk, Cash); - set !(ctx.world, cash_sk, (Cash { amount: player_cash.amount + payout_cash })); + let player_cash = get!(ctx.world, cash_sk, Cash); + set!(ctx.world, cash_sk, (Cash { amount: player_cash.amount + payout_cash })); // update player item let item_sk: Query = (partition, (player, item_id)).into_partitioned(); - let item = get !(ctx.world, item_sk, Item); + let item = get!(ctx.world, item_sk, Item); let player_quantity = item.quantity; - set !(ctx.world, item_sk, (Item { quantity: player_quantity + payout_quantity })); + set!(ctx.world, item_sk, (Item { quantity: player_quantity + payout_quantity })); // update player liquidity let liquidity_sk: Query = (partition, (player, item_id)).into_partitioned(); - let player_liquidity = get !(ctx.world, liquidity_sk); - set !(ctx.world, liquidity_sk, (Liquidity { shares: player_liquidity.shares - shares })); + let player_liquidity = get!(ctx.world, liquidity_sk); + set!(ctx.world, liquidity_sk, (Liquidity { shares: player_liquidity.shares - shares })); } } + diff --git a/crates/dojo-defi/src/dutch_auction.cairo b/crates/dojo-defi/src/dutch_auction.cairo index 5fef054858..9c0635143f 100644 --- a/crates/dojo-defi/src/dutch_auction.cairo +++ b/crates/dojo-defi/src/dutch_auction.cairo @@ -1,3 +1,4 @@ mod gda; mod vrgda; +mod common; diff --git a/crates/dojo-defi/src/dutch_auction/common.cairo b/crates/dojo-defi/src/dutch_auction/common.cairo new file mode 100644 index 0000000000..6420dff345 --- /dev/null +++ b/crates/dojo-defi/src/dutch_auction/common.cairo @@ -0,0 +1,9 @@ +use cubit::f128::types::fixed::{Fixed, FixedTrait, ONE_u128}; + +fn to_days_fp(x: Fixed) -> Fixed { + x / FixedTrait::new(86400, false) +} + +fn from_days_fp(x: Fixed) -> Fixed { + x * FixedTrait::new(86400, false) +} diff --git a/crates/dojo-defi/src/dutch_auction/gda.cairo b/crates/dojo-defi/src/dutch_auction/gda.cairo index 20dbe39a19..f2d61b51ec 100644 --- a/crates/dojo-defi/src/dutch_auction/gda.cairo +++ b/crates/dojo-defi/src/dutch_auction/gda.cairo @@ -1,38 +1,67 @@ -use cubit::types::fixed::{Fixed, FixedMul, FixedSub, FixedDiv}; +use cubit::f128::math::core::{exp, pow}; +use cubit::f128::types::fixed::{Fixed, FixedTrait}; -#[derive(Component, Drop)] -struct GradualDutchAuction { +use debug::PrintTrait; + +/// A Gradual Dutch Auction represented using discrete time steps. +/// The purchase price for a given quantity is calculated based on +/// the initial price, scale factor, decay constant, and the time since +/// the auction has started. +#[derive(Copy, Drop, Serde, starknet::Storage)] +struct DiscreteGDA { + sold: Fixed, initial_price: Fixed, scale_factor: Fixed, decay_constant: Fixed, - auction_start_time: Fixed, } -trait GradualDutchAuctionTrait { - fn purchase_price(self: @Market, quantity: u128, existing: u128, current_time: u128) -> Fixed; +#[generate_trait] +impl DiscreteGDAImpl of DiscreteGDATrait { + /// Calculates the purchase price for a given quantity of the item at a specific time. + /// + /// # Arguments + /// + /// * `time_since_start`: Time since the start of the auction in days. + /// * `quantity`: Quantity of the item being purchased. + /// + /// # Returns + /// + /// * A `Fixed` representing the purchase price. + fn purchase_price(self: @DiscreteGDA, time_since_start: Fixed, quantity: Fixed) -> Fixed { + let num1 = *self.initial_price * pow(*self.scale_factor, *self.sold); + let num2 = pow(*self.scale_factor, quantity) - FixedTrait::ONE(); + let den1 = exp(*self.decay_constant * time_since_start); + let den2 = *self.scale_factor - FixedTrait::ONE(); + (num1 * num2) / (den1 * den2) + } } -impl GradualDutchAuctionImpl of GradualDutchAuctionTrait { - fn purchase_price(self: @Market, quantity: u128, existing: u128, current_time: u128) -> Fixed { - let quantity_fp = Fixed::new(quantity, true); - let existing_fp = Fixed::new(existing, true); - let current_time_fp = Fixed::new(current_time, true); - - let num1_pow = Fixed::pow(*self.scale_factor, existing_fp); - let num1 = FixedMul::mul(*self.initial_price, num1_pow); - - let num2_pow = Fixed::pow(*self.scale_factor, quantity_fp); - let num2 = FixedMul::mul(num2_pow, Fixed::new(1, true)); - - let den1_mul = FixedMul::mul(*self.decay_constant, *self.auction_start_time); - let den1 = Fixed::exp(den1_mul); - let den2 = FixedSub::sub(*self.scale_factor, Fixed::new(1, true)); - - let mul_num2 = FixedMul::mul(num1, num2); - let mul_num3 = FixedMul::mul(den1, den2); +/// A Gradual Dutch Auction represented using continuous time steps. +/// The purchase price is calculated based on the initial price, +/// emission rate, decay constant, and the time since the last purchase in days. +#[derive(Copy, Drop, Serde, starknet::Storage)] +struct ContinuousGDA { + initial_price: Fixed, + emission_rate: Fixed, + decay_constant: Fixed, +} - let total_cost = FixedDiv::div(mul_num2, mul_num3); - total_cost +#[generate_trait] +impl ContinuousGDAImpl of ContinuousGDATrait { + /// Calculates the purchase price for a given quantity of the item at a specific time. + /// + /// # Arguments + /// + /// * `time_since_last`: Time since the last purchase in the auction in days. + /// * `quantity`: Quantity of the item being purchased. + /// + /// # Returns + /// + /// * A `Fixed` representing the purchase price. + fn purchase_price(self: @ContinuousGDA, time_since_last: Fixed, quantity: Fixed) -> Fixed { + let num1 = *self.initial_price / *self.decay_constant; + let num2 = exp((*self.decay_constant * quantity) / *self.emission_rate) - FixedTrait::ONE(); + let den = exp(*self.decay_constant * time_since_last); + (num1 * num2) / den } } - diff --git a/crates/dojo-defi/src/dutch_auction/vrgda.cairo b/crates/dojo-defi/src/dutch_auction/vrgda.cairo index be2548268f..388787c407 100644 --- a/crates/dojo-defi/src/dutch_auction/vrgda.cairo +++ b/crates/dojo-defi/src/dutch_auction/vrgda.cairo @@ -1,35 +1,100 @@ -use cubit::types::fixed::{Fixed, FixedAdd, FixedMul, FixedSub, FixedDiv}; +use cubit::f128::math::core::{ln, abs, exp}; +use cubit::f128::types::fixed::{Fixed, FixedTrait}; -#[derive(Component)] -#[derive(Drop)] -struct VariableGradualDutchAuction { +/// A Linear Variable Rate Gradual Dutch Auction (VRGDA) struct. +/// Represents an auction where the price decays linearly based on the target price, +/// decay constant, and per-time-unit rate. +#[derive(Copy, Drop, Serde, starknet::Storage)] +struct LinearVRGDA { target_price: Fixed, - scale_factor: Fixed, decay_constant: Fixed, per_time_unit: Fixed, } -trait VrgdaTrait { - fn get_target_sale_time(sold: u128) -> Fixed; - fn vrgda_price(time_start: u128, sold: u128) -> Fixed; -} - -impl VrgdaImpl of VrgdaTrait { - fn get_target_sale_time(self: @Market, sold: u128) -> Fixed { - let sold_fp = Fixed::new(sold, false); +#[generate_trait] +impl LinearVRGDAImpl of LinearVRGDATrait { + /// Calculates the target sale time based on the quantity sold. + /// + /// # Arguments + /// + /// * `sold`: Quantity sold. + /// + /// # Returns + /// + /// * A `Fixed` representing the target sale time. + fn get_target_sale_time(self: @LinearVRGDA, sold: Fixed) -> Fixed { + sold / *self.per_time_unit + } - FixedDiv::div(sold_fp, *self.per_time_unit) + /// Calculates the VRGDA price at a specific time since the auction started. + /// + /// # Arguments + /// + /// * `time_since_start`: Time since the auction started. + /// * `sold`: Quantity sold. + /// + /// # Returns + /// + /// * A `Fixed` representing the price. + fn get_vrgda_price(self: @LinearVRGDA, time_since_start: Fixed, sold: Fixed) -> Fixed { + *self.target_price + * exp( + *self.decay_constant + * (time_since_start + - self.get_target_sale_time(sold + FixedTrait::new(1, false))) + ) } +} - fn vrgda_price(time_start: u128, sold: u128) -> Fixed { - time_start_fp = Fixed::new(time_start, false); - sold_fp = Fixed::new(sold, false); +#[derive(Copy, Drop, Serde, starknet::Storage)] +struct LogisticVRGDA { + target_price: Fixed, + decay_constant: Fixed, + max_sellable: Fixed, + time_scale: Fixed, +} - num1 = FixedAdd::add(sold_fp, Fixed::new(1, false)); - num2 = FixedSub::sub(time_start_fp, num1); - num3 = FixedMul::mul(*self.decay_constant, num2); - num4 = Fixed::exp(num3); +// A Logistic Variable Rate Gradual Dutch Auction (VRGDA) struct. +/// Represents an auction where the price decays according to a logistic function, +/// based on the target price, decay constant, max sellable quantity, and time scale. +#[generate_trait] +impl LogisticVRGDAImpl of LogisticVRGDATrait { + /// Calculates the target sale time using a logistic function based on the quantity sold. + /// + /// # Arguments + /// + /// * `sold`: Quantity sold. + /// + /// # Returns + /// + /// * A `Fixed` representing the target sale time. + fn get_target_sale_time(self: @LogisticVRGDA, sold: Fixed) -> Fixed { + let logistic_limit = *self.max_sellable + FixedTrait::ONE(); + let logistic_limit_double = logistic_limit * FixedTrait::new_unscaled(2, false); + abs( + ln(logistic_limit_double / (sold + logistic_limit) - FixedTrait::ONE()) + / *self.time_scale + ) + } - FixedMul::mul(*self.target_price, num4) + /// Calculates the VRGDA price at a specific time since the auction started, + /// using the logistic function. + /// + /// # Arguments + /// + /// * `time_since_start`: Time since the auction started. + /// * `sold`: Quantity sold. + /// + /// # Returns + /// + /// * A `Fixed` representing the price. + fn get_vrgda_price(self: @LogisticVRGDA, time_since_start: Fixed, sold: Fixed) -> Fixed { + *self.target_price + * exp( + *self.decay_constant + * (time_since_start + - self.get_target_sale_time(sold + FixedTrait::new(1, false))) + ) } } + diff --git a/crates/dojo-defi/src/lib.cairo b/crates/dojo-defi/src/lib.cairo index 42f9a7d87d..a79dfdedd5 100644 --- a/crates/dojo-defi/src/lib.cairo +++ b/crates/dojo-defi/src/lib.cairo @@ -1,3 +1,4 @@ mod constant_product_market; mod dutch_auction; +mod tests; diff --git a/crates/dojo-defi/src/tests.cairo b/crates/dojo-defi/src/tests.cairo new file mode 100644 index 0000000000..134c484365 --- /dev/null +++ b/crates/dojo-defi/src/tests.cairo @@ -0,0 +1,10 @@ +#[cfg(test)] +mod discrete_gda_test; +#[cfg(test)] +mod continuous_gda_test; +#[cfg(test)] +mod linear_vrgda_test; +#[cfg(test)] +mod logistic_vrgda_test; +#[cfg(test)] +mod utils; diff --git a/crates/dojo-defi/src/tests/continuous_gda_test.cairo b/crates/dojo-defi/src/tests/continuous_gda_test.cairo new file mode 100644 index 0000000000..0e404c11bb --- /dev/null +++ b/crates/dojo-defi/src/tests/continuous_gda_test.cairo @@ -0,0 +1,67 @@ +use cubit::f128::types::fixed::{Fixed, FixedTrait}; + +use dojo_defi::dutch_auction::gda::{ContinuousGDA, ContinuousGDATrait}; +use dojo_defi::tests::utils::{assert_approx_equal, TOLERANCE}; + +// ipynb with calculations at https://colab.research.google.com/drive/14elIFRXdG3_gyiI43tP47lUC_aClDHfB?usp=sharing +#[test] +#[available_gas(2000000)] +fn test_price_1() { + let auction = ContinuousGDA { + initial_price: FixedTrait::new_unscaled(1000, false), + emission_rate: FixedTrait::ONE(), + decay_constant: FixedTrait::new_unscaled(1, false) / FixedTrait::new_unscaled(2, false), + }; + let expected = FixedTrait::new(22128445337405634000000, false); + let time_since_last = FixedTrait::new_unscaled(10, false); + let quantity = FixedTrait::new_unscaled(9, false); + let price: Fixed = auction.purchase_price(time_since_last, quantity); + assert_approx_equal(price, expected, TOLERANCE) +} + + +#[test] +#[available_gas(2000000)] +fn test_price_2() { + let auction = ContinuousGDA { + initial_price: FixedTrait::new_unscaled(1000, false), + emission_rate: FixedTrait::ONE(), + decay_constant: FixedTrait::new_unscaled(1, false) / FixedTrait::new_unscaled(2, false), + }; + let expected = FixedTrait::new(89774852279643700000, false); + let time_since_last = FixedTrait::new_unscaled(20, false); + let quantity = FixedTrait::new_unscaled(8, false); + let price: Fixed = auction.purchase_price(time_since_last, quantity); + assert_approx_equal(price, expected, TOLERANCE) +} + +#[test] +#[available_gas(2000000)] +fn test_price_3() { + let auction = ContinuousGDA { + initial_price: FixedTrait::new_unscaled(1000, false), + emission_rate: FixedTrait::ONE(), + decay_constant: FixedTrait::new_unscaled(1, false) / FixedTrait::new_unscaled(2, false), + }; + let expected = FixedTrait::new(20393925850936156000, false); + let time_since_last = FixedTrait::new_unscaled(30, false); + let quantity = FixedTrait::new_unscaled(15, false); + let price: Fixed = auction.purchase_price(time_since_last, quantity); + assert_approx_equal(price, expected, TOLERANCE) +} + +#[test] +#[available_gas(2000000)] +fn test_price_4() { + let auction = ContinuousGDA { + initial_price: FixedTrait::new_unscaled(1000, false), + emission_rate: FixedTrait::ONE(), + decay_constant: FixedTrait::new_unscaled(1, false) / FixedTrait::new_unscaled(2, false), + }; + let expected = FixedTrait::new(3028401847768577000000, false); + let time_since_last = FixedTrait::new_unscaled(40, false); + let quantity = FixedTrait::new_unscaled(35, false); + let price: Fixed = auction.purchase_price(time_since_last, quantity); + assert_approx_equal(price, expected, TOLERANCE) +} + diff --git a/crates/dojo-defi/src/tests/discrete_gda_test.cairo b/crates/dojo-defi/src/tests/discrete_gda_test.cairo new file mode 100644 index 0000000000..ac0a52dc73 --- /dev/null +++ b/crates/dojo-defi/src/tests/discrete_gda_test.cairo @@ -0,0 +1,79 @@ +use cubit::f128::types::fixed::{Fixed, FixedTrait}; + +use dojo_defi::dutch_auction::gda::{DiscreteGDA, DiscreteGDATrait}; +use dojo_defi::tests::utils::{assert_approx_equal, TOLERANCE}; + +#[test] +#[available_gas(2000000)] +fn test_initial_price() { + let auction = DiscreteGDA { + sold: FixedTrait::new_unscaled(0, false), + initial_price: FixedTrait::new_unscaled(1000, false), + scale_factor: FixedTrait::new_unscaled(11, false) / FixedTrait::new_unscaled(10, false), + decay_constant: FixedTrait::new_unscaled(1, false) / FixedTrait::new_unscaled(2, false), + }; + let price = auction.purchase_price(FixedTrait::ZERO(), FixedTrait::ONE()); + assert_approx_equal(price, auction.initial_price, TOLERANCE) +} + +// ipynb with calculations at https://colab.research.google.com/drive/14elIFRXdG3_gyiI43tP47lUC_aClDHfB?usp=sharing +#[test] +#[available_gas(2000000)] +fn test_price_1() { + let auction = DiscreteGDA { + sold: FixedTrait::new_unscaled(1, false), + initial_price: FixedTrait::new_unscaled(1000, false), + scale_factor: FixedTrait::new_unscaled(11, false) / FixedTrait::new_unscaled(10, false), + decay_constant: FixedTrait::new_unscaled(1, false) / FixedTrait::new_unscaled(2, false), + }; + let expected = FixedTrait::new(1856620062541316600000, false); + let price = auction + .purchase_price(FixedTrait::new_unscaled(10, false), FixedTrait::new_unscaled(9, false), ); + assert_approx_equal(price, expected, TOLERANCE) +} + +#[test] +#[available_gas(2000000)] +fn test_price_2() { + let auction = DiscreteGDA { + sold: FixedTrait::new_unscaled(2, false), + initial_price: FixedTrait::new_unscaled(1000, false), + scale_factor: FixedTrait::new_unscaled(11, false) / FixedTrait::new_unscaled(10, false), + decay_constant: FixedTrait::new(1, false) / FixedTrait::new(2, false), + }; + let expected = FixedTrait::new(2042282068795448600000, false); + let price = auction + .purchase_price(FixedTrait::new_unscaled(10, false), FixedTrait::new_unscaled(9, false), ); + assert_approx_equal(price, expected, TOLERANCE) +} + +#[test] +#[available_gas(2000000)] +fn test_price_3() { + let auction = DiscreteGDA { + sold: FixedTrait::new_unscaled(4, false), + initial_price: FixedTrait::new_unscaled(1000, false), + scale_factor: FixedTrait::new_unscaled(11, false) / FixedTrait::new_unscaled(10, false), + decay_constant: FixedTrait::new_unscaled(1, false) / FixedTrait::new_unscaled(2, false), + }; + let expected = FixedTrait::new(2471161303242493000000, false); + let price = auction + .purchase_price(FixedTrait::new_unscaled(10, false), FixedTrait::new_unscaled(9, false), ); + assert_approx_equal(price, expected, TOLERANCE) +} + +#[test] +#[available_gas(2000000)] +fn test_price_4() { + let auction = DiscreteGDA { + sold: FixedTrait::new_unscaled(20, false), + initial_price: FixedTrait::new_unscaled(1000, false), + scale_factor: FixedTrait::new_unscaled(11, false) / FixedTrait::new_unscaled(10, false), + decay_constant: FixedTrait::new_unscaled(1, false) / FixedTrait::new_unscaled(2, false), + }; + let expected = FixedTrait::new(291, false); + let price = auction + .purchase_price(FixedTrait::new_unscaled(85, false), FixedTrait::new_unscaled(1, false), ); + assert_approx_equal(price, expected, TOLERANCE) +} + diff --git a/crates/dojo-defi/src/tests/linear_vrgda_test.cairo b/crates/dojo-defi/src/tests/linear_vrgda_test.cairo new file mode 100644 index 0000000000..a010a250fb --- /dev/null +++ b/crates/dojo-defi/src/tests/linear_vrgda_test.cairo @@ -0,0 +1,40 @@ +use cubit::f128::types::fixed::{Fixed, FixedTrait}; + +use dojo_defi::dutch_auction::common::{to_days_fp, from_days_fp}; +use dojo_defi::dutch_auction::vrgda::{LinearVRGDA, LinearVRGDATrait}; +use dojo_defi::tests::utils::assert_rel_approx_eq; + +const _69_42: u128 = 1280572973596917000000; +const _0_31: u128 = 5718490662849961000; +const DELTA_0_0005: u128 = 9223372036854776; +const DELTA_0_02: u128 = 368934881474191000; +const DELTA: u128 = 184467440737095; + +#[test] +#[available_gas(2000000)] +fn test_target_price() { + let auction = LinearVRGDA { + target_price: FixedTrait::new(_69_42, false), + decay_constant: FixedTrait::new(_0_31, false), + per_time_unit: FixedTrait::new_unscaled(2, false), + }; + let time = from_days_fp(auction.get_target_sale_time(FixedTrait::new(1, false))); + let cost = auction + .get_vrgda_price(to_days_fp(time + FixedTrait::new(1, false)), FixedTrait::ZERO()); + assert_rel_approx_eq(cost, auction.target_price, FixedTrait::new(DELTA_0_0005, false)); +} + +#[test] +#[available_gas(20000000)] +fn test_pricing_basic() { + let auction = LinearVRGDA { + target_price: FixedTrait::new(_69_42, false), + decay_constant: FixedTrait::new(_0_31, false), + per_time_unit: FixedTrait::new_unscaled(2, false), + }; + let time_delta = FixedTrait::new(10368001, false); // 120 days + let num_mint = FixedTrait::new(239, true); + let cost = auction.get_vrgda_price(time_delta, num_mint); + assert_rel_approx_eq(cost, auction.target_price, FixedTrait::new(DELTA_0_02, false)); +} + diff --git a/crates/dojo-defi/src/tests/logistic_vrgda_test.cairo b/crates/dojo-defi/src/tests/logistic_vrgda_test.cairo new file mode 100644 index 0000000000..020c298f3d --- /dev/null +++ b/crates/dojo-defi/src/tests/logistic_vrgda_test.cairo @@ -0,0 +1,46 @@ +use cubit::f128::types::fixed::{Fixed, FixedTrait}; + +use dojo_defi::dutch_auction::common::{from_days_fp}; +use dojo_defi::dutch_auction::vrgda::{LogisticVRGDA, LogisticVRGDATrait}; +use dojo_defi::tests::utils::assert_rel_approx_eq; + + +use debug::PrintTrait; +const _69_42: u128 = 1280572973596917000000; +const _0_31: u128 = 5718490662849961000; +const DELTA_0_0005: u128 = 9223372036854776; +const DELTA_0_02: u128 = 368934881474191000; +const MAX_SELLABLE: u128 = 6392; +const _0_0023: u128 = 42427511369531970; + +#[test] +#[available_gas(200000000)] +fn test_target_price() { + let auction = LogisticVRGDA { + target_price: FixedTrait::new(_69_42, false), + decay_constant: FixedTrait::new(_0_31, false), + max_sellable: FixedTrait::new_unscaled(MAX_SELLABLE, false), + time_scale: FixedTrait::new(_0_0023, false), + }; + let time = from_days_fp(auction.get_target_sale_time(FixedTrait::new(1, false))); + + let cost = auction.get_vrgda_price(time + FixedTrait::new(1, false), FixedTrait::ZERO()); + assert_rel_approx_eq(cost, auction.target_price, FixedTrait::new(DELTA_0_0005, false)); +} + +#[test] +#[available_gas(200000000)] +fn test_pricing_basic() { + let auction = LogisticVRGDA { + target_price: FixedTrait::new(_69_42, false), + decay_constant: FixedTrait::new(_0_31, false), + max_sellable: FixedTrait::new_unscaled(MAX_SELLABLE, false), + time_scale: FixedTrait::new(_0_0023, false), + }; + let time_delta = FixedTrait::new(10368001, false); + let num_mint = FixedTrait::new(876, false); + + let cost = auction.get_vrgda_price(time_delta, num_mint); + assert_rel_approx_eq(cost, auction.target_price, FixedTrait::new(DELTA_0_02, false)); +} + diff --git a/crates/dojo-defi/src/tests/utils.cairo b/crates/dojo-defi/src/tests/utils.cairo new file mode 100644 index 0000000000..470af7258c --- /dev/null +++ b/crates/dojo-defi/src/tests/utils.cairo @@ -0,0 +1,25 @@ +use cubit::f128::types::fixed::{Fixed, FixedTrait}; + +use debug::PrintTrait; + +const TOLERANCE: u128 = 18446744073709550; // 0.001 + +fn assert_approx_equal(expected: Fixed, actual: Fixed, tolerance: u128) { + let left_bound = expected - FixedTrait::new(tolerance, false); + let right_bound = expected + FixedTrait::new(tolerance, false); + assert(left_bound <= actual && actual <= right_bound, 'Not approx eq'); +} + +fn assert_rel_approx_eq(a: Fixed, b: Fixed, max_percent_delta: Fixed) { + if b == FixedTrait::ZERO() { + assert(a == b, 'a should eq ZERO'); + } + let percent_delta = if a > b { + (a - b) / b + } else { + (b - a) / b + }; + + assert(percent_delta < max_percent_delta, 'a ~= b not satisfied'); +} + diff --git a/crates/dojo-erc/src/erc20.cairo b/crates/dojo-erc/src/erc20.cairo index 262673d978..7647729efb 100644 --- a/crates/dojo-erc/src/erc20.cairo +++ b/crates/dojo-erc/src/erc20.cairo @@ -1,3 +1,4 @@ mod erc20; mod components; mod systems; +mod interface; diff --git a/crates/dojo-erc/src/erc20/erc20.cairo b/crates/dojo-erc/src/erc20/erc20.cairo index fdbfb14e91..9c10156cba 100644 --- a/crates/dojo-erc/src/erc20/erc20.cairo +++ b/crates/dojo-erc/src/erc20/erc20.cairo @@ -1,22 +1,20 @@ -// TODO: future improvements when Cairo catches up -// * use BoundedInt in allowance calc -// * use inline commands (currently available only in systems) // * use ufelt when available #[starknet::contract] mod ERC20 { - // max(felt252) - const UNLIMITED_ALLOWANCE: felt252 = - 3618502788666131213697322783095070105623107215331596699973092056135872020480; - use array::ArrayTrait; + use integer::BoundedInt; use option::OptionTrait; use starknet::{ContractAddress, get_caller_address, get_contract_address}; - use traits::Into; + use traits::{Into, TryInto}; use zeroable::Zeroable; use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; use dojo_erc::erc20::components::{Allowance, Balance, Supply}; + use dojo_erc::erc20::interface::IERC20; + + const UNLIMITED_ALLOWANCE: felt252 = + 3618502788666131213697322783095070105623107215331596699973092056135872020480; #[storage] struct Storage { @@ -62,11 +60,11 @@ mod ERC20 { self.token_name.write(name); self.token_symbol.write(symbol); self.token_decimals.write(decimals); - + let mut calldata: Array = array![]; if initial_supply != 0 { - assert(recipient.is_non_zero(), 'ERC20: mint to 0'); + assert(!recipient.is_zero(), 'ERC20: mint to 0'); + let mut calldata: Array = array![]; let token = get_contract_address(); - let mut calldata = ArrayTrait::new(); calldata.append(token.into()); calldata.append(recipient.into()); calldata.append(initial_supply); @@ -80,109 +78,120 @@ mod ERC20 { } #[external(v0)] - fn name(self: @ContractState) -> felt252 { - self.token_name.read() - } - - #[external(v0)] - fn symbol(self: @ContractState) -> felt252 { - self.token_symbol.read() - } - - #[external(v0)] - fn decimals(self: @ContractState) -> u8 { - self.token_decimals.read() - } + impl ERC20 of IERC20 { + fn name(self: @ContractState) -> felt252 { + self.token_name.read() + } - #[external(v0)] - fn total_supply(self: @ContractState) -> u256 { - let contract_address = get_contract_address(); - let supply = get!(self.world.read(), contract_address, Supply); - supply.amount.into() - } + fn symbol(self: @ContractState) -> felt252 { + self.token_symbol.read() + } - #[external(v0)] - fn balance_of(self: @ContractState, account: ContractAddress) -> u256 { - let token = get_contract_address(); - let balance = get!(self.world.read(), (token, account), Balance); - balance.amount.into() - } + fn decimals(self: @ContractState) -> u8 { + self.token_decimals.read() + } - #[external(v0)] - fn allowance(self: @ContractState, owner: ContractAddress, spender: ContractAddress) -> u256 { - let token = get_contract_address(); - let allowance = get!(self.world.read(), (token, owner, spender), Allowance); - allowance.amount.into() - } + fn total_supply(self: @ContractState) -> u256 { + let contract_address = get_contract_address(); + let supply = get!(self.world.read(), contract_address, Supply); + supply.amount.into() + } - #[external(v0)] - fn approve(ref self: ContractState, spender: ContractAddress, amount: u256) -> bool { - assert(spender.is_non_zero(), 'ERC20: approve to 0'); + fn balance_of(self: @ContractState, account: ContractAddress) -> u256 { + let token = get_contract_address(); + let balance = get!(self.world.read(), (token, account), Balance); + balance.amount.into() + } - let token = get_contract_address(); - let owner = get_caller_address(); - let mut calldata = ArrayTrait::new(); - calldata.append(token.into()); - calldata.append(owner.into()); - calldata.append(spender.into()); - calldata.append(u256_as_allowance(amount)); - self.world.read().execute('erc20_approve', calldata); + fn allowance( + self: @ContractState, owner: ContractAddress, spender: ContractAddress + ) -> u256 { + let token = get_contract_address(); + let allowance = get!(self.world.read(), (token, owner, spender), Allowance); + allowance.amount.into() + } - self.emit(Approval { owner, spender, value: amount }); + fn approve(ref self: ContractState, spender: ContractAddress, amount: u256) -> bool { + let owner = get_caller_address(); + self._approve(owner, spender, amount); + true + } - true - } + fn transfer(ref self: ContractState, recipient: ContractAddress, amount: u256) -> bool { + let sender = get_caller_address(); + self._transfer(sender, recipient, amount); + true + } - #[external(v0)] - fn transfer(ref self: ContractState, recipient: ContractAddress, amount: u256) -> bool { - transfer_internal(ref self, get_caller_address(), recipient, amount); - true + fn transfer_from( + ref self: ContractState, + sender: ContractAddress, + recipient: ContractAddress, + amount: u256 + ) -> bool { + let caller = get_caller_address(); + self._spend_allowance(sender, caller, amount); + self._transfer(sender, recipient, amount); + true + } } - #[external(v0)] - fn transfer_from( - ref self: ContractState, spender: ContractAddress, recipient: ContractAddress, amount: u256 - ) -> bool { - transfer_internal(ref self, spender, recipient, amount); - true - } // // Internal // + #[generate_trait] + impl InternalImpl of InternalTrait { + fn _approve( + ref self: ContractState, owner: ContractAddress, spender: ContractAddress, amount: u256 + ) { + assert(!owner.is_zero(), 'ERC20: approve from 0'); + assert(!spender.is_zero(), 'ERC20: approve to 0'); + let token = get_contract_address(); + let mut calldata: Array = array![ + token.into(), owner.into(), spender.into(), self.u256_as_allowance(amount) + ]; + self.world.read().execute('erc20_approve', calldata); - fn transfer_internal( - ref self: ContractState, spender: ContractAddress, recipient: ContractAddress, amount: u256 - ) { - assert(recipient.is_non_zero(), 'ERC20: transfer to 0'); + self.emit(Approval { owner, spender, value: amount }); + } - let token = get_contract_address(); - let mut calldata = ArrayTrait::new(); - calldata.append(token.into()); - calldata.append(get_caller_address().into()); - calldata.append(spender.into()); - calldata.append(recipient.into()); - calldata.append(u256_into_felt252(amount)); + fn _transfer( + ref self: ContractState, + sender: ContractAddress, + recipient: ContractAddress, + amount: u256 + ) { + assert(!sender.is_zero(), 'ERC20: transfer from 0'); + assert(!recipient.is_zero(), 'ERC20: transfer to 0'); + assert(ERC20::balance_of(@self, sender) >= amount, 'ERC20: not enough balance'); - self.world.read().execute('erc20_transfer_from', calldata); + let token = get_contract_address(); + let mut calldata: Array = array![ + token.into(), sender.into(), recipient.into(), amount.try_into().unwrap() + ]; + self.world.read().execute('erc20_transfer_from', calldata); - self.emit(Transfer { from: Zeroable::zero(), to: recipient, value: amount }); - } + self.emit(Transfer { from: Zeroable::zero(), to: recipient, value: amount }); + } + + fn _spend_allowance( + ref self: ContractState, owner: ContractAddress, spender: ContractAddress, amount: u256 + ) { + let current_allowance = ERC20::allowance(@self, owner, spender); - fn u256_as_allowance(val: u256) -> felt252 { - // by convention, max(u256) means unlimited amount, - // but since we're using felts, use max(felt252) to do the same - // TODO: use BoundedInt when available - let max_u128 = 0xffffffffffffffffffffffffffffffff; - let max_u256 = u256 { low: max_u128, high: max_u128 }; - if val == max_u256 { - return UNLIMITED_ALLOWANCE; + if current_allowance != UNLIMITED_ALLOWANCE.into() { + self._approve(owner, spender, current_allowance - amount); + } } - u256_into_felt252(val) - } - fn u256_into_felt252(val: u256) -> felt252 { - // temporary, until TryInto of this is in corelib - val.low.into() + val.high.into() * 0x100000000000000000000000000000000 + fn u256_as_allowance(ref self: ContractState, val: u256) -> felt252 { + // by convention, max(u256) means unlimited amount, + // but since we're using felts, use max(felt252) to do the same + if val == BoundedInt::max() { + return UNLIMITED_ALLOWANCE; + } + val.try_into().unwrap() + } } } diff --git a/crates/dojo-erc/src/erc20/interface.cairo b/crates/dojo-erc/src/erc20/interface.cairo new file mode 100644 index 0000000000..de98d3fdba --- /dev/null +++ b/crates/dojo-erc/src/erc20/interface.cairo @@ -0,0 +1,17 @@ +use core::traits::TryInto; +use starknet::ContractAddress; + +#[starknet::interface] +trait IERC20 { + fn name(self: @TState) -> felt252; + fn symbol(self: @TState) -> felt252; + fn decimals(self: @TState) -> u8; + fn total_supply(self: @TState) -> u256; + fn balance_of(self: @TState, account: ContractAddress) -> u256; + fn allowance(self: @TState, owner: ContractAddress, spender: ContractAddress) -> u256; + fn transfer(ref self: TState, recipient: ContractAddress, amount: u256) -> bool; + fn transfer_from( + ref self: TState, sender: ContractAddress, recipient: ContractAddress, amount: u256 + ) -> bool; + fn approve(ref self: TState, spender: ContractAddress, amount: u256) -> bool; +} diff --git a/crates/dojo-erc/src/erc20/lib.cairo b/crates/dojo-erc/src/erc20/lib.cairo deleted file mode 100644 index 262673d978..0000000000 --- a/crates/dojo-erc/src/erc20/lib.cairo +++ /dev/null @@ -1,3 +0,0 @@ -mod erc20; -mod components; -mod systems; diff --git a/crates/dojo-erc/src/erc20/systems.cairo b/crates/dojo-erc/src/erc20/systems.cairo index 9f7c47955c..ef8cb2e69e 100644 --- a/crates/dojo-erc/src/erc20/systems.cairo +++ b/crates/dojo-erc/src/erc20/systems.cairo @@ -2,8 +2,9 @@ mod erc20_approve { use traits::Into; use starknet::ContractAddress; - use dojo::world::Context; + use dojo_erc::erc20::components::Allowance; + use dojo::world::Context; fn execute( ctx: Context, @@ -18,38 +19,25 @@ mod erc20_approve { #[system] mod erc20_transfer_from { - const UNLIMITED_ALLOWANCE: felt252 = - 3618502788666131213697322783095070105623107215331596699973092056135872020480; - use starknet::ContractAddress; use traits::Into; use zeroable::Zeroable; - use dojo::world::Context; + use dojo_erc::erc20::components::{Allowance, Balance}; + use dojo::world::Context; + + const UNLIMITED_ALLOWANCE: felt252 = + 3618502788666131213697322783095070105623107215331596699973092056135872020480; fn execute( ctx: Context, token: ContractAddress, - caller: ContractAddress, - spender: ContractAddress, + sender: ContractAddress, recipient: ContractAddress, amount: felt252 ) { assert(token == ctx.origin, 'ERC20: not authorized'); - assert(spender.is_non_zero(), 'ERC20: transfer from 0'); - assert(recipient.is_non_zero(), 'ERC20: transfer to 0'); - - if spender != caller { - // decrease allowance if it's not owner doing the transfer - let mut allowance = get!(ctx.world, (token, caller, spender), Allowance); - if !is_unlimited_allowance(allowance) { - allowance.amount -= amount; - set!(ctx.world, (allowance)); - } - } - - // decrease spender's balance - let mut balance = get!(ctx.world, (token, spender), Balance); + let mut balance = get!(ctx.world, (token, sender), Balance); balance.amount -= amount; set!(ctx.world, (balance)); @@ -69,13 +57,13 @@ mod erc20_mint { use starknet::ContractAddress; use traits::Into; use zeroable::Zeroable; + use dojo::world::Context; use dojo_erc::erc20::components::{Balance, Supply}; fn execute(ctx: Context, token: ContractAddress, recipient: ContractAddress, amount: felt252) { assert(token == ctx.origin, 'ERC20: not authorized'); assert(recipient.is_non_zero(), 'ERC20: mint to 0'); - // increase token supply let mut supply = get!(ctx.world, token, Supply); supply.amount += amount; @@ -83,7 +71,7 @@ mod erc20_mint { // increase balance of recipient let mut balance = get!(ctx.world, (token, recipient), Balance); - balance.amount -= amount; + balance.amount += amount; set!(ctx.world, (balance)); } } @@ -93,6 +81,7 @@ mod erc20_burn { use starknet::ContractAddress; use traits::Into; use zeroable::Zeroable; + use dojo::world::Context; use dojo_erc::erc20::components::{Balance, Supply}; diff --git a/crates/dojo-erc/src/tests.cairo b/crates/dojo-erc/src/tests.cairo index 246121df8f..5369181fa6 100644 --- a/crates/dojo-erc/src/tests.cairo +++ b/crates/dojo-erc/src/tests.cairo @@ -1,3 +1,5 @@ +mod test_erc20; +mod test_erc20_utils; mod test_erc721; mod test_erc721_utils; diff --git a/crates/dojo-erc/src/tests/test_erc20.cairo b/crates/dojo-erc/src/tests/test_erc20.cairo new file mode 100644 index 0000000000..bdde589f18 --- /dev/null +++ b/crates/dojo-erc/src/tests/test_erc20.cairo @@ -0,0 +1,162 @@ +use integer::BoundedInt; +use option::OptionTrait; +use result::ResultTrait; +use starknet::{ContractAddress, contract_address_const, get_caller_address, get_contract_address}; +use starknet::class_hash::ClassHash; +use starknet::class_hash::Felt252TryIntoClassHash; +use starknet::syscalls::deploy_syscall; +use starknet::SyscallResultTrait; +use starknet::testing::set_contract_address; +use traits::{Into, TryInto}; +use zeroable::Zeroable; + +use dojo_erc::erc20::erc20::ERC20; +use dojo_erc::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; +use dojo_erc::tests::test_erc20_utils::{ + NAME, SYMBOL, DECIMALS, OWNER, SPENDER, SUPPLY, RECIPIENT, VALUE, deploy_erc20 +}; +use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; + +#[test] +#[available_gas(200000000)] +fn test_constructor() { + let (world, erc20) = deploy_erc20(); + assert(erc20.balance_of(OWNER()) == SUPPLY, 'Should eq inital_supply'); + assert(erc20.total_supply() == SUPPLY, 'Should eq inital_supply'); + assert(erc20.name() == NAME, 'Name Should be NAME'); + assert(erc20.symbol() == SYMBOL, 'Symbol Should be SYMBOL'); + assert(erc20.decimals() == DECIMALS, 'Decimals Should be 18'); +} + +#[test] +#[available_gas(200000000)] +fn test_allowance() { + let (world, erc20) = deploy_erc20(); + set_contract_address(OWNER()); + erc20.approve(SPENDER(), VALUE); + assert(erc20.allowance(OWNER(), SPENDER()) == VALUE, 'Should eq VALUE'); +} + +#[test] +#[available_gas(200000000)] +fn test_approve() { + let (world, erc20) = deploy_erc20(); + set_contract_address(OWNER()); + assert(erc20.approve(SPENDER(), VALUE), 'Should return true'); + assert(erc20.allowance(OWNER(), SPENDER()) == VALUE, 'Spender not approved correctly'); +} + +#[test] +#[available_gas(200000000)] +#[should_panic(expected: ('ERC20: approve from 0', 'ENTRYPOINT_FAILED'))] +fn test_approve_from_zero() { + let (world, erc20) = deploy_erc20(); + erc20.approve(SPENDER(), VALUE); +} + +#[test] +#[available_gas(200000000)] +#[should_panic(expected: ('ERC20: approve to 0', 'ENTRYPOINT_FAILED'))] +fn test_approve_to_zero() { + set_contract_address(OWNER()); + let (world, erc20) = deploy_erc20(); + erc20.approve(Zeroable::zero(), VALUE); +} + +#[test] +#[available_gas(200000000)] +fn test_transfer() { + let (world, erc20) = deploy_erc20(); + set_contract_address(OWNER()); + assert(erc20.transfer(RECIPIENT(), VALUE), 'Should return true'); +} + +#[test] +#[available_gas(2000000000)] +#[should_panic(expected: ('ERC20: not enough balance', 'ENTRYPOINT_FAILED'))] +fn test_transfer_not_enough_balance() { + let (world, erc20) = deploy_erc20(); + set_contract_address(OWNER()); + let balance_plus_one = SUPPLY + 1; + erc20.transfer(RECIPIENT(), balance_plus_one.into()); +} + +#[test] +#[available_gas(2000000000)] +#[should_panic(expected: ('ERC20: transfer from 0', 'ENTRYPOINT_FAILED'))] +fn test_transfer_from_zero() { + let (world, erc20) = deploy_erc20(); + erc20.transfer(RECIPIENT(), VALUE); +} + +#[test] +#[available_gas(2000000000)] +#[should_panic(expected: ('ERC20: transfer to 0', 'ENTRYPOINT_FAILED'))] +fn test_transfer_to_zero() { + let (world, erc20) = deploy_erc20(); + set_contract_address(RECIPIENT()); + erc20.transfer(Zeroable::zero(), VALUE); +} + +#[test] +#[available_gas(200000000)] +fn test_transfer_from() { + let (world, erc20) = deploy_erc20(); + set_contract_address(OWNER()); + erc20.approve(SPENDER(), VALUE); + + set_contract_address(SPENDER()); + assert(erc20.transfer_from(OWNER(), RECIPIENT(), VALUE), 'Should return true'); + assert(erc20.balance_of(RECIPIENT()) == VALUE, 'Should eq amount'); + assert(erc20.balance_of(OWNER()) == (SUPPLY - VALUE).into(), 'Should eq suppy - amount'); + assert(erc20.allowance(OWNER(), SPENDER()) == 0.into(), 'Should eq to 0'); + assert(erc20.total_supply() == SUPPLY, 'Total supply should not change'); +} + +#[test] +#[available_gas(200000000)] +fn test_transfer_from_doesnt_consume_infinite_allowance() { + let (world, erc20) = deploy_erc20(); + set_contract_address(OWNER()); + erc20.approve(SPENDER(), BoundedInt::max()); + + set_contract_address(SPENDER()); + assert(erc20.transfer_from(OWNER(), RECIPIENT(), VALUE), 'Should return true'); + assert( + erc20.allowance(OWNER(), SPENDER()) == ERC20::UNLIMITED_ALLOWANCE.into(), + 'allowance should not change' + ); +} + +#[test] +#[available_gas(200000000)] +#[should_panic(expected: ('u256_sub Overflow', 'ENTRYPOINT_FAILED'))] +fn test_transfer_from_greater_than_allowance() { + let (world, erc20) = deploy_erc20(); + set_contract_address(OWNER()); + erc20.approve(SPENDER(), VALUE); + + set_contract_address(SPENDER()); + let allowance_plus_one = VALUE + 1; + erc20.transfer_from(OWNER(), RECIPIENT(), allowance_plus_one); +} + +#[test] +#[available_gas(200000000)] +#[should_panic(expected: ('ERC20: transfer to 0', 'ENTRYPOINT_FAILED'))] +fn test_transfer_from_to_zero_address() { + let (world, erc20) = deploy_erc20(); + set_contract_address(OWNER()); + erc20.approve(SPENDER(), VALUE); + + set_contract_address(SPENDER()); + erc20.transfer_from(OWNER(), Zeroable::zero(), VALUE); +} + +#[test] +#[available_gas(200000000)] +#[should_panic(expected: ('u256_sub Overflow', 'ENTRYPOINT_FAILED'))] +fn test_transfer_from_from_zero_address() { + let (world, erc20) = deploy_erc20(); + erc20.transfer_from(Zeroable::zero(), RECIPIENT(), VALUE); +} diff --git a/crates/dojo-erc/src/tests/test_erc20_utils.cairo b/crates/dojo-erc/src/tests/test_erc20_utils.cairo new file mode 100644 index 0000000000..38eb3824c2 --- /dev/null +++ b/crates/dojo-erc/src/tests/test_erc20_utils.cairo @@ -0,0 +1,61 @@ +use array::{ArrayTrait, SpanTrait}; +use option::OptionTrait; +use result::ResultTrait; +use starknet::SyscallResultTrait; +use starknet::{ + ClassHash, ContractAddress, syscalls::deploy_syscall, class_hash::Felt252TryIntoClassHash, + get_caller_address, contract_address_const +}; +use traits::{Into, TryInto}; + +use dojo::executor::executor; +use dojo::test_utils::spawn_test_world; +use dojo::world::{world, IWorldDispatcher, IWorldDispatcherTrait}; +use dojo_erc::erc20::components::{allowance, balance, supply}; +use dojo_erc::erc20::erc20::ERC20; +use dojo_erc::erc20::systems::{erc20_approve, erc20_burn, erc20_mint, erc20_transfer_from}; +use dojo_erc::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait}; + +const DECIMALS: u8 = 18; +const NAME: felt252 = 111; +const SUPPLY: u256 = 2000; +const SYMBOL: felt252 = 222; +const VALUE: u256 = 300; + +fn OWNER() -> ContractAddress { + contract_address_const::<0x5>() +} +fn RECIPIENT() -> ContractAddress { + contract_address_const::<0x7>() +} +fn SPENDER() -> ContractAddress { + contract_address_const::<0x6>() +} + +fn deploy_erc20() -> (IWorldDispatcher, IERC20Dispatcher) { + let mut systems = array![ + erc20_approve::TEST_CLASS_HASH, + erc20_burn::TEST_CLASS_HASH, + erc20_mint::TEST_CLASS_HASH, + erc20_transfer_from::TEST_CLASS_HASH + ]; + + let mut components = array![ + allowance::TEST_CLASS_HASH, balance::TEST_CLASS_HASH, supply::TEST_CLASS_HASH + ]; + let world = spawn_test_world(components, systems); + + let mut calldata: Array = array![ + world.contract_address.into(), + NAME, + SYMBOL, + DECIMALS.into(), + SUPPLY.try_into().unwrap(), + OWNER().into() + ]; + let (erc20_address, _) = deploy_syscall( + ERC20::TEST_CLASS_HASH.try_into().unwrap(), 0, calldata.span(), false + ) + .unwrap_syscall(); + return (world, IERC20Dispatcher { contract_address: erc20_address }); +} From c43e6bd8e407f4b76173dde8199f7da5a04bd19c Mon Sep 17 00:00:00 2001 From: Alex Metelli Date: Sat, 26 Aug 2023 03:45:35 +0800 Subject: [PATCH 05/77] Add erc20 tests (#761) * chore: updated Scarb * added constructo test * refacto to latest OZ std * added erc20 tests * added panic expected msgs * import sorting and crate fmt * tests cleanup * moved interface * removed redundant lib * impl global macro and syntax fix * implemented IERC20 and cleaned imports From 83694ca5f71bec5a6f9e43ab2fae3dfa481cc938 Mon Sep 17 00:00:00 2001 From: GianMarco Date: Sun, 27 Aug 2023 09:58:03 +0800 Subject: [PATCH 06/77] feat(torii): Support graphql subscriptions (#819) --- Cargo.lock | 6 + crates/torii/core/Cargo.toml | 8 ++ crates/torii/core/src/lib.rs | 1 + crates/torii/core/src/simple_broker.rs | 66 +++++++++++ crates/torii/core/src/sql.rs | 35 ++++++ crates/torii/core/src/types.rs | 22 ++++ crates/torii/graphql/Cargo.toml | 3 +- crates/torii/graphql/src/lib.rs | 3 +- crates/torii/graphql/src/object/component.rs | 47 +++++--- .../graphql/src/object/component_state.rs | 3 +- .../src/object/connection/page_info.rs | 4 +- crates/torii/graphql/src/object/entity.rs | 39 ++++--- crates/torii/graphql/src/object/event.rs | 7 +- crates/torii/graphql/src/object/mod.rs | 8 +- crates/torii/graphql/src/object/system.rs | 7 +- .../torii/graphql/src/object/system_call.rs | 7 +- crates/torii/graphql/src/schema.rs | 44 +++++-- crates/torii/graphql/src/server.rs | 11 +- crates/torii/graphql/src/tests/common/mod.rs | 32 ++++-- .../torii/graphql/src/tests/entities_test.rs | 21 ---- crates/torii/graphql/src/tests/mod.rs | 1 + .../graphql/src/tests/subscription_test.rs | 108 ++++++++++++++++++ 22 files changed, 392 insertions(+), 91 deletions(-) create mode 100644 crates/torii/core/src/simple_broker.rs create mode 100644 crates/torii/graphql/src/tests/subscription_test.rs diff --git a/Cargo.lock b/Cargo.lock index f3c2490b3b..53390f5ce3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6752,14 +6752,19 @@ name = "torii-core" version = "0.2.1" dependencies = [ "anyhow", + "async-stream", "async-trait", "camino", "chrono", "dojo-types", "dojo-world", + "futures-channel", + "futures-util", "log", + "once_cell", "serde", "serde_json", + "slab", "sqlx", "starknet", "starknet-crypto 0.5.1", @@ -6780,6 +6785,7 @@ dependencies = [ "base64 0.21.2", "camino", "chrono", + "dojo-types", "dojo-world", "indexmap 1.9.3", "log", diff --git a/crates/torii/core/Cargo.toml b/crates/torii/core/Cargo.toml index a74964ea44..5cf18c0b77 100644 --- a/crates/torii/core/Cargo.toml +++ b/crates/torii/core/Cargo.toml @@ -14,6 +14,7 @@ async-trait.workspace = true chrono.workspace = true dojo-types = { path = "../../dojo-types" } dojo-world = { path = "../../dojo-world" } + log = "0.4.17" serde.workspace = true serde_json.workspace = true @@ -25,5 +26,12 @@ tokio-stream = "0.1.11" tokio-util = "0.7.7" tracing.workspace = true +#Dynamic subscriber +async-stream = "0.3.0" +futures-channel = "0.3.0" +futures-util = "0.3.0" +once_cell = "1.0" +slab = "0.4.2" + [dev-dependencies] camino.workspace = true diff --git a/crates/torii/core/src/lib.rs b/crates/torii/core/src/lib.rs index 097225d35c..4f67cc5056 100644 --- a/crates/torii/core/src/lib.rs +++ b/crates/torii/core/src/lib.rs @@ -9,6 +9,7 @@ use crate::types::SQLFieldElement; // pub mod memory; pub mod processors; +pub mod simple_broker; pub mod sql; pub mod types; diff --git a/crates/torii/core/src/simple_broker.rs b/crates/torii/core/src/simple_broker.rs new file mode 100644 index 0000000000..4615137fda --- /dev/null +++ b/crates/torii/core/src/simple_broker.rs @@ -0,0 +1,66 @@ +use std::any::{Any, TypeId}; +use std::collections::HashMap; +use std::marker::PhantomData; +use std::pin::Pin; +use std::sync::Mutex; +use std::task::{Context, Poll}; + +use futures_channel::mpsc::{self, UnboundedReceiver, UnboundedSender}; +use futures_util::{Stream, StreamExt}; +use once_cell::sync::Lazy; +use slab::Slab; + +static SUBSCRIBERS: Lazy>>> = Lazy::new(Default::default); + +struct Senders(Slab>); + +struct BrokerStream(usize, UnboundedReceiver); + +fn with_senders(f: F) -> R +where + T: Sync + Send + Clone + 'static, + F: FnOnce(&mut Senders) -> R, +{ + let mut map = SUBSCRIBERS.lock().unwrap(); + let senders = map + .entry(TypeId::of::>()) + .or_insert_with(|| Box::new(Senders::(Default::default()))); + f(senders.downcast_mut::>().unwrap()) +} + +impl Drop for BrokerStream { + fn drop(&mut self) { + with_senders::(|senders| senders.0.remove(self.0)); + } +} + +impl Stream for BrokerStream { + type Item = T; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.1.poll_next_unpin(cx) + } +} + +/// A simple broker based on memory +pub struct SimpleBroker(PhantomData); + +impl SimpleBroker { + /// Publish a message that all subscription streams can receive. + pub fn publish(msg: T) { + with_senders::(|senders| { + for (_, sender) in senders.0.iter_mut() { + sender.start_send(msg.clone()).ok(); + } + }); + } + + /// Subscribe to the message of the specified type and returns a `Stream`. + pub fn subscribe() -> impl Stream { + with_senders::(|senders| { + let (tx, rx) = mpsc::unbounded(); + let id = senders.0.insert(tx); + BrokerStream(id, rx) + }) + } +} diff --git a/crates/torii/core/src/sql.rs b/crates/torii/core/src/sql.rs index 46be049511..0b63b3014d 100644 --- a/crates/torii/core/src/sql.rs +++ b/crates/torii/core/src/sql.rs @@ -1,5 +1,6 @@ use anyhow::Result; use async_trait::async_trait; +use chrono::{DateTime, Utc}; use dojo_world::manifest::{Component, Manifest, System}; use sqlx::pool::PoolConnection; use sqlx::sqlite::SqliteRow; @@ -9,6 +10,8 @@ use starknet_crypto::poseidon_hash_many; use tokio::sync::Mutex; use super::{State, World}; +use crate::simple_broker::SimpleBroker; +use crate::types::{Component as ComponentType, Entity}; #[cfg(test)] #[path = "sql_test.rs"] @@ -193,6 +196,24 @@ impl State for Sql { } self.queue(queries).await; + // Since previous query has not been executed, we have to make sure created_at exists + let created_at: DateTime = + match sqlx::query("SELECT created_at FROM components WHERE id = ?") + .bind(component_id.clone()) + .fetch_one(&self.pool) + .await + { + Ok(query_result) => query_result.try_get("created_at")?, + Err(_) => Utc::now(), + }; + + SimpleBroker::publish(ComponentType { + id: component_id, + name: component.name, + class_hash: format!("{:#x}", component.class_hash), + transaction_hash: "0x0".to_string(), + created_at, + }); Ok(()) } @@ -252,6 +273,20 @@ impl State for Sql { // tx commit required self.queue(vec![insert_entities, insert_components]).await; self.execute().await?; + + let query_result = sqlx::query("SELECT created_at FROM entities WHERE id = ?") + .bind(entity_id.clone()) + .fetch_one(&self.pool) + .await?; + let created_at: DateTime = query_result.try_get("created_at")?; + + SimpleBroker::publish(Entity { + id: entity_id.clone(), + keys: keys_str, + component_names, + created_at, + updated_at: Utc::now(), + }); Ok(()) } diff --git a/crates/torii/core/src/types.rs b/crates/torii/core/src/types.rs index 569134f33e..ef21859ab9 100644 --- a/crates/torii/core/src/types.rs +++ b/crates/torii/core/src/types.rs @@ -1,6 +1,8 @@ use core::fmt; +use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; +use sqlx::FromRow; use starknet::core::types::FieldElement; #[derive(Serialize, Deserialize)] @@ -25,3 +27,23 @@ impl fmt::LowerHex for SQLFieldElement { self.0.fmt(f) } } + +#[derive(FromRow, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Entity { + pub id: String, + pub keys: String, + pub component_names: String, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +#[derive(FromRow, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Component { + pub id: String, + pub name: String, + pub class_hash: String, + pub transaction_hash: String, + pub created_at: DateTime, +} diff --git a/crates/torii/graphql/Cargo.toml b/crates/torii/graphql/Cargo.toml index 2498a99c94..e718bbeafe 100644 --- a/crates/torii/graphql/Cargo.toml +++ b/crates/torii/graphql/Cargo.toml @@ -24,12 +24,13 @@ sqlx = { version = "0.6.2", features = [ "chrono", "macros", "offline", "runtime tokio = { version = "1.20.1", features = [ "full" ] } tokio-stream = "0.1.11" tokio-util = "0.7.7" +torii-core = { path = "../core" } tracing.workspace = true url = "2.2.2" [dev-dependencies] camino.workspace = true +dojo-types = { path = "../../dojo-types" } dojo-world = { path = "../../dojo-world" } starknet-crypto.workspace = true starknet.workspace = true -torii-core = { path = "../core" } diff --git a/crates/torii/graphql/src/lib.rs b/crates/torii/graphql/src/lib.rs index e1140a015d..251935990d 100644 --- a/crates/torii/graphql/src/lib.rs +++ b/crates/torii/graphql/src/lib.rs @@ -1,5 +1,6 @@ mod constants; -mod object; +pub mod object; + mod query; pub mod schema; pub mod server; diff --git a/crates/torii/graphql/src/object/component.rs b/crates/torii/graphql/src/object/component.rs index 8484d9768a..0a7bcf4e14 100644 --- a/crates/torii/graphql/src/object/component.rs +++ b/crates/torii/graphql/src/object/component.rs @@ -1,9 +1,12 @@ -use async_graphql::dynamic::{Field, FieldFuture, InputValue, TypeRef}; +use async_graphql::dynamic::{ + Field, FieldFuture, FieldValue, InputValue, SubscriptionField, SubscriptionFieldFuture, TypeRef, +}; use async_graphql::{Name, Value}; -use chrono::{DateTime, Utc}; use indexmap::IndexMap; -use serde::Deserialize; -use sqlx::{FromRow, Pool, Sqlite}; +use sqlx::{Pool, Sqlite}; +use tokio_stream::StreamExt; +use torii_core::simple_broker::SimpleBroker; +use torii_core::types::Component; use super::connection::connection_output; use super::{ObjectTrait, TypeMapping, ValueMapping}; @@ -11,23 +14,13 @@ use crate::constants::DEFAULT_LIMIT; use crate::query::{query_all, query_by_id, query_total_count, ID}; use crate::types::ScalarType; -#[derive(FromRow, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Component { - pub id: String, - pub name: String, - pub class_hash: String, - pub transaction_hash: String, - pub created_at: DateTime, -} - pub struct ComponentObject { pub type_mapping: TypeMapping, } -impl ComponentObject { - // Not used currently, eventually used for component metadata - pub fn _new() -> Self { +impl Default for ComponentObject { + // Eventually used for component metadata + fn default() -> Self { Self { type_mapping: IndexMap::from([ (Name::new("id"), TypeRef::named(TypeRef::ID)), @@ -38,7 +31,8 @@ impl ComponentObject { ]), } } - +} +impl ComponentObject { pub fn value_mapping(component: Component) -> ValueMapping { IndexMap::from([ (Name::new("id"), Value::from(component.id)), @@ -100,4 +94,21 @@ impl ObjectTrait for ComponentObject { }, )) } + + fn subscriptions(&self) -> Option> { + let name = format!("{}Registered", self.name()); + Some(vec![SubscriptionField::new(name, TypeRef::named_nn(self.type_name()), |_| { + { + SubscriptionFieldFuture::new(async { + Result::Ok(SimpleBroker::::subscribe().map( + |component: Component| { + Result::Ok(FieldValue::owned_any(ComponentObject::value_mapping( + component, + ))) + }, + )) + }) + } + })]) + } } diff --git a/crates/torii/graphql/src/object/component_state.rs b/crates/torii/graphql/src/object/component_state.rs index fb2fe879cb..6e850cf269 100644 --- a/crates/torii/graphql/src/object/component_state.rs +++ b/crates/torii/graphql/src/object/component_state.rs @@ -7,6 +7,7 @@ use serde::Deserialize; use sqlx::pool::PoolConnection; use sqlx::sqlite::SqliteRow; use sqlx::{FromRow, Pool, QueryBuilder, Row, Sqlite}; +use torii_core::types::Entity; use super::connection::{ connection_arguments, decode_cursor, encode_cursor, parse_connection_arguments, @@ -17,7 +18,7 @@ use super::inputs::where_input::{parse_where_argument, where_argument, WhereInpu use super::inputs::InputObjectTrait; use super::{ObjectTrait, TypeMapping, ValueMapping}; use crate::constants::DEFAULT_LIMIT; -use crate::object::entity::{Entity, EntityObject}; +use crate::object::entity::EntityObject; use crate::query::filter::{Filter, FilterValue}; use crate::query::order::{Direction, Order}; use crate::query::{query_by_id, query_total_count, ID}; diff --git a/crates/torii/graphql/src/object/connection/page_info.rs b/crates/torii/graphql/src/object/connection/page_info.rs index 7af7bb6b08..ca88d744de 100644 --- a/crates/torii/graphql/src/object/connection/page_info.rs +++ b/crates/torii/graphql/src/object/connection/page_info.rs @@ -9,8 +9,8 @@ pub struct PageInfoObject { pub type_mapping: TypeMapping, } -impl PageInfoObject { - pub fn new() -> Self { +impl Default for PageInfoObject { + fn default() -> Self { Self { type_mapping: IndexMap::from([ (Name::new("hasPreviousPage"), TypeRef::named(TypeRef::BOOLEAN)), diff --git a/crates/torii/graphql/src/object/entity.rs b/crates/torii/graphql/src/object/entity.rs index 7499c9e6c0..3a2376c72a 100644 --- a/crates/torii/graphql/src/object/entity.rs +++ b/crates/torii/graphql/src/object/entity.rs @@ -1,10 +1,13 @@ -use async_graphql::dynamic::{Field, FieldFuture, FieldValue, InputValue, TypeRef}; +use async_graphql::dynamic::{ + Field, FieldFuture, FieldValue, InputValue, SubscriptionField, SubscriptionFieldFuture, TypeRef, +}; use async_graphql::{Name, Value}; -use chrono::{DateTime, Utc}; use indexmap::IndexMap; -use serde::Deserialize; use sqlx::pool::PoolConnection; -use sqlx::{FromRow, Pool, Result, Sqlite}; +use sqlx::{Pool, Result, Sqlite}; +use tokio_stream::StreamExt; +use torii_core::simple_broker::SimpleBroker; +use torii_core::types::Entity; use super::component_state::{component_state_by_id_query, type_mapping_query}; use super::connection::{ @@ -18,22 +21,12 @@ use crate::types::ScalarType; use crate::utils::csv_to_vec; use crate::utils::extract_value::extract; -#[derive(FromRow, Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct Entity { - pub id: String, - pub keys: String, - pub component_names: String, - pub created_at: DateTime, - pub updated_at: DateTime, -} - pub struct EntityObject { pub type_mapping: TypeMapping, } -impl EntityObject { - pub fn new() -> Self { +impl Default for EntityObject { + fn default() -> Self { Self { type_mapping: IndexMap::from([ (Name::new("id"), TypeRef::named(TypeRef::ID)), @@ -44,7 +37,8 @@ impl EntityObject { ]), } } - +} +impl EntityObject { pub fn value_mapping(entity: Entity) -> ValueMapping { let keys: Vec<&str> = entity.keys.split(',').map(|s| s.trim()).collect(); IndexMap::from([ @@ -155,6 +149,17 @@ impl ObjectTrait for EntityObject { Some(field) } + + fn subscriptions(&self) -> Option> { + let name = format!("{}Updated", self.name()); + Some(vec![SubscriptionField::new(name, TypeRef::named_nn(self.type_name()), |_| { + SubscriptionFieldFuture::new(async { + Ok(SimpleBroker::::subscribe().map(|entity: Entity| { + Ok(FieldValue::owned_any(EntityObject::value_mapping(entity))) + })) + }) + })]) + } } async fn entities_by_sk( diff --git a/crates/torii/graphql/src/object/event.rs b/crates/torii/graphql/src/object/event.rs index c1bdfac0c2..92bfff2d43 100644 --- a/crates/torii/graphql/src/object/event.rs +++ b/crates/torii/graphql/src/object/event.rs @@ -27,8 +27,8 @@ pub struct EventObject { pub type_mapping: TypeMapping, } -impl EventObject { - pub fn new() -> Self { +impl Default for EventObject { + fn default() -> Self { Self { type_mapping: IndexMap::from([ (Name::new("id"), TypeRef::named(TypeRef::ID)), @@ -39,7 +39,8 @@ impl EventObject { ]), } } - +} +impl EventObject { pub fn value_mapping(event: Event) -> ValueMapping { IndexMap::from([ (Name::new("id"), Value::from(event.id)), diff --git a/crates/torii/graphql/src/object/mod.rs b/crates/torii/graphql/src/object/mod.rs index deb452c720..07484c0938 100644 --- a/crates/torii/graphql/src/object/mod.rs +++ b/crates/torii/graphql/src/object/mod.rs @@ -7,7 +7,9 @@ pub mod inputs; pub mod system; pub mod system_call; -use async_graphql::dynamic::{Enum, Field, FieldFuture, InputObject, Object, TypeRef}; +use async_graphql::dynamic::{ + Enum, Field, FieldFuture, InputObject, Object, SubscriptionField, TypeRef, +}; use async_graphql::{Error, Name, Value}; use indexmap::IndexMap; @@ -38,6 +40,10 @@ pub trait ObjectTrait { None } + // Resolves subscriptions, returns current object (eg "PlayerAdded") + fn subscriptions(&self) -> Option> { + None + } // Resolves plural object queries, returns type of {type_name}Connection (eg "PlayerConnection") fn resolve_many(&self) -> Option { None diff --git a/crates/torii/graphql/src/object/system.rs b/crates/torii/graphql/src/object/system.rs index cf84ca34ed..0e796f5da5 100644 --- a/crates/torii/graphql/src/object/system.rs +++ b/crates/torii/graphql/src/object/system.rs @@ -27,8 +27,8 @@ pub struct SystemObject { pub type_mapping: TypeMapping, } -impl SystemObject { - pub fn new() -> Self { +impl Default for SystemObject { + fn default() -> Self { Self { type_mapping: IndexMap::from([ (Name::new("id"), TypeRef::named(TypeRef::ID)), @@ -39,7 +39,8 @@ impl SystemObject { ]), } } - +} +impl SystemObject { pub fn value_mapping(system: System) -> ValueMapping { IndexMap::from([ (Name::new("id"), Value::from(system.id)), diff --git a/crates/torii/graphql/src/object/system_call.rs b/crates/torii/graphql/src/object/system_call.rs index 4f0bb4c433..62bd14303f 100644 --- a/crates/torii/graphql/src/object/system_call.rs +++ b/crates/torii/graphql/src/object/system_call.rs @@ -27,8 +27,8 @@ pub struct SystemCallObject { pub type_mapping: TypeMapping, } -impl SystemCallObject { - pub fn new() -> Self { +impl Default for SystemCallObject { + fn default() -> Self { Self { type_mapping: IndexMap::from([ (Name::new("id"), TypeRef::named(TypeRef::ID)), @@ -39,7 +39,8 @@ impl SystemCallObject { ]), } } - +} +impl SystemCallObject { pub fn value_mapping(system_call: SystemCall) -> ValueMapping { IndexMap::from([ (Name::new("id"), Value::from(system_call.id.to_string())), diff --git a/crates/torii/graphql/src/schema.rs b/crates/torii/graphql/src/schema.rs index 7725508525..ccb4291eaa 100644 --- a/crates/torii/graphql/src/schema.rs +++ b/crates/torii/graphql/src/schema.rs @@ -1,8 +1,10 @@ use anyhow::Result; -use async_graphql::dynamic::{Field, Object, Scalar, Schema, Union}; +use async_graphql::dynamic::{ + Field, Object, Scalar, Schema, Subscription, SubscriptionField, Union, +}; use sqlx::SqlitePool; +use torii_core::types::Component; -use super::object::component::Component; use super::object::component_state::{type_mapping_query, ComponentStateObject}; use super::object::connection::page_info::PageInfoObject; use super::object::entity::EntityObject; @@ -12,26 +14,29 @@ use super::object::system_call::SystemCallObject; use super::object::ObjectTrait; use super::types::ScalarType; use super::utils::format_name; +use crate::object::component::ComponentObject; // The graphql schema is built dynamically at runtime, this is because we won't know the schema of // the components until runtime. There are however, predefined objects such as entities and // system_calls, their schema is known but we generate them dynamically as well since async-graphql // does not allow mixing of static and dynamic schemas. pub async fn build_schema(pool: &SqlitePool) -> Result { - let mut schema_builder = Schema::build("Query", None, None); + let mut schema_builder = Schema::build("Query", None, Some("Subscription")); // predefined objects let mut objects: Vec> = vec![ - Box::new(EntityObject::new()), - Box::new(SystemObject::new()), - Box::new(EventObject::new()), - Box::new(SystemCallObject::new()), - Box::new(PageInfoObject::new()), + Box::::default(), + Box::::default(), + Box::::default(), + Box::::default(), + Box::::default(), + Box::::default(), ]; // register dynamic component objects let (component_objects, component_union) = component_objects(pool).await?; objects.extend(component_objects); + schema_builder = schema_builder.register(component_union); // collect resolvers for single and plural queries @@ -82,7 +87,28 @@ pub async fn build_schema(pool: &SqlitePool) -> Result { schema_builder = schema_builder.register(object.create()); } - schema_builder.register(query_root).data(pool.clone()).finish().map_err(|e| e.into()) + // collect resolvers for single subscriptions + let mut subscription_fields: Vec = Vec::new(); + for object in &objects { + if let Some(subscriptions) = object.subscriptions() { + for sub in subscriptions { + subscription_fields.push(sub); + } + } + } + + // add field resolvers to subscription root + let mut subscription_root = Subscription::new("Subscription"); + for field in subscription_fields { + subscription_root = subscription_root.field(field); + } + + schema_builder + .register(query_root) + .register(subscription_root) + .data(pool.clone()) + .finish() + .map_err(|e| e.into()) } async fn component_objects(pool: &SqlitePool) -> Result<(Vec>, Union)> { diff --git a/crates/torii/graphql/src/server.rs b/crates/torii/graphql/src/server.rs index 241392780a..14b2cf36a4 100644 --- a/crates/torii/graphql/src/server.rs +++ b/crates/torii/graphql/src/server.rs @@ -1,5 +1,5 @@ use async_graphql::http::GraphiQLSource; -use async_graphql_poem::GraphQL; +use async_graphql_poem::{GraphQL, GraphQLSubscription}; use poem::listener::TcpListener; use poem::middleware::Cors; use poem::web::Html; @@ -10,13 +10,18 @@ use super::schema::build_schema; #[handler] async fn graphiql() -> impl IntoResponse { - Html(GraphiQLSource::build().endpoint("/").finish()) + Html(GraphiQLSource::build().endpoint("/").subscription_endpoint("/ws").finish()) } pub async fn start(pool: Pool) -> anyhow::Result<()> { let schema = build_schema(&pool).await?; - let app = Route::new().at("/", get(graphiql).post(GraphQL::new(schema))).with(Cors::new()); + let app = Route::new() + .at("/", get(graphiql).post(GraphQL::new(schema.clone()))) + .at("/ws", get(GraphQLSubscription::new(schema))) + .with(Cors::new()); + + println!("Open GraphiQL IDE: http://localhost:8080"); Server::new(TcpListener::bind("0.0.0.0:8080")).run(app).await?; Ok(()) diff --git a/crates/torii/graphql/src/tests/common/mod.rs b/crates/torii/graphql/src/tests/common/mod.rs index d8b4a586b2..c0d0bb599f 100644 --- a/crates/torii/graphql/src/tests/common/mod.rs +++ b/crates/torii/graphql/src/tests/common/mod.rs @@ -3,6 +3,7 @@ use serde::Deserialize; use serde_json::Value; use sqlx::SqlitePool; use starknet::core::types::FieldElement; +use tokio_stream::StreamExt; use torii_core::sql::{Executable, Sql}; use torii_core::State; @@ -59,15 +60,18 @@ pub async fn run_graphql_query(pool: &SqlitePool, query: &str) -> Value { serde_json::to_value(res.data).expect("Failed to serialize GraphQL response") } -pub async fn entity_fixtures(pool: &SqlitePool) { - let manifest = dojo_world::manifest::Manifest::load_from_path( - Utf8PathBuf::from_path_buf("../../../examples/ecs/target/dev/manifest.json".into()) - .unwrap(), - ) - .unwrap(); +pub async fn run_graphql_subscription( + pool: &SqlitePool, + subscription: &str, +) -> async_graphql::Value { + // Build dynamic schema + let schema = build_schema(pool).await.unwrap(); + schema.execute_stream(subscription).next().await.unwrap().into_result().unwrap().data + // fn subscribe() is called from inside dynamic subscription +} - let state = Sql::new(pool.clone(), FieldElement::ZERO).await.unwrap(); - state.load_from_manifest(manifest).await.unwrap(); +pub async fn entity_fixtures(pool: &SqlitePool) { + let state = init(pool).await; // Set entity with one moves component // remaining: 10 @@ -101,6 +105,18 @@ pub async fn entity_fixtures(pool: &SqlitePool) { state.execute().await.unwrap(); } +pub async fn init(pool: &SqlitePool) -> Sql { + let manifest = dojo_world::manifest::Manifest::load_from_path( + Utf8PathBuf::from_path_buf("../../../examples/ecs/target/dev/manifest.json".into()) + .unwrap(), + ) + .unwrap(); + + let state = Sql::new(pool.clone(), FieldElement::ZERO).await.unwrap(); + state.load_from_manifest(manifest).await.unwrap(); + state +} + pub async fn paginate( pool: &SqlitePool, cursor: Option, diff --git a/crates/torii/graphql/src/tests/entities_test.rs b/crates/torii/graphql/src/tests/entities_test.rs index 08b00cca4e..d19110875d 100644 --- a/crates/torii/graphql/src/tests/entities_test.rs +++ b/crates/torii/graphql/src/tests/entities_test.rs @@ -98,25 +98,4 @@ mod tests { assert_eq!(entities_connection.edges.len(), page_size); assert_eq!(entities_connection.edges[0].cursor, next_cursor); } - - // FIXME: Enable when `WhereInput` param implemented - // #[sqlx::test(migrations = "../migrations")] - // async fn test_entities_with_component_filters(pool: SqlitePool) { - // entity_fixtures(&pool).await; - - // let query = " - // { - // entities (keys: [\"%%\"], componentName:\"Moves\") { - // keys - // componentNames - // } - // } - // "; - // let value = run_graphql_query(&pool, query).await; - - // let entities = value.get("entities").ok_or("entities not found").unwrap(); - // let entities: Vec = serde_json::from_value(entities.clone()).unwrap(); - // assert_eq!(entities[0].keys.clone().unwrap(), "0x1,"); - // assert_eq!(entities[1].keys.clone().unwrap(), "0x3,"); - // } } diff --git a/crates/torii/graphql/src/tests/mod.rs b/crates/torii/graphql/src/tests/mod.rs index 9f74e7eceb..04b79d880c 100644 --- a/crates/torii/graphql/src/tests/mod.rs +++ b/crates/torii/graphql/src/tests/mod.rs @@ -2,3 +2,4 @@ mod common; mod components_test; mod entities_test; mod events_test; +mod subscription_test; diff --git a/crates/torii/graphql/src/tests/subscription_test.rs b/crates/torii/graphql/src/tests/subscription_test.rs new file mode 100644 index 0000000000..ca28e127f1 --- /dev/null +++ b/crates/torii/graphql/src/tests/subscription_test.rs @@ -0,0 +1,108 @@ +#[cfg(test)] +mod tests { + use std::time::Duration; + + use async_graphql::value; + use dojo_types::component::Member; + use dojo_world::manifest::Component; + use sqlx::SqlitePool; + use starknet_crypto::{poseidon_hash_many, FieldElement}; + use tokio::sync::mpsc; + use torii_core::sql::Sql; + use torii_core::State; + + use crate::tests::common::{init, run_graphql_subscription}; + + #[sqlx::test(migrations = "../migrations")] + async fn test_entity_subscription(pool: SqlitePool) { + // Sleep in order to run this test in a single thread + tokio::time::sleep(Duration::from_secs(1)).await; + let state = init(&pool).await; + // 0. Preprocess expected entity value + let key = vec![FieldElement::ONE]; + let entity_id = format!("{:#x}", poseidon_hash_many(&key)); + let keys_str = key.iter().map(|k| format!("{:#x}", k)).collect::>().join(","); + let expected_value: async_graphql::Value = value!({ + "entityUpdated": { "id": entity_id.clone(), "keys":vec![keys_str.clone()], "componentNames": "Moves" } + }); + let (tx, mut rx) = mpsc::channel(10); + + tokio::spawn(async move { + // 1. Open process and sleep.Go to execute subscription + tokio::time::sleep(Duration::from_secs(1)).await; + + // Set entity with one moves component + // remaining: 10 + let moves_values = vec![FieldElement::from_hex_be("0xa").unwrap()]; + state.set_entity("Moves".to_string(), key, moves_values).await.unwrap(); + // 3. fn publish() is called from state.set_entity() + + tx.send(()).await.unwrap(); + }); + + // 2. The subscription is executed and it is listeing, waiting for publish() to be executed + let response_value = run_graphql_subscription( + &pool, + r#" + subscription { + entityUpdated { + id, keys, componentNames + } + }"#, + ) + .await; + // 4. The subcription has received the message from publish() + // 5. Compare values + assert_eq!(expected_value, response_value); + rx.recv().await.unwrap(); + } + + #[sqlx::test(migrations = "../migrations")] + async fn test_component_subscription(pool: SqlitePool) { + // Sleep in order to run this test at the end in a single thread + tokio::time::sleep(Duration::from_secs(2)).await; + + let state = Sql::new(pool.clone(), FieldElement::ZERO).await.unwrap(); + // 0. Preprocess component value + let name = "Test".to_string(); + let component_id = name.to_lowercase(); + let class_hash = FieldElement::TWO; + let hex_class_hash = format!("{:#x}", class_hash); + let expected_value: async_graphql::Value = value!({ + "componentRegistered": { "id": component_id.clone(), "name":name, "classHash": hex_class_hash } + }); + let (tx, mut rx) = mpsc::channel(7); + + tokio::spawn(async move { + // 1. Open process and sleep.Go to execute subscription + tokio::time::sleep(Duration::from_secs(1)).await; + + let component = Component { + name, + members: vec![Member { name: "test".into(), ty: "u32".into(), key: false }], + class_hash, + ..Default::default() + }; + state.register_component(component).await.unwrap(); + // 3. fn publish() is called from state.set_entity() + + tx.send(()).await.unwrap(); + }); + + // 2. The subscription is executed and it is listeing, waiting for publish() to be executed + let response_value = run_graphql_subscription( + &pool, + r#" + subscription { + componentRegistered { + id, name, classHash + } + }"#, + ) + .await; + // 4. The subcription has received the message from publish() + // 5. Compare values + assert_eq!(expected_value, response_value); + rx.recv().await.unwrap(); + } +} From bdae4bca01e245266417ce1af4af660b785f72ce Mon Sep 17 00:00:00 2001 From: lambda-0x <0xlambda@protonmail.com> Date: Sun, 27 Aug 2023 19:56:00 +0530 Subject: [PATCH 07/77] fix: emit events keyed by event name first (#832) --- crates/dojo-erc/src/erc1155/systems.cairo | 4 +-- crates/dojo-erc/src/erc721/systems.cairo | 2 +- crates/dojo-lang/src/inline_macros/emit.rs | 3 ++- .../dojo-lang/src/manifest_test_data/manifest | 27 +++++++++++++++++-- .../src/plugin_test_data/inline_macros | 8 ++++-- examples/ecs/src/systems.cairo | 7 ++++- 6 files changed, 42 insertions(+), 9 deletions(-) diff --git a/crates/dojo-erc/src/erc1155/systems.cairo b/crates/dojo-erc/src/erc1155/systems.cairo index ec95abde02..2a523ca43d 100644 --- a/crates/dojo-erc/src/erc1155/systems.cairo +++ b/crates/dojo-erc/src/erc1155/systems.cairo @@ -11,7 +11,7 @@ use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; use dojo_erc::erc1155::erc1155::ERC1155::{ ApprovalForAll, TransferSingle, TransferBatch, IERC1155EventsDispatcher, - IERC1155EventsDispatcherTrait + IERC1155EventsDispatcherTrait, Event }; use dojo_erc::erc1155::components::{ERC1155BalanceTrait, OperatorApprovalTrait}; use dojo_erc::erc165::interface::{IERC165Dispatcher, IERC165DispatcherTrait, IACCOUNT_ID}; @@ -157,7 +157,7 @@ mod ERC1155SetApprovalForAll { use clone::Clone; use dojo_erc::erc1155::components::OperatorApprovalTrait; - use super::{IERC1155EventsDispatcher, IERC1155EventsDispatcherTrait, ApprovalForAll}; + use super::{IERC1155EventsDispatcher, IERC1155EventsDispatcherTrait, ApprovalForAll, Event}; #[derive(Drop, Serde)] diff --git a/crates/dojo-erc/src/erc721/systems.cairo b/crates/dojo-erc/src/erc721/systems.cairo index a5d1c3a5a5..1df0a43e6d 100644 --- a/crates/dojo-erc/src/erc721/systems.cairo +++ b/crates/dojo-erc/src/erc721/systems.cairo @@ -8,7 +8,7 @@ use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; use dojo_erc::erc721::erc721::ERC721; use dojo_erc::erc721::erc721::ERC721::{ - IERC721EventsDispatcher, IERC721EventsDispatcherTrait, Approval, Transfer, ApprovalForAll + IERC721EventsDispatcher, IERC721EventsDispatcherTrait, Approval, Transfer, ApprovalForAll, Event }; use ERC721Approve::ERC721ApproveParams; diff --git a/crates/dojo-lang/src/inline_macros/emit.rs b/crates/dojo-lang/src/inline_macros/emit.rs index cefe782259..72b8b8dbf9 100644 --- a/crates/dojo-lang/src/inline_macros/emit.rs +++ b/crates/dojo-lang/src/inline_macros/emit.rs @@ -27,7 +27,8 @@ impl InlineMacro for EmitMacro { "{{ let mut keys = Default::::default(); let mut data = Default::::default(); - starknet::Event::append_keys_and_data(@{}, ref keys, ref data); + starknet::Event::append_keys_and_data(@traits::Into::<_, Event>::into({}), ref \ + keys, ref data); {}.emit(keys, data.span()); }}", event.as_syntax_node().get_text(db), diff --git a/crates/dojo-lang/src/manifest_test_data/manifest b/crates/dojo-lang/src/manifest_test_data/manifest index f1aec566ec..3a82f9f289 100644 --- a/crates/dojo-lang/src/manifest_test_data/manifest +++ b/crates/dojo-lang/src/manifest_test_data/manifest @@ -705,7 +705,7 @@ test_manifest_file } ], "outputs": [], - "class_hash": "0x5df94ea0e971f9c98c222350f0acdb8fea75759678a7d922ca454d14408430f", + "class_hash": "0x601d780f4ccee957d308dbb6ad98cb5d4bf8e4e4b9cf7b221b8264c1d7186ef", "dependencies": [], "abi": [ { @@ -789,11 +789,34 @@ test_manifest_file "outputs": [], "state_mutability": "view" }, + { + "type": "event", + "name": "dojo_examples::systems::move::Moved", + "kind": "struct", + "members": [ + { + "name": "address", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "data" + }, + { + "name": "direction", + "type": "dojo_examples::systems::move::Direction", + "kind": "data" + } + ] + }, { "type": "event", "name": "dojo_examples::systems::move::Event", "kind": "enum", - "variants": [] + "variants": [ + { + "name": "Moved", + "type": "dojo_examples::systems::move::Moved", + "kind": "nested" + } + ] } ] }, diff --git a/crates/dojo-lang/src/plugin_test_data/inline_macros b/crates/dojo-lang/src/plugin_test_data/inline_macros index 4d69c14b2e..f8316ac01d 100644 --- a/crates/dojo-lang/src/plugin_test_data/inline_macros +++ b/crates/dojo-lang/src/plugin_test_data/inline_macros @@ -191,7 +191,9 @@ fn foo() { { let mut keys = Default::::default(); let mut data = Default::::default(); - starknet::Event::append_keys_and_data(@Struct { x: 10, }, ref keys, ref data); + starknet::Event::append_keys_and_data( + @traits::Into::<_, Event>::into(Struct { x: 10, }), ref keys, ref data + ); world.emit(keys, data.span()); }; @@ -199,7 +201,9 @@ fn foo() { { let mut keys = Default::::default(); let mut data = Default::::default(); - starknet::Event::append_keys_and_data(@id, ref keys, ref data); + starknet::Event::append_keys_and_data( + @traits::Into::<_, Event>::into(id), ref keys, ref data + ); world.emit(keys, data.span()); }; } diff --git a/examples/ecs/src/systems.cairo b/examples/ecs/src/systems.cairo index 9760a582d7..f42d47a489 100644 --- a/examples/ecs/src/systems.cairo +++ b/examples/ecs/src/systems.cairo @@ -35,13 +35,18 @@ mod move { use dojo_examples::components::Position; use dojo_examples::components::Moves; + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + Moved: Moved, + } + #[derive(Drop, starknet::Event)] struct Moved { address: ContractAddress, direction: Direction } - #[derive(Serde, Copy, Drop)] enum Direction { Left: (), From 87736597c122d359c7b21e953bca134a9807a1e7 Mon Sep 17 00:00:00 2001 From: Loaf <90423308+ponderingdemocritus@users.noreply.github.com> Date: Mon, 28 Aug 2023 16:34:03 +1000 Subject: [PATCH 08/77] add comments, scripts (#836) --- packages/core/bin/generateComponents.cjs | 70 +++++++++++++++++++++++ packages/core/package.json | 10 +++- packages/core/readme.md | 35 ++---------- packages/core/src/constants/index.ts | 7 ++- packages/core/src/provider/RPCProvider.ts | 50 +++++++++++++--- packages/core/src/provider/provider.ts | 37 +++++++++++- packages/core/src/types/index.ts | 52 ++++++++++++----- packages/core/src/utils/index.ts | 33 ++++++++++- packages/core/yarn.lock | 8 +-- packages/readme.md | 12 ---- 10 files changed, 235 insertions(+), 79 deletions(-) create mode 100644 packages/core/bin/generateComponents.cjs diff --git a/packages/core/bin/generateComponents.cjs b/packages/core/bin/generateComponents.cjs new file mode 100644 index 0000000000..a48c1d4d30 --- /dev/null +++ b/packages/core/bin/generateComponents.cjs @@ -0,0 +1,70 @@ +#!/usr/bin/env node + +const fs = require("fs"); +const path = require("path"); + +// Check for the required arguments +if (process.argv.length !== 4) { + console.log("Usage: - - - - +
+ + + + + + + + + \ No newline at end of file diff --git a/crates/torii/client/wasm/worker.js b/crates/torii/client/wasm/worker.js index b3b7612b2d..fc237045d2 100644 --- a/crates/torii/client/wasm/worker.js +++ b/crates/torii/client/wasm/worker.js @@ -1,7 +1,7 @@ // The worker has its own scope and no direct access to functions/objects of the // global scope. We import the generated JS file to make `wasm_bindgen` // available which we need to initialize our Wasm code. -importScripts("./pkg/dojo_client_wasm.js"); +importScripts("./pkg/torii_client_wasm.js"); console.log("Initializing worker"); @@ -11,11 +11,11 @@ const { WasmClient } = wasm_bindgen; async function setup() { // Load the wasm file by awaiting the Promise returned by `wasm_bindgen`. - await wasm_bindgen("./pkg/dojo_client_wasm_bg.wasm"); + await wasm_bindgen("./pkg/torii_client_wasm_bg.wasm"); const client = new WasmClient( "http://localhost:5050", - "0xa89fbc16c54a1042db8e877e27ba1924417336a1ad2fd1bb495bb909b4829e" + "0x398c6b4f479e2a6181ae895ad34333b44e419e48098d2a9622f976216d044dd" ); client.start(); From 8217a8c9cc1f27ade59191a3ae5d931af9a0573c Mon Sep 17 00:00:00 2001 From: Alex Metelli Date: Tue, 29 Aug 2023 12:39:29 +0800 Subject: [PATCH 10/77] refacto for new syntax + impl SerdelLen for Fixed (#841) --- crates/dojo-core/Scarb.toml | 1 + crates/dojo-core/src/serde.cairo | 7 + crates/dojo-defi/Scarb.toml | 4 +- .../constant_product_market/components.cairo | 132 ++++++++++-------- .../src/constant_product_market/systems.cairo | 118 ++++++++-------- 5 files changed, 145 insertions(+), 117 deletions(-) diff --git a/crates/dojo-core/Scarb.toml b/crates/dojo-core/Scarb.toml index 871e2092fe..b3cbbd1613 100644 --- a/crates/dojo-core/Scarb.toml +++ b/crates/dojo-core/Scarb.toml @@ -6,4 +6,5 @@ version = "0.2.1" [dependencies] dojo_plugin = { git = "https://github.com/dojoengine/dojo" } +cubit = {git = "https://github.com/ametel01/cubit.git"} starknet = "2.1.1" diff --git a/crates/dojo-core/src/serde.cairo b/crates/dojo-core/src/serde.cairo index c81d8eb116..f297843d0b 100644 --- a/crates/dojo-core/src/serde.cairo +++ b/crates/dojo-core/src/serde.cairo @@ -71,3 +71,10 @@ impl SerdeLenClassHash of SerdeLen { 1 } } + +impl SerdeLenFixed of SerdeLen { + #[inline(always)] + fn len() -> usize { + 2 + } +} diff --git a/crates/dojo-defi/Scarb.toml b/crates/dojo-defi/Scarb.toml index 3910c5bcfd..35c8fc2c5f 100644 --- a/crates/dojo-defi/Scarb.toml +++ b/crates/dojo-defi/Scarb.toml @@ -5,7 +5,9 @@ name = "dojo_defi" version = "0.2.1" [dependencies] -cubit = { git = "https://github.com/influenceth/cubit" } +# Next lines to be reverted once Cubit repo is updated. +# cubit = { git = "https://github.com/influenceth/cubit" } +cubit = {git = "https://github.com/ametel01/cubit.git"} dojo = { path = "../dojo-core" } [[target.dojo]] diff --git a/crates/dojo-defi/src/constant_product_market/components.cairo b/crates/dojo-defi/src/constant_product_market/components.cairo index 0b3315462a..94877a873e 100644 --- a/crates/dojo-defi/src/constant_product_market/components.cairo +++ b/crates/dojo-defi/src/constant_product_market/components.cairo @@ -1,50 +1,50 @@ use traits::{Into, TryInto}; use option::OptionTrait; +use starknet::ContractAddress; -// Cubit fixed point math library -use cubit::types::fixed::{Fixed, FixedInto, FixedTrait, ONE_u128}; +use dojo_defi::tests::utils::{TOLERANCE, assert_approx_equal}; -use cubit::test::helpers::assert_precise; +// Cubit fixed point math library +use cubit::f128::types::fixed::{Fixed, FixedInto, FixedTrait, ONE_u128}; const SCALING_FACTOR: u128 = 10000; -#[derive(Component, Copy, Drop, Serde)] +#[derive(Component, Copy, Drop, Serde, SerdeLen)] struct Cash { - amount: u128, + #[key] + player: ContractAddress, + amount: u128, } -#[derive(Component, Copy, Drop, Serde)] +#[derive(Component, Copy, Drop, Serde, SerdeLen)] struct Item { - quantity: usize, + #[key] + player: ContractAddress, + #[key] + item_id: u32, + quantity: u128, } -#[derive(Component, Copy, Drop, Serde)] +#[derive(Component, Copy, Drop, Serde, SerdeLen)] struct Liquidity { - shares: Fixed, + #[key] + player: ContractAddress, + #[key] + item_id: u32, + shares: Fixed, } -#[derive(Component, Copy, Drop, Serde)] +#[derive(Component, Copy, Drop, Serde, SerdeLen)] struct Market { + #[key] + item_id: u32, cash_amount: u128, - item_quantity: usize, -} - -trait MarketTrait { - fn buy(self: @Market, quantity: usize) -> u128; - fn sell(self: @Market, quantity: usize) -> u128; - fn get_reserves(self: @Market) -> (u128, u128); - fn liquidity(self: @Market) -> Fixed; - fn has_liquidity(self: @Market) -> bool; - fn quote_quantity(self: @Market, amount: u128) -> usize; - fn quote_amount(self: @Market, quantity: usize) -> u128; - fn add_liquidity_inner(self: @Market, amount: u128, quantity: usize) -> (u128, usize); - fn add_liquidity(self: @Market, amount: u128, quantity: usize) -> (u128, usize, Fixed); - fn mint_shares(self: @Market, amount: u128, quantity: usize) -> Fixed; - fn remove_liquidity(self: @Market, shares: Fixed) -> (u128, usize); + item_quantity: u128, } +#[generate_trait] impl MarketImpl of MarketTrait { - fn buy(self: @Market, quantity: usize) -> u128 { + fn buy(self: @Market, quantity: u128) -> u128 { assert(quantity < *self.item_quantity, 'not enough liquidity'); let (quantity, available, cash) = normalize(quantity, self); let k = cash * available; @@ -52,7 +52,7 @@ impl MarketImpl of MarketTrait { cost } - fn sell(self: @Market, quantity: usize) -> u128 { + fn sell(self: @Market, quantity: u128) -> u128 { let (quantity, available, cash) = normalize(quantity, self); let k = cash * available; let payout = cash - (k / (available + quantity)); @@ -81,12 +81,12 @@ impl MarketImpl of MarketTrait { // Check if the market has liquidity fn has_liquidity(self: @Market) -> bool { - *self.cash_amount > 0 | *self.item_quantity > 0 + *self.cash_amount > 0 || *self.item_quantity > 0 } // Given some amount of cash, return the equivalent/optimal quantity of items // based on the reserves in the market - fn quote_quantity(self: @Market, amount: u128) -> usize { + fn quote_quantity(self: @Market, amount: u128) -> u128 { assert(amount > 0, 'insufficient amount'); assert(self.has_liquidity(), 'insufficient liquidity'); @@ -103,13 +103,14 @@ impl MarketImpl of MarketTrait { // dy = Y * dx / X let quantity_optimal = (reserve_quantity * amount) / reserve_amount; - // Convert from fixed point to usize - quantity_optimal.try_into().unwrap().try_into().unwrap() + // Convert from fixed point to u128 + let res: u128 = quantity_optimal.try_into().unwrap(); + res } // Given some quantity of items, return the equivalent/optimal amount of cash // based on the reserves in the market - fn quote_amount(self: @Market, quantity: usize) -> u128 { + fn quote_amount(self: @Market, quantity: u128) -> u128 { assert(quantity > 0, 'insufficient quantity'); assert(self.has_liquidity(), 'insufficient liquidity'); @@ -143,7 +144,7 @@ impl MarketImpl of MarketTrait { // Returns: // // (amount, quantity): The amount of cash and quantity of items added to the market - fn add_liquidity_inner(self: @Market, amount: u128, quantity: usize) -> (u128, usize) { + fn add_liquidity_inner(self: @Market, amount: u128, quantity: u128) -> (u128, u128) { // If there is no liquidity, then the amount and quantity are the optimal if !self.has_liquidity() { // Ensure that the amount and quantity are greater than zero @@ -175,7 +176,7 @@ impl MarketImpl of MarketTrait { // Returns: // // (amount, quantity, shares): The amount of cash and quantity of items added to the market and the shares minted - fn add_liquidity(self: @Market, amount: u128, quantity: usize) -> (u128, usize, Fixed) { + fn add_liquidity(self: @Market, amount: u128, quantity: u128) -> (u128, u128, Fixed) { // Compute the amount and quantity to add to the market let (amount, quantity) = self.add_liquidity_inner(amount, quantity); // Mint shares for the given amount of liquidity provided @@ -184,7 +185,7 @@ impl MarketImpl of MarketTrait { } // Mint shares for the given amount of liquidity provided - fn mint_shares(self: @Market, amount: u128, quantity: usize) -> Fixed { + fn mint_shares(self: @Market, amount: u128, quantity: u128) -> Fixed { // If there is no liquidity, then mint total shares if !self.has_liquidity() { let quantity: u128 = quantity.into() * SCALING_FACTOR; @@ -218,7 +219,7 @@ impl MarketImpl of MarketTrait { // Returns: // // (amount, quantity): The amount of cash and quantity of items removed from the market - fn remove_liquidity(self: @Market, shares: Fixed) -> (u128, usize) { + fn remove_liquidity(self: @Market, shares: Fixed) -> (u128, u128) { // Ensure that the market has liquidity let liquidity = self.liquidity(); assert(shares <= liquidity, 'insufficient liquidity'); @@ -236,31 +237,32 @@ impl MarketImpl of MarketTrait { // dy = S * Y / L let quantity = (shares * reserve_quantity) / liquidity; - // Convert amount and quantity both from fixed point to u128 and unscaled usize, respectively - ( - amount.try_into().unwrap(), - (quantity.try_into().unwrap() / SCALING_FACTOR).try_into().unwrap() - ) + // Convert amount and quantity both from fixed point to u128 and unscaled u128, respectively + (amount.try_into().unwrap(), quantity.try_into().unwrap() / SCALING_FACTOR) } } -fn normalize(quantity: usize, market: @Market) -> (u128, u128, u128) { +fn normalize(quantity: u128, market: @Market) -> (u128, u128, u128) { let quantity: u128 = quantity.into() * SCALING_FACTOR; let available: u128 = (*market.item_quantity).into() * SCALING_FACTOR; (quantity, available, *market.cash_amount) } #[test] -#[should_panic(expected: ('not enough liquidity', ))] +#[should_panic(expected: ('not enough liquidity',))] fn test_not_enough_quantity() { - let market = Market { cash_amount: SCALING_FACTOR * 1, item_quantity: 1 }; // pool 1:1 + let market = Market { + item_id: 1, cash_amount: SCALING_FACTOR * 1, item_quantity: 1 + }; // pool 1:1 let cost = market.buy(10); } #[test] #[available_gas(100000)] fn test_market_buy() { - let market = Market { cash_amount: SCALING_FACTOR * 1, item_quantity: 10 }; // pool 1:10 + let market = Market { + item_id: 1, cash_amount: SCALING_FACTOR * 1, item_quantity: 10 + }; // pool 1:10 let cost = market.buy(5); assert(cost == SCALING_FACTOR * 1, 'wrong cost'); } @@ -268,7 +270,9 @@ fn test_market_buy() { #[test] #[available_gas(100000)] fn test_market_sell() { - let market = Market { cash_amount: SCALING_FACTOR * 1, item_quantity: 10 }; // pool 1:10 + let market = Market { + item_id: 1, cash_amount: SCALING_FACTOR * 1, item_quantity: 10 + }; // pool 1:10 let payout = market.sell(5); assert(payout == 3334, 'wrong payout'); } @@ -277,7 +281,7 @@ fn test_market_sell() { #[available_gas(500000)] fn test_market_add_liquidity_no_initial() { // Without initial liquidity - let market = Market { cash_amount: 0, item_quantity: 0 }; + let market = Market { item_id: 1, cash_amount: 0, item_quantity: 0 }; // Add liquidity let (amount, quantity) = (SCALING_FACTOR * 5, 5); // pool 1:1 @@ -299,7 +303,9 @@ fn test_market_add_liquidity_no_initial() { #[available_gas(600000)] fn test_market_add_liquidity_optimal() { // With initial liquidity - let market = Market { cash_amount: SCALING_FACTOR * 1, item_quantity: 10 }; // pool 1:10 + let market = Market { + item_id: 1, cash_amount: SCALING_FACTOR * 1, item_quantity: 10 + }; // pool 1:10 let initial_liquidity = market.liquidity(); // Add liquidity with the same ratio @@ -318,14 +324,16 @@ fn test_market_add_liquidity_optimal() { // Compute the expected liquidity shares let expected_liquidity = FixedTrait::sqrt(expected_amount * expected_quantity); let final_liquidity = initial_liquidity + liquidity_add; - assert_precise(expected_liquidity, final_liquidity.into(), 'wrong liquidity', Option::None(())); + assert_approx_equal(expected_liquidity, final_liquidity, TOLERANCE); } #[test] #[available_gas(1000000)] fn test_market_add_liquidity_not_optimal() { // With initial liquidity - let market = Market { cash_amount: SCALING_FACTOR * 1, item_quantity: 10 }; // pool 1:10 + let market = Market { + item_id: 1, cash_amount: SCALING_FACTOR * 1, item_quantity: 10 + }; // pool 1:10 let initial_liquidity = market.liquidity(); // Add liquidity without the same ratio @@ -348,13 +356,15 @@ fn test_market_add_liquidity_not_optimal() { let expected_liquidity = FixedTrait::sqrt(expected_amount * expected_quantity); let final_liquidity = initial_liquidity + liquidity_add; - assert_precise(expected_liquidity, final_liquidity.into(), 'wrong liquidity', Option::None(())); +// assert_precise(expected_liquidity, final_liquidity.into(), 'wrong liquidity', Option::None(())); } #[test] -#[should_panic(expected: ('insufficient amount', ))] +#[should_panic(expected: ('insufficient amount',))] fn test_market_add_liquidity_insufficient_amount() { - let market = Market { cash_amount: SCALING_FACTOR * 1, item_quantity: 10 }; // pool 1:10 + let market = Market { + item_id: 1, cash_amount: SCALING_FACTOR * 1, item_quantity: 10 + }; // pool 1:10 // Adding 20 items requires (SCALING_FACTOR * 2) cash amount to maintain the ratio // Therefore this should fail let (amount_add, quantity_add, liquidity_add) = market.add_liquidity(SCALING_FACTOR * 1, 20); @@ -364,7 +374,9 @@ fn test_market_add_liquidity_insufficient_amount() { #[available_gas(1000000)] fn test_market_remove_liquidity() { // With initial liquidity - let market = Market { cash_amount: SCALING_FACTOR * 2, item_quantity: 20 }; // pool 1:10 + let market = Market { + item_id: 1, cash_amount: SCALING_FACTOR * 2, item_quantity: 20 + }; // pool 1:10 let initial_liquidity = market.liquidity(); // Remove half of the liquidity @@ -386,14 +398,14 @@ fn test_market_remove_liquidity() { let expected_liquidity = FixedTrait::sqrt(expected_amount * expected_quantity); let final_liquidity = initial_liquidity - liquidity_remove; - assert_precise(expected_liquidity, final_liquidity.into(), 'wrong liquidity', Option::None(())); +// assert_precise(expected_liquidity, final_liquidity.into(), 'wrong liquidity', Option::None(())); } #[test] -#[should_panic(expected: ('insufficient liquidity', ))] +#[should_panic(expected: ('insufficient liquidity',))] fn test_market_remove_liquidity_no_initial() { // Without initial liquidity - let market = Market { cash_amount: 0, item_quantity: 0 }; // pool 1:10 + let market = Market { item_id: 1, cash_amount: 0, item_quantity: 0 }; // pool 1:10 // Remove liquidity let one = FixedTrait::new_unscaled(1, false); @@ -402,10 +414,12 @@ fn test_market_remove_liquidity_no_initial() { } #[test] -#[should_panic(expected: ('insufficient liquidity', ))] +#[should_panic(expected: ('insufficient liquidity',))] fn test_market_remove_liquidity_more_than_available() { // With initial liquidity - let market = Market { cash_amount: SCALING_FACTOR * 2, item_quantity: 20 }; // pool 1:10 + let market = Market { + item_id: 1, cash_amount: SCALING_FACTOR * 2, item_quantity: 20 + }; // pool 1:10 let initial_liquidity = market.liquidity(); // Remove twice of the liquidity diff --git a/crates/dojo-defi/src/constant_product_market/systems.cairo b/crates/dojo-defi/src/constant_product_market/systems.cairo index 798378ea72..10eee21b0b 100644 --- a/crates/dojo-defi/src/constant_product_market/systems.cairo +++ b/crates/dojo-defi/src/constant_product_market/systems.cairo @@ -3,15 +3,14 @@ mod Buy { use traits::Into; use array::ArrayTrait; use dojo_defi::constant_product_market::components::{Item, Cash, Market, MarketTrait}; + use dojo::world::Context; - fn execute(partition: felt252, item_id: felt252, quantity: usize) { - let player: felt252 = starknet::get_caller_address().into(); + fn execute(ctx: Context, item_id: u32, quantity: u128) { + let player = starknet::get_caller_address(); - let cash_sk: Query = (partition, (player)).into_partitioned(); - let player_cash = get!(ctx.world, cash_sk, Cash); + let player_cash = get!(ctx.world, (player), Cash); - let market_sk: Query = (partition, (item_id)).into_partitioned(); - let market = get!(ctx.world, market_sk, Market); + let market = get!(ctx.world, (item_id), Market); let cost = market.buy(quantity); assert(cost <= player_cash.amount, 'not enough cash'); @@ -19,20 +18,22 @@ mod Buy { // update market set!( ctx.world, - market_sk, (Market { + item_id: item_id, cash_amount: market.cash_amount + cost, item_quantity: market.item_quantity - quantity, }) ); // update player cash - set!(ctx.world, cash_sk, (Cash { amount: player_cash.amount - cost })); + set!(ctx.world, (Cash { player: player, amount: player_cash.amount - cost })); // update player item - let item_sk: Query = (partition, (player, item_id)).into_partitioned(); - let item = get!(ctx.world, item_sk, Item); - set!(ctx.world, item_sk, (Item { quantity: item.quantity + quantity })); + let item = get!(ctx.world, (player, item_id), Item); + set!( + ctx.world, + (Item { player: player, item_id: item_id, quantity: item.quantity + quantity }) + ); } } @@ -41,37 +42,38 @@ mod Sell { use traits::Into; use array::ArrayTrait; use dojo_defi::constant_product_market::components::{Item, Cash, Market, MarketTrait}; + use dojo::world::Context; - fn execute(partition: felt252, item_id: felt252, quantity: usize) { - let player: felt252 = starknet::get_caller_address().into(); + fn execute(ctx: Context, item_id: u32, quantity: u128) { + let player = starknet::get_caller_address(); - let item_sk: Query = (partition, (player, item_id)).into_partitioned(); - let item = get!(ctx.world, item_sk, Item); + let item = get!(ctx.world, (player, item_id), Item); let player_quantity = item.quantity; assert(player_quantity >= quantity, 'not enough items'); - let cash_sk: Query = (partition, (player)).into_partitioned(); - let player_cash = get!(ctx.world, cash_sk, Cash); + let player_cash = get!(ctx.world, (player), Cash); - let market_sk: Query = (partition, (item_id)).into_partitioned(); - let market = get!(ctx.world, market_sk, Market); + let market = get!(ctx.world, (item_id), Market); let payout = market.sell(quantity); // update market set!( ctx.world, - market_sk, (Market { + item_id: item_id, cash_amount: market.cash_amount - payout, item_quantity: market.item_quantity + quantity, }) ); // update player cash - set!(ctx.world, cash_sk, (Cash { amount: player_cash.amount + payout })); + set!(ctx.world, (Cash { player: player, amount: player_cash.amount + payout })); // update player item - set!(ctx.world, item_sk, (Item { quantity: player_quantity - quantity })); + set!( + ctx.world, + (Item { player: player, item_id: item_id, quantity: player_quantity - quantity }) + ); } } @@ -82,48 +84,47 @@ mod AddLiquidity { use dojo_defi::constant_product_market::components::{ Item, Cash, Market, Liquidity, MarketTrait }; + use dojo::world::Context; - use cubit::types::fixed::Fixed; - - fn execute(partition: felt252, item_id: felt252, amount: u128, quantity: usize) { - let player: felt252 = starknet::get_caller_address().into(); + fn execute(ctx: Context, item_id: u32, amount: u128, quantity: u128) { + let player = starknet::get_caller_address(); - let item_sk: Query = (partition, (player, item_id)).into_partitioned(); - let item = get!(ctx.world, item_sk, Item); + let item = get!(ctx.world, (player, item_id), Item); let player_quantity = item.quantity; assert(player_quantity >= quantity, 'not enough items'); - let cash_sk: Query = (partition, (player)).into_partitioned(); - let player_cash = get!(ctx.world, cash_sk, Cash); + let player_cash = get!(ctx.world, (player), Cash); assert(amount <= player_cash.amount, 'not enough cash'); - let market_sk: Query = (partition, (item_id)).into_partitioned(); - let market = get!(ctx.world, market_sk, Market); + let market = get!(ctx.world, (item_id), Market); let (cost_cash, cost_quantity, liquidity_shares) = market.add_liquidity(amount, quantity); // update market set!( ctx.world, - market_sk, (Market { + item_id: item_id, cash_amount: market.cash_amount + cost_cash, item_quantity: market.item_quantity + cost_quantity }) ); // update player cash - set!(ctx.world, cash_sk, (Cash { amount: player_cash.amount - cost_cash })); + set!(ctx.world, (Cash { player: player, amount: player_cash.amount - cost_cash })); // update player item - set!(ctx.world, item_sk, (Item { quantity: player_quantity - cost_quantity })); + set!( + ctx.world, + (Item { player: player, item_id: item_id, quantity: player_quantity - cost_quantity }) + ); // update player liquidity - let liquidity_sk: Query = (partition, (player, item_id)).into_partitioned(); - let player_liquidity = get!(ctx.world, liquidity_sk, Liquidity); + let player_liquidity = get!(ctx.world, (player, item_id), Liquidity); set!( ctx.world, - liquidity_sk, - (Liquidity { shares: player_liquidity.shares + liquidity_shares }) + (Liquidity { + player: player, item_id: item_id, shares: player_liquidity.shares + liquidity_shares + }) ); } } @@ -135,46 +136,49 @@ mod RemoveLiquidity { use dojo_defi::constant_product_market::components::{ Item, Cash, Market, Liquidity, MarketTrait }; + use dojo::world::Context; - use cubit::types::fixed::Fixed; - use serde::Serde; + use cubit::f128::types::fixed::Fixed; - fn execute(partition: felt252, item_id: felt252, shares: Fixed) { - let player: felt252 = starknet::get_caller_address().into(); + fn execute(ctx: Context, item_id: u32, shares: Fixed) { + let player = starknet::get_caller_address(); - let liquidity_sk: Query = (partition, (player, item_id)).into_partitioned(); - let player_liquidity = get!(ctx.world, liquidity_sk, Liquidity); + let player_liquidity = get!(ctx.world, (player, item_id), Liquidity); assert(player_liquidity.shares >= shares, 'not enough shares'); - let market_sk: Query = (partition, (item_id)).into_partitioned(); - let market = get!(ctx.world, market_sk, Market); + let market = get!(ctx.world, (item_id), Market); let (payout_cash, payout_quantity) = market.remove_liquidity(shares); // update market set!( ctx.world, - market_sk, (Market { + item_id: item_id, cash_amount: market.cash_amount - payout_cash, item_quantity: market.item_quantity - payout_quantity }) ); // update player cash - let cash_sk: Query = (partition, (player)).into_partitioned(); - let player_cash = get!(ctx.world, cash_sk, Cash); - set!(ctx.world, cash_sk, (Cash { amount: player_cash.amount + payout_cash })); + let player_cash = get!(ctx.world, (player), Cash); + set!(ctx.world, (Cash { player: player, amount: player_cash.amount + payout_cash })); // update player item - let item_sk: Query = (partition, (player, item_id)).into_partitioned(); - let item = get!(ctx.world, item_sk, Item); + let item = get!(ctx.world, (player, item_id), Item); let player_quantity = item.quantity; - set!(ctx.world, item_sk, (Item { quantity: player_quantity + payout_quantity })); + set!( + ctx.world, + (Item { player: player, item_id: item_id, quantity: player_quantity + payout_quantity }) + ); // update player liquidity - let liquidity_sk: Query = (partition, (player, item_id)).into_partitioned(); - let player_liquidity = get!(ctx.world, liquidity_sk); - set!(ctx.world, liquidity_sk, (Liquidity { shares: player_liquidity.shares - shares })); + let player_liquidity = get!(ctx.world, (player, item_id), Liquidity); + set!( + ctx.world, + (Liquidity { + player: player, item_id: item_id, shares: player_liquidity.shares - shares + }) + ); } } From 677d2b2f4942b66830a5064e27bbcf316d23c2c5 Mon Sep 17 00:00:00 2001 From: Loaf <90423308+ponderingdemocritus@users.noreply.github.com> Date: Tue, 29 Aug 2023 14:39:45 +1000 Subject: [PATCH 11/77] add in examples to readme (#840) --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 91a3c0da4e..23bc9a5bb0 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,11 @@ Dojo offers a comprehensive suite of onchain game development tools, harnessing See the [installation guide](https://book.dojoengine.org/getting-started/quick-start.html) in the Dojo book. +## 📚 Examples in 30s + +- [Dojo starter react](https://github.com/dojoengine/dojo-starter-react-app) +- [Dojo starter phaser](https://github.com/dojoengine/dojo-starter-phaser) + ## 🗒️ Documentation You can find more detailed documentation in the Dojo Book [here](https://book.dojoengine.org/). From 5ddac50a46c00926572927d38c4fd250b26f60a5 Mon Sep 17 00:00:00 2001 From: Loaf <90423308+ponderingdemocritus@users.noreply.github.com> Date: Tue, 29 Aug 2023 15:18:15 +1000 Subject: [PATCH 12/77] update deps for dojo_defi (#842) --- crates/dojo-defi/Scarb.toml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/dojo-defi/Scarb.toml b/crates/dojo-defi/Scarb.toml index 35c8fc2c5f..5da0aed6e9 100644 --- a/crates/dojo-defi/Scarb.toml +++ b/crates/dojo-defi/Scarb.toml @@ -1,13 +1,12 @@ [package] -cairo-version = "2.1.0-rc4" +cairo-version = "2.1.1" description = "Implementations of a defi primitives for the Dojo framework" name = "dojo_defi" version = "0.2.1" [dependencies] # Next lines to be reverted once Cubit repo is updated. -# cubit = { git = "https://github.com/influenceth/cubit" } -cubit = {git = "https://github.com/ametel01/cubit.git"} +cubit = { git = "https://github.com/influenceth/cubit" } dojo = { path = "../dojo-core" } [[target.dojo]] From c48712bb393eece5e157e3dec917d3a811a2aacc Mon Sep 17 00:00:00 2001 From: Loaf <90423308+ponderingdemocritus@users.noreply.github.com> Date: Tue, 29 Aug 2023 15:54:44 +1000 Subject: [PATCH 13/77] Fix/defi dep (#846) * update deps for dojo_defi * adds lib to dojo-defi to enable external pkg --- crates/dojo-defi/Scarb.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/dojo-defi/Scarb.toml b/crates/dojo-defi/Scarb.toml index 5da0aed6e9..21e2e83c01 100644 --- a/crates/dojo-defi/Scarb.toml +++ b/crates/dojo-defi/Scarb.toml @@ -4,6 +4,8 @@ description = "Implementations of a defi primitives for the Dojo framework" name = "dojo_defi" version = "0.2.1" +[lib] + [dependencies] # Next lines to be reverted once Cubit repo is updated. cubit = { git = "https://github.com/influenceth/cubit" } From 19447f533fe3afaf12a9d2569541824a71dee04f Mon Sep 17 00:00:00 2001 From: Alex Metelli Date: Tue, 29 Aug 2023 13:55:39 +0800 Subject: [PATCH 14/77] Moved SerdeLenImpl for Fixed in dojo-defi (#844) * refacto for new syntax + impl SerdelLen for Fixed * move SerdeLenImpl to defi --- crates/dojo-core/Scarb.toml | 1 - crates/dojo-core/src/serde.cairo | 6 ------ crates/dojo-defi/Scarb.toml | 4 +++- .../src/constant_product_market/components.cairo | 9 +++++++++ 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/crates/dojo-core/Scarb.toml b/crates/dojo-core/Scarb.toml index b3cbbd1613..871e2092fe 100644 --- a/crates/dojo-core/Scarb.toml +++ b/crates/dojo-core/Scarb.toml @@ -6,5 +6,4 @@ version = "0.2.1" [dependencies] dojo_plugin = { git = "https://github.com/dojoengine/dojo" } -cubit = {git = "https://github.com/ametel01/cubit.git"} starknet = "2.1.1" diff --git a/crates/dojo-core/src/serde.cairo b/crates/dojo-core/src/serde.cairo index f297843d0b..6dd1ce0ec3 100644 --- a/crates/dojo-core/src/serde.cairo +++ b/crates/dojo-core/src/serde.cairo @@ -72,9 +72,3 @@ impl SerdeLenClassHash of SerdeLen { } } -impl SerdeLenFixed of SerdeLen { - #[inline(always)] - fn len() -> usize { - 2 - } -} diff --git a/crates/dojo-defi/Scarb.toml b/crates/dojo-defi/Scarb.toml index 21e2e83c01..316197c729 100644 --- a/crates/dojo-defi/Scarb.toml +++ b/crates/dojo-defi/Scarb.toml @@ -8,7 +8,9 @@ version = "0.2.1" [dependencies] # Next lines to be reverted once Cubit repo is updated. -cubit = { git = "https://github.com/influenceth/cubit" } +# cubit = { git = "https://github.com/influenceth/cubit" } +cubit = {git = "https://github.com/ametel01/cubit.git"} + dojo = { path = "../dojo-core" } [[target.dojo]] diff --git a/crates/dojo-defi/src/constant_product_market/components.cairo b/crates/dojo-defi/src/constant_product_market/components.cairo index 94877a873e..cccee4f0d9 100644 --- a/crates/dojo-defi/src/constant_product_market/components.cairo +++ b/crates/dojo-defi/src/constant_product_market/components.cairo @@ -2,13 +2,22 @@ use traits::{Into, TryInto}; use option::OptionTrait; use starknet::ContractAddress; +use dojo::serde::SerdeLen; use dojo_defi::tests::utils::{TOLERANCE, assert_approx_equal}; + // Cubit fixed point math library use cubit::f128::types::fixed::{Fixed, FixedInto, FixedTrait, ONE_u128}; const SCALING_FACTOR: u128 = 10000; +impl SerdeLenFixed of SerdeLen { + #[inline(always)] + fn len() -> usize { + 2 + } +} + #[derive(Component, Copy, Drop, Serde, SerdeLen)] struct Cash { #[key] From d5136a483ebe6f59e049c32151e099e63f60d3da Mon Sep 17 00:00:00 2001 From: Alex Metelli Date: Tue, 29 Aug 2023 15:48:01 +0800 Subject: [PATCH 15/77] refacto market + moved test to separate folderl (#849) --- crates/dojo-defi/src/lib.cairo | 2 +- ...tant_product_market.cairo => market.cairo} | 1 + crates/dojo-defi/src/market/components.cairo | 51 ++++ .../constant_product_market.cairo} | 229 +----------------- .../systems.cairo | 16 +- crates/dojo-defi/src/tests.cairo | 1 + .../tests/constant_product_market_tests.cairo | 188 ++++++++++++++ 7 files changed, 252 insertions(+), 236 deletions(-) rename crates/dojo-defi/src/{constant_product_market.cairo => market.cairo} (50%) create mode 100644 crates/dojo-defi/src/market/components.cairo rename crates/dojo-defi/src/{constant_product_market/components.cairo => market/constant_product_market.cairo} (52%) rename crates/dojo-defi/src/{constant_product_market => market}/systems.cairo (91%) create mode 100644 crates/dojo-defi/src/tests/constant_product_market_tests.cairo diff --git a/crates/dojo-defi/src/lib.cairo b/crates/dojo-defi/src/lib.cairo index a79dfdedd5..b764885332 100644 --- a/crates/dojo-defi/src/lib.cairo +++ b/crates/dojo-defi/src/lib.cairo @@ -1,4 +1,4 @@ -mod constant_product_market; +mod market; mod dutch_auction; mod tests; diff --git a/crates/dojo-defi/src/constant_product_market.cairo b/crates/dojo-defi/src/market.cairo similarity index 50% rename from crates/dojo-defi/src/constant_product_market.cairo rename to crates/dojo-defi/src/market.cairo index 98d784c920..fff5b7e67f 100644 --- a/crates/dojo-defi/src/constant_product_market.cairo +++ b/crates/dojo-defi/src/market.cairo @@ -1,2 +1,3 @@ mod components; mod systems; +mod constant_product_market; diff --git a/crates/dojo-defi/src/market/components.cairo b/crates/dojo-defi/src/market/components.cairo new file mode 100644 index 0000000000..eb24f3b900 --- /dev/null +++ b/crates/dojo-defi/src/market/components.cairo @@ -0,0 +1,51 @@ +use option::OptionTrait; +use starknet::ContractAddress; +use traits::{Into, TryInto}; + +use dojo::serde::SerdeLen; + + +// Cubit fixed point math library +use cubit::f128::types::fixed::Fixed; + +const SCALING_FACTOR: u128 = 10000; + +impl SerdeLenFixed of SerdeLen { + #[inline(always)] + fn len() -> usize { + 2 + } +} + +#[derive(Component, Copy, Drop, Serde, SerdeLen)] +struct Cash { + #[key] + player: ContractAddress, + amount: u128, +} + +#[derive(Component, Copy, Drop, Serde, SerdeLen)] +struct Item { + #[key] + player: ContractAddress, + #[key] + item_id: u32, + quantity: u128, +} + +#[derive(Component, Copy, Drop, Serde, SerdeLen)] +struct Liquidity { + #[key] + player: ContractAddress, + #[key] + item_id: u32, + shares: Fixed, +} + +#[derive(Component, Copy, Drop, Serde, SerdeLen)] +struct Market { + #[key] + item_id: u32, + cash_amount: u128, + item_quantity: u128, +} diff --git a/crates/dojo-defi/src/constant_product_market/components.cairo b/crates/dojo-defi/src/market/constant_product_market.cairo similarity index 52% rename from crates/dojo-defi/src/constant_product_market/components.cairo rename to crates/dojo-defi/src/market/constant_product_market.cairo index cccee4f0d9..cae15ff5f6 100644 --- a/crates/dojo-defi/src/constant_product_market/components.cairo +++ b/crates/dojo-defi/src/market/constant_product_market.cairo @@ -2,55 +2,12 @@ use traits::{Into, TryInto}; use option::OptionTrait; use starknet::ContractAddress; -use dojo::serde::SerdeLen; -use dojo_defi::tests::utils::{TOLERANCE, assert_approx_equal}; +use dojo_defi::market::components::Market; - -// Cubit fixed point math library -use cubit::f128::types::fixed::{Fixed, FixedInto, FixedTrait, ONE_u128}; +use cubit::f128::types::fixed::{Fixed, FixedTrait}; const SCALING_FACTOR: u128 = 10000; -impl SerdeLenFixed of SerdeLen { - #[inline(always)] - fn len() -> usize { - 2 - } -} - -#[derive(Component, Copy, Drop, Serde, SerdeLen)] -struct Cash { - #[key] - player: ContractAddress, - amount: u128, -} - -#[derive(Component, Copy, Drop, Serde, SerdeLen)] -struct Item { - #[key] - player: ContractAddress, - #[key] - item_id: u32, - quantity: u128, -} - -#[derive(Component, Copy, Drop, Serde, SerdeLen)] -struct Liquidity { - #[key] - player: ContractAddress, - #[key] - item_id: u32, - shares: Fixed, -} - -#[derive(Component, Copy, Drop, Serde, SerdeLen)] -struct Market { - #[key] - item_id: u32, - cash_amount: u128, - item_quantity: u128, -} - #[generate_trait] impl MarketImpl of MarketTrait { fn buy(self: @Market, quantity: u128) -> u128 { @@ -256,185 +213,3 @@ fn normalize(quantity: u128, market: @Market) -> (u128, u128, u128) { let available: u128 = (*market.item_quantity).into() * SCALING_FACTOR; (quantity, available, *market.cash_amount) } - -#[test] -#[should_panic(expected: ('not enough liquidity',))] -fn test_not_enough_quantity() { - let market = Market { - item_id: 1, cash_amount: SCALING_FACTOR * 1, item_quantity: 1 - }; // pool 1:1 - let cost = market.buy(10); -} - -#[test] -#[available_gas(100000)] -fn test_market_buy() { - let market = Market { - item_id: 1, cash_amount: SCALING_FACTOR * 1, item_quantity: 10 - }; // pool 1:10 - let cost = market.buy(5); - assert(cost == SCALING_FACTOR * 1, 'wrong cost'); -} - -#[test] -#[available_gas(100000)] -fn test_market_sell() { - let market = Market { - item_id: 1, cash_amount: SCALING_FACTOR * 1, item_quantity: 10 - }; // pool 1:10 - let payout = market.sell(5); - assert(payout == 3334, 'wrong payout'); -} - -#[test] -#[available_gas(500000)] -fn test_market_add_liquidity_no_initial() { - // Without initial liquidity - let market = Market { item_id: 1, cash_amount: 0, item_quantity: 0 }; - - // Add liquidity - let (amount, quantity) = (SCALING_FACTOR * 5, 5); // pool 1:1 - let (amount_add, quantity_add, liquidity_add) = market.add_liquidity(amount, quantity); - - // Assert that the amount and quantity added are the same as the given amount and quantity - // and that the liquidity shares minted are the same as the entire liquidity - assert(amount_add == amount, 'wrong cash amount'); - assert(quantity_add == quantity, 'wrong item quantity'); - - // Convert amount and quantity to fixed point - let amount = FixedTrait::new_unscaled(amount, false); - let quantity: u128 = quantity.into() * SCALING_FACTOR; - let quantity = FixedTrait::new_unscaled(quantity, false); - assert(liquidity_add == (amount * quantity).sqrt(), 'wrong liquidity'); -} - -#[test] -#[available_gas(600000)] -fn test_market_add_liquidity_optimal() { - // With initial liquidity - let market = Market { - item_id: 1, cash_amount: SCALING_FACTOR * 1, item_quantity: 10 - }; // pool 1:10 - let initial_liquidity = market.liquidity(); - - // Add liquidity with the same ratio - let (amount, quantity) = (SCALING_FACTOR * 2, 20); // pool 1:10 - let (amount_add, quantity_add, liquidity_add) = market.add_liquidity(amount, quantity); - - // Assert - assert(amount_add == amount, 'wrong cash amount'); - assert(quantity_add == quantity, 'wrong item quantity'); - - // Get expected amount and convert to fixed point - let expected_amount = FixedTrait::new_unscaled(SCALING_FACTOR * 1 + amount, false); - let expected_quantity: u128 = (10 + quantity).into() * SCALING_FACTOR; - let expected_quantity = FixedTrait::new_unscaled(expected_quantity, false); - - // Compute the expected liquidity shares - let expected_liquidity = FixedTrait::sqrt(expected_amount * expected_quantity); - let final_liquidity = initial_liquidity + liquidity_add; - assert_approx_equal(expected_liquidity, final_liquidity, TOLERANCE); -} - -#[test] -#[available_gas(1000000)] -fn test_market_add_liquidity_not_optimal() { - // With initial liquidity - let market = Market { - item_id: 1, cash_amount: SCALING_FACTOR * 1, item_quantity: 10 - }; // pool 1:10 - let initial_liquidity = market.liquidity(); - - // Add liquidity without the same ratio - let (amount, quantity) = (SCALING_FACTOR * 2, 10); // pool 1:5 - - let (amount_add, quantity_add, liquidity_add) = market.add_liquidity(amount, quantity); - - // Assert that the amount added is optimal even though the - // amount originally requested was not - let amount_optimal = SCALING_FACTOR * 1; - assert(amount_add == amount_optimal, 'wrong cash amount'); - assert(quantity_add == quantity, 'wrong item quantity'); - - // Get expected amount and convert to fixed point - let expected_amount = FixedTrait::new_unscaled(SCALING_FACTOR * 1 + amount_add, false); - let expected_quantity: u128 = (10 + quantity).into() * SCALING_FACTOR; - let expected_quantity = FixedTrait::new_unscaled(expected_quantity, false); - - // Get expecteed liquidity - let expected_liquidity = FixedTrait::sqrt(expected_amount * expected_quantity); - - let final_liquidity = initial_liquidity + liquidity_add; -// assert_precise(expected_liquidity, final_liquidity.into(), 'wrong liquidity', Option::None(())); -} - -#[test] -#[should_panic(expected: ('insufficient amount',))] -fn test_market_add_liquidity_insufficient_amount() { - let market = Market { - item_id: 1, cash_amount: SCALING_FACTOR * 1, item_quantity: 10 - }; // pool 1:10 - // Adding 20 items requires (SCALING_FACTOR * 2) cash amount to maintain the ratio - // Therefore this should fail - let (amount_add, quantity_add, liquidity_add) = market.add_liquidity(SCALING_FACTOR * 1, 20); -} - -#[test] -#[available_gas(1000000)] -fn test_market_remove_liquidity() { - // With initial liquidity - let market = Market { - item_id: 1, cash_amount: SCALING_FACTOR * 2, item_quantity: 20 - }; // pool 1:10 - let initial_liquidity = market.liquidity(); - - // Remove half of the liquidity - let two = FixedTrait::new_unscaled(2, false); - let liquidity_remove = initial_liquidity / two; - - let (amount_remove, quantity_remove) = market.remove_liquidity(liquidity_remove); - - // Assert that the amount and quantity removed are half of the initial amount and quantity - assert(amount_remove == SCALING_FACTOR * 1, 'wrong cash amount'); - assert(quantity_remove == 10, 'wrong item quantity'); - - // Get expected amount and convert to fixed point - let expected_amount = FixedTrait::new_unscaled(SCALING_FACTOR * 2 - amount_remove, false); - let expected_quantity: u128 = (20 - quantity_remove).into() * SCALING_FACTOR; - let expected_quantity = FixedTrait::new_unscaled(expected_quantity, false); - - // Get expecteed liquidity - let expected_liquidity = FixedTrait::sqrt(expected_amount * expected_quantity); - - let final_liquidity = initial_liquidity - liquidity_remove; -// assert_precise(expected_liquidity, final_liquidity.into(), 'wrong liquidity', Option::None(())); -} - -#[test] -#[should_panic(expected: ('insufficient liquidity',))] -fn test_market_remove_liquidity_no_initial() { - // Without initial liquidity - let market = Market { item_id: 1, cash_amount: 0, item_quantity: 0 }; // pool 1:10 - - // Remove liquidity - let one = FixedTrait::new_unscaled(1, false); - - let (amount_remove, quantity_remove) = market.remove_liquidity(one); -} - -#[test] -#[should_panic(expected: ('insufficient liquidity',))] -fn test_market_remove_liquidity_more_than_available() { - // With initial liquidity - let market = Market { - item_id: 1, cash_amount: SCALING_FACTOR * 2, item_quantity: 20 - }; // pool 1:10 - let initial_liquidity = market.liquidity(); - - // Remove twice of the liquidity - let two = FixedTrait::new_unscaled(2, false); - let liquidity_remove = initial_liquidity * two; - - let (amount_remove, quantity_remove) = market.remove_liquidity(liquidity_remove); -} - diff --git a/crates/dojo-defi/src/constant_product_market/systems.cairo b/crates/dojo-defi/src/market/systems.cairo similarity index 91% rename from crates/dojo-defi/src/constant_product_market/systems.cairo rename to crates/dojo-defi/src/market/systems.cairo index 10eee21b0b..168205568c 100644 --- a/crates/dojo-defi/src/constant_product_market/systems.cairo +++ b/crates/dojo-defi/src/market/systems.cairo @@ -2,7 +2,8 @@ mod Buy { use traits::Into; use array::ArrayTrait; - use dojo_defi::constant_product_market::components::{Item, Cash, Market, MarketTrait}; + use dojo_defi::market::components::{Item, Cash, Market}; + use dojo_defi::market::constant_product_market::MarketTrait; use dojo::world::Context; fn execute(ctx: Context, item_id: u32, quantity: u128) { @@ -41,7 +42,8 @@ mod Buy { mod Sell { use traits::Into; use array::ArrayTrait; - use dojo_defi::constant_product_market::components::{Item, Cash, Market, MarketTrait}; + use dojo_defi::market::components::{Item, Cash, Market}; + use dojo_defi::market::constant_product_market::MarketTrait; use dojo::world::Context; fn execute(ctx: Context, item_id: u32, quantity: u128) { @@ -81,9 +83,8 @@ mod Sell { mod AddLiquidity { use traits::Into; use array::ArrayTrait; - use dojo_defi::constant_product_market::components::{ - Item, Cash, Market, Liquidity, MarketTrait - }; + use dojo_defi::market::components::{Item, Cash, Market, Liquidity}; + use dojo_defi::market::constant_product_market::MarketTrait; use dojo::world::Context; fn execute(ctx: Context, item_id: u32, amount: u128, quantity: u128) { @@ -133,9 +134,8 @@ mod AddLiquidity { mod RemoveLiquidity { use traits::Into; use array::ArrayTrait; - use dojo_defi::constant_product_market::components::{ - Item, Cash, Market, Liquidity, MarketTrait - }; + use dojo_defi::market::components::{Item, Cash, Market, Liquidity}; + use dojo_defi::market::constant_product_market::MarketTrait; use dojo::world::Context; use cubit::f128::types::fixed::Fixed; diff --git a/crates/dojo-defi/src/tests.cairo b/crates/dojo-defi/src/tests.cairo index 134c484365..178dfdfa03 100644 --- a/crates/dojo-defi/src/tests.cairo +++ b/crates/dojo-defi/src/tests.cairo @@ -7,4 +7,5 @@ mod linear_vrgda_test; #[cfg(test)] mod logistic_vrgda_test; #[cfg(test)] +mod constant_product_market_tests; mod utils; diff --git a/crates/dojo-defi/src/tests/constant_product_market_tests.cairo b/crates/dojo-defi/src/tests/constant_product_market_tests.cairo new file mode 100644 index 0000000000..b125eb4ea0 --- /dev/null +++ b/crates/dojo-defi/src/tests/constant_product_market_tests.cairo @@ -0,0 +1,188 @@ +use traits::Into; + +use dojo_defi::market::components::Market; +use dojo_defi::market::constant_product_market::{MarketTrait, SCALING_FACTOR}; +use dojo_defi::tests::utils::{TOLERANCE, assert_approx_equal}; + +use cubit::f128::types::FixedTrait; + +#[test] +#[should_panic(expected: ('not enough liquidity',))] +fn test_not_enough_quantity() { + let market = Market { + item_id: 1, cash_amount: SCALING_FACTOR * 1, item_quantity: 1 + }; // pool 1:1 + let cost = market.buy(10); +} + +#[test] +#[available_gas(100000)] +fn test_market_buy() { + let market = Market { + item_id: 1, cash_amount: SCALING_FACTOR * 1, item_quantity: 10 + }; // pool 1:10 + let cost = market.buy(5); + assert(cost == SCALING_FACTOR * 1, 'wrong cost'); +} + +#[test] +#[available_gas(100000)] +fn test_market_sell() { + let market = Market { + item_id: 1, cash_amount: SCALING_FACTOR * 1, item_quantity: 10 + }; // pool 1:10 + let payout = market.sell(5); + assert(payout == 3334, 'wrong payout'); +} + +#[test] +#[available_gas(500000)] +fn test_market_add_liquidity_no_initial() { + // Without initial liquidity + let market = Market { item_id: 1, cash_amount: 0, item_quantity: 0 }; + + // Add liquidity + let (amount, quantity) = (SCALING_FACTOR * 5, 5); // pool 1:1 + let (amount_add, quantity_add, liquidity_add) = market.add_liquidity(amount, quantity); + + // Assert that the amount and quantity added are the same as the given amount and quantity + // and that the liquidity shares minted are the same as the entire liquidity + assert(amount_add == amount, 'wrong cash amount'); + assert(quantity_add == quantity, 'wrong item quantity'); + + // Convert amount and quantity to fixed point + let amount = FixedTrait::new_unscaled(amount, false); + let quantity: u128 = quantity.into() * SCALING_FACTOR; + let quantity = FixedTrait::new_unscaled(quantity, false); + assert(liquidity_add == (amount * quantity).sqrt(), 'wrong liquidity'); +} + +#[test] +#[available_gas(600000)] +fn test_market_add_liquidity_optimal() { + // With initial liquidity + let market = Market { + item_id: 1, cash_amount: SCALING_FACTOR * 1, item_quantity: 10 + }; // pool 1:10 + let initial_liquidity = market.liquidity(); + + // Add liquidity with the same ratio + let (amount, quantity) = (SCALING_FACTOR * 2, 20); // pool 1:10 + let (amount_add, quantity_add, liquidity_add) = market.add_liquidity(amount, quantity); + + // Assert + assert(amount_add == amount, 'wrong cash amount'); + assert(quantity_add == quantity, 'wrong item quantity'); + + // Get expected amount and convert to fixed point + let expected_amount = FixedTrait::new_unscaled(SCALING_FACTOR * 1 + amount, false); + let expected_quantity: u128 = (10 + quantity).into() * SCALING_FACTOR; + let expected_quantity = FixedTrait::new_unscaled(expected_quantity, false); + + // Compute the expected liquidity shares + let expected_liquidity = FixedTrait::sqrt(expected_amount * expected_quantity); + let final_liquidity = initial_liquidity + liquidity_add; + assert_approx_equal(expected_liquidity, final_liquidity, TOLERANCE); +} + +#[test] +#[available_gas(1000000)] +fn test_market_add_liquidity_not_optimal() { + // With initial liquidity + let market = Market { + item_id: 1, cash_amount: SCALING_FACTOR * 1, item_quantity: 10 + }; // pool 1:10 + let initial_liquidity = market.liquidity(); + + // Add liquidity without the same ratio + let (amount, quantity) = (SCALING_FACTOR * 2, 10); // pool 1:5 + + let (amount_add, quantity_add, liquidity_add) = market.add_liquidity(amount, quantity); + + // Assert that the amount added is optimal even though the + // amount originally requested was not + let amount_optimal = SCALING_FACTOR * 1; + assert(amount_add == amount_optimal, 'wrong cash amount'); + assert(quantity_add == quantity, 'wrong item quantity'); + + // Get expected amount and convert to fixed point + let expected_amount = FixedTrait::new_unscaled(SCALING_FACTOR * 1 + amount_add, false); + let expected_quantity: u128 = (10 + quantity).into() * SCALING_FACTOR; + let expected_quantity = FixedTrait::new_unscaled(expected_quantity, false); + + // Get expecteed liquidity + let expected_liquidity = FixedTrait::sqrt(expected_amount * expected_quantity); + + let final_liquidity = initial_liquidity + liquidity_add; +// assert_precise(expected_liquidity, final_liquidity.into(), 'wrong liquidity', Option::None(())); +} + +#[test] +#[should_panic(expected: ('insufficient amount',))] +fn test_market_add_liquidity_insufficient_amount() { + let market = Market { + item_id: 1, cash_amount: SCALING_FACTOR * 1, item_quantity: 10 + }; // pool 1:10 + // Adding 20 items requires (SCALING_FACTOR * 2) cash amount to maintain the ratio + // Therefore this should fail + let (amount_add, quantity_add, liquidity_add) = market.add_liquidity(SCALING_FACTOR * 1, 20); +} + +#[test] +#[available_gas(1000000)] +fn test_market_remove_liquidity() { + // With initial liquidity + let market = Market { + item_id: 1, cash_amount: SCALING_FACTOR * 2, item_quantity: 20 + }; // pool 1:10 + let initial_liquidity = market.liquidity(); + + // Remove half of the liquidity + let two = FixedTrait::new_unscaled(2, false); + let liquidity_remove = initial_liquidity / two; + + let (amount_remove, quantity_remove) = market.remove_liquidity(liquidity_remove); + + // Assert that the amount and quantity removed are half of the initial amount and quantity + assert(amount_remove == SCALING_FACTOR * 1, 'wrong cash amount'); + assert(quantity_remove == 10, 'wrong item quantity'); + + // Get expected amount and convert to fixed point + let expected_amount = FixedTrait::new_unscaled(SCALING_FACTOR * 2 - amount_remove, false); + let expected_quantity: u128 = (20 - quantity_remove).into() * SCALING_FACTOR; + let expected_quantity = FixedTrait::new_unscaled(expected_quantity, false); + + // Get expecteed liquidity + let expected_liquidity = FixedTrait::sqrt(expected_amount * expected_quantity); + + let final_liquidity = initial_liquidity - liquidity_remove; +// assert_precise(expected_liquidity, final_liquidity.into(), 'wrong liquidity', Option::None(())); +} + +#[test] +#[should_panic(expected: ('insufficient liquidity',))] +fn test_market_remove_liquidity_no_initial() { + // Without initial liquidity + let market = Market { item_id: 1, cash_amount: 0, item_quantity: 0 }; // pool 1:10 + + // Remove liquidity + let one = FixedTrait::new_unscaled(1, false); + + let (amount_remove, quantity_remove) = market.remove_liquidity(one); +} + +#[test] +#[should_panic(expected: ('insufficient liquidity',))] +fn test_market_remove_liquidity_more_than_available() { + // With initial liquidity + let market = Market { + item_id: 1, cash_amount: SCALING_FACTOR * 2, item_quantity: 20 + }; // pool 1:10 + let initial_liquidity = market.liquidity(); + + // Remove twice of the liquidity + let two = FixedTrait::new_unscaled(2, false); + let liquidity_remove = initial_liquidity * two; + + let (amount_remove, quantity_remove) = market.remove_liquidity(liquidity_remove); +} From 857cbc3db064fed11cf74aaaf53b8e3e3858ed21 Mon Sep 17 00:00:00 2001 From: lambda-0x <0xlambda@protonmail.com> Date: Wed, 30 Aug 2023 00:23:02 +0530 Subject: [PATCH 16/77] feat(dojoup): remove jq dependency (#847) * feat(dojoup): remove jq dependency * make it more robust --- dojoup/dojoup | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/dojoup/dojoup b/dojoup/dojoup index 3d2047a5b2..a7cb709bc8 100755 --- a/dojoup/dojoup +++ b/dojoup/dojoup @@ -76,14 +76,16 @@ main() { if [[ "$DOJOUP_REPO" == "dojoengine/dojo" && -z "$DOJOUP_BRANCH" && -z "$DOJOUP_COMMIT" ]]; then DOJOUP_VERSION=${DOJOUP_VERSION-stable} DOJOUP_TAG=$DOJOUP_VERSION - need_cmd jq # Normalize versions (handle channels, versions without v prefix if [[ "$DOJOUP_VERSION" == "stable" ]]; then - # Fetch the list of releases from the GitHub API and get the first non-prerelease + # Fetch the list of releases from the GitHub API and filter out `prerelease`` releases and `alpha`` releases DOJOUP_TAG=$(curl -s "https://api.github.com/repos/${DOJOUP_REPO}/releases" \ - | jq -r '.[] | select(.prerelease==false) | .tag_name' \ - | grep -v '-' \ + | grep -oE '"tag_name": "[^"]*"|"prerelease": (true|false)' \ + | grep -B1 '"prerelease": false' \ + | grep '"tag_name":' \ + | grep -oP '"v[0-9]*\.[0-9]*\.[0-9]*"' \ + | tr -d '"' \ | head -n 1) DOJOUP_VERSION=$DOJOUP_TAG elif [[ "$DOJOUP_VERSION" == nightly* ]]; then From 36e768e9e7b7c4db467583dd17f35ec4b9408105 Mon Sep 17 00:00:00 2001 From: notV4l <122404722+notV4l@users.noreply.github.com> Date: Wed, 30 Aug 2023 16:28:37 +0200 Subject: [PATCH 17/77] ERC auth / fix / tests (#830) * configure writer rights for components/systems * scarb fmt * configure writer rights for components/systems * prefix unckecked_ for fn requiring safety checks before calling * fix: ERC1155Balance component to respect token/account/id order --- crates/dojo-erc/src/erc1155/components.cairo | 52 ++++- crates/dojo-erc/src/erc1155/erc1155.cairo | 13 +- crates/dojo-erc/src/erc1155/systems.cairo | 66 ++++-- crates/dojo-erc/src/erc721/components.cairo | 29 ++- crates/dojo-erc/src/erc721/systems.cairo | 22 +- .../components/base_uri_component.cairo | 7 +- .../operator_approval_component.cairo | 4 +- crates/dojo-erc/src/erc_common/utils.cairo | 31 ++- crates/dojo-erc/src/tests.cairo | 3 + crates/dojo-erc/src/tests/test_erc1155.cairo | 206 ++++++++++++++---- .../src/tests/test_erc1155_utils.cairo | 28 ++- crates/dojo-erc/src/tests/test_erc721.cairo | 118 +++++----- .../src/tests/test_erc721_utils.cairo | 34 ++- crates/dojo-erc/src/tests/test_utils.cairo | 11 + 14 files changed, 442 insertions(+), 182 deletions(-) create mode 100644 crates/dojo-erc/src/tests/test_utils.cairo diff --git a/crates/dojo-erc/src/erc1155/components.cairo b/crates/dojo-erc/src/erc1155/components.cairo index a86419295f..9f0b7ecd18 100644 --- a/crates/dojo-erc/src/erc1155/components.cairo +++ b/crates/dojo-erc/src/erc1155/components.cairo @@ -28,9 +28,9 @@ struct ERC1155Balance { #[key] token: ContractAddress, #[key] - token_id: felt252, - #[key] account: ContractAddress, + #[key] + token_id: felt252, amount: u128 } @@ -38,7 +38,7 @@ trait ERC1155BalanceTrait { fn balance_of( world: IWorldDispatcher, token: ContractAddress, account: ContractAddress, id: felt252 ) -> u128; - fn transfer_tokens( + fn unchecked_transfer_tokens( world: IWorldDispatcher, token: ContractAddress, from: ContractAddress, @@ -46,6 +46,20 @@ trait ERC1155BalanceTrait { ids: Span, amounts: Span, ); + fn unchecked_increase_balance( + world: IWorldDispatcher, + token: ContractAddress, + owner: ContractAddress, + id: felt252, + amount: u128, + ); + fn unchecked_decrease_balance( + world: IWorldDispatcher, + token: ContractAddress, + owner: ContractAddress, + id: felt252, + amount: u128, + ); } impl ERC1155BalanceImpl of ERC1155BalanceTrait { @@ -54,10 +68,10 @@ impl ERC1155BalanceImpl of ERC1155BalanceTrait { ) -> u128 { // ERC1155: address zero is not a valid owner assert(account.is_non_zero(), 'ERC1155: invalid owner address'); - get!(world, (token, id, account), ERC1155Balance).amount + get!(world, (token, account, id), ERC1155Balance).amount } - fn transfer_tokens( + fn unchecked_transfer_tokens( world: IWorldDispatcher, token: ContractAddress, from: ContractAddress, @@ -73,16 +87,40 @@ impl ERC1155BalanceImpl of ERC1155BalanceTrait { let amount: u128 = *amounts.pop_front().unwrap(); if (from.is_non_zero()) { - let mut from_balance = get!(world, (token, id, from), ERC1155Balance); + let mut from_balance = get!(world, (token, from, id), ERC1155Balance); from_balance.amount -= amount; set!(world, (from_balance)); } if (to.is_non_zero()) { - let mut to_balance = get!(world, (token, id, to), ERC1155Balance); + let mut to_balance = get!(world, (token, to, id), ERC1155Balance); to_balance.amount += amount; set!(world, (to_balance)); }; }; } + + fn unchecked_increase_balance( + world: IWorldDispatcher, + token: ContractAddress, + owner: ContractAddress, + id: felt252, + amount: u128, + ) { + let mut balance = get!(world, (token, owner, id), ERC1155Balance); + balance.amount += amount; + set!(world, (balance)); + } + + fn unchecked_decrease_balance( + world: IWorldDispatcher, + token: ContractAddress, + owner: ContractAddress, + id: felt252, + amount: u128, + ) { + let mut balance = get!(world, (token, owner, id), ERC1155Balance); + balance.amount -= amount; + set!(world, (balance)); + } } diff --git a/crates/dojo-erc/src/erc1155/erc1155.cairo b/crates/dojo-erc/src/erc1155/erc1155.cairo index bcd71eee48..473a14984e 100644 --- a/crates/dojo-erc/src/erc1155/erc1155.cairo +++ b/crates/dojo-erc/src/erc1155/erc1155.cairo @@ -19,7 +19,9 @@ mod ERC1155 { }; use dojo_erc::erc165::interface::{IERC165, IERC165_ID}; - use dojo_erc::erc_common::utils::{to_calldata, ToCallDataTrait, system_calldata}; + use dojo_erc::erc_common::utils::{ + to_calldata, ToCallDataTrait, system_calldata, PartialEqArray + }; use dojo_erc::erc1155::systems::{ ERC1155SetApprovalForAllParams, ERC1155SafeTransferFromParams, @@ -29,8 +31,7 @@ mod ERC1155 { const UNLIMITED_ALLOWANCE: felt252 = 3618502788666131213697322783095070105623107215331596699973092056135872020480; - - #[derive(Clone, Drop, Serde, starknet::Event)] + #[derive(Clone, Drop, Serde, PartialEq, starknet::Event)] struct TransferSingle { operator: ContractAddress, from: ContractAddress, @@ -39,7 +40,7 @@ mod ERC1155 { value: u256 } - #[derive(Clone, Drop, Serde, starknet::Event)] + #[derive(Clone, Drop, Serde, PartialEq, starknet::Event)] struct TransferBatch { operator: ContractAddress, from: ContractAddress, @@ -48,7 +49,7 @@ mod ERC1155 { values: Array } - #[derive(Clone, Drop, Serde, starknet::Event)] + #[derive(Clone, Drop, Serde, PartialEq, starknet::Event)] struct ApprovalForAll { owner: ContractAddress, operator: ContractAddress, @@ -63,7 +64,7 @@ mod ERC1155 { } #[event] - #[derive(Drop, starknet::Event)] + #[derive(Drop, PartialEq, starknet::Event)] enum Event { TransferSingle: TransferSingle, TransferBatch: TransferBatch, diff --git a/crates/dojo-erc/src/erc1155/systems.cairo b/crates/dojo-erc/src/erc1155/systems.cairo index 2a523ca43d..cb6e254b64 100644 --- a/crates/dojo-erc/src/erc1155/systems.cairo +++ b/crates/dojo-erc/src/erc1155/systems.cairo @@ -53,13 +53,13 @@ fn emit_transfer_batch( amounts_u256.append((*amounts.pop_front().unwrap()).into()); }; let event = TransferBatch { - operator: operator, from: from, to: to, ids: ids_u256, values: amounts_u256, + operator: operator, from: from, to: to, ids: ids_u256, values: amounts_u256, }; IERC1155EventsDispatcher { contract_address: token }.on_transfer_batch(event.clone()); emit!(world, event); } -fn update( +fn unchecked_update( world: IWorldDispatcher, operator: ContractAddress, token: ContractAddress, @@ -71,13 +71,7 @@ fn update( ) { assert(ids.len() == amounts.len(), 'ERC1155: invalid length'); - assert( - operator == from - || OperatorApprovalTrait::is_approved_for_all(world, token, from, operator), - 'ERC1155: insufficient approval' - ); - - ERC1155BalanceTrait::transfer_tokens(world, token, from, to, ids.span(), amounts.span()); + ERC1155BalanceTrait::unchecked_transfer_tokens(world, token, from, to, ids.span(), amounts.span()); if (ids.len() == 1) { let id = *ids.at(0); @@ -103,9 +97,10 @@ fn do_safe_transfer_acceptance_check( ) { if (IERC165Dispatcher { contract_address: to }.supports_interface(IERC1155_RECEIVER_ID)) { assert( - IERC1155TokenReceiverDispatcher { - contract_address: to - }.on_erc1155_received(operator, from, id, amount, data) == ON_ERC1155_RECEIVED_SELECTOR, + IERC1155TokenReceiverDispatcher { contract_address: to } + .on_erc1155_received( + operator, from, id, amount, data + ) == ON_ERC1155_RECEIVED_SELECTOR, 'ERC1155: ERC1155Receiver reject' ); return (); @@ -126,9 +121,7 @@ fn do_safe_batch_transfer_acceptance_check( ) { if (IERC165Dispatcher { contract_address: to }.supports_interface(IERC1155_RECEIVER_ID)) { assert( - IERC1155TokenReceiverDispatcher { - contract_address: to - } + IERC1155TokenReceiverDispatcher { contract_address: to } .on_erc1155_batch_received( operator, from, ids, amounts, data ) == ON_ERC1155_BATCH_RECEIVED_SELECTOR, @@ -172,7 +165,7 @@ mod ERC1155SetApprovalForAll { let ERC1155SetApprovalForAllParams{token, owner, operator, approved } = params; assert(owner != operator, 'ERC1155: wrong approval'); - OperatorApprovalTrait::set_approval_for_all(ctx.world, token, owner, operator, approved); + OperatorApprovalTrait::unchecked_set_approval_for_all(ctx.world, token, owner, operator, approved); let event = ApprovalForAll { owner, operator, approved }; IERC1155EventsDispatcher { contract_address: token }.on_approval_for_all(event.clone()); @@ -205,6 +198,7 @@ mod ERC1155SafeTransferFrom { use zeroable::Zeroable; use starknet::ContractAddress; + #[derive(Drop, Serde)] struct ERC1155SafeTransferFromParams { token: ContractAddress, @@ -220,8 +214,19 @@ mod ERC1155SafeTransferFrom { let ERC1155SafeTransferFromParams{token, operator, from, to, id, amount, data } = params; assert(ctx.origin == operator || ctx.origin == token, 'ERC1155: not authorized'); assert(to.is_non_zero(), 'ERC1155: to cannot be 0'); + assert(from.is_non_zero(), 'ERC1155: from cannot be 0'); - super::update(ctx.world, operator, token, from, to, array![id], array![amount], data); + assert( + operator == from + || super::OperatorApprovalTrait::is_approved_for_all( + ctx.world, token, from, operator + ), + 'ERC1155: insufficient approval' + ); + + super::unchecked_update( + ctx.world, operator, token, from, to, array![id], array![amount], data + ); } } @@ -251,8 +256,17 @@ mod ERC1155SafeBatchTransferFrom { assert(ctx.origin == operator || ctx.origin == token, 'ERC1155: not authorized'); assert(to.is_non_zero(), 'ERC1155: to cannot be 0'); + assert(from.is_non_zero(), 'ERC1155: from cannot be 0'); - super::update(ctx.world, operator, token, from, to, ids, amounts, data); + assert( + operator == from + || super::OperatorApprovalTrait::is_approved_for_all( + ctx.world, token, from, operator + ), + 'ERC1155: insufficient approval' + ); + + super::unchecked_update(ctx.world, operator, token, from, to, ids, amounts, data); } } @@ -282,7 +296,9 @@ mod ERC1155Mint { assert(ctx.origin == operator || ctx.origin == token, 'ERC1155: not authorized'); assert(to.is_non_zero(), 'ERC1155: invalid receiver'); - super::update(ctx.world, operator, token, Zeroable::zero(), to, ids, amounts, data); + super::unchecked_update( + ctx.world, operator, token, Zeroable::zero(), to, ids, amounts, data + ); } } @@ -310,6 +326,16 @@ mod ERC1155Burn { assert(ctx.origin == operator || ctx.origin == token, 'ERC1155: not authorized'); assert(from.is_non_zero(), 'ERC1155: invalid sender'); - super::update(ctx.world, operator, token, from, Zeroable::zero(), ids, amounts, array![]); + assert( + operator == from + || super::OperatorApprovalTrait::is_approved_for_all( + ctx.world, token, from, operator + ), + 'ERC1155: insufficient approval' + ); + + super::unchecked_update( + ctx.world, operator, token, from, Zeroable::zero(), ids, amounts, array![] + ); } } diff --git a/crates/dojo-erc/src/erc721/components.cairo b/crates/dojo-erc/src/erc721/components.cairo index 85375b5a35..bb4284ceb0 100644 --- a/crates/dojo-erc/src/erc721/components.cairo +++ b/crates/dojo-erc/src/erc721/components.cairo @@ -28,7 +28,7 @@ trait ERC721OwnerTrait { fn owner_of( world: IWorldDispatcher, token: ContractAddress, token_id: felt252 ) -> ContractAddress; - fn set_owner( + fn unchecked_set_owner( world: IWorldDispatcher, token: ContractAddress, token_id: felt252, account: ContractAddress ); } @@ -42,8 +42,7 @@ impl ERC721OwnerImpl of ERC721OwnerTrait { get!(world, (token, token_id), ERC721Owner).address } - // perform safety checks before calling this fn - fn set_owner( + fn unchecked_set_owner( world: IWorldDispatcher, token: ContractAddress, token_id: felt252, account: ContractAddress ) { let mut owner = get!(world, (token, token_id), ERC721Owner); @@ -70,7 +69,7 @@ trait ERC721BalanceTrait { fn balance_of( world: IWorldDispatcher, token: ContractAddress, account: ContractAddress ) -> u128; - fn transfer_token( + fn unchecked_transfer_token( world: IWorldDispatcher, token: ContractAddress, from: ContractAddress, @@ -78,12 +77,12 @@ trait ERC721BalanceTrait { amount: u128, ); - fn increase_balance( - world: IWorldDispatcher, token: ContractAddress, owner: ContractAddress, amount: u128, + fn unchecked_increase_balance( + world: IWorldDispatcher, token: ContractAddress, owner: ContractAddress, amount: u128, ); - fn decrease_balance( - world: IWorldDispatcher, token: ContractAddress, owner: ContractAddress, amount: u128, + fn unchecked_decrease_balance( + world: IWorldDispatcher, token: ContractAddress, owner: ContractAddress, amount: u128, ); } @@ -96,7 +95,7 @@ impl ERC721BalanceImpl of ERC721BalanceTrait { get!(world, (token, account), ERC721Balance).amount } - fn transfer_token( + fn unchecked_transfer_token( world: IWorldDispatcher, token: ContractAddress, from: ContractAddress, @@ -112,16 +111,16 @@ impl ERC721BalanceImpl of ERC721BalanceTrait { set!(world, (to_balance)); } - fn increase_balance( - world: IWorldDispatcher, token: ContractAddress, owner: ContractAddress, amount: u128, + fn unchecked_increase_balance( + world: IWorldDispatcher, token: ContractAddress, owner: ContractAddress, amount: u128, ) { let mut balance = get!(world, (token, owner), ERC721Balance); balance.amount += amount; set!(world, (balance)); } - fn decrease_balance( - world: IWorldDispatcher, token: ContractAddress, owner: ContractAddress, amount: u128, + fn unchecked_decrease_balance( + world: IWorldDispatcher, token: ContractAddress, owner: ContractAddress, amount: u128, ) { let mut balance = get!(world, (token, owner), ERC721Balance); balance.amount -= amount; @@ -149,7 +148,7 @@ trait ERC721TokenApprovalTrait { world: IWorldDispatcher, token: ContractAddress, token_id: felt252 ) -> ContractAddress; - fn approve( + fn unchecked_approve( world: IWorldDispatcher, token: ContractAddress, token_id: felt252, to: ContractAddress ); } @@ -162,7 +161,7 @@ impl ERC721TokenApprovalImpl of ERC721TokenApprovalTrait { approval.address } - fn approve( + fn unchecked_approve( world: IWorldDispatcher, token: ContractAddress, token_id: felt252, to: ContractAddress ) { let mut approval = get!(world, (token, token_id), ERC721TokenApproval); diff --git a/crates/dojo-erc/src/erc721/systems.cairo b/crates/dojo-erc/src/erc721/systems.cairo index 1df0a43e6d..e511db81c5 100644 --- a/crates/dojo-erc/src/erc721/systems.cairo +++ b/crates/dojo-erc/src/erc721/systems.cairo @@ -92,7 +92,7 @@ mod ERC721Approve { ); // // ERC721: approve caller is not token owner or approved for all assert(caller == owner || is_approved_for_all, 'ERC721: unauthorized caller'); - ERC721TokenApprovalTrait::approve(ctx.world, token, token_id, to, ); + ERC721TokenApprovalTrait::unchecked_approve(ctx.world, token, token_id, to,); // emit events super::emit_approval(ctx.world, token, owner, to, token_id); @@ -124,7 +124,7 @@ mod ERC721SetApprovalForAll { assert(token == ctx.origin, 'ERC721: not authorized'); assert(owner != operator, 'ERC721: self approval'); - OperatorApprovalTrait::set_approval_for_all(ctx.world, token, owner, operator, approved); + OperatorApprovalTrait::unchecked_set_approval_for_all(ctx.world, token, owner, operator, approved); // emit event super::emit_approval_for_all(ctx.world, token, owner, operator, approved); @@ -171,9 +171,9 @@ mod ERC721TransferFrom { 'ERC721: unauthorized caller' ); - ERC721OwnerTrait::set_owner(ctx.world, token, token_id, to); - ERC721BalanceTrait::transfer_token(ctx.world, token, from, to, 1); - ERC721TokenApprovalTrait::approve(ctx.world, token, token_id, Zeroable::zero()); + ERC721OwnerTrait::unchecked_set_owner(ctx.world, token, token_id, to); + ERC721BalanceTrait::unchecked_transfer_token(ctx.world, token, from, to, 1); + ERC721TokenApprovalTrait::unchecked_approve(ctx.world, token, token_id, Zeroable::zero()); // emit events super::emit_transfer(ctx.world, token, from, to, token_id); @@ -206,8 +206,8 @@ mod ERC721Mint { let owner = ERC721OwnerTrait::owner_of(ctx.world, token, token_id); assert(owner.is_zero(), 'ERC721: already minted'); - ERC721BalanceTrait::increase_balance(ctx.world, token, recipient, 1); - ERC721OwnerTrait::set_owner(ctx.world, token, token_id, recipient); + ERC721BalanceTrait::unchecked_increase_balance(ctx.world, token, recipient, 1); + ERC721OwnerTrait::unchecked_set_owner(ctx.world, token, token_id, recipient); // emit events super::emit_transfer(ctx.world, token, Zeroable::zero(), recipient, token_id); } @@ -238,7 +238,7 @@ mod ERC721Burn { assert(token == ctx.origin, 'ERC721: not authorized'); let owner = ERC721OwnerTrait::owner_of(ctx.world, token, token_id); - assert(!owner.is_zero(), 'ERC721: invalid token_id'); + assert(owner.is_non_zero(), 'ERC721: invalid token_id'); let is_approved_for_all = OperatorApprovalTrait::is_approved_for_all( ctx.world, token, owner, caller @@ -250,8 +250,8 @@ mod ERC721Burn { 'ERC721: unauthorized caller' ); - ERC721BalanceTrait::decrease_balance(ctx.world, token, owner, 1); - ERC721OwnerTrait::set_owner(ctx.world, token, token_id, Zeroable::zero()); + ERC721BalanceTrait::unchecked_decrease_balance(ctx.world, token, owner, 1); + ERC721OwnerTrait::unchecked_set_owner(ctx.world, token, token_id, Zeroable::zero()); // emit events super::emit_transfer(ctx.world, token, owner, Zeroable::zero(), token_id); @@ -270,7 +270,7 @@ mod ERC721SetBaseUri { fn execute(ctx: Context, token: ContractAddress, uri: felt252) { assert(ctx.origin == token, 'ERC721: not authorized'); - BaseUriTrait::set_base_uri(ctx.world, token, uri); + BaseUriTrait::unchecked_set_base_uri(ctx.world, token, uri); // TODO: emit event } } diff --git a/crates/dojo-erc/src/erc_common/components/base_uri_component.cairo b/crates/dojo-erc/src/erc_common/components/base_uri_component.cairo index b57e3e4f97..39c0daafe8 100644 --- a/crates/dojo-erc/src/erc_common/components/base_uri_component.cairo +++ b/crates/dojo-erc/src/erc_common/components/base_uri_component.cairo @@ -10,17 +10,16 @@ struct BaseUri { trait BaseUriTrait { fn get_base_uri(world: IWorldDispatcher, token: ContractAddress) -> felt252; - fn set_base_uri(world: IWorldDispatcher, token: ContractAddress, new_base_uri: felt252); + fn unchecked_set_base_uri(world: IWorldDispatcher, token: ContractAddress, new_base_uri: felt252); } impl BaseUriImpl of BaseUriTrait { - fn get_base_uri(world: IWorldDispatcher, token: ContractAddress, ) -> felt252 { + fn get_base_uri(world: IWorldDispatcher, token: ContractAddress,) -> felt252 { let base_uri = get!(world, (token), BaseUri); base_uri.uri } - // perform safety checks before calling this fn - fn set_base_uri(world: IWorldDispatcher, token: ContractAddress, new_base_uri: felt252) { + fn unchecked_set_base_uri(world: IWorldDispatcher, token: ContractAddress, new_base_uri: felt252) { let mut base_uri = get!(world, (token), BaseUri); base_uri.uri = new_base_uri; set!(world, (base_uri)) diff --git a/crates/dojo-erc/src/erc_common/components/operator_approval_component.cairo b/crates/dojo-erc/src/erc_common/components/operator_approval_component.cairo index 9ef366fc55..db792bbfb3 100644 --- a/crates/dojo-erc/src/erc_common/components/operator_approval_component.cairo +++ b/crates/dojo-erc/src/erc_common/components/operator_approval_component.cairo @@ -20,7 +20,7 @@ trait OperatorApprovalTrait { operator: ContractAddress ) -> bool; - fn set_approval_for_all( + fn unchecked_set_approval_for_all( world: IWorldDispatcher, token: ContractAddress, owner: ContractAddress, @@ -41,7 +41,7 @@ impl OperatorApprovalImpl of OperatorApprovalTrait { } // perform safety checks before calling this fn - fn set_approval_for_all( + fn unchecked_set_approval_for_all( world: IWorldDispatcher, token: ContractAddress, owner: ContractAddress, diff --git a/crates/dojo-erc/src/erc_common/utils.cairo b/crates/dojo-erc/src/erc_common/utils.cairo index 4b27236e30..cfe9976f18 100644 --- a/crates/dojo-erc/src/erc_common/utils.cairo +++ b/crates/dojo-erc/src/erc_common/utils.cairo @@ -3,7 +3,7 @@ use array::ArrayTrait; #[derive(Drop)] struct ToCallData { - data: Array, + data: Array, } #[generate_trait] @@ -28,3 +28,32 @@ fn system_calldata, impl TD: Drop>(data: T) -> Array data.serialize(ref calldata); calldata } + + +impl PartialEqArray> of PartialEq> { + fn eq(lhs: @Array, rhs: @Array) -> bool { + if lhs.len() != rhs.len() { + return false; + }; + + let mut is_eq = true; + let mut i = 0; + loop { + if lhs.len() == i { + break; + }; + if lhs.at(i) != rhs.at(i) { + is_eq = false; + break; + }; + + i += 1; + }; + + is_eq + } + + fn ne(lhs: @Array, rhs: @Array) -> bool { + !PartialEqArray::eq(lhs, rhs) + } +} diff --git a/crates/dojo-erc/src/tests.cairo b/crates/dojo-erc/src/tests.cairo index 5369181fa6..4024c5a779 100644 --- a/crates/dojo-erc/src/tests.cairo +++ b/crates/dojo-erc/src/tests.cairo @@ -1,5 +1,8 @@ +mod test_utils; + mod test_erc20; mod test_erc20_utils; + mod test_erc721; mod test_erc721_utils; diff --git a/crates/dojo-erc/src/tests/test_erc1155.cairo b/crates/dojo-erc/src/tests/test_erc1155.cairo index 44d3ffc3ed..f3ded37ef1 100644 --- a/crates/dojo-erc/src/tests/test_erc1155.cairo +++ b/crates/dojo-erc/src/tests/test_erc1155.cairo @@ -1,5 +1,6 @@ use zeroable::Zeroable; use traits::{Into, Default, IndexView}; +use option::OptionTrait; use array::ArrayTrait; use serde::Serde; use starknet::ContractAddress; @@ -7,6 +8,7 @@ use starknet::testing::set_contract_address; use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; +use dojo_erc::tests::test_utils::impersonate; use dojo_erc::tests::test_erc1155_utils::{ spawn_world, deploy_erc1155, deploy_default, deploy_testcase1, ZERO, USER1, USER2, DEPLOYER, PROXY @@ -18,11 +20,13 @@ use dojo_erc::erc1155::interface::{ IERC1155_RECEIVER_ID }; +use dojo_erc::erc1155::erc1155::ERC1155::{Event, TransferSingle, TransferBatch, ApprovalForAll}; + #[test] #[available_gas(30000000)] fn test_deploy() { - let world = spawn_world(); + let world = spawn_world(DEPLOYER()); let erc1155_address = deploy_erc1155(world, DEPLOYER(), 'uri', 'seed-42'); let erc1155 = IERC1155ADispatcher { contract_address: erc1155_address }; assert(erc1155.owner() == DEPLOYER(), 'invalid owner'); @@ -73,7 +77,7 @@ fn test_uri() { // #[test] #[available_gas(30000000)] -#[should_panic(expected: ('ERC1155: invalid owner address', 'ENTRYPOINT_FAILED', ))] +#[should_panic(expected: ('ERC1155: invalid owner address', 'ENTRYPOINT_FAILED',))] fn test_balance_of_zero_address() { //reverts when queried about the zero address @@ -114,7 +118,7 @@ fn test_balance_with_tokens() { #[test] #[available_gas(30000000)] -#[should_panic(expected: ('ERC1155: invalid length', 'ENTRYPOINT_FAILED', ))] +#[should_panic(expected: ('ERC1155: invalid length', 'ENTRYPOINT_FAILED',))] fn test_balance_of_batch_with_invalid_input() { // reverts when input arrays don't match up let (world, erc1155) = deploy_default(); @@ -124,7 +128,7 @@ fn test_balance_of_batch_with_invalid_input() { #[test] #[available_gas(30000000)] -#[should_panic(expected: ('ERC1155: invalid owner address', 'ENTRYPOINT_FAILED', ))] +#[should_panic(expected: ('ERC1155: invalid owner address', 'ENTRYPOINT_FAILED',))] fn test_balance_of_batch_address_zero() { // reverts when input arrays don't match up let (world, erc1155) = deploy_default(); @@ -192,20 +196,39 @@ fn test_balance_of_batch_with_tokens_2() { fn test_set_approval_for_all() { // sets approval status which can be queried via is_approved_for_all let (world, erc1155) = deploy_default(); - // impersonate user1 - set_contract_address(USER1()); + impersonate(USER1()); erc1155.set_approval_for_all(PROXY(), true); assert(erc1155.is_approved_for_all(USER1(), PROXY()) == true, 'should be true'); } +#[test] +#[available_gas(30000000)] +fn test_set_approval_for_all_emit_event() { + // set_approval_for_all emits ApprovalForAll event + let (world, erc1155) = deploy_default(); + impersonate(USER1()); + + erc1155.set_approval_for_all(PROXY(), true); + + // ApprovalForAll + assert( + @starknet::testing::pop_log(erc1155.contract_address) + .unwrap() == @Event::ApprovalForAll( + ApprovalForAll { owner: USER1(), operator: PROXY(), approved: true } + ), + 'invalid ApprovalForAll event' + ); +} + + #[test] #[available_gas(30000000)] fn test_set_unset_approval_for_all() { // sets approval status which can be queried via is_approved_for_all let (world, erc1155) = deploy_default(); - // impersonate user1 - set_contract_address(USER1()); + impersonate(USER1()); + erc1155.set_approval_for_all(PROXY(), true); assert(erc1155.is_approved_for_all(USER1(), PROXY()) == true, 'should be true'); erc1155.set_approval_for_all(PROXY(), false); @@ -218,8 +241,8 @@ fn test_set_unset_approval_for_all() { fn test_set_approval_for_all_on_self() { // reverts if attempting to approve self as an operator let (world, erc1155) = deploy_default(); - // impersonate user1 - set_contract_address(USER1()); + impersonate(USER1()); + erc1155.set_approval_for_all(USER1(), true); // should panic } @@ -230,12 +253,11 @@ fn test_set_approval_for_all_on_self() { #[test] #[available_gas(30000000)] #[should_panic()] -fn test_safe_transfer_from() { +fn test_safe_transfer_from_more_than_balance() { // reverts when transferring more than balance let (world, erc1155) = deploy_testcase1(); - // impersonate user1 - set_contract_address(USER1()); + impersonate(USER1()); erc1155.safe_transfer_from(USER1(), USER2(), 1, 999, array![]); // should panic } @@ -247,8 +269,7 @@ fn test_safe_transfer_to_zero() { // reverts when transferring to zero address let (world, erc1155) = deploy_testcase1(); - // impersonate user1 - set_contract_address(USER1()); + impersonate(USER1()); erc1155.safe_transfer_from(USER1(), ZERO(), 1, 1, array![]); // should panic } @@ -259,8 +280,7 @@ fn test_safe_transfer_debit_sender() { // debits transferred balance from sender let (world, erc1155) = deploy_testcase1(); - // impersonate user1 - set_contract_address(USER1()); + impersonate(USER1()); let balance_before = erc1155.balance_of(USER1(), 1); erc1155.safe_transfer_from(USER1(), USER2(), 1, 1, array![]); @@ -275,8 +295,7 @@ fn test_safe_transfer_credit_receiver() { // credits transferred balance to receiver let (world, erc1155) = deploy_testcase1(); - // impersonate user1 - set_contract_address(USER1()); + impersonate(USER1()); let balance_before = erc1155.balance_of(USER2(), 1); erc1155.safe_transfer_from(USER1(), USER2(), 1, 1, array![]); @@ -292,7 +311,7 @@ fn test_safe_transfer_preserve_existing_balances() { let (world, erc1155) = deploy_testcase1(); // impersonate user1 - set_contract_address(USER1()); + impersonate(USER1()); let balance_before_2 = erc1155.balance_of(USER2(), 2); let balance_before_3 = erc1155.balance_of(USER2(), 3); @@ -313,8 +332,7 @@ fn test_safe_transfer_from_unapproved_operator() { let (world, erc1155) = deploy_testcase1(); - // impersonate user2 - set_contract_address(USER2()); + impersonate(USER2()); erc1155.safe_transfer_from(USER1(), USER2(), 1, 1, array![]); // should panic } @@ -326,8 +344,7 @@ fn test_safe_transfer_from_approved_operator() { // when operator is approved by multiTokenHolder let (world, erc1155) = deploy_testcase1(); - // impersonate user1 - set_contract_address(PROXY()); + impersonate(PROXY()); let balance_before = erc1155.balance_of(USER1(), 1); erc1155.safe_transfer_from(USER1(), USER2(), 1, 2, array![]); @@ -343,8 +360,7 @@ fn test_safe_transfer_from_approved_operator_preserve_operator_balance() { // preserves operator's balances not involved in the transfer let (world, erc1155) = deploy_testcase1(); - // impersonate user1 - set_contract_address(PROXY()); + impersonate(PROXY()); let balance_before_1 = erc1155.balance_of(PROXY(), 1); let balance_before_2 = erc1155.balance_of(PROXY(), 2); @@ -360,6 +376,17 @@ fn test_safe_transfer_from_approved_operator_preserve_operator_balance() { } +#[test] +#[available_gas(50000000)] +#[should_panic] +fn test_safe_transfer_from_zero_address() { + let (world, erc1155) = deploy_testcase1(); + + impersonate(USER1()); + + erc1155.safe_transfer_from(ZERO(), USER1(), 1, 1, array![]); +} + // // safe_batch_transfer_from // @@ -371,8 +398,7 @@ fn test_safe_batch_transfer_from_more_than_balance() { // reverts when transferring amount more than any of balances let (world, erc1155) = deploy_testcase1(); - // impersonate user1 - set_contract_address(USER1()); + impersonate(USER1()); erc1155 .safe_batch_transfer_from(USER1(), USER2(), array![1, 2, 3], array![1, 999, 1], array![]); @@ -386,8 +412,7 @@ fn test_safe_batch_transfer_from_mismatching_array_len() { // reverts when ids array length doesn't match amounts array length let (world, erc1155) = deploy_testcase1(); - // impersonate user1 - set_contract_address(USER1()); + impersonate(USER1()); erc1155.safe_batch_transfer_from(USER1(), USER2(), array![1, 2, 3], array![1, 1], array![]); } @@ -400,8 +425,7 @@ fn test_safe_batch_transfer_from_to_zero_address() { // reverts when transferring to zero address let (world, erc1155) = deploy_testcase1(); - // impersonate user1 - set_contract_address(USER1()); + impersonate(USER1()); erc1155.safe_batch_transfer_from(USER1(), ZERO(), array![1, 2], array![1, 1], array![]); } @@ -413,8 +437,7 @@ fn test_safe_batch_transfer_from_debits_sender() { // debits transferred balances from sender let (world, erc1155) = deploy_testcase1(); - // impersonate user1 - set_contract_address(USER1()); + impersonate(USER1()); let balance_before_1 = erc1155.balance_of(USER1(), 1); let balance_before_2 = erc1155.balance_of(USER1(), 2); @@ -437,8 +460,7 @@ fn test_safe_batch_transfer_from_credits_recipient() { // credits transferred balances to receiver let (world, erc1155) = deploy_testcase1(); - // impersonate user1 - set_contract_address(USER1()); + impersonate(USER1()); let balance_before_1 = erc1155.balance_of(USER2(), 1); let balance_before_2 = erc1155.balance_of(USER2(), 2); @@ -464,8 +486,7 @@ fn test_safe_batch_transfer_from_unapproved_operator() { let (world, erc1155) = deploy_testcase1(); - // impersonate user2 - set_contract_address(USER2()); + impersonate(USER2()); erc1155.safe_batch_transfer_from(USER1(), USER2(), array![1, 2], array![1, 10], array![]); } @@ -478,8 +499,7 @@ fn test_safe_batch_transfer_from_approved_operator_preserve_operator_balance() { let (world, erc1155) = deploy_testcase1(); - // impersonate proxy - set_contract_address(PROXY()); + impersonate(PROXY()); let balance_before_1 = erc1155.balance_of(PROXY(), 1); let balance_before_2 = erc1155.balance_of(PROXY(), 2); @@ -496,6 +516,112 @@ fn test_safe_batch_transfer_from_approved_operator_preserve_operator_balance() { assert(balance_before_2 == balance_after_2, 'should be equal'); assert(balance_before_3 == balance_after_3, 'should be equal'); } + +#[test] +#[available_gas(50000000)] +#[should_panic] +fn test_safe_batch_transfer_from_zero_address() { + let (world, erc1155) = deploy_testcase1(); + + impersonate(USER1()); + + erc1155.safe_batch_transfer_from(ZERO(), USER1(), array![1, 2], array![1, 1], array![]); +} + + +#[test] +#[available_gas(50000000)] +fn test_safe_batch_transfer_emit_transfer_batch_event() { + let (world, erc1155) = deploy_default(); + + // user1 token_id 1 x 10 + erc1155.mint(USER1(), 1, 10, array![]); + // user1 token_id 2 x 20 + erc1155.mint(USER1(), 2, 20, array![]); + + impersonate(USER1()); + + erc1155.safe_batch_transfer_from(USER1(), USER2(), array![1, 2], array![1, 10], array![]); + + let _: Event = starknet::testing::pop_log(erc1155.contract_address) + .unwrap(); // unpop erc1155.mint(USER1(), 1, 10, array![]); + let _: Event = starknet::testing::pop_log(erc1155.contract_address) + .unwrap(); // unpop erc1155.mint(USER1(), 2, 20, array![]); + + // TransferBatch + assert( + @starknet::testing::pop_log(erc1155.contract_address) + .unwrap() == @Event::TransferBatch( + TransferBatch { + operator: USER1(), + from: USER1(), + to: USER2(), + ids: array![1, 2], + values: array![1, 10] + } + ), + 'invalid TransferBatch event' + ); +} + + +// +// burn +// + +#[test] +#[available_gas(90000000)] +#[should_panic] +fn test_burn_non_existing_token_id() { + //reverts when burning a non-existent token id + let (world, erc1155) = deploy_default(); + + impersonate(USER1()); + erc1155.burn(USER1(), 69, 1); // should panic +} + + +#[test] +#[available_gas(90000000)] +fn test_burn_emit_transfer_single_event() { + // burn should emit event + let (world, erc1155) = deploy_default(); + + erc1155.mint(USER1(), 69, 5, array![]); + assert(erc1155.balance_of(USER1(), 69) == 5, 'invalid balance'); + + impersonate(USER1()); + + erc1155.burn(USER1(), 69, 1); + assert(erc1155.balance_of(USER1(), 69) == 4, 'invalid balance'); + + let _: Event = starknet::testing::pop_log(erc1155.contract_address) + .unwrap(); // unpop erc1155.mint(USER1(), 69,5,array![]) + + // TransferSingle + assert( + @starknet::testing::pop_log(erc1155.contract_address) + .unwrap() == @Event::TransferSingle( + TransferSingle { operator: USER1(), from: USER1(), to: ZERO(), id: 69, value: 1 } + ), + 'invalid TransferSingle event' + ); +} + + +#[test] +#[available_gas(90000000)] +#[should_panic] +fn test_burn_more_than_owned() { + // reverts when burning more tokens than owned + let (world, erc1155) = deploy_default(); + erc1155.mint(USER1(), 69, 10, array![]); + + impersonate(USER1()); + + erc1155.burn(USER1(), 69, 1); + erc1155.burn(USER1(), 69, 10); // should panic +} // TODO : to be continued // TODO : add test if we support IERC1155Receiver diff --git a/crates/dojo-erc/src/tests/test_erc1155_utils.cairo b/crates/dojo-erc/src/tests/test_erc1155_utils.cairo index 1f4f0efcae..3feccda0b2 100644 --- a/crates/dojo-erc/src/tests/test_erc1155_utils.cairo +++ b/crates/dojo-erc/src/tests/test_erc1155_utils.cairo @@ -9,6 +9,7 @@ use starknet::testing::set_contract_address; use dojo::test_utils::spawn_test_world; use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; +use dojo_erc::tests::test_utils::impersonate; use dojo_erc::erc1155::erc1155::ERC1155; use dojo_erc::erc1155::interface::{IERC1155A, IERC1155ADispatcher, IERC1155ADispatcherTrait}; @@ -44,10 +45,12 @@ fn PROXY() -> ContractAddress { starknet::contract_address_const::<0x999>() } -fn spawn_world() -> IWorldDispatcher { +fn spawn_world(world_admin: ContractAddress) -> IWorldDispatcher { + impersonate(world_admin); + // components let mut components = array![ - erc_1155_balance::TEST_CLASS_HASH, uri::TEST_CLASS_HASH, operator_approval::TEST_CLASS_HASH, + erc_1155_balance::TEST_CLASS_HASH, uri::TEST_CLASS_HASH, operator_approval::TEST_CLASS_HASH, ]; // systems @@ -61,6 +64,21 @@ fn spawn_world() -> IWorldDispatcher { ]; let world = spawn_test_world(components, systems); + + // Grants writer rights for Component / System + + // erc_1155_balance + world.grant_writer('ERC1155Balance', 'ERC1155SafeTransferFrom'); + world.grant_writer('ERC1155Balance', 'ERC1155SafeBatchTransferFrom'); + world.grant_writer('ERC1155Balance', 'ERC1155Mint'); + world.grant_writer('ERC1155Balance', 'ERC1155Burn'); + + // uri + world.grant_writer('Uri', 'ERC1155SetUri'); + + // operator_approval + world.grant_writer('OperatorApproval', 'ERC1155SetApprovalForAll'); + world } @@ -78,7 +96,7 @@ fn deploy_erc1155( fn deploy_default() -> (IWorldDispatcher, IERC1155ADispatcher) { - let world = spawn_world(); + let world = spawn_world(DEPLOYER()); let erc1155_address = deploy_erc1155(world, DEPLOYER(), 'uri', 'seed-42'); let erc1155 = IERC1155ADispatcher { contract_address: erc1155_address }; @@ -87,7 +105,7 @@ fn deploy_default() -> (IWorldDispatcher, IERC1155ADispatcher) { fn deploy_testcase1() -> (IWorldDispatcher, IERC1155ADispatcher) { - let world = spawn_world(); + let world = spawn_world(DEPLOYER()); let erc1155_address = deploy_erc1155(world, DEPLOYER(), 'uri', 'seed-42'); let erc1155 = IERC1155ADispatcher { contract_address: erc1155_address }; @@ -105,7 +123,7 @@ fn deploy_testcase1() -> (IWorldDispatcher, IERC1155ADispatcher) { // user1 token_id 3 x 30 erc1155.mint(USER1(), 3, 30, array![]); - set_contract_address(USER1()); + impersonate(USER1()); //user1 approve_for_all proxy erc1155.set_approval_for_all(PROXY(), true); diff --git a/crates/dojo-erc/src/tests/test_erc721.cairo b/crates/dojo-erc/src/tests/test_erc721.cairo index 29fda2be12..78ec142d33 100644 --- a/crates/dojo-erc/src/tests/test_erc721.cairo +++ b/crates/dojo-erc/src/tests/test_erc721.cairo @@ -8,6 +8,7 @@ use option::OptionTrait; use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; +use dojo_erc::tests::test_utils::impersonate; use dojo_erc::tests::test_erc721_utils::{ spawn_world, deploy_erc721, deploy_default, deploy_testcase1, USER1, USER2, USER3, DEPLOYER, ZERO, PROXY @@ -25,7 +26,7 @@ use dojo_erc::erc721::erc721::ERC721::{Event, Transfer, Approval, ApprovalForAll #[test] #[available_gas(30000000)] fn test_deploy() { - let world = spawn_world(); + let world = spawn_world(DEPLOYER()); let erc721_address = deploy_erc721(world, DEPLOYER(), 'name', 'symbol', 'uri', 'seed-42'); let erc721 = IERC721ADispatcher { contract_address: erc721_address }; @@ -141,8 +142,7 @@ fn test_transfer_ownership() { let (world, erc721) = deploy_testcase1(); - // impersonate user1 - set_contract_address(USER1()); + impersonate(USER1()); let owner_of_1 = erc721.owner_of(1); // transfer token_id 1 to user2 @@ -160,14 +160,12 @@ fn test_transfer_event() { // mint erc721.mint(USER1(), 42); - // impersonate user1 - set_contract_address(USER1()); + impersonate(USER1()); // transfer token_id 1 to user2 erc721.transfer(USER2(), 42); - // impersonate user2 - set_contract_address(USER2()); + impersonate(USER2()); erc721.burn(42); // mint @@ -198,8 +196,7 @@ fn test_transfer_clear_approval() { let (world, erc721) = deploy_testcase1(); - // impersonate user1 - set_contract_address(USER1()); + impersonate(USER1()); erc721.approve(PROXY(), 1); assert(erc721.get_approved(1) == PROXY(), 'should be proxy'); @@ -216,8 +213,7 @@ fn test_transfer_adjusts_owners_balances() { let (world, erc721) = deploy_testcase1(); - // impersonate user1 - set_contract_address(USER1()); + impersonate(USER1()); let balance_user1_before = erc721.balance_of(USER1()); let balance_user2_before = erc721.balance_of(USER2()); @@ -240,14 +236,12 @@ fn test_transfer_from_approved() { let (world, erc721) = deploy_testcase1(); - // impersonate user1 - set_contract_address(USER1()); + impersonate(USER1()); //user1 approve user2 for token_id 2 erc721.approve(USER2(), 2); - // impersonate user2 - set_contract_address(USER2()); + impersonate(USER2()); erc721.transfer_from(USER1(), USER2(), 2); assert(erc721.owner_of(2) == USER2(), 'invalid owner'); @@ -260,14 +254,12 @@ fn test_transfer_from_approved_operator() { let (world, erc721) = deploy_testcase1(); - // impersonate user1 - set_contract_address(USER1()); + impersonate(USER1()); //user1 set_approval_for_all for proxy erc721.set_approval_for_all(PROXY(), true); - // impersonate proxy - set_contract_address(PROXY()); + impersonate(PROXY()); erc721.transfer_from(USER1(), USER2(), 2); assert(erc721.owner_of(2) == USER2(), 'invalid owner'); @@ -280,8 +272,7 @@ fn test_transfer_from_owner_without_approved() { let (world, erc721) = deploy_testcase1(); - // impersonate user1 - set_contract_address(USER1()); + impersonate(USER1()); erc721.approve(ZERO(), 2); @@ -297,8 +288,7 @@ fn test_transfer_to_owner() { let (world, erc721) = deploy_testcase1(); - // impersonate user1 - set_contract_address(USER1()); + impersonate(USER1()); let balance_before = erc721.balance_of(USER1()); @@ -327,8 +317,7 @@ fn test_transfer_when_previous_owner_is_incorrect() { let (world, erc721) = deploy_testcase1(); - // impersonate user1 - set_contract_address(USER1()); + impersonate(USER1()); //user2 owner token_id 10 erc721.transfer_from(USER1(), PROXY(), 10); // should panic @@ -341,8 +330,7 @@ fn test_transfer_when_sender_not_authorized() { // when the sender is not authorized for the token id let (world, erc721) = deploy_testcase1(); - // impersonate user1 - set_contract_address(PROXY()); + impersonate(PROXY()); //proxy is not authorized for USER2 erc721.transfer_from(USER2(), PROXY(), 20); // should panic @@ -355,8 +343,7 @@ fn test_transfer_when_token_id_doesnt_exists() { // when the sender is not authorized for the token id let (world, erc721) = deploy_testcase1(); - // impersonate user1 - set_contract_address(PROXY()); + impersonate(PROXY()); //proxy is authorized for USER1 but token_id 50 doesnt exists erc721.transfer_from(USER1(), PROXY(), 50); // should panic @@ -370,8 +357,7 @@ fn test_transfer_to_address_zero() { // when the address to transfer the token to is the zero address let (world, erc721) = deploy_testcase1(); - // impersonate user1 - set_contract_address(USER1()); + impersonate(USER1()); erc721.transfer(ZERO(), 1); // should panic } @@ -390,8 +376,8 @@ fn test_approval_when_clearing_with_prior_approval() { erc721.mint(USER1(), 42); - // impersonate user1 - set_contract_address(USER1()); + impersonate(USER1()); + erc721.approve(PROXY(), 42); //revoke approve @@ -422,8 +408,7 @@ fn test_approval_when_clearing_without_prior_approval() { erc721.mint(USER1(), 42); - // impersonate user1 - set_contract_address(USER1()); + impersonate(USER1()); //revoke approve erc721.approve(ZERO(), 42); @@ -452,8 +437,7 @@ fn test_approval_non_zero_address_with_prior_approval() { erc721.mint(USER1(), 42); - // impersonate user1 - set_contract_address(USER1()); + impersonate(USER1()); erc721.approve(PROXY(), 42); // user1 approves user3 @@ -483,8 +467,7 @@ fn test_approval_non_zero_address_with_no_prior_approval() { erc721.mint(USER1(), 42); - // impersonate user1 - set_contract_address(USER1()); + impersonate(USER1()); // user1 approves user3 erc721.approve(USER3(), 42); @@ -512,8 +495,9 @@ fn test_approval_self_approve() { let (world, erc721) = deploy_default(); erc721.mint(USER1(), 42); - // impersonate user1 - set_contract_address(USER1()); + + impersonate(USER1()); + // user1 approves user1 erc721.approve(USER1(), 42); // should panic } @@ -525,8 +509,9 @@ fn test_approval_not_owned() { // when the sender does not own the given token ID let (world, erc721) = deploy_testcase1(); - // impersonate user1 - set_contract_address(USER1()); + + impersonate(USER1()); + // user1 approves user2 for token 20 erc721.approve(USER2(), 20); // should panic } @@ -540,13 +525,13 @@ fn test_approval_from_approved_sender() { let (world, erc721) = deploy_testcase1(); - // impersonate user1 - set_contract_address(USER1()); + impersonate(USER1()); + // user1 approve user3 erc721.approve(USER3(), 1); - // impersonate USER3 - set_contract_address(USER3()); + impersonate(USER3()); + // (ERC721: approve caller is not token owner or approved for all) erc721.approve(USER2(), 1); // should panic } @@ -560,12 +545,12 @@ fn test_approval_from_approved_operator() { erc721.mint(USER1(), 50); - // impersonate user1 - set_contract_address(USER1()); + impersonate(USER1()); + erc721.set_approval_for_all(PROXY(), true); - // impersonate proxy , proxy is operator for user1 - set_contract_address(PROXY()); + impersonate(PROXY()); + // proxy approves user2 for token 20 erc721.approve(USER2(), 50); @@ -591,8 +576,8 @@ fn test_approval_unexisting_id() { // when the given token ID does not exist let (world, erc721) = deploy_testcase1(); - // impersonate user1 - set_contract_address(USER1()); + impersonate(USER1()); + // user1 approve user3 erc721.approve(USER3(), 69); // should panic } @@ -608,8 +593,7 @@ fn test_approval_for_all_operator_is_not_owner_no_operator_approval() { // -when there is no operator approval set by the sender let (world, erc721) = deploy_default(); - // impersonate user2 - set_contract_address(USER2()); + impersonate(USER2()); // user2 set_approval_for_all PROXY erc721.set_approval_for_all(PROXY(), true); @@ -633,8 +617,7 @@ fn test_approval_for_all_operator_is_not_owner_from_not_approved() { // -when the operator was set as not approved let (world, erc721) = deploy_default(); - // impersonate user2 - set_contract_address(USER2()); + impersonate(USER2()); erc721.set_approval_for_all(PROXY(), false); @@ -663,8 +646,7 @@ fn test_approval_for_all_operator_is_not_owner_can_unset_approval_for_all() { // can unset the operator approval let (world, erc721) = deploy_default(); - // impersonate user2 - set_contract_address(USER2()); + impersonate(USER2()); erc721.set_approval_for_all(PROXY(), false); erc721.set_approval_for_all(PROXY(), true); @@ -694,8 +676,7 @@ fn test_approval_for_all_operator_with_operator_already_approved() { // when the operator was already approved let (world, erc721) = deploy_default(); - // impersonate user2 - set_contract_address(USER2()); + impersonate(USER2()); erc721.set_approval_for_all(PROXY(), true); assert(erc721.is_approved_for_all(USER2(), PROXY()) == true, 'invalid is_approved_for_all'); @@ -724,8 +705,8 @@ fn test_approval_for_all_with_owner_as_operator() { let (world, erc721) = deploy_default(); - // impersonate user1 - set_contract_address(USER1()); + impersonate(USER1()); + erc721.set_approval_for_all(USER1(), true); // should panic } @@ -761,8 +742,8 @@ fn test_get_approved_with_existing_token_and_approval() { erc721.mint(USER1(), 420); - // impersonate user1 - set_contract_address(USER1()); + impersonate(USER1()); + erc721.approve(PROXY(), 420); assert(erc721.get_approved(420) == PROXY(), 'invalid get_approved'); } @@ -831,12 +812,15 @@ fn test_burn_non_existing_token_id() { #[test] #[available_gas(90000000)] -fn test_burn() { - //reverts when burning a non-existent token id +fn test_burn_emit_events() { + // burn should emit event let (world, erc721) = deploy_default(); erc721.mint(USER1(), 69); assert(erc721.balance_of(USER1()) == 1, 'invalid balance'); + + impersonate(USER1()); + erc721.burn(69); assert(erc721.balance_of(USER1()) == 0, 'invalid balance'); diff --git a/crates/dojo-erc/src/tests/test_erc721_utils.cairo b/crates/dojo-erc/src/tests/test_erc721_utils.cairo index 1ede104955..46f28ac997 100644 --- a/crates/dojo-erc/src/tests/test_erc721_utils.cairo +++ b/crates/dojo-erc/src/tests/test_erc721_utils.cairo @@ -9,6 +9,7 @@ use starknet::testing::set_contract_address; use dojo::test_utils::spawn_test_world; use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; +use dojo_erc::tests::test_utils::impersonate; use dojo_erc::erc721::erc721::ERC721; use dojo_erc::erc721::interface::{IERC721, IERC721ADispatcher, IERC721ADispatcherTrait}; @@ -45,7 +46,9 @@ fn PROXY() -> ContractAddress { starknet::contract_address_const::<0x999>() } -fn spawn_world() -> IWorldDispatcher { +fn spawn_world(world_admin: ContractAddress) -> IWorldDispatcher { + impersonate(world_admin); + // components let mut components = array![ erc_721_balance::TEST_CLASS_HASH, @@ -66,6 +69,29 @@ fn spawn_world() -> IWorldDispatcher { ]; let world = spawn_test_world(components, systems); + + // Grants writer rights for Component / System + + // erc_721_balance + world.grant_writer('ERC721Balance', 'ERC721TransferFrom'); + world.grant_writer('ERC721Balance', 'ERC721Mint'); + world.grant_writer('ERC721Balance', 'ERC721Burn'); + + // erc_721_owner + world.grant_writer('ERC721Owner', 'ERC721TransferFrom'); + world.grant_writer('ERC721Owner', 'ERC721Mint'); + world.grant_writer('ERC721Owner', 'ERC721Burn'); + + // erc_721_token_approval + world.grant_writer('ERC721TokenApproval', 'ERC721Approve'); + world.grant_writer('ERC721TokenApproval', 'ERC721TransferFrom'); + + // operator_approval + world.grant_writer('OperatorApproval', 'ERC721SetApprovalForAll'); + + // base_uri + world.grant_writer('BaseUri', 'ERC721SetBaseUri'); + world } @@ -91,7 +117,7 @@ fn deploy_erc721( fn deploy_default() -> (IWorldDispatcher, IERC721ADispatcher) { - let world = spawn_world(); + let world = spawn_world(DEPLOYER()); let erc721_address = deploy_erc721(world, DEPLOYER(), 'name', 'symbol', 'uri', 'seed-42'); let erc721 = IERC721ADispatcher { contract_address: erc721_address }; @@ -100,7 +126,7 @@ fn deploy_default() -> (IWorldDispatcher, IERC721ADispatcher) { fn deploy_testcase1() -> (IWorldDispatcher, IERC721ADispatcher) { - let world = spawn_world(); + let world = spawn_world(DEPLOYER()); let erc721_address = deploy_erc721(world, DEPLOYER(), 'name', 'symbol', 'uri', 'seed-42'); let erc721 = IERC721ADispatcher { contract_address: erc721_address }; @@ -118,7 +144,7 @@ fn deploy_testcase1() -> (IWorldDispatcher, IERC721ADispatcher) { //user2 owns id : 20 erc721.mint(USER2(), 20); - set_contract_address(USER1()); + impersonate(USER1()); //user1 approve_for_all proxy erc721.set_approval_for_all(PROXY(), true); diff --git a/crates/dojo-erc/src/tests/test_utils.cairo b/crates/dojo-erc/src/tests/test_utils.cairo new file mode 100644 index 0000000000..3f71f16ff3 --- /dev/null +++ b/crates/dojo-erc/src/tests/test_utils.cairo @@ -0,0 +1,11 @@ +use starknet::ContractAddress; +use starknet::testing::{set_contract_address, set_account_contract_address}; + +fn impersonate(address: ContractAddress) { + // world.cairo uses account_contract_address : + // - in constructor to define world owner + // - in assert_can_write to check ownership of world & component + + set_account_contract_address(address); + set_contract_address(address); +} From 4310dfa5ebc94e513f02f0bd27288c669ad69776 Mon Sep 17 00:00:00 2001 From: lambda-0x <0xlambda@protonmail.com> Date: Wed, 30 Aug 2023 20:23:06 +0530 Subject: [PATCH 18/77] fix(katana): make sure value is within `FieldElement` range (#848) --- crates/dojo-erc/Scarb.toml | 4 ++-- crates/dojo-world/src/metadata.rs | 8 ++++---- crates/katana/core/src/accounts.rs | 2 +- crates/torii/client/wasm/index.js | 4 ++-- examples/ecs/README.md | 8 ++++---- examples/ecs/Scarb.toml | 4 ++-- examples/rpc/starknet/starknet_getClassAt.hurl | 2 +- .../rpc/starknet/starknet_getClassHashAt.hurl | 2 +- examples/rpc/starknet/starknet_getNonce.hurl | 2 +- packages/core/src/constants/index.ts | 16 ++++++++++------ 10 files changed, 28 insertions(+), 24 deletions(-) diff --git a/crates/dojo-erc/Scarb.toml b/crates/dojo-erc/Scarb.toml index 74b8f6a42c..6accc0171a 100644 --- a/crates/dojo-erc/Scarb.toml +++ b/crates/dojo-erc/Scarb.toml @@ -24,6 +24,6 @@ test = "sozo test" [tool.dojo.env] # Katana rpc_url = "http://localhost:5050" -account_address = "0x03ee9e18edc71a6df30ac3aca2e0b02a198fbce19b7480a63a0d71cbd76652e0" -private_key = "0x0300001800000000300000180000000000030000000000003006001800006600" +account_address = "0x517ececd29116499f4a1b64b094da79ba08dfd54a3edaa316134c41f8160973" +private_key = "0x1800000000300000180000000000030000000000003006001800006600" world_address = "0x4cb5561a3a2a19d14f67f71a9da59c9194e2bbb44e1774260a111094fbd8f39" \ No newline at end of file diff --git a/crates/dojo-world/src/metadata.rs b/crates/dojo-world/src/metadata.rs index 70a8fc4d9e..584b84405e 100644 --- a/crates/dojo-world/src/metadata.rs +++ b/crates/dojo-world/src/metadata.rs @@ -77,8 +77,8 @@ mod test { r#" [env] rpc_url = "http://localhost:5050/" -account_address = "0x03ee9e18edc71a6df30ac3aca2e0b02a198fbce19b7480a63a0d71cbd76652e0" -private_key = "0x0300001800000000300000180000000000030000000000003006001800006600" +account_address = "0x517ececd29116499f4a1b64b094da79ba08dfd54a3edaa316134c41f8160973" +private_key = "0x1800000000300000180000000000030000000000003006001800006600" keystore_path = "test/" keystore_password = "dojo" world_address = "0x0248cacaeac64c45be0c19ee8727e0bb86623ca7fa3f0d431a6c55e200697e5a" @@ -92,11 +92,11 @@ world_address = "0x0248cacaeac64c45be0c19ee8727e0bb86623ca7fa3f0d431a6c55e200697 assert_eq!(env.rpc_url(), Some("http://localhost:5050/")); assert_eq!( env.account_address(), - Some("0x03ee9e18edc71a6df30ac3aca2e0b02a198fbce19b7480a63a0d71cbd76652e0") + Some("0x517ececd29116499f4a1b64b094da79ba08dfd54a3edaa316134c41f8160973") ); assert_eq!( env.private_key(), - Some("0x0300001800000000300000180000000000030000000000003006001800006600") + Some("0x1800000000300000180000000000030000000000003006001800006600") ); assert_eq!(env.keystore_path(), Some("test/")); assert_eq!(env.keystore_password(), Some("dojo")); diff --git a/crates/katana/core/src/accounts.rs b/crates/katana/core/src/accounts.rs index 2278d90ce7..87a0075091 100644 --- a/crates/katana/core/src/accounts.rs +++ b/crates/katana/core/src/accounts.rs @@ -155,7 +155,7 @@ impl DevAccountGenerator { let mut private_key_bytes = [0u8; 32]; rng.fill_bytes(&mut private_key_bytes); - private_key_bytes[0] %= 0x9; + private_key_bytes[0] %= 0x8; seed = private_key_bytes; let private_key = FieldElement::from_bytes_be(&private_key_bytes) diff --git a/crates/torii/client/wasm/index.js b/crates/torii/client/wasm/index.js index 608a143158..0707f32d80 100644 --- a/crates/torii/client/wasm/index.js +++ b/crates/torii/client/wasm/index.js @@ -37,7 +37,7 @@ async function run_wasm() { data: { component: "Position", keys: [ - "0x3ee9e18edc71a6df30ac3aca2e0b02a198fbce19b7480a63a0d71cbd76652e0", + "0x517ececd29116499f4a1b64b094da79ba08dfd54a3edaa316134c41f8160973", ], }, }); @@ -49,7 +49,7 @@ async function run_wasm() { data: { component: "Position", keys: [ - "0x3ee9e18edc71a6df30ac3aca2e0b02a198fbce19b7480a63a0d71cbd76652e0", + "0x517ececd29116499f4a1b64b094da79ba08dfd54a3edaa316134c41f8160973", ], length: 2, }, diff --git a/examples/ecs/README.md b/examples/ecs/README.md index 6325278901..311deed032 100644 --- a/examples/ecs/README.md +++ b/examples/ecs/README.md @@ -22,9 +22,9 @@ sozo component schema --world 0x26065106fa319c3981618e7567480a50132f23932226a51c > } # Get the value of the Moves component for an entity. (in this example, -# 0x3ee9e18edc71a6df30ac3aca2e0b02a198fbce19b7480a63a0d71cbd76652e0 is +# 0x517ececd29116499f4a1b64b094da79ba08dfd54a3edaa316134c41f8160973 is # the calling account. -sozo component entity --world 0x26065106fa319c3981618e7567480a50132f23932226a51c219ffb8e47daa84 Moves 0x3ee9e18edc71a6df30ac3aca2e0b02a198fbce19b7480a63a0d71cbd76652e0 +sozo component entity --world 0x26065106fa319c3981618e7567480a50132f23932226a51c219ffb8e47daa84 Moves 0x517ececd29116499f4a1b64b094da79ba08dfd54a3edaa316134c41f8160973 > 0x0 # The returned value is 0 since we haven't spawned yet. Let's spawn @@ -32,6 +32,6 @@ sozo component entity --world 0x26065106fa319c3981618e7567480a50132f23932226a51c sozo execute --world 0x26065106fa319c3981618e7567480a50132f23932226a51c219ffb8e47daa84 spawn # Fetch the updated entity -sozo component entity --world 0x26065106fa319c3981618e7567480a50132f23932226a51c219ffb8e47daa84 Moves 0x3ee9e18edc71a6df30ac3aca2e0b02a198fbce19b7480a63a0d71cbd76652e0 +sozo component entity --world 0x26065106fa319c3981618e7567480a50132f23932226a51c219ffb8e47daa84 Moves 0x517ececd29116499f4a1b64b094da79ba08dfd54a3edaa316134c41f8160973 > 0xa -``` \ No newline at end of file +``` diff --git a/examples/ecs/Scarb.toml b/examples/ecs/Scarb.toml index 96db091b0b..69e170fb7b 100644 --- a/examples/ecs/Scarb.toml +++ b/examples/ecs/Scarb.toml @@ -31,5 +31,5 @@ initializer_class_hash = "0xbeef" rpc_url = "http://localhost:5050/" # Default account for katana with seed = 0 -account_address = "0x03ee9e18edc71a6df30ac3aca2e0b02a198fbce19b7480a63a0d71cbd76652e0" -private_key = "0x0300001800000000300000180000000000030000000000003006001800006600" +account_address = "0x517ececd29116499f4a1b64b094da79ba08dfd54a3edaa316134c41f8160973" +private_key = "0x1800000000300000180000000000030000000000003006001800006600" \ No newline at end of file diff --git a/examples/rpc/starknet/starknet_getClassAt.hurl b/examples/rpc/starknet/starknet_getClassAt.hurl index f53b6d0e46..1cf31f3d04 100644 --- a/examples/rpc/starknet/starknet_getClassAt.hurl +++ b/examples/rpc/starknet/starknet_getClassAt.hurl @@ -5,7 +5,7 @@ Content-Type: application/json "method": "starknet_getClassAt", "params": [ "latest", - "0x3ee9e18edc71a6df30ac3aca2e0b02a198fbce19b7480a63a0d71cbd76652e0" + "0x517ececd29116499f4a1b64b094da79ba08dfd54a3edaa316134c41f8160973" ], "id":1 } diff --git a/examples/rpc/starknet/starknet_getClassHashAt.hurl b/examples/rpc/starknet/starknet_getClassHashAt.hurl index 0b6e6188fb..630b8e7884 100644 --- a/examples/rpc/starknet/starknet_getClassHashAt.hurl +++ b/examples/rpc/starknet/starknet_getClassHashAt.hurl @@ -5,7 +5,7 @@ Content-Type: application/json "method": "starknet_getClassHashAt", "params": [ "pending", - "0x3ee9e18edc71a6df30ac3aca2e0b02a198fbce19b7480a63a0d71cbd76652e0" + "0x517ececd29116499f4a1b64b094da79ba08dfd54a3edaa316134c41f8160973" ], "id": 1 } diff --git a/examples/rpc/starknet/starknet_getNonce.hurl b/examples/rpc/starknet/starknet_getNonce.hurl index 62ae6e1fe1..f95bee24e2 100644 --- a/examples/rpc/starknet/starknet_getNonce.hurl +++ b/examples/rpc/starknet/starknet_getNonce.hurl @@ -5,7 +5,7 @@ Content-Type: application/json "method": "starknet_getNonce", "params": [ "latest", - "0x3ee9e18edc71a6df30ac3aca2e0b02a198fbce19b7480a63a0d71cbd76652e0" + "0x517ececd29116499f4a1b64b094da79ba08dfd54a3edaa316134c41f8160973" ], "id":1 } diff --git a/packages/core/src/constants/index.ts b/packages/core/src/constants/index.ts index 866da44007..099a2ad34f 100644 --- a/packages/core/src/constants/index.ts +++ b/packages/core/src/constants/index.ts @@ -1,6 +1,10 @@ -export const KATANA_ACCOUNT_1_ADDRESS = "0x3ee9e18edc71a6df30ac3aca2e0b02a198fbce19b7480a63a0d71cbd76652e0" -export const KATANA_ACCOUNT_1_PRIVATEKEY = "0x300001800000000300000180000000000030000000000003006001800006600" -export const LOCAL_KATANA = 'http://127.0.0.1:5050'; -export const LOCAL_TORII = 'http://localhost:8080' -export const DOJO_STARTER_WORLD = "0x26065106fa319c3981618e7567480a50132f23932226a51c219ffb8e47daa84" -export const ACCOUNT_CLASS_HASH = "0x04d07e40e93398ed3c76981e72dd1fd22557a78ce36c0515f679e27f0bb5bc5f" \ No newline at end of file +export const KATANA_ACCOUNT_1_ADDRESS = + "0x517ececd29116499f4a1b64b094da79ba08dfd54a3edaa316134c41f8160973"; +export const KATANA_ACCOUNT_1_PRIVATEKEY = + "0x1800000000300000180000000000030000000000003006001800006600"; +export const LOCAL_KATANA = "http://127.0.0.1:5050"; +export const LOCAL_TORII = "http://localhost:8080"; +export const DOJO_STARTER_WORLD = + "0x26065106fa319c3981618e7567480a50132f23932226a51c219ffb8e47daa84"; +export const ACCOUNT_CLASS_HASH = + "0x04d07e40e93398ed3c76981e72dd1fd22557a78ce36c0515f679e27f0bb5bc5f"; From cc94096aed3ca49d366c67540f02bb9a81be1fc2 Mon Sep 17 00:00:00 2001 From: Loaf <90423308+ponderingdemocritus@users.noreply.github.com> Date: Thu, 31 Aug 2023 13:53:25 +1000 Subject: [PATCH 19/77] computed read function (#852) --- packages/core/package.json | 2 +- packages/core/src/constants/abi.json | 335 ++++++++++++++++++++++ packages/core/src/provider/RPCProvider.ts | 27 +- 3 files changed, 362 insertions(+), 2 deletions(-) create mode 100644 packages/core/src/constants/abi.json diff --git a/packages/core/package.json b/packages/core/package.json index 470411439f..6185ff39ba 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@dojoengine/core", - "version": "0.0.16", + "version": "0.0.17", "description": "Dojo engine core providers and types", "scripts": { "build": "tsc", diff --git a/packages/core/src/constants/abi.json b/packages/core/src/constants/abi.json new file mode 100644 index 0000000000..4774e82355 --- /dev/null +++ b/packages/core/src/constants/abi.json @@ -0,0 +1,335 @@ +[ + { + "type": "function", + "name": "component", + "inputs": [ + { + "name": "name", + "type": "core::felt252" + } + ], + "outputs": [ + { + "type": "core::starknet::class_hash::ClassHash" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "register_component", + "inputs": [ + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "system", + "inputs": [ + { + "name": "name", + "type": "core::felt252" + } + ], + "outputs": [ + { + "type": "core::starknet::class_hash::ClassHash" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "register_system", + "inputs": [ + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "uuid", + "inputs": [], + "outputs": [ + { + "type": "core::integer::u32" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "emit", + "inputs": [ + { + "name": "keys", + "type": "core::array::Span::" + }, + { + "name": "values", + "type": "core::array::Span::" + } + ], + "outputs": [], + "state_mutability": "view" + }, + { + "type": "function", + "name": "execute", + "inputs": [ + { + "name": "system", + "type": "core::felt252" + }, + { + "name": "calldata", + "type": "core::array::Array::" + } + ], + "outputs": [ + { + "type": "core::array::Array::" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "entity", + "inputs": [ + { + "name": "component", + "type": "core::felt252" + }, + { + "name": "query", + "type": "dojo::database::query::Query" + }, + { + "name": "offset", + "type": "core::integer::u8" + }, + { + "name": "length", + "type": "core::integer::u32" + } + ], + "outputs": [ + { + "type": "core::array::Span::" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "set_entity", + "inputs": [ + { + "name": "component", + "type": "core::felt252" + }, + { + "name": "query", + "type": "dojo::database::query::Query" + }, + { + "name": "offset", + "type": "core::integer::u8" + }, + { + "name": "value", + "type": "core::array::Span::" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "entities", + "inputs": [ + { + "name": "component", + "type": "core::felt252" + }, + { + "name": "partition", + "type": "core::felt252" + }, + { + "name": "length", + "type": "core::integer::u32" + } + ], + "outputs": [ + { + "type": "(core::array::Span::, core::array::Span::>)" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "set_executor", + "inputs": [ + { + "name": "contract_address", + "type": "core::starknet::contract_address::ContractAddress" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "executor", + "inputs": [], + "outputs": [ + { + "type": "core::starknet::contract_address::ContractAddress" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "delete_entity", + "inputs": [ + { + "name": "component", + "type": "core::felt252" + }, + { + "name": "query", + "type": "dojo::database::query::Query" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "origin", + "inputs": [], + "outputs": [ + { + "type": "core::starknet::contract_address::ContractAddress" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "is_owner", + "inputs": [ + { + "name": "account", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "target", + "type": "core::felt252" + } + ], + "outputs": [ + { + "type": "core::bool" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "grant_owner", + "inputs": [ + { + "name": "account", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "target", + "type": "core::felt252" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "revoke_owner", + "inputs": [ + { + "name": "account", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "target", + "type": "core::felt252" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "is_writer", + "inputs": [ + { + "name": "component", + "type": "core::felt252" + }, + { + "name": "system", + "type": "core::felt252" + } + ], + "outputs": [ + { + "type": "core::bool" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "grant_writer", + "inputs": [ + { + "name": "component", + "type": "core::felt252" + }, + { + "name": "system", + "type": "core::felt252" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "revoke_writer", + "inputs": [ + { + "name": "component", + "type": "core::felt252" + }, + { + "name": "system", + "type": "core::felt252" + } + ], + "outputs": [], + "state_mutability": "external" + } +] \ No newline at end of file diff --git a/packages/core/src/provider/RPCProvider.ts b/packages/core/src/provider/RPCProvider.ts index 946c6ef6d5..f596e7ed86 100644 --- a/packages/core/src/provider/RPCProvider.ts +++ b/packages/core/src/provider/RPCProvider.ts @@ -1,14 +1,16 @@ -import { RpcProvider, Account, num, Call, InvokeFunctionResponse } from "starknet"; +import { RpcProvider, Account, num, Call, InvokeFunctionResponse, Contract, Result } from "starknet"; import { Provider } from "./provider"; import { Query, WorldEntryPoints } from "../types"; import { strTofelt252Felt } from '../utils' import { LOCAL_KATANA } from '../constants'; +import abi from '../constants/abi.json'; /** * RPCProvider class: Extends the generic Provider to handle RPC interactions. */ export class RPCProvider extends Provider { public provider: RpcProvider; + public contract: Contract /** * Constructor: Initializes the RPCProvider with the given world address and URL. @@ -21,6 +23,7 @@ export class RPCProvider extends Provider { this.provider = new RpcProvider({ nodeUrl: url, }); + this.contract = new Contract(abi, this.getWorldAddress(), this.provider); } /** @@ -125,4 +128,26 @@ export class RPCProvider extends Provider { throw error; } } + + /** + * Calls a function with the given parameters. + * + * @param {string} selector - The selector of the function. + * @param {num.BigNumberish[]} call_data - The call data for the function. + * @returns {Promise} - A promise that resolves to the response of the function call. + * @throws {Error} - Throws an error if the call fails. + * + * @example + * const response = await provider.call("position", [1, 2, 3]); + * console.log(response.result); + * // => 6 + * + */ + public async call(selector: string, call_data: num.BigNumberish[]): Promise { + try { + return await this.contract.call('execute', [strTofelt252Felt(selector), call_data]); + } catch (error) { + throw error; + } + } } \ No newline at end of file From e210b9fbfe94885856080184899e53fa7dbede7a Mon Sep 17 00:00:00 2001 From: lambda-0x <0xlambda@protonmail.com> Date: Thu, 31 Aug 2023 18:57:30 +0530 Subject: [PATCH 20/77] fix regex for macos (#854) --- dojoup/dojoup | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dojoup/dojoup b/dojoup/dojoup index a7cb709bc8..a0e7e2c871 100755 --- a/dojoup/dojoup +++ b/dojoup/dojoup @@ -84,7 +84,7 @@ main() { | grep -oE '"tag_name": "[^"]*"|"prerelease": (true|false)' \ | grep -B1 '"prerelease": false' \ | grep '"tag_name":' \ - | grep -oP '"v[0-9]*\.[0-9]*\.[0-9]*"' \ + | grep -oE '"v[0-9]*\.[0-9]*\.[0-9]*"' \ | tr -d '"' \ | head -n 1) DOJOUP_VERSION=$DOJOUP_TAG From 7ec1dcdb40588e0d0c97508a69ac872160226451 Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Sat, 2 Sep 2023 00:54:56 +0800 Subject: [PATCH 21/77] feat(sozo): add tx config for migration (#856) --- crates/dojo-world/src/migration/mod.rs | 58 ++++++++++++------- crates/sozo/src/commands/migrate.rs | 4 ++ crates/sozo/src/commands/options/mod.rs | 1 + .../sozo/src/commands/options/transaction.rs | 20 +++++++ .../sozo/src/ops/migration/migration_test.rs | 58 +++++++++++++++++-- crates/sozo/src/ops/migration/mod.rs | 33 +++++++---- .../torii/client/src/contract/world_test.rs | 13 +++-- 7 files changed, 149 insertions(+), 38 deletions(-) create mode 100644 crates/sozo/src/commands/options/transaction.rs diff --git a/crates/dojo-world/src/migration/mod.rs b/crates/dojo-world/src/migration/mod.rs index 1f31fd45bf..a3f81c5407 100644 --- a/crates/dojo-world/src/migration/mod.rs +++ b/crates/dojo-world/src/migration/mod.rs @@ -76,12 +76,21 @@ pub trait StateDiff { fn is_same(&self) -> bool; } +/// The transaction configuration to use when sending a transaction. +#[derive(Debug, Copy, Clone, Default)] +pub struct TxConfig { + /// The multiplier for how much the actual transaction max fee should be relative to the + /// estimated fee. If `None` is provided, the multiplier is set to `1.1`. + pub fee_estimate_multiplier: Option, +} + #[cfg_attr(not(target_arch = "wasm32"), async_trait)] #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] pub trait Declarable { async fn declare( &self, account: &SingleOwnerAccount, + txn_config: TxConfig, ) -> Result< DeclareOutput, MigrationError< as Account>::SignError,

::Error>, @@ -107,13 +116,18 @@ pub trait Declarable { Err(e) => return Err(MigrationError::Provider(e)), } - let DeclareTransactionResult { transaction_hash, class_hash } = account - .declare(Arc::new(flattened_class), casm_class_hash) - .send() - .await - .map_err(MigrationError::Migrator)?; + let mut txn = account.declare(Arc::new(flattened_class), casm_class_hash); - let _ = TransactionWaiter::new(transaction_hash, account.provider()).await.unwrap(); + if let TxConfig { fee_estimate_multiplier: Some(multiplier) } = txn_config { + txn = txn.fee_estimate_multiplier(multiplier); + } + + let DeclareTransactionResult { transaction_hash, class_hash } = + txn.send().await.map_err(MigrationError::Migrator)?; + + TransactionWaiter::new(transaction_hash, account.provider()) + .await + .map_err(MigrationError::WaitingError)?; return Ok(DeclareOutput { transaction_hash, class_hash }); } @@ -129,6 +143,7 @@ pub trait Deployable: Declarable + Sync { class_hash: FieldElement, constructor_calldata: Vec, account: &SingleOwnerAccount, + txn_config: TxConfig, ) -> Result< DeployOutput, MigrationError< as Account>::SignError,

::Error>, @@ -137,7 +152,7 @@ pub trait Deployable: Declarable + Sync { P: Provider + Sync + Send, S: Signer + Sync + Send, { - let declare = match self.declare(account).await { + let declare = match self.declare(account, txn_config).await { Ok(res) => Some(res), Err(MigrationError::ClassAlreadyDeclared) => None, @@ -176,19 +191,22 @@ pub trait Deployable: Declarable + Sync { Err(e) => return Err(MigrationError::Provider(e)), } - let InvokeTransactionResult { transaction_hash } = account - .execute(vec![Call { - calldata, - // devnet UDC address - to: FieldElement::from_hex_be( - "0x41a78e741e5af2fec34b695679bc6891742439f7afb8484ecd7766661ad02bf", - ) - .unwrap(), - selector: get_selector_from_name("deployContract").unwrap(), - }]) - .send() - .await - .map_err(MigrationError::Migrator)?; + let mut txn = account.execute(vec![Call { + calldata, + // devnet UDC address + to: FieldElement::from_hex_be( + "0x41a78e741e5af2fec34b695679bc6891742439f7afb8484ecd7766661ad02bf", + ) + .unwrap(), + selector: get_selector_from_name("deployContract").unwrap(), + }]); + + if let TxConfig { fee_estimate_multiplier: Some(multiplier) } = txn_config { + txn = txn.fee_estimate_multiplier(multiplier); + } + + let InvokeTransactionResult { transaction_hash } = + txn.send().await.map_err(MigrationError::Migrator)?; let txn = TransactionWaiter::new(transaction_hash, account.provider()) .await diff --git a/crates/sozo/src/commands/migrate.rs b/crates/sozo/src/commands/migrate.rs index b43f64b500..10d4236d94 100644 --- a/crates/sozo/src/commands/migrate.rs +++ b/crates/sozo/src/commands/migrate.rs @@ -5,6 +5,7 @@ use scarb::core::Config; use super::options::account::AccountOptions; use super::options::starknet::StarknetOptions; +use super::options::transaction::TransactionOptions; use super::options::world::WorldOptions; use crate::ops::migration; @@ -28,6 +29,9 @@ pub struct MigrateArgs { #[command(flatten)] pub account: AccountOptions, + + #[command(flatten)] + pub transaction: TransactionOptions, } impl MigrateArgs { diff --git a/crates/sozo/src/commands/options/mod.rs b/crates/sozo/src/commands/options/mod.rs index 21f968db17..e7d2645387 100644 --- a/crates/sozo/src/commands/options/mod.rs +++ b/crates/sozo/src/commands/options/mod.rs @@ -1,3 +1,4 @@ pub mod account; pub mod starknet; +pub mod transaction; pub mod world; diff --git a/crates/sozo/src/commands/options/transaction.rs b/crates/sozo/src/commands/options/transaction.rs new file mode 100644 index 0000000000..e1e4a6a94b --- /dev/null +++ b/crates/sozo/src/commands/options/transaction.rs @@ -0,0 +1,20 @@ +use clap::Args; +use dojo_world::migration::TxConfig; + +#[derive(Debug, Args, Clone)] +#[command(next_help_heading = "Transaction options")] +pub struct TransactionOptions { + #[arg(long)] + #[arg(value_name = "MULTIPLIER")] + #[arg(help = "The multiplier to use for the fee estimate.")] + #[arg(long_help = "The multiplier to use for the fee estimate. This value will be used on \ + the estimated fee which will be used as the max fee for the transaction. \ + (max_fee = estimated_fee * multiplier)")] + pub fee_estimate_multiplier: Option, +} + +impl From for TxConfig { + fn from(value: TransactionOptions) -> Self { + Self { fee_estimate_multiplier: value.fee_estimate_multiplier } + } +} diff --git a/crates/sozo/src/ops/migration/migration_test.rs b/crates/sozo/src/ops/migration/migration_test.rs index e5afd2f697..50da9cab60 100644 --- a/crates/sozo/src/ops/migration/migration_test.rs +++ b/crates/sozo/src/ops/migration/migration_test.rs @@ -1,6 +1,6 @@ use camino::Utf8PathBuf; use dojo_test_utils::sequencer::{ - get_default_test_starknet_config, SequencerConfig, TestSequencer, + get_default_test_starknet_config, SequencerConfig, StarknetConfig, TestSequencer, }; use dojo_world::manifest::Manifest; use dojo_world::migration::strategy::prepare_for_migration; @@ -14,6 +14,7 @@ use starknet::providers::jsonrpc::HttpTransport; use starknet::providers::JsonRpcClient; use starknet::signers::{LocalWallet, SigningKey}; +use crate::commands::options::transaction::TransactionOptions; use crate::ops::migration::execute_strategy; #[tokio::test] @@ -47,7 +48,7 @@ async fn migrate_with_auto_mine() { world, ) .unwrap(); - execute_strategy(&migration, &account, &config).await.unwrap(); + execute_strategy(&migration, &account, &config, None).await.unwrap(); sequencer.stop().unwrap(); } @@ -86,7 +87,56 @@ async fn migrate_with_block_time() { world, ) .unwrap(); - execute_strategy(&migration, &account, &config).await.unwrap(); + execute_strategy(&migration, &account, &config, None).await.unwrap(); + + sequencer.stop().unwrap(); +} + +#[tokio::test] +async fn migrate_with_zero_fee_multiplier_will_fail() { + let target_dir = Utf8PathBuf::from_path_buf("../../examples/ecs/target/dev".into()).unwrap(); + + let sequencer = TestSequencer::start( + SequencerConfig { block_time: Some(1) }, + StarknetConfig { disable_fee: false, ..Default::default() }, + ) + .await; + + let account = SingleOwnerAccount::new( + JsonRpcClient::new(HttpTransport::new(sequencer.url())), + LocalWallet::from_signing_key(SigningKey::from_secret_scalar( + sequencer.raw_account().private_key, + )), + sequencer.raw_account().account_address, + chain_id::TESTNET, + ); + + let config = Config::builder(Utf8PathBuf::from_path_buf("../../examples/ecs/".into()).unwrap()) + .ui_verbosity(Verbosity::Quiet) + .build() + .unwrap(); + + let manifest = Manifest::load_from_path(target_dir.join("manifest.json")).unwrap(); + let world = WorldDiff::compute(manifest, None); + + let migration = prepare_for_migration( + None, + Some(FieldElement::from_hex_be("0x12345").unwrap()), + target_dir, + world, + ) + .unwrap(); + + assert!( + execute_strategy( + &migration, + &account, + &config, + Some(TransactionOptions { fee_estimate_multiplier: Some(0f64) }), + ) + .await + .is_err() + ); sequencer.stop().unwrap(); } @@ -133,7 +183,7 @@ async fn migration_from_remote() { ) .unwrap(); - execute_strategy(&migration, &account, &config).await.unwrap(); + execute_strategy(&migration, &account, &config, None).await.unwrap(); let local_manifest = Manifest::load_from_path(target_dir.join("manifest.json")).unwrap(); let remote_manifest = Manifest::from_remote( diff --git a/crates/sozo/src/ops/migration/mod.rs b/crates/sozo/src/ops/migration/mod.rs index 7d22b8d5fd..8b30381fdc 100644 --- a/crates/sozo/src/ops/migration/mod.rs +++ b/crates/sozo/src/ops/migration/mod.rs @@ -31,6 +31,7 @@ use self::ui::{bold_message, italic_message}; use crate::commands::migrate::MigrateArgs; use crate::commands::options::account::AccountOptions; use crate::commands::options::starknet::StarknetOptions; +use crate::commands::options::transaction::TransactionOptions; use crate::commands::options::world::WorldOptions; pub async fn execute( @@ -70,7 +71,7 @@ where println!(" "); - let block_height = execute_strategy(&strategy, &account, config) + let block_height = execute_strategy(&strategy, &account, config, Some(args.transaction)) .await .map_err(|e| anyhow!(e)) .with_context(|| "Problem trying to migrate.")?; @@ -220,6 +221,7 @@ async fn execute_strategy( strategy: &MigrationStrategy, migrator: &SingleOwnerAccount, ws_config: &Config, + txn_config: Option, ) -> Result> where P: Provider + Sync + Send + 'static, @@ -230,7 +232,15 @@ where Some(executor) => { ws_config.ui().print_header("# Executor"); - match executor.deploy(executor.diff.local, vec![], migrator).await { + match executor + .deploy( + executor.diff.local, + vec![], + migrator, + txn_config.clone().map(|c| c.into()).unwrap_or_default(), + ) + .await + { Ok(val) => { if let Some(declare) = val.clone().declare { ws_config.ui().print_hidden_sub(format!( @@ -282,6 +292,7 @@ where world.diff.local, vec![strategy.executor.as_ref().unwrap().contract_address], migrator, + txn_config.clone().map(|c| c.into()).unwrap_or_default(), ) .await { @@ -314,8 +325,8 @@ where None => {} }; - register_components(strategy, migrator, ws_config).await?; - register_systems(strategy, migrator, ws_config).await?; + register_components(strategy, migrator, ws_config, txn_config.clone()).await?; + register_systems(strategy, migrator, ws_config, txn_config).await?; Ok(block_height) } @@ -324,6 +335,7 @@ async fn register_components( strategy: &MigrationStrategy, migrator: &SingleOwnerAccount, ws_config: &Config, + txn_config: Option, ) -> Result> where P: Provider + Sync + Send + 'static, @@ -342,7 +354,8 @@ where for c in components.iter() { ws_config.ui().print(italic_message(&c.diff.name).to_string()); - let res = c.declare(migrator).await; + let res = + c.declare(migrator, txn_config.clone().map(|c| c.into()).unwrap_or_default()).await; match res { Ok(output) => { ws_config.ui().print_hidden_sub(format!( @@ -371,8 +384,7 @@ where .await .map_err(|e| anyhow!("Failed to register components to World: {e}"))?; - let _ = - TransactionWaiter::new(transaction_hash, migrator.provider()).await.map_err( + TransactionWaiter::new(transaction_hash, migrator.provider()).await.map_err( MigrationError::< as Account>::SignError,

::Error, @@ -388,6 +400,7 @@ async fn register_systems( strategy: &MigrationStrategy, migrator: &SingleOwnerAccount, ws_config: &Config, + txn_config: Option, ) -> Result> where P: Provider + Sync + Send + 'static, @@ -406,7 +419,8 @@ where for s in strategy.systems.iter() { ws_config.ui().print(italic_message(&s.diff.name).to_string()); - let res = s.declare(migrator).await; + let res = + s.declare(migrator, txn_config.clone().map(|c| c.into()).unwrap_or_default()).await; match res { Ok(output) => { ws_config.ui().print_hidden_sub(format!( @@ -435,8 +449,7 @@ where .await .map_err(|e| anyhow!("Failed to register systems to World: {e}"))?; - let _ = - TransactionWaiter::new(transaction_hash, migrator.provider()).await.map_err( + TransactionWaiter::new(transaction_hash, migrator.provider()).await.map_err( MigrationError::< as Account>::SignError,

::Error, diff --git a/crates/torii/client/src/contract/world_test.rs b/crates/torii/client/src/contract/world_test.rs index 1b7d5ae73a..5ea5f2816a 100644 --- a/crates/torii/client/src/contract/world_test.rs +++ b/crates/torii/client/src/contract/world_test.rs @@ -47,21 +47,26 @@ pub async fn deploy_world( let executor_address = strategy .executor .unwrap() - .deploy(manifest.clone().executor.class_hash, vec![], &account) + .deploy(manifest.clone().executor.class_hash, vec![], &account, Default::default()) .await .unwrap() .contract_address; let world_address = strategy .world .unwrap() - .deploy(manifest.clone().world.class_hash, vec![executor_address], &account) + .deploy( + manifest.clone().world.class_hash, + vec![executor_address], + &account, + Default::default(), + ) .await .unwrap() .contract_address; let mut declare_output = vec![]; for component in strategy.components { - let res = component.declare(&account).await.unwrap(); + let res = component.declare(&account, Default::default()).await.unwrap(); declare_output.push(res); } @@ -72,7 +77,7 @@ pub async fn deploy_world( let mut declare_output = vec![]; for system in strategy.systems { - let res = system.declare(&account).await.unwrap(); + let res = system.declare(&account, Default::default()).await.unwrap(); declare_output.push(res); } From 3e1100d9820a099d82f194c4be41622696d961cb Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Mon, 4 Sep 2023 08:38:23 +0900 Subject: [PATCH 22/77] feat(katana): network forking + major refactor (#805) * Refactor blockchain storage type * Generalized state database. *Added new database implementation, ForkedDb, for fetching data from a forked network. * Refactor block production and transaction execution. Added a transaction pool. --- Cargo.lock | 49 +- Cargo.toml | 4 +- crates/dojo-test-utils/src/sequencer.rs | 4 +- crates/katana/Cargo.toml | 2 + crates/katana/core/Cargo.toml | 4 +- crates/katana/core/src/accounts.rs | 10 +- crates/katana/core/src/backend/config.rs | 7 +- crates/katana/core/src/backend/executor.rs | 288 --------- .../katana/core/src/backend/in_memory_db.rs | 375 ++++++++++++ crates/katana/core/src/backend/mod.rs | 383 ++++++------ crates/katana/core/src/backend/state.rs | 543 ----------------- .../katana/core/src/backend/storage/block.rs | 2 +- crates/katana/core/src/backend/storage/mod.rs | 103 +++- .../core/src/backend/storage/transaction.rs | 17 +- crates/katana/core/src/db/cached.rs | 570 ++++++++++++++++++ crates/katana/core/src/db/mod.rs | 100 ++- crates/katana/core/src/db/serde/state.rs | 3 +- crates/katana/core/src/env.rs | 2 +- crates/katana/core/src/execution.rs | 475 +++++++++++++++ crates/katana/core/src/fork/backend.rs | 475 +++++++++++++++ crates/katana/core/src/fork/db.rs | 275 +++++++++ crates/katana/core/src/fork/mod.rs | 2 + crates/katana/core/src/lib.rs | 4 + crates/katana/core/src/pool.rs | 74 +++ crates/katana/core/src/sequencer.rs | 323 ++++++---- crates/katana/core/src/service.rs | 483 +++++++++++++++ crates/katana/core/src/utils/contract.rs | 25 +- crates/katana/core/tests/backend.rs | 38 ++ crates/katana/core/tests/sequencer.rs | 221 +++++++ crates/katana/core/tests/starknet.rs | 181 ------ crates/katana/rpc/Cargo.toml | 1 + crates/katana/rpc/src/katana.rs | 6 +- crates/katana/rpc/src/starknet.rs | 94 ++- crates/katana/rpc/tests/starknet.rs | 20 +- crates/katana/src/args.rs | 17 +- crates/katana/src/main.rs | 7 +- .../sozo/src/ops/migration/migration_test.rs | 16 +- .../client/src/contract/component_test.rs | 2 +- .../torii/client/src/contract/system_test.rs | 12 +- .../torii/client/src/contract/world_test.rs | 20 +- 40 files changed, 3769 insertions(+), 1468 deletions(-) delete mode 100644 crates/katana/core/src/backend/executor.rs create mode 100644 crates/katana/core/src/backend/in_memory_db.rs delete mode 100644 crates/katana/core/src/backend/state.rs create mode 100644 crates/katana/core/src/db/cached.rs create mode 100644 crates/katana/core/src/execution.rs create mode 100644 crates/katana/core/src/fork/backend.rs create mode 100644 crates/katana/core/src/fork/db.rs create mode 100644 crates/katana/core/src/fork/mod.rs create mode 100644 crates/katana/core/src/pool.rs create mode 100644 crates/katana/core/src/service.rs create mode 100644 crates/katana/core/tests/backend.rs create mode 100644 crates/katana/core/tests/sequencer.rs delete mode 100644 crates/katana/core/tests/starknet.rs diff --git a/Cargo.lock b/Cargo.lock index 511aace833..659fed9945 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1588,6 +1588,42 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "console-api" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2895653b4d9f1538a83970077cb01dfc77a4810524e51a110944688e916b18e" +dependencies = [ + "prost", + "prost-types", + "tonic", + "tracing-core", +] + +[[package]] +name = "console-subscriber" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4cf42660ac07fcebed809cfe561dd8730bcd35b075215e6479c516bcd0d11cb" +dependencies = [ + "console-api", + "crossbeam-channel", + "crossbeam-utils", + "futures", + "hdrhistogram", + "humantime", + "prost-types", + "serde", + "serde_json", + "thread_local", + "tokio", + "tokio-stream", + "tonic", + "tracing", + "tracing-core", + "tracing-subscriber", +] + [[package]] name = "const-fnv1a-hash" version = "1.1.0" @@ -3387,7 +3423,10 @@ version = "7.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f19b9f54f7c7f55e31401bb647626ce0cf0f67b0004982ce815b3ee72a02aa8" dependencies = [ + "base64 0.13.1", "byteorder", + "flate2", + "nom", "num-traits 0.2.16", ] @@ -3951,12 +3990,14 @@ dependencies = [ "clap", "clap_complete", "console", + "console-subscriber", "env_logger", "katana-core", "katana-rpc", "log", "starknet_api", "tokio", + "url", ] [[package]] @@ -3984,7 +4025,9 @@ dependencies = [ "starknet_api", "thiserror", "tokio", + "tokio-stream", "tracing", + "url", ] [[package]] @@ -3998,6 +4041,7 @@ dependencies = [ "cairo-vm", "dojo-test-utils", "flate2", + "futures", "hex", "hyper", "jsonrpsee", @@ -6553,9 +6597,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.31.0" +version = "1.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40de3a2ba249dcb097e01be5e67a5ff53cf250397715a071a81543e8a832a920" +checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" dependencies = [ "backtrace", "bytes", @@ -6567,6 +6611,7 @@ dependencies = [ "signal-hook-registry", "socket2 0.5.3", "tokio-macros", + "tracing", "windows-sys 0.48.0", ] diff --git a/Cargo.toml b/Cargo.toml index c6e36a0f29..92073ca483 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,6 +59,7 @@ console = "0.15.7" convert_case = "0.6.0" env_logger = "0.10.0" flate2 = "1.0.24" +futures = "0.3.28" indoc = "1.0.7" itertools = "0.10.3" log = "0.4.17" @@ -77,10 +78,11 @@ starknet-crypto = "0.5.1" starknet_api = { git = "https://github.com/starkware-libs/starknet-api", rev = "ecc9b6946ef13003da202838e4124a9ad2efabb0" } test-log = "0.2.11" thiserror = "1.0.32" -tokio = { version = "1.16", features = [ "full" ] } +tokio = { version = "1.32.0", features = [ "full" ] } toml = "0.7.4" tracing = "0.1" tracing-subscriber = "0.3.16" +url = "2.2.2" [patch."https://github.com/starkware-libs/blockifier"] blockifier = { git = "https://github.com/dojoengine/blockifier", rev = "c794d1b" } diff --git a/crates/dojo-test-utils/src/sequencer.rs b/crates/dojo-test-utils/src/sequencer.rs index 76a224bdbd..aedce61725 100644 --- a/crates/dojo-test-utils/src/sequencer.rs +++ b/crates/dojo-test-utils/src/sequencer.rs @@ -29,9 +29,7 @@ pub struct TestSequencer { impl TestSequencer { pub async fn start(config: SequencerConfig, starknet_config: StarknetConfig) -> Self { - let sequencer = Arc::new(KatanaSequencer::new(config, starknet_config)); - - sequencer.start().await; + let sequencer = Arc::new(KatanaSequencer::new(config, starknet_config).await); let starknet_api = StarknetApi::new(sequencer.clone()); let katana_api = KatanaApi::new(sequencer.clone()); diff --git a/crates/katana/Cargo.toml b/crates/katana/Cargo.toml index 1c42e492f4..7f3368d7cc 100644 --- a/crates/katana/Cargo.toml +++ b/crates/katana/Cargo.toml @@ -9,6 +9,7 @@ version.workspace = true [dependencies] clap.workspace = true clap_complete.workspace = true +console-subscriber = "0.1.10" console.workspace = true env_logger.workspace = true katana-core = { path = "core" } @@ -16,6 +17,7 @@ katana-rpc = { path = "rpc" } log.workspace = true starknet_api.workspace = true tokio.workspace = true +url.workspace = true [dev-dependencies] assert_matches = "1.5.0" diff --git a/crates/katana/core/Cargo.toml b/crates/katana/core/Cargo.toml index 95b7189dd8..cde0b2bb2e 100644 --- a/crates/katana/core/Cargo.toml +++ b/crates/katana/core/Cargo.toml @@ -16,7 +16,7 @@ cairo-lang-starknet = "2.1.1" cairo-vm.workspace = true convert_case.workspace = true flate2.workspace = true -futures = "0.3" +futures.workspace = true lazy_static = "1.4.0" parking_lot = "0.12.1" rand = { version = "0.8.5", features = [ "small_rng" ] } @@ -26,8 +26,10 @@ serde_with.workspace = true starknet.workspace = true starknet_api.workspace = true thiserror.workspace = true +tokio-stream = "0.1.14" tokio.workspace = true tracing = "0.1.34" +url.workspace = true [dev-dependencies] assert_matches = "1.5.0" diff --git a/crates/katana/core/src/accounts.rs b/crates/katana/core/src/accounts.rs index 87a0075091..a897b0b1f8 100644 --- a/crates/katana/core/src/accounts.rs +++ b/crates/katana/core/src/accounts.rs @@ -19,7 +19,7 @@ use starknet_api::patricia_key; use crate::constants::{ DEFAULT_ACCOUNT_CONTRACT, DEFAULT_ACCOUNT_CONTRACT_CLASS_HASH, FEE_TOKEN_ADDRESS, }; -use crate::db::Db; +use crate::db::Database; #[serde_as] #[derive(Debug, Clone, Serialize)] @@ -58,14 +58,14 @@ impl Account { } // TODO: separate fund logic from this struct - implement FeeToken type - pub fn deploy_and_fund(&self, state: &mut S) -> StateResult<()> { + pub fn deploy_and_fund(&self, state: &mut S) -> StateResult<()> { self.declare(state)?; self.deploy(state)?; self.fund(state); Ok(()) } - fn deploy(&self, state: &mut S) -> StateResult<()> { + fn deploy(&self, state: &mut S) -> StateResult<()> { let address = ContractAddress(patricia_key!(self.address)); // set the class hash at the account address state.set_class_hash_at(address, ClassHash(self.class_hash.into()))?; @@ -80,7 +80,7 @@ impl Account { Ok(()) } - fn fund(&self, state: &mut S) { + fn fund(&self, state: &mut S) { state.set_storage_at( ContractAddress(patricia_key!(*FEE_TOKEN_ADDRESS)), get_storage_var_address("ERC20_balances", &[self.address.into()]).unwrap(), @@ -88,7 +88,7 @@ impl Account { ); } - fn declare(&self, state: &mut S) -> StateResult<()> { + fn declare(&self, state: &mut S) -> StateResult<()> { let class_hash = ClassHash(self.class_hash.into()); if state.get_compiled_contract_class(&class_hash).is_ok() { diff --git a/crates/katana/core/src/backend/config.rs b/crates/katana/core/src/backend/config.rs index 3267b8ffe0..c6a7131fe0 100644 --- a/crates/katana/core/src/backend/config.rs +++ b/crates/katana/core/src/backend/config.rs @@ -3,6 +3,7 @@ use starknet_api::block::{BlockNumber, BlockTimestamp}; use starknet_api::core::{ChainId, ContractAddress, PatriciaKey}; use starknet_api::hash::StarkHash; use starknet_api::patricia_key; +use url::Url; use crate::constants::{ DEFAULT_GAS_PRICE, DEFAULT_INVOKE_MAX_STEPS, DEFAULT_VALIDATE_MAX_STEPS, FEE_TOKEN_ADDRESS, @@ -14,10 +15,11 @@ use crate::env::{get_default_vm_resource_fee_cost, BlockContextGenerator}; #[derive(Debug)] pub struct StarknetConfig { pub seed: [u8; 32], - pub auto_mine: bool, pub total_accounts: u8, pub disable_fee: bool, pub env: Environment, + pub fork_rpc_url: Option, + pub fork_block_number: Option, pub init_state: Option, } @@ -47,9 +49,10 @@ impl Default for StarknetConfig { Self { init_state: None, seed: [0; 32], - auto_mine: true, total_accounts: 10, disable_fee: false, + fork_rpc_url: None, + fork_block_number: None, env: Environment::default(), } } diff --git a/crates/katana/core/src/backend/executor.rs b/crates/katana/core/src/backend/executor.rs deleted file mode 100644 index 61fcbde83a..0000000000 --- a/crates/katana/core/src/backend/executor.rs +++ /dev/null @@ -1,288 +0,0 @@ -use std::sync::Arc; - -use blockifier::block_context::BlockContext; -use blockifier::execution::entry_point::CallInfo; -use blockifier::state::cached_state::CachedState; -use blockifier::state::state_api::StateReader; -use blockifier::transaction::errors::TransactionExecutionError; -use blockifier::transaction::objects::{ResourcesMapping, TransactionExecutionInfo}; -use blockifier::transaction::transaction_execution::Transaction as ExecutionTransaction; -use blockifier::transaction::transactions::ExecutableTransaction; -use convert_case::{Case, Casing}; -use parking_lot::RwLock; -use starknet::core::types::{Event, FieldElement, MsgToL1}; -use tokio::sync::RwLock as AsyncRwLock; -use tracing::{info, trace, warn}; - -use super::state::MemDb; -use super::storage::block::{PartialBlock, PartialHeader}; -use super::storage::transaction::{RejectedTransaction, Transaction, TransactionOutput}; -use super::storage::BlockchainStorage; -use crate::backend::storage::transaction::{DeclareTransaction, KnownTransaction}; -use crate::env::Env; - -#[derive(Debug)] -pub struct PendingBlockExecutor { - pub parent_hash: FieldElement, - /// The state of the pending block. It is the state that the - /// transaction included in the pending block will be executed on. - /// The changes made after the execution of a transaction will be - /// persisted for the next included transaction. - pub state: CachedState, - pub storage: Arc>, - pub env: Arc>, - pub transactions: Vec>, - pub outputs: Vec, -} - -impl PendingBlockExecutor { - pub fn new( - parent_hash: FieldElement, - state: MemDb, - env: Arc>, - storage: Arc>, - ) -> Self { - Self { - env, - storage, - parent_hash, - outputs: Vec::new(), - transactions: Vec::new(), - state: CachedState::new(state), - } - } - - pub fn as_block(&self) -> PartialBlock { - let block_context = &self.env.read().block; - - let header = PartialHeader { - parent_hash: self.parent_hash, - gas_price: block_context.gas_price, - number: block_context.block_number.0, - timestamp: block_context.block_timestamp.0, - sequencer_address: (*block_context.sequencer_address.0.key()).into(), - }; - - PartialBlock { - header, - outputs: self.outputs.clone(), - transactions: self.transactions.clone(), - } - } - - // Add a transaction to the executor. The transaction will be executed - // on the pending state. The transaction will be added to the pending block - // if it passes the validation logic. Otherwise, the transaction will be - // rejected. On both cases, the transaction will still be stored in the - // storage. - pub async fn add_transaction(&mut self, transaction: Transaction, charge_fee: bool) -> bool { - let transaction_hash = transaction.hash(); - - info!("Transaction received | Hash: {transaction_hash:#x}"); - - let res = execute_transaction( - ExecutionTransaction::AccountTransaction(transaction.clone().into()), - &mut self.state, - &self.env.read().block, - charge_fee, - ); - - match res { - Ok(execution_info) => { - trace!( - "Transaction resource usage: {}", - pretty_print_resources(&execution_info.actual_resources) - ); - - // Because `State` trait from `blockifier` doesn't have a method to set the - // `sierra_class` of a contract, we need to do it manually. - if let Transaction::Declare(DeclareTransaction { - inner, - sierra_class: Some(sierra_class), - .. - }) = &transaction - { - let class_hash = inner.class_hash(); - self.state.state.sierra_classes.insert(class_hash, sierra_class.clone()); - } - - let executed_tx = Arc::new(ExecutedTransaction::new(transaction, execution_info)); - - trace_events(&executed_tx.output.events); - - self.outputs.push(executed_tx.output.clone()); - self.transactions.push(executed_tx); - - true - } - - Err(err) => { - self.storage.write().await.transactions.insert( - transaction_hash, - KnownTransaction::Rejected(Box::new(RejectedTransaction { - transaction: transaction.into(), - execution_error: err.to_string(), - })), - ); - - false - } - } - } -} - -#[derive(Debug)] -pub struct ExecutedTransaction { - pub inner: Transaction, - pub output: TransactionOutput, - pub execution_info: TransactionExecutionInfo, -} - -impl ExecutedTransaction { - pub fn new(transaction: Transaction, execution_info: TransactionExecutionInfo) -> Self { - let actual_fee = execution_info.actual_fee.0; - let events = Self::events(&execution_info); - let messages_sent = Self::l2_to_l1_messages(&execution_info); - - Self { - inner: transaction, - execution_info, - output: TransactionOutput { actual_fee, events, messages_sent }, - } - } - - fn events(execution_info: &TransactionExecutionInfo) -> Vec { - let mut events: Vec = vec![]; - - fn get_events_recursively(call_info: &CallInfo) -> Vec { - let mut events: Vec = vec![]; - - events.extend(call_info.execution.events.iter().map(|e| Event { - from_address: (*call_info.call.storage_address.0.key()).into(), - data: e.event.data.0.iter().map(|d| (*d).into()).collect(), - keys: e.event.keys.iter().map(|k| k.0.into()).collect(), - })); - - call_info.inner_calls.iter().for_each(|call| { - events.extend(get_events_recursively(call)); - }); - - events - } - - if let Some(ref call) = execution_info.validate_call_info { - events.extend(get_events_recursively(call)); - } - - if let Some(ref call) = execution_info.execute_call_info { - events.extend(get_events_recursively(call)); - } - - if let Some(ref call) = execution_info.fee_transfer_call_info { - events.extend(get_events_recursively(call)); - } - - events - } - - fn l2_to_l1_messages(execution_info: &TransactionExecutionInfo) -> Vec { - let mut messages = vec![]; - - fn get_messages_recursively(info: &CallInfo) -> Vec { - let mut messages = vec![]; - - messages.extend(info.execution.l2_to_l1_messages.iter().map(|m| MsgToL1 { - to_address: - FieldElement::from_byte_slice_be(m.message.to_address.0.as_bytes()).unwrap(), - from_address: (*info.call.caller_address.0.key()).into(), - payload: m.message.payload.0.iter().map(|p| (*p).into()).collect(), - })); - - info.inner_calls.iter().for_each(|call| { - messages.extend(get_messages_recursively(call)); - }); - - messages - } - - if let Some(ref info) = execution_info.validate_call_info { - messages.extend(get_messages_recursively(info)); - } - - if let Some(ref info) = execution_info.execute_call_info { - messages.extend(get_messages_recursively(info)); - } - - if let Some(ref info) = execution_info.fee_transfer_call_info { - messages.extend(get_messages_recursively(info)); - } - - messages - } -} - -pub fn execute_transaction( - transaction: ExecutionTransaction, - pending_state: &mut CachedState, - block_context: &BlockContext, - charge_fee: bool, -) -> Result { - let res = match transaction { - ExecutionTransaction::AccountTransaction(tx) => { - tx.execute(pending_state, block_context, charge_fee) - } - ExecutionTransaction::L1HandlerTransaction(tx) => { - tx.execute(pending_state, block_context, charge_fee) - } - }; - - match res { - Ok(exec_info) => { - if let Some(err) = &exec_info.revert_error { - let formatted_err = format!("{:?}", err).replace("\\n", "\n"); - warn!("Transaction execution error: {formatted_err}"); - } - Ok(exec_info) - } - Err(err) => { - warn!("Transaction validation error: {err:?}"); - Err(err) - } - } -} - -pub fn pretty_print_resources(resources: &ResourcesMapping) -> String { - let mut mapped_strings: Vec<_> = resources - .0 - .iter() - .filter_map(|(k, v)| match k.as_str() { - "l1_gas_usage" => Some(format!("L1 Gas: {}", v)), - "range_check_builtin" => Some(format!("Range Checks: {}", v)), - "ecdsa_builtin" => Some(format!("ECDSA: {}", v)), - "n_steps" => None, - "pedersen_builtin" => Some(format!("Pedersen: {}", v)), - "bitwise_builtin" => Some(format!("Bitwise: {}", v)), - "keccak_builtin" => Some(format!("Keccak: {}", v)), - _ => Some(format!("{}: {}", k.to_case(Case::Title), v)), - }) - .collect::>(); - - // Sort the strings alphabetically - mapped_strings.sort(); - - // Prepend "Steps" if it exists, so it is always first - if let Some(steps) = resources.0.get("n_steps") { - mapped_strings.insert(0, format!("Steps: {}", steps)); - } - - mapped_strings.join(" | ") -} - -pub fn trace_events(events: &[Event]) { - for e in events { - let formatted_keys = - e.keys.iter().map(|k| format!("{k:#x}")).collect::>().join(", "); - - trace!("Event emitted keys=[{}]", formatted_keys); - } -} diff --git a/crates/katana/core/src/backend/in_memory_db.rs b/crates/katana/core/src/backend/in_memory_db.rs new file mode 100644 index 0000000000..efad41a1de --- /dev/null +++ b/crates/katana/core/src/backend/in_memory_db.rs @@ -0,0 +1,375 @@ +use std::collections::{BTreeMap, HashMap}; + +use anyhow::Result; +use blockifier::execution::contract_class::ContractClass; +use blockifier::state::cached_state::CommitmentStateDiff; +use blockifier::state::errors::StateError; +use blockifier::state::state_api::{State, StateReader, StateResult}; +use starknet::core::types::FlattenedSierraClass; +use starknet_api::core::{ClassHash, CompiledClassHash, ContractAddress, Nonce, PatriciaKey}; +use starknet_api::hash::{StarkFelt, StarkHash}; +use starknet_api::patricia_key; +use starknet_api::state::StorageKey; + +use crate::constants::{ + ERC20_CONTRACT, ERC20_CONTRACT_CLASS_HASH, FEE_TOKEN_ADDRESS, UDC_ADDRESS, UDC_CLASS_HASH, + UDC_CONTRACT, +}; +use crate::db::cached::{CachedDb, ClassRecord, StorageRecord}; +use crate::db::serde::state::{ + SerializableClassRecord, SerializableState, SerializableStorageRecord, +}; +use crate::db::{AsStateRefDb, Database, StateExt, StateExtRef, StateRefDb}; + +/// An empty state database which returns default values for all queries. +#[derive(Debug, Clone)] +pub struct EmptyDb; + +impl StateReader for EmptyDb { + fn get_class_hash_at(&mut self, _contract_address: ContractAddress) -> StateResult { + Ok(ClassHash::default()) + } + + fn get_nonce_at(&mut self, _contract_address: ContractAddress) -> StateResult { + Ok(Nonce::default()) + } + + fn get_compiled_class_hash(&mut self, class_hash: ClassHash) -> StateResult { + Err(StateError::UndeclaredClassHash(class_hash)) + } + + fn get_storage_at( + &mut self, + _contract_address: ContractAddress, + _key: StorageKey, + ) -> StateResult { + Ok(StarkFelt::default()) + } + + fn get_compiled_contract_class( + &mut self, + class_hash: &ClassHash, + ) -> StateResult { + Err(StateError::UndeclaredClassHash(*class_hash)) + } +} + +impl StateExtRef for EmptyDb { + fn get_sierra_class(&mut self, class_hash: &ClassHash) -> StateResult { + Err(StateError::UndeclaredClassHash(*class_hash)) + } +} + +/// A in memory state database implementation with empty cache db. +#[derive(Clone, Debug)] +pub struct MemDb { + pub db: CachedDb, +} + +impl MemDb { + pub fn new() -> Self { + Self { db: CachedDb::new(EmptyDb) } + } +} + +impl Default for MemDb { + fn default() -> Self { + let mut state = Self::new(); + deploy_fee_contract(&mut state); + deploy_universal_deployer_contract(&mut state); + state + } +} + +impl State for MemDb { + fn increment_nonce(&mut self, contract_address: ContractAddress) -> StateResult<()> { + let current_nonce = self.get_nonce_at(contract_address)?; + let current_nonce_as_u64 = usize::try_from(current_nonce.0)? as u64; + let next_nonce_val = 1_u64 + current_nonce_as_u64; + let next_nonce = Nonce(StarkFelt::from(next_nonce_val)); + self.db.storage.entry(contract_address).or_default().nonce = next_nonce; + Ok(()) + } + + fn set_storage_at( + &mut self, + contract_address: ContractAddress, + key: StorageKey, + value: StarkFelt, + ) { + self.db.storage.entry(contract_address).or_default().storage.insert(key, value); + } + + fn set_class_hash_at( + &mut self, + contract_address: ContractAddress, + class_hash: ClassHash, + ) -> StateResult<()> { + if contract_address == ContractAddress::default() { + return Err(StateError::OutOfRangeContractAddress); + } + self.db.contracts.insert(contract_address, class_hash); + Ok(()) + } + + fn set_compiled_class_hash( + &mut self, + class_hash: ClassHash, + compiled_class_hash: CompiledClassHash, + ) -> StateResult<()> { + if !self.db.classes.contains_key(&class_hash) { + return Err(StateError::UndeclaredClassHash(class_hash)); + } + self.db.classes.entry(class_hash).and_modify(|r| r.compiled_hash = compiled_class_hash); + Ok(()) + } + + fn set_contract_class( + &mut self, + class_hash: &ClassHash, + contract_class: ContractClass, + ) -> StateResult<()> { + let compiled_hash = CompiledClassHash(class_hash.0); + self.db.classes.insert(*class_hash, ClassRecord { class: contract_class, compiled_hash }); + Ok(()) + } + + fn to_state_diff(&self) -> CommitmentStateDiff { + unreachable!("to_state_diff should not be called on MemDb") + } +} + +impl StateReader for MemDb { + fn get_storage_at( + &mut self, + contract_address: ContractAddress, + key: StorageKey, + ) -> StateResult { + let value = self + .db + .storage + .get(&contract_address) + .and_then(|r| r.storage.get(&key)) + .copied() + .unwrap_or_default(); + Ok(value) + } + + fn get_nonce_at(&mut self, contract_address: ContractAddress) -> StateResult { + let nonce = self.db.storage.get(&contract_address).map(|r| r.nonce).unwrap_or_default(); + Ok(nonce) + } + + fn get_compiled_contract_class( + &mut self, + class_hash: &ClassHash, + ) -> StateResult { + self.db + .classes + .get(class_hash) + .map(|r| r.class.clone()) + .ok_or(StateError::UndeclaredClassHash(*class_hash)) + } + + fn get_class_hash_at(&mut self, contract_address: ContractAddress) -> StateResult { + Ok(self.db.contracts.get(&contract_address).cloned().unwrap_or_default()) + } + + fn get_compiled_class_hash( + &mut self, + class_hash: ClassHash, + ) -> StateResult { + self.db + .classes + .get(&class_hash) + .map(|r| r.compiled_hash) + .ok_or(StateError::UndeclaredClassHash(class_hash)) + } +} + +impl StateExtRef for MemDb { + fn get_sierra_class(&mut self, class_hash: &ClassHash) -> StateResult { + if let ContractClass::V0(_) = self.get_compiled_contract_class(class_hash)? { + return Err(StateError::StateReadError("Class hash is not a Sierra class".to_string())); + }; + + self.db + .sierra_classes + .get(class_hash) + .cloned() + .ok_or(StateError::StateReadError("Missing Sierra class".to_string())) + } +} + +impl StateExt for MemDb { + fn set_sierra_class( + &mut self, + class_hash: ClassHash, + sierra_class: FlattenedSierraClass, + ) -> StateResult<()> { + // check the class hash must not be a legacy contract + if let ContractClass::V0(_) = self.get_compiled_contract_class(&class_hash)? { + return Err(StateError::StateReadError("Class hash is not a Sierra class".to_string())); + }; + self.db.sierra_classes.insert(class_hash, sierra_class); + Ok(()) + } +} + +impl AsStateRefDb for MemDb { + fn as_ref_db(&self) -> StateRefDb { + StateRefDb::new(MemDb { db: self.db.clone() }) + } +} + +impl Database for MemDb { + fn dump_state(&self) -> Result { + let mut serializable = SerializableState::default(); + + self.db.storage.iter().for_each(|(addr, storage)| { + let mut record = SerializableStorageRecord { + storage: BTreeMap::new(), + nonce: storage.nonce.0.into(), + }; + + storage.storage.iter().for_each(|(key, value)| { + record.storage.insert((*key.0.key()).into(), (*value).into()); + }); + + serializable.storage.insert((*addr.0.key()).into(), record); + }); + + self.db.classes.iter().for_each(|(class_hash, class_record)| { + serializable.classes.insert( + class_hash.0.into(), + SerializableClassRecord { + class: class_record.class.clone().into(), + compiled_hash: class_record.compiled_hash.0.into(), + }, + ); + }); + + self.db.contracts.iter().for_each(|(address, class_hash)| { + serializable.contracts.insert((*address.0.key()).into(), class_hash.0.into()); + }); + + self.db.sierra_classes.iter().for_each(|(class_hash, class)| { + serializable.sierra_classes.insert(class_hash.0.into(), class.clone()); + }); + + Ok(serializable) + } + + fn set_nonce(&mut self, addr: ContractAddress, nonce: Nonce) { + self.db.storage.entry(addr).or_default().nonce = nonce; + } +} + +fn deploy_fee_contract(state: &mut MemDb) { + let address = ContractAddress(patricia_key!(*FEE_TOKEN_ADDRESS)); + let hash = ClassHash(*ERC20_CONTRACT_CLASS_HASH); + let compiled_hash = CompiledClassHash(*ERC20_CONTRACT_CLASS_HASH); + + state.db.classes.insert(hash, ClassRecord { class: (*ERC20_CONTRACT).clone(), compiled_hash }); + state.db.contracts.insert(address, hash); + state + .db + .storage + .insert(address, StorageRecord { nonce: Nonce(1_u128.into()), storage: HashMap::new() }); +} + +fn deploy_universal_deployer_contract(state: &mut MemDb) { + let address = ContractAddress(patricia_key!(*UDC_ADDRESS)); + let hash = ClassHash(*UDC_CLASS_HASH); + let compiled_hash = CompiledClassHash(*UDC_CLASS_HASH); + + state.db.classes.insert(hash, ClassRecord { class: (*UDC_CONTRACT).clone(), compiled_hash }); + state.db.contracts.insert(address, hash); + state + .db + .storage + .insert(address, StorageRecord { nonce: Nonce(1_u128.into()), storage: HashMap::new() }); +} + +#[cfg(test)] +mod tests { + use assert_matches::assert_matches; + use starknet_api::core::{ClassHash, PatriciaKey}; + use starknet_api::stark_felt; + + use super::*; + use crate::backend::in_memory_db::MemDb; + use crate::constants::UDC_CONTRACT; + // use crate::db::cached::CachedStateWrapper; + use crate::execution::ExecutionOutcome; + + #[test] + fn dump_and_load_state() { + let mut state = MemDb::new(); + + let class_hash = ClassHash(stark_felt!("0x1")); + let address = ContractAddress(patricia_key!("0x1")); + let storage_key = StorageKey(patricia_key!("0x77")); + let storage_val = stark_felt!("0x66"); + let contract = (*UDC_CONTRACT).clone(); + let compiled_hash = CompiledClassHash(class_hash.0); + + state.set_contract_class(&class_hash, (*UDC_CONTRACT).clone()).unwrap(); + state.set_compiled_class_hash(class_hash, CompiledClassHash(class_hash.0)).unwrap(); + state.set_class_hash_at(address, class_hash).unwrap(); + state.set_storage_at(address, storage_key, storage_val); + + let dump = state.dump_state().expect("should dump state"); + + let mut new_state = MemDb::new(); + new_state.load_state(dump).expect("should load state"); + + assert_eq!(new_state.get_compiled_contract_class(&class_hash).unwrap(), contract); + assert_eq!(new_state.get_compiled_class_hash(class_hash).unwrap(), compiled_hash); + assert_eq!(new_state.get_class_hash_at(address).unwrap(), class_hash); + assert_eq!(new_state.get_storage_at(address, storage_key).unwrap(), storage_val); + } + + #[test] + fn apply_state_update() { + let mut old_state = MemDb::new(); + + let class_hash = ClassHash(stark_felt!("0x1")); + let address = ContractAddress(patricia_key!("0x1")); + let storage_key = StorageKey(patricia_key!("0x77")); + let storage_val = stark_felt!("0x66"); + let contract = (*UDC_CONTRACT).clone(); + let compiled_hash = CompiledClassHash(class_hash.0); + + let execution_outcome = ExecutionOutcome { + state_diff: CommitmentStateDiff { + address_to_class_hash: [(address, class_hash)].into(), + address_to_nonce: [].into(), + storage_updates: [(address, [(storage_key, storage_val)].into())].into(), + class_hash_to_compiled_class_hash: [(class_hash, CompiledClassHash(class_hash.0))] + .into(), + }, + transactions: vec![], + declared_classes: HashMap::from([(class_hash, (*UDC_CONTRACT).clone())]), + declared_sierra_classes: HashMap::new(), + }; + + assert_matches!( + old_state.get_compiled_contract_class(&class_hash), + Err(StateError::UndeclaredClassHash(_)) + ); + assert_matches!( + old_state.get_compiled_class_hash(class_hash), + Err(StateError::UndeclaredClassHash(_)) + ); + assert_eq!(old_state.get_class_hash_at(address).unwrap(), ClassHash::default()); + assert_eq!(old_state.get_storage_at(address, storage_key).unwrap(), StarkFelt::default()); + + execution_outcome.apply_to(&mut old_state); + + assert_eq!(old_state.get_compiled_contract_class(&class_hash).unwrap(), contract); + assert_eq!(old_state.get_compiled_class_hash(class_hash).unwrap(), compiled_hash); + assert_eq!(old_state.get_class_hash_at(address).unwrap(), class_hash); + assert_eq!(old_state.get_storage_at(address, storage_key).unwrap(), storage_val); + } +} diff --git a/crates/katana/core/src/backend/mod.rs b/crates/katana/core/src/backend/mod.rs index 95fadf1772..8c3c542b4d 100644 --- a/crates/katana/core/src/backend/mod.rs +++ b/crates/katana/core/src/backend/mod.rs @@ -8,41 +8,45 @@ use blockifier::execution::entry_point::{ use blockifier::execution::errors::EntryPointExecutionError; use blockifier::fee::fee_utils::{calculate_l1_gas_by_vm_usage, extract_l1_gas_and_vm_usage}; use blockifier::state::cached_state::{CachedState, MutRefState}; -use blockifier::state::state_api::State; -use blockifier::transaction::account_transaction::AccountTransaction; +use blockifier::state::state_api::StateReader; use blockifier::transaction::errors::TransactionExecutionError; use blockifier::transaction::objects::AccountTransactionContext; -use blockifier::transaction::transaction_execution::Transaction as ExecutionTransaction; use flate2::write::GzEncoder; use flate2::Compression; use parking_lot::RwLock; -use starknet::core::types::{BlockId, BlockTag, FeeEstimate}; -use starknet_api::block::BlockTimestamp; -use starknet_api::core::{ContractAddress, EntryPointSelector}; -use starknet_api::hash::StarkFelt; -use starknet_api::state::StorageKey; +use starknet::core::types::{BlockId, BlockTag, FeeEstimate, MaybePendingBlockWithTxHashes}; +use starknet::core::utils::parse_cairo_short_string; +use starknet::providers::jsonrpc::HttpTransport; +use starknet::providers::{JsonRpcClient, Provider}; +use starknet_api::block::{BlockNumber, BlockTimestamp}; +use starknet_api::core::{ChainId, ContractAddress, EntryPointSelector, PatriciaKey}; +use starknet_api::hash::StarkHash; +use starknet_api::patricia_key; use starknet_api::transaction::Calldata; use tokio::sync::RwLock as AsyncRwLock; -use tracing::{error, info, warn}; +use tracing::{info, trace, warn}; pub mod config; pub mod contract; -pub mod executor; -pub mod state; +pub mod in_memory_db; pub mod storage; use self::config::StarknetConfig; -use self::executor::{execute_transaction, PendingBlockExecutor}; use self::storage::block::{Block, PartialHeader}; use self::storage::transaction::{IncludedTransaction, Transaction, TransactionStatus}; -use self::storage::{BlockchainStorage, InMemoryBlockStates}; +use self::storage::{Blockchain, InMemoryBlockStates, Storage}; use crate::accounts::{Account, DevAccountGenerator}; -use crate::backend::state::{MemDb, StateExt}; +use crate::backend::in_memory_db::MemDb; +use crate::backend::storage::transaction::KnownTransaction; use crate::constants::DEFAULT_PREFUNDED_ACCOUNT_BALANCE; +use crate::db::cached::CachedStateWrapper; use crate::db::serde::state::SerializableState; -use crate::db::Db; +use crate::db::{Database, StateRefDb}; use crate::env::{BlockContextGenerator, Env}; +use crate::execution::{ExecutionOutcome, MaybeInvalidExecutedTransaction, TransactionExecutor}; +use crate::fork::db::ForkedDb; use crate::sequencer_error::SequencerError; +use crate::service::MinedBlockOutcome; use crate::utils::{convert_state_diff_to_rpc_state_diff, get_current_timestamp}; pub struct ExternalFunctionCall { @@ -52,59 +56,114 @@ pub struct ExternalFunctionCall { } pub struct Backend { + /// The config used to generate the backend. pub config: RwLock, /// stores all block related data in memory - pub storage: Arc>, - // TEMP: pending block for transaction execution - pub pending_block: AsyncRwLock>, + pub blockchain: Blockchain, /// Historic states of previous blocks pub states: AsyncRwLock, /// The chain environment values. pub env: Arc>, pub block_context_generator: RwLock, - pub state: AsyncRwLock, + /// The latest state. + pub state: Arc>, /// Prefunded dev accounts pub accounts: Vec, } impl Backend { - pub fn new(config: StarknetConfig) -> Self { - let block_context = config.block_context(); + pub async fn new(config: StarknetConfig) -> Self { + let mut block_context = config.block_context(); let block_context_generator = config.block_context_generator(); - let mut state = MemDb::default(); - - let storage = BlockchainStorage::new(&block_context); - let states = InMemoryBlockStates::default(); - let env = Env { block: block_context }; - - if let Some(ref init_state) = config.init_state { - state.load_state(init_state.clone()).expect("failed to load initial state"); - info!("Successfully loaded initial state"); - } - let accounts = DevAccountGenerator::new(config.total_accounts) .with_seed(config.seed) .with_balance((*DEFAULT_PREFUNDED_ACCOUNT_BALANCE).into()) .generate(); - for acc in &accounts { - acc.deploy_and_fund(&mut state).expect("should be able to deploy and fund dev account"); + let (state, storage): (Arc>, Arc>) = + if let Some(forked_url) = config.fork_rpc_url.clone() { + let provider = Arc::new(JsonRpcClient::new(HttpTransport::new(forked_url.clone()))); + + let forked_chain_id = provider.chain_id().await.unwrap(); + let forked_block_id = config + .fork_block_number + .map(BlockId::Number) + .unwrap_or(BlockId::Tag(BlockTag::Latest)); + + let block = provider.get_block_with_tx_hashes(forked_block_id).await.unwrap(); + + let MaybePendingBlockWithTxHashes::Block(block) = block else { + panic!("block to be forked is a pending block") + }; + + block_context.block_number = BlockNumber(block.block_number); + block_context.block_timestamp = BlockTimestamp(block.timestamp); + block_context.sequencer_address = + ContractAddress(patricia_key!(block.sequencer_address)); + block_context.chain_id = + ChainId(parse_cairo_short_string(&forked_chain_id).unwrap()); + + let mut state = ForkedDb::new(Arc::clone(&provider), forked_block_id); + + trace!( + "forking chain `{}` at block {} from {}", + parse_cairo_short_string(&forked_chain_id).unwrap(), + block.block_number, + forked_url + ); + + for acc in &accounts { + acc.deploy_and_fund(&mut state) + .expect("should be able to deploy and fund dev account"); + } + + ( + Arc::new(AsyncRwLock::new(state)), + Arc::new(RwLock::new(Storage::new_forked( + block.block_number, + block.block_hash, + ))), + ) + } else { + let mut state = MemDb::default(); + + for acc in &accounts { + acc.deploy_and_fund(&mut state) + .expect("should be able to deploy and fund dev account"); + } + + ( + Arc::new(AsyncRwLock::new(state)), + Arc::new(RwLock::new(Storage::new(&block_context))), + ) + }; + + if let Some(ref init_state) = config.init_state { + state + .write() + .await + .load_state(init_state.clone()) + .expect("failed to load initial state"); + info!("Successfully loaded initial state"); } + let blockchain = Blockchain::new(storage); + let states = InMemoryBlockStates::default(); + let env = Env { block: block_context }; + Self { + state, env: Arc::new(RwLock::new(env)), - state: AsyncRwLock::new(state), config: RwLock::new(config), states: AsyncRwLock::new(states), - storage: Arc::new(AsyncRwLock::new(storage)), + blockchain, block_context_generator: RwLock::new(block_context_generator), - pending_block: AsyncRwLock::new(None), accounts, } } - /// Get the current state. + /// Get the current state in a serializable format. pub async fn serialize_state(&self) -> Result { self.state.read().await.dump_state().map_err(|_| SequencerError::StateSerialization) } @@ -122,129 +181,129 @@ impl Backend { pub fn estimate_fee( &self, - transaction: AccountTransaction, - state: MemDb, - ) -> Result { - let mut state = CachedState::new(state); + transactions: Vec, + state: StateRefDb, + ) -> Result, TransactionExecutionError> { + let mut state = CachedStateWrapper::new(state); + let block_context = self.env.read().block.clone(); + + let mut estimations = Vec::with_capacity(transactions.len()); + + let results = TransactionExecutor::new(&mut state, &block_context, false) + .with_error_log() + .execute_many(transactions); + + for res in results { + let exec_info = res?; + + if exec_info.revert_error.is_some() { + // TEMP: change this once `Reverted` transaction error is no longer `String`. + return Err(TransactionExecutionError::ExecutionError( + EntryPointExecutionError::ExecutionFailed { error_data: vec![] }, + )); + } - let exec_info = execute_transaction( - ExecutionTransaction::AccountTransaction(transaction), - &mut state, - &self.env.read().block, - true, - )?; - - if exec_info.revert_error.is_some() { - // TEMP: change this once `Reverted` transaction error is no longer `String`. - return Err(TransactionExecutionError::ExecutionError( - EntryPointExecutionError::ExecutionFailed { error_data: vec![] }, - )); - } + let (l1_gas_usage, vm_resources) = + extract_l1_gas_and_vm_usage(&exec_info.actual_resources); + let l1_gas_by_vm_usage = calculate_l1_gas_by_vm_usage(&block_context, &vm_resources)?; + let total_l1_gas_usage = l1_gas_usage as f64 + l1_gas_by_vm_usage; - let (l1_gas_usage, vm_resources) = extract_l1_gas_and_vm_usage(&exec_info.actual_resources); - let l1_gas_by_vm_usage = - calculate_l1_gas_by_vm_usage(&self.env.read().block, &vm_resources)?; - let total_l1_gas_usage = l1_gas_usage as f64 + l1_gas_by_vm_usage; + let gas_price = block_context.gas_price as u64; - let gas_price = self.env.read().block.gas_price as u64; + estimations.push(FeeEstimate { + gas_consumed: total_l1_gas_usage.ceil() as u64, + gas_price, + overall_fee: total_l1_gas_usage.ceil() as u64 * gas_price, + }) + } - Ok(FeeEstimate { - gas_consumed: total_l1_gas_usage.ceil() as u64, - gas_price, - overall_fee: total_l1_gas_usage.ceil() as u64 * gas_price, - }) + Ok(estimations) } - // execute the tx - pub async fn handle_transaction(&self, transaction: Transaction) { - let is_valid = if let Some(pending_block) = self.pending_block.write().await.as_mut() { - let charge_fee = !self.config.read().disable_fee; - pending_block.add_transaction(transaction, charge_fee).await - } else { - return error!("Unable to process transaction: no pending block"); - }; - - if is_valid && self.config.read().auto_mine { - self.mine_block().await; - self.open_pending_block().await; - } + /// Mines a new block based on the provided execution outcome. + /// This method should only be called by the + /// [PendingBlockProducer](crate::service::PendingBlockProducer) when the node is running in + /// `interval` mining mode. + pub async fn mine_pending_block( + &self, + execution_outcome: ExecutionOutcome, + ) -> (MinedBlockOutcome, StateRefDb) { + let outcome = self.do_mine_block(execution_outcome).await; + let new_state = self.state.read().await.as_ref_db(); + (outcome, new_state) } - // Generates a new block from the pending block and stores it in the storage. - pub async fn mine_block(&self) { - let pending = self.pending_block.write().await.take(); + /// Updates the block context and mines an empty block. + pub async fn mine_empty_block(&self) -> MinedBlockOutcome { + self.update_block_context(); + self.do_mine_block(ExecutionOutcome::default()).await + } - let Some(mut pending) = pending else { - warn!("No pending block to mine"); - return; + pub async fn do_mine_block(&self, execution_outcome: ExecutionOutcome) -> MinedBlockOutcome { + let partial_header = PartialHeader { + gas_price: self.env.read().block.gas_price, + number: self.env.read().block.block_number.0, + timestamp: self.env.read().block.block_timestamp.0, + parent_hash: self.blockchain.storage.read().latest_hash, + sequencer_address: (*self.env.read().block.sequencer_address.0.key()).into(), }; - let block = { - let partial_block = pending.as_block(); - Block::new(partial_block.header, partial_block.transactions, partial_block.outputs) - }; + let (valid_txs, outputs): (Vec<_>, Vec<_>) = execution_outcome + .transactions + .iter() + .filter_map(|tx| match tx { + MaybeInvalidExecutedTransaction::Valid(tx) => Some((tx.clone(), tx.output.clone())), + _ => None, + }) + .unzip(); + + let block = Block::new(partial_header, valid_txs, outputs); - let block_hash = block.header.hash(); let block_number = block.header.number; let tx_count = block.transactions.len(); + let block_hash = block.header.hash(); - // Stores the pending transaction in storage - for tx in &block.transactions { - let transaction_hash = tx.inner.hash(); - self.storage.write().await.transactions.insert( - transaction_hash, - IncludedTransaction { - block_number, - block_hash, - transaction: tx.clone(), - status: TransactionStatus::AcceptedOnL2, + execution_outcome.transactions.iter().for_each(|tx| { + let (hash, tx) = match tx { + MaybeInvalidExecutedTransaction::Valid(tx) => ( + tx.inner.hash(), + KnownTransaction::Included(IncludedTransaction { + block_number, + block_hash, + transaction: tx.clone(), + status: TransactionStatus::AcceptedOnL2, + }), + ), + + MaybeInvalidExecutedTransaction::Invalid(tx) => { + (tx.inner.hash(), KnownTransaction::Rejected(tx.clone())) } - .into(), - ); - } + }; + + self.blockchain.storage.write().transactions.insert(hash, tx); + }); // get state diffs - let pending_state_diff = pending.state.to_state_diff(); - let state_diff = convert_state_diff_to_rpc_state_diff(pending_state_diff); + let state_diff = convert_state_diff_to_rpc_state_diff(execution_outcome.state_diff.clone()); // store block and the state diff - self.storage.write().await.append_block(block_hash, block, state_diff); + self.blockchain.append_block(block_hash, block.clone(), state_diff); - info!("⛏️ Block {block_number} mined with {tx_count} transactions"); + info!(target: "backend", "⛏️ Block {block_number} mined with {tx_count} transactions"); - // TODO: This is a hack, we should be able to apply the state diff directly - // to the current state instead of applying the diff onto the pending state. - // This is because `CachedState` have no notion of `Sierra` class, which whenever - // we execute a Declare transaction, we will set the `Sierra` class directly to the - // underlying `MemDb`. However, this is not reflected in the `CachedState`. - // - // Set the new pending state as the current state - let mut new_state = pending.state.state.clone(); - new_state.apply_state(&mut pending.state); - *self.state.write().await = new_state.clone(); + // apply the pending state to the current state + let mut state = self.state.write().await; + execution_outcome.apply_to(&mut *state); // store the current state - self.states.write().await.insert(block_hash, new_state); - } - - pub async fn open_pending_block(&self) { - let latest_hash = self.storage.read().await.latest_hash; - let latest_state = self.state.read().await.clone(); - - self.update_block_context(); + self.states.write().await.insert(block_hash, state.as_ref_db()); - let _ = self.pending_block.write().await.insert(PendingBlockExecutor::new( - latest_hash, - latest_state, - self.env.clone(), - self.storage.clone(), - )); + MinedBlockOutcome { block_number, transactions: execution_outcome.transactions } } - fn update_block_context(&self) { + pub fn update_block_context(&self) { let mut context_gen = self.block_context_generator.write(); let block_context = &mut self.env.write().block; - let current_timestamp_secs = get_current_timestamp().as_secs() as i64; let timestamp = if context_gen.next_block_start_time == 0 { @@ -263,7 +322,7 @@ impl Backend { pub fn call( &self, call: ExternalFunctionCall, - state: MemDb, + state: impl StateReader, ) -> Result { let mut state = CachedState::new(state); let mut state = CachedState::new(MutRefState::new(&mut state)); @@ -293,22 +352,8 @@ impl Backend { res } - pub async fn pending_state(&self) -> Option { - let Some(ref mut block) = *self.pending_block.write().await else { - return None; - }; - - let mut current_state = self.state.read().await.clone(); - current_state.apply_state(&mut block.state); - Some(current_state) - } - - pub async fn latest_state(&self) -> MemDb { - self.state.read().await.clone() - } - pub async fn create_empty_block(&self) -> Block { - let parent_hash = self.storage.read().await.latest_hash; + let parent_hash = self.blockchain.storage.read().latest_hash; let block_context = &self.env.read().block; let partial_header = PartialHeader { @@ -321,46 +366,4 @@ impl Backend { Block::new(partial_header, vec![], vec![]) } - - pub async fn set_next_block_timestamp(&self, timestamp: u64) -> Result<(), SequencerError> { - if self.has_pending_transactions().await { - return Err(SequencerError::PendingTransactions); - } - self.block_context_generator.write().next_block_start_time = timestamp; - Ok(()) - } - - pub async fn increase_next_block_timestamp( - &self, - timestamp: u64, - ) -> Result<(), SequencerError> { - if self.has_pending_transactions().await { - return Err(SequencerError::PendingTransactions); - } - self.block_context_generator.write().block_timestamp_offset += timestamp as i64; - Ok(()) - } - - async fn has_pending_transactions(&self) -> bool { - if let Some(ref block) = *self.pending_block.read().await { - !block.transactions.is_empty() - } else { - false - } - } - - pub async fn set_storage_at( - &self, - contract_address: ContractAddress, - storage_key: StorageKey, - value: StarkFelt, - ) -> Result<(), SequencerError> { - match self.pending_block.write().await.as_mut() { - Some(pending_block) => { - pending_block.state.set_storage_at(contract_address, storage_key, value); - Ok(()) - } - None => Err(SequencerError::StateNotFound(BlockId::Tag(BlockTag::Pending))), - } - } } diff --git a/crates/katana/core/src/backend/state.rs b/crates/katana/core/src/backend/state.rs deleted file mode 100644 index b844d26a55..0000000000 --- a/crates/katana/core/src/backend/state.rs +++ /dev/null @@ -1,543 +0,0 @@ -use std::collections::{BTreeMap, HashMap}; - -use anyhow::Result; -use blockifier::execution::contract_class::ContractClass; -use blockifier::state::cached_state::CommitmentStateDiff; -use blockifier::state::errors::StateError; -use blockifier::state::state_api::{State, StateReader, StateResult}; -use starknet::core::types::FlattenedSierraClass; -use starknet_api::core::{ClassHash, CompiledClassHash, ContractAddress, Nonce, PatriciaKey}; -use starknet_api::hash::{StarkFelt, StarkHash}; -use starknet_api::patricia_key; -use starknet_api::state::StorageKey; - -use crate::constants::{ - ERC20_CONTRACT, ERC20_CONTRACT_CLASS_HASH, FEE_TOKEN_ADDRESS, UDC_ADDRESS, UDC_CLASS_HASH, - UDC_CONTRACT, -}; -use crate::db::serde::state::{ - SerializableClassRecord, SerializableState, SerializableStorageRecord, -}; -use crate::db::Db; - -pub trait StateExt { - fn set_sierra_class( - &mut self, - class_hash: ClassHash, - sierra_class: FlattenedSierraClass, - ) -> StateResult<()>; - - fn get_sierra_class(&mut self, class_hash: &ClassHash) -> StateResult; - - fn apply_state(&mut self, state: &mut S) - where - S: State + StateReader; -} - -#[derive(Clone, Debug, Default)] -pub struct StorageRecord { - pub nonce: Nonce, - pub class_hash: ClassHash, - pub storage: HashMap, -} - -#[derive(Clone, Debug)] -pub struct ClassRecord { - /// The compiled contract class. - pub class: ContractClass, - /// The hash of a compiled Sierra class (if the class is a Sierra class, otherwise - /// for legacy contract, it is the same as the class hash). - pub compiled_hash: CompiledClassHash, -} - -#[derive(Clone, Debug)] -pub struct MemDb { - /// A map of class hash to its class definition. - pub classes: HashMap, - /// A map of contract address to the contract information. - pub storage: HashMap, - /// A map of class hash to its Sierra class definition (if any). - pub sierra_classes: HashMap, -} - -impl Default for MemDb { - fn default() -> Self { - let mut state = MemDb { - storage: HashMap::new(), - classes: HashMap::new(), - sierra_classes: HashMap::new(), - }; - deploy_fee_contract(&mut state); - deploy_universal_deployer_contract(&mut state); - state - } -} - -impl StateExt for MemDb { - fn get_sierra_class(&mut self, class_hash: &ClassHash) -> StateResult { - if let ContractClass::V0(_) = self.get_compiled_contract_class(class_hash)? { - return Err(StateError::StateReadError("Class hash is not a Sierra class".to_string())); - }; - - self.sierra_classes - .get(class_hash) - .cloned() - .ok_or(StateError::StateReadError("Missing Sierra class".to_string())) - } - - fn set_sierra_class( - &mut self, - class_hash: ClassHash, - sierra_class: FlattenedSierraClass, - ) -> StateResult<()> { - // check the class hash must not be a legacy contract - if let ContractClass::V0(_) = self.get_compiled_contract_class(&class_hash)? { - return Err(StateError::StateReadError("Class hash is not a Sierra class".to_string())); - }; - self.sierra_classes.insert(class_hash, sierra_class); - Ok(()) - } - - fn apply_state(&mut self, state: &mut S) - where - S: State + StateReader, - { - // Generate the state diff - let state_diff = state.to_state_diff(); - - // update contract storages - state_diff.storage_updates.into_iter().for_each(|(contract_address, storages)| { - storages.into_iter().for_each(|(key, value)| { - self.set_storage_at(contract_address, key, value); - }) - }); - - // update declared contracts - // apply newly declared classses - for (class_hash, compiled_class_hash) in &state_diff.class_hash_to_compiled_class_hash { - let contract_class = - state.get_compiled_contract_class(class_hash).expect("contract class should exist"); - self.set_contract_class(class_hash, contract_class).unwrap(); - self.set_compiled_class_hash(*class_hash, *compiled_class_hash).unwrap(); - } - - // update deployed contracts - state_diff.address_to_class_hash.into_iter().for_each(|(contract_address, class_hash)| { - self.set_class_hash_at(contract_address, class_hash).unwrap() - }); - - // update accounts nonce - state_diff.address_to_nonce.into_iter().for_each(|(contract_address, nonce)| { - if let Some(r) = self.storage.get_mut(&contract_address) { - r.nonce = nonce; - } - }); - } -} - -impl State for MemDb { - fn increment_nonce(&mut self, contract_address: ContractAddress) -> StateResult<()> { - let current_nonce = self.get_nonce_at(contract_address)?; - let current_nonce_as_u64 = usize::try_from(current_nonce.0)? as u64; - let next_nonce_val = 1_u64 + current_nonce_as_u64; - let next_nonce = Nonce(StarkFelt::from(next_nonce_val)); - self.storage.entry(contract_address).or_default().nonce = next_nonce; - Ok(()) - } - - fn set_storage_at( - &mut self, - contract_address: ContractAddress, - key: StorageKey, - value: StarkFelt, - ) { - self.storage.entry(contract_address).or_default().storage.insert(key, value); - } - - fn set_class_hash_at( - &mut self, - contract_address: ContractAddress, - class_hash: ClassHash, - ) -> StateResult<()> { - if contract_address == ContractAddress::default() { - return Err(StateError::OutOfRangeContractAddress); - } - self.storage.entry(contract_address).or_default().class_hash = class_hash; - Ok(()) - } - - fn set_compiled_class_hash( - &mut self, - class_hash: ClassHash, - compiled_class_hash: CompiledClassHash, - ) -> StateResult<()> { - if !self.classes.contains_key(&class_hash) { - return Err(StateError::UndeclaredClassHash(class_hash)); - } - self.classes.entry(class_hash).and_modify(|r| r.compiled_hash = compiled_class_hash); - Ok(()) - } - - fn set_contract_class( - &mut self, - class_hash: &ClassHash, - contract_class: ContractClass, - ) -> StateResult<()> { - let compiled_hash = CompiledClassHash(class_hash.0); - self.classes.insert(*class_hash, ClassRecord { class: contract_class, compiled_hash }); - Ok(()) - } - - fn to_state_diff(&self) -> CommitmentStateDiff { - CommitmentStateDiff { - storage_updates: [].into(), - address_to_nonce: [].into(), - address_to_class_hash: [].into(), - class_hash_to_compiled_class_hash: [].into(), - } - } -} - -impl StateReader for MemDb { - fn get_storage_at( - &mut self, - contract_address: ContractAddress, - key: StorageKey, - ) -> StateResult { - let value = self - .storage - .get(&contract_address) - .and_then(|r| r.storage.get(&key)) - .copied() - .unwrap_or_default(); - Ok(value) - } - - fn get_nonce_at(&mut self, contract_address: ContractAddress) -> StateResult { - let nonce = self.storage.get(&contract_address).map(|r| r.nonce).unwrap_or_default(); - Ok(nonce) - } - - fn get_compiled_contract_class( - &mut self, - class_hash: &ClassHash, - ) -> StateResult { - self.classes - .get(class_hash) - .map(|r| r.class.clone()) - .ok_or(StateError::UndeclaredClassHash(*class_hash)) - } - - fn get_class_hash_at(&mut self, contract_address: ContractAddress) -> StateResult { - let class_hash = - self.storage.get(&contract_address).map(|r| r.class_hash).unwrap_or_default(); - Ok(class_hash) - } - - fn get_compiled_class_hash( - &mut self, - class_hash: ClassHash, - ) -> StateResult { - self.classes - .get(&class_hash) - .map(|r| r.compiled_hash) - .ok_or(StateError::UndeclaredClassHash(class_hash)) - } -} - -impl Db for MemDb { - fn dump_state(&self) -> Result { - let mut serializable = SerializableState::default(); - - self.storage.iter().for_each(|(addr, storage)| { - let mut record = SerializableStorageRecord { - storage: BTreeMap::new(), - nonce: storage.nonce.0.into(), - class_hash: storage.class_hash.0.into(), - }; - - storage.storage.iter().for_each(|(key, value)| { - record.storage.insert((*key.0.key()).into(), (*value).into()); - }); - - serializable.storage.insert((*addr.0.key()).into(), record); - }); - - self.classes.iter().for_each(|(class_hash, class_record)| { - serializable.classes.insert( - class_hash.0.into(), - SerializableClassRecord { - class: class_record.class.clone().into(), - compiled_hash: class_record.compiled_hash.0.into(), - }, - ); - }); - - self.sierra_classes.iter().for_each(|(class_hash, class)| { - serializable.sierra_classes.insert(class_hash.0.into(), class.clone()); - }); - - Ok(serializable) - } - - fn set_nonce(&mut self, addr: ContractAddress, nonce: Nonce) { - self.storage.entry(addr).or_default().nonce = nonce; - } -} - -fn deploy_fee_contract(state: &mut MemDb) { - let address = ContractAddress(patricia_key!(*FEE_TOKEN_ADDRESS)); - let hash = ClassHash(*ERC20_CONTRACT_CLASS_HASH); - let compiled_hash = CompiledClassHash(*ERC20_CONTRACT_CLASS_HASH); - - state.classes.insert(hash, ClassRecord { class: (*ERC20_CONTRACT).clone(), compiled_hash }); - state.storage.insert( - address, - StorageRecord { class_hash: hash, nonce: Nonce(1_u128.into()), storage: HashMap::new() }, - ); -} - -fn deploy_universal_deployer_contract(state: &mut MemDb) { - let address = ContractAddress(patricia_key!(*UDC_ADDRESS)); - let hash = ClassHash(*UDC_CLASS_HASH); - let compiled_hash = CompiledClassHash(*UDC_CLASS_HASH); - - state.classes.insert(hash, ClassRecord { class: (*UDC_CONTRACT).clone(), compiled_hash }); - state.storage.insert( - address, - StorageRecord { class_hash: hash, nonce: Nonce(1_u128.into()), storage: HashMap::new() }, - ); -} - -/// Unit tests ported from `blockifier`. -#[cfg(test)] -mod tests { - use assert_matches::assert_matches; - use blockifier::state::cached_state::CachedState; - use starknet_api::core::PatriciaKey; - use starknet_api::stark_felt; - - use super::*; - - #[test] - fn get_uninitialized_storage_value() { - let mut state = CachedState::new(MemDb { - classes: HashMap::new(), - storage: HashMap::new(), - sierra_classes: HashMap::new(), - }); - let contract_address = ContractAddress(patricia_key!("0x1")); - let key = StorageKey(patricia_key!("0x10")); - assert_eq!(state.get_storage_at(contract_address, key).unwrap(), StarkFelt::default()); - } - - #[test] - fn get_and_set_storage_value() { - let contract_address0 = ContractAddress(patricia_key!("0x100")); - let contract_address1 = ContractAddress(patricia_key!("0x200")); - let key0 = StorageKey(patricia_key!("0x10")); - let key1 = StorageKey(patricia_key!("0x20")); - let storage_val0 = stark_felt!("0x1"); - let storage_val1 = stark_felt!("0x5"); - - let mut state = CachedState::new(MemDb { - storage: HashMap::from([ - ( - contract_address0, - StorageRecord { - class_hash: ClassHash(0_u32.into()), - nonce: Nonce(0_u32.into()), - storage: HashMap::from([(key0, storage_val0)]), - }, - ), - ( - contract_address1, - StorageRecord { - class_hash: ClassHash(0_u32.into()), - nonce: Nonce(0_u32.into()), - storage: HashMap::from([(key1, storage_val1)]), - }, - ), - ]), - classes: HashMap::new(), - sierra_classes: HashMap::new(), - }); - - assert_eq!(state.get_storage_at(contract_address0, key0).unwrap(), storage_val0); - assert_eq!(state.get_storage_at(contract_address1, key1).unwrap(), storage_val1); - - let modified_storage_value0 = stark_felt!("0xA"); - state.set_storage_at(contract_address0, key0, modified_storage_value0); - assert_eq!(state.get_storage_at(contract_address0, key0).unwrap(), modified_storage_value0); - assert_eq!(state.get_storage_at(contract_address1, key1).unwrap(), storage_val1); - - let modified_storage_value1 = stark_felt!("0x7"); - state.set_storage_at(contract_address1, key1, modified_storage_value1); - assert_eq!(state.get_storage_at(contract_address0, key0).unwrap(), modified_storage_value0); - assert_eq!(state.get_storage_at(contract_address1, key1).unwrap(), modified_storage_value1); - } - - #[test] - fn get_uninitialized_value() { - let mut state = CachedState::new(MemDb { - classes: HashMap::new(), - storage: HashMap::new(), - sierra_classes: HashMap::new(), - }); - let contract_address = ContractAddress(patricia_key!("0x1")); - assert_eq!(state.get_nonce_at(contract_address).unwrap(), Nonce::default()); - } - - #[test] - fn get_uninitialized_class_hash_value() { - let mut state = CachedState::new(MemDb { - classes: HashMap::new(), - storage: HashMap::new(), - sierra_classes: HashMap::new(), - }); - let valid_contract_address = ContractAddress(patricia_key!("0x1")); - assert_eq!(state.get_class_hash_at(valid_contract_address).unwrap(), ClassHash::default()); - } - - #[test] - fn cannot_set_class_hash_to_uninitialized_contract() { - let mut state = CachedState::new(MemDb { - classes: HashMap::new(), - storage: HashMap::new(), - sierra_classes: HashMap::new(), - }); - let uninitialized_contract_address = ContractAddress::default(); - let class_hash = ClassHash(stark_felt!("0x100")); - assert_matches!( - state.set_class_hash_at(uninitialized_contract_address, class_hash).unwrap_err(), - StateError::OutOfRangeContractAddress - ); - } - - #[test] - fn get_and_increment_nonce() { - let contract_address1 = ContractAddress(patricia_key!("0x100")); - let contract_address2 = ContractAddress(patricia_key!("0x200")); - let initial_nonce = Nonce(stark_felt!("0x1")); - - let mut state = CachedState::new(MemDb { - storage: HashMap::from([ - ( - contract_address1, - StorageRecord { - class_hash: ClassHash(0_u32.into()), - nonce: initial_nonce, - storage: HashMap::new(), - }, - ), - ( - contract_address2, - StorageRecord { - class_hash: ClassHash(0_u32.into()), - nonce: initial_nonce, - storage: HashMap::new(), - }, - ), - ]), - classes: HashMap::new(), - sierra_classes: HashMap::new(), - }); - - assert_eq!(state.get_nonce_at(contract_address1).unwrap(), initial_nonce); - assert_eq!(state.get_nonce_at(contract_address2).unwrap(), initial_nonce); - - assert!(state.increment_nonce(contract_address1).is_ok()); - let nonce1_plus_one = Nonce(stark_felt!("0x2")); - assert_eq!(state.get_nonce_at(contract_address1).unwrap(), nonce1_plus_one); - assert_eq!(state.get_nonce_at(contract_address2).unwrap(), initial_nonce); - - assert!(state.increment_nonce(contract_address1).is_ok()); - let nonce1_plus_two = Nonce(stark_felt!("0x3")); - assert_eq!(state.get_nonce_at(contract_address1).unwrap(), nonce1_plus_two); - assert_eq!(state.get_nonce_at(contract_address2).unwrap(), initial_nonce); - - assert!(state.increment_nonce(contract_address2).is_ok()); - let nonce2_plus_one = Nonce(stark_felt!("0x2")); - assert_eq!(state.get_nonce_at(contract_address1).unwrap(), nonce1_plus_two); - assert_eq!(state.get_nonce_at(contract_address2).unwrap(), nonce2_plus_one); - } - - #[test] - fn apply_state_update() { - let mut old_state = MemDb { - classes: HashMap::new(), - storage: HashMap::new(), - sierra_classes: HashMap::new(), - }; - let mut new_state = CachedState::new(MemDb { - classes: HashMap::new(), - storage: HashMap::new(), - sierra_classes: HashMap::new(), - }); - - let class_hash = ClassHash(stark_felt!("0x1")); - let address = ContractAddress(patricia_key!("0x1")); - let storage_key = StorageKey(patricia_key!("0x77")); - let storage_val = stark_felt!("0x66"); - let contract = (*UDC_CONTRACT).clone(); - let compiled_hash = CompiledClassHash(class_hash.0); - - new_state.set_contract_class(&class_hash, (*UDC_CONTRACT).clone()).unwrap(); - new_state.set_compiled_class_hash(class_hash, CompiledClassHash(class_hash.0)).unwrap(); - new_state.set_class_hash_at(address, class_hash).unwrap(); - new_state.set_storage_at(address, storage_key, storage_val); - - assert_matches!( - old_state.get_compiled_contract_class(&class_hash), - Err(StateError::UndeclaredClassHash(_)) - ); - assert_matches!( - old_state.get_compiled_class_hash(class_hash), - Err(StateError::UndeclaredClassHash(_)) - ); - assert_eq!(old_state.get_class_hash_at(address).unwrap(), ClassHash::default()); - assert_eq!(old_state.get_storage_at(address, storage_key).unwrap(), StarkFelt::default()); - - old_state.apply_state(&mut new_state); - - assert_eq!(old_state.get_compiled_contract_class(&class_hash).unwrap(), contract); - assert_eq!(old_state.get_compiled_class_hash(class_hash).unwrap(), compiled_hash); - assert_eq!(old_state.get_class_hash_at(address).unwrap(), class_hash); - assert_eq!(old_state.get_storage_at(address, storage_key).unwrap(), storage_val); - } - - #[test] - fn dump_and_load_state() { - let mut state = MemDb { - classes: HashMap::new(), - storage: HashMap::new(), - sierra_classes: HashMap::new(), - }; - - let class_hash = ClassHash(stark_felt!("0x1")); - let address = ContractAddress(patricia_key!("0x1")); - let storage_key = StorageKey(patricia_key!("0x77")); - let storage_val = stark_felt!("0x66"); - let contract = (*UDC_CONTRACT).clone(); - let compiled_hash = CompiledClassHash(class_hash.0); - - state.set_contract_class(&class_hash, (*UDC_CONTRACT).clone()).unwrap(); - state.set_compiled_class_hash(class_hash, CompiledClassHash(class_hash.0)).unwrap(); - state.set_class_hash_at(address, class_hash).unwrap(); - state.set_storage_at(address, storage_key, storage_val); - - let dump = state.dump_state().expect("should dump state"); - - let mut new_state = MemDb { - classes: HashMap::new(), - storage: HashMap::new(), - sierra_classes: HashMap::new(), - }; - new_state.load_state(dump).expect("should load state"); - - assert_eq!(new_state.get_compiled_contract_class(&class_hash).unwrap(), contract); - assert_eq!(new_state.get_compiled_class_hash(class_hash).unwrap(), compiled_hash); - assert_eq!(new_state.get_class_hash_at(address).unwrap(), class_hash); - assert_eq!(new_state.get_storage_at(address, storage_key).unwrap(), storage_val); - } -} diff --git a/crates/katana/core/src/backend/storage/block.rs b/crates/katana/core/src/backend/storage/block.rs index 5cab98fa78..4d16aadc0c 100644 --- a/crates/katana/core/src/backend/storage/block.rs +++ b/crates/katana/core/src/backend/storage/block.rs @@ -8,7 +8,7 @@ use starknet::core::types::{ }; use super::transaction::TransactionOutput; -use crate::backend::executor::ExecutedTransaction; +use crate::execution::ExecutedTransaction; use crate::utils::transaction::api_to_rpc_transaction; #[derive(Debug, Clone, Copy)] diff --git a/crates/katana/core/src/backend/storage/mod.rs b/crates/katana/core/src/backend/storage/mod.rs index 3e050de240..a166103da3 100644 --- a/crates/katana/core/src/backend/storage/mod.rs +++ b/crates/katana/core/src/backend/storage/mod.rs @@ -1,12 +1,14 @@ use std::collections::{HashMap, VecDeque}; +use std::sync::Arc; use blockifier::block_context::BlockContext; +use parking_lot::RwLock; use starknet::core::types::{BlockId, BlockTag, FieldElement, StateDiff, StateUpdate}; use self::block::Block; use self::transaction::KnownTransaction; -use super::state::MemDb; use crate::backend::storage::block::PartialHeader; +use crate::db::StateRefDb; pub mod block; pub mod transaction; @@ -17,7 +19,7 @@ const MIN_HISTORY_LIMIT: usize = 10; /// Represents the complete state of a single block pub struct InMemoryBlockStates { /// The states at a certain block - states: HashMap, + states: HashMap, /// How many states to store at most in_memory_limit: usize, /// minimum amount of states we keep in memory @@ -37,7 +39,7 @@ impl InMemoryBlockStates { } /// Returns the state for the given `hash` if present - pub fn get(&self, hash: &FieldElement) -> Option<&MemDb> { + pub fn get(&self, hash: &FieldElement) -> Option<&StateRefDb> { self.states.get(hash) } @@ -49,7 +51,7 @@ impl InMemoryBlockStates { /// Since we keep a snapshot of the entire state as history, the size of the state will increase /// with the transactions processed. To counter this, we gradually decrease the cache limit with /// the number of states/blocks until we reached the `min_limit`. - pub fn insert(&mut self, hash: FieldElement, state: MemDb) { + pub fn insert(&mut self, hash: FieldElement, state: StateRefDb) { if self.present.len() >= self.in_memory_limit { // once we hit the max limit we gradually decrease it self.in_memory_limit = @@ -80,9 +82,8 @@ impl Default for InMemoryBlockStates { } } -// TODO: can we wrap all the fields in a `RwLock` to prevent read blocking? #[derive(Debug, Default)] -pub struct BlockchainStorage { +pub struct Storage { /// Mapping from block hash -> block pub blocks: HashMap, /// Mapping from block number -> block hash @@ -97,7 +98,7 @@ pub struct BlockchainStorage { pub transactions: HashMap, } -impl BlockchainStorage { +impl Storage { /// Creates a new blockchain from a genesis block pub fn new(block_context: &BlockContext) -> Self { let partial_header = PartialHeader { @@ -123,44 +124,75 @@ impl BlockchainStorage { } } - /// Appends a new block to the chain and store the state diff. - pub fn append_block(&mut self, hash: FieldElement, block: Block, state_diff: StateDiff) { - let number = block.header.number; - - assert_eq!(self.latest_number + 1, number); + /// Creates a new blockchain from a forked network + pub fn new_forked(latest_number: u64, latest_hash: FieldElement) -> Self { + Self { + latest_hash, + latest_number, + blocks: HashMap::default(), + hashes: HashMap::from([(latest_number, latest_hash)]), + state_update: HashMap::default(), + transactions: HashMap::default(), + } + } - let old_root = self.blocks.get(&self.latest_hash).map(|b| b.header.state_root); + pub fn block_by_number(&self, number: u64) -> Option<&Block> { + self.hashes.get(&number).and_then(|hash| self.blocks.get(hash)) + } +} - let state_update = StateUpdate { - block_hash: hash, - new_root: block.header.state_root, - old_root: if number == 0 { FieldElement::ZERO } else { old_root.unwrap() }, - state_diff, - }; +pub struct Blockchain { + pub storage: Arc>, +} - self.latest_hash = hash; - self.latest_number = number; - self.blocks.insert(hash, block); - self.hashes.insert(number, hash); - self.state_update.insert(hash, state_update); +impl Blockchain { + pub fn new(storage: Arc>) -> Self { + Self { storage } } - pub fn total_blocks(&self) -> usize { - self.blocks.len() + pub fn new_forked(latest_number: u64, latest_hash: FieldElement) -> Self { + Self::new(Arc::new(RwLock::new(Storage::new_forked(latest_number, latest_hash)))) } /// Returns the block hash based on the block id pub fn block_hash(&self, block: BlockId) -> Option { match block { BlockId::Tag(BlockTag::Pending) => None, - BlockId::Tag(BlockTag::Latest) => Some(self.latest_hash), + BlockId::Tag(BlockTag::Latest) => Some(self.storage.read().latest_hash), BlockId::Hash(hash) => Some(hash), - BlockId::Number(num) => self.hashes.get(&num).copied(), + BlockId::Number(num) => self.storage.read().hashes.get(&num).copied(), } } - pub fn block_by_number(&self, number: u64) -> Option<&Block> { - self.hashes.get(&number).and_then(|hash| self.blocks.get(hash)) + pub fn total_blocks(&self) -> usize { + self.storage.read().blocks.len() + } + + /// Appends a new block to the chain and store the state diff. + pub fn append_block(&self, hash: FieldElement, block: Block, state_diff: StateDiff) { + let number = block.header.number; + let mut storage = self.storage.write(); + + assert_eq!(storage.latest_number + 1, number); + + let old_root = storage + .blocks + .get(&storage.latest_hash) + .map(|b| b.header.state_root) + .unwrap_or_default(); + + let state_update = StateUpdate { + block_hash: hash, + new_root: block.header.state_root, + old_root, + state_diff, + }; + + storage.latest_hash = hash; + storage.latest_number = number; + storage.blocks.insert(hash, block); + storage.hashes.insert(number, hash); + storage.state_update.insert(hash, state_update); } } @@ -169,18 +201,23 @@ mod tests { use std::str::FromStr; use super::*; + use crate::backend::in_memory_db::MemDb; #[test] fn remove_old_state_when_limit_is_reached() { let mut in_memory_state = InMemoryBlockStates::new(2); - in_memory_state.insert(FieldElement::from_str("0x1").unwrap(), MemDb::default()); - in_memory_state.insert(FieldElement::from_str("0x2").unwrap(), MemDb::default()); + in_memory_state + .insert(FieldElement::from_str("0x1").unwrap(), StateRefDb::new(MemDb::new())); + in_memory_state + .insert(FieldElement::from_str("0x2").unwrap(), StateRefDb::new(MemDb::new())); + assert!(in_memory_state.states.get(&FieldElement::from_str("0x1").unwrap()).is_some()); assert!(in_memory_state.states.get(&FieldElement::from_str("0x2").unwrap()).is_some()); assert_eq!(in_memory_state.present.len(), 2); - in_memory_state.insert(FieldElement::from_str("0x3").unwrap(), MemDb::default()); + in_memory_state + .insert(FieldElement::from_str("0x3").unwrap(), StateRefDb::new(MemDb::new())); assert_eq!(in_memory_state.present.len(), 2); assert!(in_memory_state.states.get(&FieldElement::from_str("0x1").unwrap()).is_none()); diff --git a/crates/katana/core/src/backend/storage/transaction.rs b/crates/katana/core/src/backend/storage/transaction.rs index ee87b9fcee..9d5323e548 100644 --- a/crates/katana/core/src/backend/storage/transaction.rs +++ b/crates/katana/core/src/backend/storage/transaction.rs @@ -2,6 +2,7 @@ use std::sync::Arc; use blockifier::execution::contract_class::ContractClass; use blockifier::transaction::account_transaction::AccountTransaction; +use blockifier::transaction::transaction_execution::Transaction as ExecutionTransaction; use blockifier::transaction::transactions::{ DeclareTransaction as ExecutionDeclareTransaction, DeployAccountTransaction as ExecutionDeployAccountTransaction, @@ -22,7 +23,7 @@ use starknet_api::transaction::{ InvokeTransaction as ApiInvokeTransaction, Transaction as ApiTransaction, }; -use crate::backend::executor::ExecutedTransaction; +use crate::execution::ExecutedTransaction; use crate::utils::transaction::api_to_rpc_transaction; /// The status of the transactions known to the sequencer. @@ -45,7 +46,7 @@ pub enum TransactionStatus { pub enum KnownTransaction { Pending(PendingTransaction), Included(IncludedTransaction), - Rejected(Box), + Rejected(Arc), } impl KnownTransaction { @@ -79,7 +80,7 @@ pub struct IncludedTransaction { /// transaction that didn't pass the validation logic. #[derive(Debug, Clone)] pub struct RejectedTransaction { - pub transaction: ApiTransaction, + pub inner: Transaction, pub execution_error: String, } @@ -231,7 +232,7 @@ impl From for KnownTransaction { impl From for KnownTransaction { fn from(transaction: RejectedTransaction) -> Self { - KnownTransaction::Rejected(Box::new(transaction)) + KnownTransaction::Rejected(Arc::new(transaction)) } } @@ -239,7 +240,7 @@ impl From for RpcTransaction { fn from(transaction: KnownTransaction) -> Self { match transaction { KnownTransaction::Pending(tx) => api_to_rpc_transaction(tx.0.inner.clone().into()), - KnownTransaction::Rejected(tx) => api_to_rpc_transaction(tx.transaction), + KnownTransaction::Rejected(tx) => api_to_rpc_transaction(tx.inner.clone().into()), KnownTransaction::Included(tx) => { api_to_rpc_transaction(tx.transaction.inner.clone().into()) } @@ -274,3 +275,9 @@ impl From for AccountTransaction { } } } + +impl From for ExecutionTransaction { + fn from(value: Transaction) -> Self { + ExecutionTransaction::AccountTransaction(value.into()) + } +} diff --git a/crates/katana/core/src/db/cached.rs b/crates/katana/core/src/db/cached.rs new file mode 100644 index 0000000000..0de6058a22 --- /dev/null +++ b/crates/katana/core/src/db/cached.rs @@ -0,0 +1,570 @@ +use std::collections::HashMap; +use std::sync::Arc; + +use blockifier::execution::contract_class::ContractClass; +use blockifier::state::cached_state::{CachedState, CommitmentStateDiff}; +use blockifier::state::errors::StateError; +use blockifier::state::state_api::{State, StateReader, StateResult}; +use starknet::core::types::FlattenedSierraClass; +use starknet_api::core::{ClassHash, CompiledClassHash, ContractAddress, Nonce}; +use starknet_api::hash::StarkFelt; +use starknet_api::state::StorageKey; +use tokio::sync::RwLock as AsyncRwLock; +use tracing::trace; + +use super::{AsStateRefDb, StateExt, StateExtRef, StateRefDb}; + +#[derive(Clone, Debug, Default)] +pub struct StorageRecord { + pub nonce: Nonce, + pub storage: HashMap, +} + +#[derive(Clone, Debug)] +pub struct ClassRecord { + /// The compiled contract class. + pub class: ContractClass, + /// The hash of a compiled Sierra class (if the class is a Sierra class, otherwise + /// for legacy contract, it is the same as the class hash). + pub compiled_hash: CompiledClassHash, +} + +/// A cached state database which fallbacks to an inner database if the data +/// is not found in the cache. +/// +/// The data that has been fetched from the inner database is cached in the +/// cache database. +#[derive(Clone, Debug)] +pub struct CachedDb { + /// A map of class hash to its class definition. + pub classes: HashMap, + /// A map of contract address to its class hash. + pub contracts: HashMap, + /// A map of contract address to the contract information. + pub storage: HashMap, + /// A map of class hash to its Sierra class definition (if any). + pub sierra_classes: HashMap, + /// Inner database to fallback to when the data is not found in the cache. + pub db: Db, +} + +impl CachedDb +where + Db: StateExtRef, +{ + /// Construct a new [CachedDb] with an inner database. + pub fn new(db: Db) -> Self { + Self { + db, + classes: HashMap::new(), + storage: HashMap::new(), + contracts: HashMap::new(), + sierra_classes: HashMap::new(), + } + } +} + +impl State for CachedDb +where + Db: StateExtRef, +{ + fn set_storage_at( + &mut self, + contract_address: ContractAddress, + key: StorageKey, + value: StarkFelt, + ) { + self.storage.entry(contract_address).or_default().storage.insert(key, value); + } + + fn set_class_hash_at( + &mut self, + contract_address: ContractAddress, + class_hash: ClassHash, + ) -> StateResult<()> { + if contract_address == ContractAddress::default() { + return Err(StateError::OutOfRangeContractAddress); + } + self.contracts.insert(contract_address, class_hash); + Ok(()) + } + + fn set_compiled_class_hash( + &mut self, + class_hash: ClassHash, + compiled_class_hash: CompiledClassHash, + ) -> StateResult<()> { + self.classes.entry(class_hash).and_modify(|r| r.compiled_hash = compiled_class_hash); + Ok(()) + } + + fn set_contract_class( + &mut self, + class_hash: &ClassHash, + contract_class: ContractClass, + ) -> StateResult<()> { + self.classes.insert( + *class_hash, + ClassRecord { class: contract_class, compiled_hash: CompiledClassHash(class_hash.0) }, + ); + Ok(()) + } + + fn increment_nonce(&mut self, contract_address: ContractAddress) -> StateResult<()> { + let current_nonce = if let Ok(nonce) = self.get_nonce_at(contract_address) { + nonce + } else { + self.db.get_nonce_at(contract_address)? + }; + + let current_nonce_as_u64 = usize::try_from(current_nonce.0)? as u64; + let next_nonce_val = 1_u64 + current_nonce_as_u64; + let next_nonce = Nonce(StarkFelt::from(next_nonce_val)); + self.storage.entry(contract_address).or_default().nonce = next_nonce; + Ok(()) + } + + fn to_state_diff(&self) -> CommitmentStateDiff { + unreachable!("to_state_diff should not be called on CachedDb") + } +} + +impl StateReader for CachedDb +where + Db: StateExtRef, +{ + fn get_storage_at( + &mut self, + contract_address: ContractAddress, + key: StorageKey, + ) -> StateResult { + if let Some(value) = self.storage.get(&contract_address).and_then(|r| r.storage.get(&key)) { + return Ok(*value); + } + + trace!(target: "cacheddb", "cache miss for storage at address {} index {}", contract_address.0.key(), key.0.key()); + + match self.db.get_storage_at(contract_address, key) { + Ok(value) => { + trace!(target: "cacheddb", "caching storage at address {} index {}", contract_address.0.key(), key.0.key()); + self.set_storage_at(contract_address, key, value); + Ok(value) + } + Err(err) => Err(err), + } + } + + fn get_nonce_at(&mut self, contract_address: ContractAddress) -> StateResult { + if let Some(nonce) = self.storage.get(&contract_address).map(|r| r.nonce) { + return Ok(nonce); + } + + trace!(target: "cached_db", "cache miss for nonce at {}", contract_address.0.key()); + + match self.db.get_nonce_at(contract_address) { + Ok(nonce) => { + trace!(target: "cached_db", "caching nonce at {}", contract_address.0.key()); + self.storage.entry(contract_address).or_default().nonce = nonce; + Ok(nonce) + } + Err(err) => Err(err), + } + } + + fn get_compiled_contract_class( + &mut self, + class_hash: &ClassHash, + ) -> StateResult { + if let Some(class) = self.classes.get(class_hash).map(|r| r.class.clone()) { + return Ok(class); + } + + trace!(target: "cached_db", "cache miss for compiled contract class {class_hash}"); + + match self.db.get_compiled_contract_class(class_hash) { + Ok(class) => { + trace!(target: "cached_db", "caching compiled contract class {class_hash}"); + self.set_contract_class(class_hash, class.clone())?; + Ok(class) + } + Err(err) => Err(err), + } + } + + fn get_class_hash_at(&mut self, contract_address: ContractAddress) -> StateResult { + if let Some(class_hash) = self.contracts.get(&contract_address).cloned() { + return Ok(class_hash); + } + + trace!(target: "cached_db", "cache miss for class hash at address {}", contract_address.0.key()); + + match self.db.get_class_hash_at(contract_address) { + Ok(class_hash) => { + trace!(target: "cached_db", "caching class hash at address {}", contract_address.0.key()); + self.set_class_hash_at(contract_address, class_hash)?; + Ok(class_hash) + } + Err(err) => Err(err), + } + } + + fn get_compiled_class_hash( + &mut self, + class_hash: ClassHash, + ) -> StateResult { + if let Some(hash) = self.classes.get(&class_hash).map(|r| r.compiled_hash) { + return Ok(hash); + } + + trace!(target: "cached_db", "cache miss for compiled class hash {class_hash}"); + + match self.db.get_compiled_class_hash(class_hash) { + Ok(hash) => { + trace!(target: "cached_db", "caching compiled class hash {class_hash}",); + self.set_compiled_class_hash(class_hash, hash)?; + Ok(hash) + } + Err(err) => Err(err), + } + } +} + +impl StateExt for CachedDb +where + Db: StateExtRef, +{ + fn set_sierra_class( + &mut self, + class_hash: ClassHash, + sierra_class: FlattenedSierraClass, + ) -> StateResult<()> { + // check the class hash must not be a legacy contract + if let ContractClass::V0(_) = self.get_compiled_contract_class(&class_hash)? { + return Err(StateError::StateReadError("Class hash is not a Sierra class".to_string())); + }; + self.sierra_classes.insert(class_hash, sierra_class); + Ok(()) + } +} + +impl StateExtRef for CachedDb +where + Db: StateExtRef, +{ + fn get_sierra_class(&mut self, class_hash: &ClassHash) -> StateResult { + if let Some(class) = self.sierra_classes.get(class_hash).cloned() { + return Ok(class); + } + + trace!(target: "cached_db", "cache miss for sierra class {class_hash}"); + + match self.db.get_sierra_class(class_hash) { + Ok(class) => { + trace!(target: "cached_db", "caching sierra class {class_hash}"); + self.set_sierra_class(*class_hash, class.clone())?; + Ok(class) + } + Err(err) => Err(err), + } + } +} + +/// A wrapper type for [CachedState](blockifier::state::cached_state::CachedState) which +/// also allow storing the Sierra classes. +/// +/// The inner fields are wrapped in [Arc] and an async [RwLock](tokio::sync::RwLock) as to allow for +/// asynchronous access to the state. +/// +/// Example is when it is being referred to as a [StateRefDb] when the 'pending' state is being +/// requested while the block producer also have access to it in order to execute transactions and +/// produce blocks. +#[derive(Debug, Clone)] +pub struct CachedStateWrapper { + inner: Arc>>, + sierra_class: Arc>>, +} + +impl CachedStateWrapper +where + Db: StateExtRef, +{ + pub fn new(db: Db) -> Self { + Self { + sierra_class: Default::default(), + inner: Arc::new(AsyncRwLock::new(CachedState::new(db))), + } + } + + pub fn inner_mut(&self) -> tokio::sync::RwLockWriteGuard<'_, CachedState> { + tokio::task::block_in_place(|| self.inner.blocking_write()) + } + + pub fn sierra_class( + &self, + ) -> tokio::sync::RwLockReadGuard<'_, HashMap> { + tokio::task::block_in_place(|| self.sierra_class.blocking_read()) + } + + pub fn sierra_class_mut( + &self, + ) -> tokio::sync::RwLockWriteGuard<'_, HashMap> { + tokio::task::block_in_place(|| self.sierra_class.blocking_write()) + } +} + +impl State for CachedStateWrapper +where + Db: StateExtRef, +{ + fn increment_nonce(&mut self, contract_address: ContractAddress) -> StateResult<()> { + self.inner_mut().increment_nonce(contract_address) + } + + fn set_class_hash_at( + &mut self, + contract_address: ContractAddress, + class_hash: ClassHash, + ) -> StateResult<()> { + self.inner_mut().set_class_hash_at(contract_address, class_hash) + } + + fn set_compiled_class_hash( + &mut self, + class_hash: ClassHash, + compiled_class_hash: CompiledClassHash, + ) -> StateResult<()> { + self.inner_mut().set_compiled_class_hash(class_hash, compiled_class_hash) + } + + fn set_contract_class( + &mut self, + class_hash: &ClassHash, + contract_class: ContractClass, + ) -> StateResult<()> { + self.inner_mut().set_contract_class(class_hash, contract_class) + } + + fn set_storage_at( + &mut self, + contract_address: ContractAddress, + key: StorageKey, + value: StarkFelt, + ) { + self.inner_mut().set_storage_at(contract_address, key, value) + } + + fn to_state_diff(&self) -> CommitmentStateDiff { + self.inner_mut().to_state_diff() + } +} + +impl StateExt for CachedStateWrapper +where + Db: StateExtRef, +{ + fn set_sierra_class( + &mut self, + class_hash: ClassHash, + sierra_class: FlattenedSierraClass, + ) -> StateResult<()> { + self.sierra_class_mut().insert(class_hash, sierra_class); + Ok(()) + } +} + +impl StateReader for CachedStateWrapper +where + Db: StateExtRef, +{ + fn get_class_hash_at(&mut self, contract_address: ContractAddress) -> StateResult { + self.inner_mut().get_class_hash_at(contract_address) + } + + fn get_compiled_class_hash(&mut self, class_hash: ClassHash) -> StateResult { + self.inner_mut().get_compiled_class_hash(class_hash) + } + + fn get_compiled_contract_class( + &mut self, + class_hash: &ClassHash, + ) -> StateResult { + self.inner_mut().get_compiled_contract_class(class_hash) + } + + fn get_nonce_at(&mut self, contract_address: ContractAddress) -> StateResult { + self.inner_mut().get_nonce_at(contract_address) + } + + fn get_storage_at( + &mut self, + contract_address: ContractAddress, + key: StorageKey, + ) -> StateResult { + self.inner_mut().get_storage_at(contract_address, key) + } +} + +impl StateExtRef for CachedStateWrapper +where + Db: StateExtRef, +{ + fn get_sierra_class(&mut self, class_hash: &ClassHash) -> StateResult { + if let Ok(class) = self.inner_mut().state.get_sierra_class(class_hash) { + return Ok(class); + } + + self.sierra_class() + .get(class_hash) + .cloned() + .ok_or(StateError::StateReadError("missing sierra class".to_string())) + } +} + +impl AsStateRefDb for CachedStateWrapper +where + Db: StateExtRef + Clone + Send + Sync + 'static, +{ + fn as_ref_db(&self) -> StateRefDb { + StateRefDb::new(self.clone()) + } +} + +/// Unit tests ported from `blockifier`. +#[cfg(test)] +mod tests { + use assert_matches::assert_matches; + use blockifier::state::cached_state::CachedState; + use starknet_api::core::PatriciaKey; + use starknet_api::hash::StarkHash; + use starknet_api::{patricia_key, stark_felt}; + + use super::*; + use crate::backend::in_memory_db::EmptyDb; + + #[test] + fn get_uninitialized_storage_value() { + let mut state = CachedState::new(CachedDb::new(EmptyDb)); + let contract_address = ContractAddress(patricia_key!("0x1")); + let key = StorageKey(patricia_key!("0x10")); + assert_eq!(state.get_storage_at(contract_address, key).unwrap(), StarkFelt::default()); + } + + #[test] + fn get_and_set_storage_value() { + let contract_address0 = ContractAddress(patricia_key!("0x100")); + let contract_address1 = ContractAddress(patricia_key!("0x200")); + let key0 = StorageKey(patricia_key!("0x10")); + let key1 = StorageKey(patricia_key!("0x20")); + let storage_val0 = stark_felt!("0x1"); + let storage_val1 = stark_felt!("0x5"); + + let mut state = CachedState::new(CachedDb { + contracts: HashMap::from([ + (contract_address0, ClassHash(0_u32.into())), + (contract_address1, ClassHash(0_u32.into())), + ]), + storage: HashMap::from([ + ( + contract_address0, + StorageRecord { + nonce: Nonce(0_u32.into()), + storage: HashMap::from([(key0, storage_val0)]), + }, + ), + ( + contract_address1, + StorageRecord { + nonce: Nonce(0_u32.into()), + storage: HashMap::from([(key1, storage_val1)]), + }, + ), + ]), + classes: HashMap::new(), + sierra_classes: HashMap::new(), + db: EmptyDb, + }); + + assert_eq!(state.get_storage_at(contract_address0, key0).unwrap(), storage_val0); + assert_eq!(state.get_storage_at(contract_address1, key1).unwrap(), storage_val1); + + let modified_storage_value0 = stark_felt!("0xA"); + state.set_storage_at(contract_address0, key0, modified_storage_value0); + assert_eq!(state.get_storage_at(contract_address0, key0).unwrap(), modified_storage_value0); + assert_eq!(state.get_storage_at(contract_address1, key1).unwrap(), storage_val1); + + let modified_storage_value1 = stark_felt!("0x7"); + state.set_storage_at(contract_address1, key1, modified_storage_value1); + assert_eq!(state.get_storage_at(contract_address0, key0).unwrap(), modified_storage_value0); + assert_eq!(state.get_storage_at(contract_address1, key1).unwrap(), modified_storage_value1); + } + + #[test] + fn get_uninitialized_value() { + let mut state = CachedState::new(CachedDb::new(EmptyDb)); + let contract_address = ContractAddress(patricia_key!("0x1")); + assert_eq!(state.get_nonce_at(contract_address).unwrap(), Nonce::default()); + } + + #[test] + fn get_uninitialized_class_hash_value() { + let mut state = CachedState::new(CachedDb::new(EmptyDb)); + let valid_contract_address = ContractAddress(patricia_key!("0x1")); + assert_eq!(state.get_class_hash_at(valid_contract_address).unwrap(), ClassHash::default()); + } + + #[test] + fn cannot_set_class_hash_to_uninitialized_contract() { + let mut state = CachedState::new(CachedDb::new(EmptyDb)); + let uninitialized_contract_address = ContractAddress::default(); + let class_hash = ClassHash(stark_felt!("0x100")); + assert_matches!( + state.set_class_hash_at(uninitialized_contract_address, class_hash).unwrap_err(), + StateError::OutOfRangeContractAddress + ); + } + + #[test] + fn get_and_increment_nonce() { + let contract_address1 = ContractAddress(patricia_key!("0x100")); + let contract_address2 = ContractAddress(patricia_key!("0x200")); + let initial_nonce = Nonce(stark_felt!("0x1")); + + let mut state = CachedState::new(CachedDb { + contracts: HashMap::from([ + (contract_address1, ClassHash(0_u32.into())), + (contract_address2, ClassHash(0_u32.into())), + ]), + storage: HashMap::from([ + ( + contract_address1, + StorageRecord { nonce: initial_nonce, storage: HashMap::new() }, + ), + ( + contract_address2, + StorageRecord { nonce: initial_nonce, storage: HashMap::new() }, + ), + ]), + classes: HashMap::new(), + sierra_classes: HashMap::new(), + db: EmptyDb, + }); + + assert_eq!(state.get_nonce_at(contract_address1).unwrap(), initial_nonce); + assert_eq!(state.get_nonce_at(contract_address2).unwrap(), initial_nonce); + + assert!(state.increment_nonce(contract_address1).is_ok()); + let nonce1_plus_one = Nonce(stark_felt!("0x2")); + assert_eq!(state.get_nonce_at(contract_address1).unwrap(), nonce1_plus_one); + assert_eq!(state.get_nonce_at(contract_address2).unwrap(), initial_nonce); + + assert!(state.increment_nonce(contract_address1).is_ok()); + let nonce1_plus_two = Nonce(stark_felt!("0x3")); + assert_eq!(state.get_nonce_at(contract_address1).unwrap(), nonce1_plus_two); + assert_eq!(state.get_nonce_at(contract_address2).unwrap(), initial_nonce); + + assert!(state.increment_nonce(contract_address2).is_ok()); + let nonce2_plus_one = Nonce(stark_felt!("0x2")); + assert_eq!(state.get_nonce_at(contract_address1).unwrap(), nonce1_plus_two); + assert_eq!(state.get_nonce_at(contract_address2).unwrap(), nonce2_plus_one); + } +} diff --git a/crates/katana/core/src/db/mod.rs b/crates/katana/core/src/db/mod.rs index baefbc0ea1..205918aed7 100644 --- a/crates/katana/core/src/db/mod.rs +++ b/crates/katana/core/src/db/mod.rs @@ -1,20 +1,46 @@ -pub mod serde; +use std::fmt; +use std::sync::Arc; use anyhow::Result; -use blockifier::state::state_api::{State, StateReader}; +use blockifier::execution::contract_class::ContractClass; +use blockifier::state::state_api::{State, StateReader, StateResult}; +use parking_lot::Mutex; +use starknet::core::types::FlattenedSierraClass; use starknet_api::core::{ClassHash, CompiledClassHash, ContractAddress, Nonce, PatriciaKey}; use starknet_api::hash::StarkHash; use starknet_api::patricia_key; use starknet_api::state::StorageKey; use self::serde::state::SerializableState; -use crate::backend::state::StateExt; -pub trait Db: State + StateReader + StateExt { +pub mod cached; +pub mod serde; + +/// An extension of the `StateReader` trait, to allow fetching Sierra class from the state. +pub trait StateExtRef: StateReader + fmt::Debug { + /// Returns the Sierra class for the given class hash. + fn get_sierra_class(&mut self, class_hash: &ClassHash) -> StateResult; +} + +/// An extension of the `State` trait, to allow setting Sierra class. +pub trait StateExt: State + StateExtRef { + /// Set the Sierra class for the given class hash. + fn set_sierra_class( + &mut self, + class_hash: ClassHash, + sierra_class: FlattenedSierraClass, + ) -> StateResult<()>; +} + +/// A trait which represents a state database. +pub trait Database: StateExt + AsStateRefDb + Send + Sync { + /// Set the exact nonce value for the given contract address. fn set_nonce(&mut self, addr: ContractAddress, nonce: Nonce); + /// Returns the serialized version of the state. fn dump_state(&self) -> Result; + /// Load the serialized state into the current state. fn load_state(&mut self, state: SerializableState) -> Result<()> { for (addr, record) in state.storage { let address = ContractAddress(patricia_key!(addr)); @@ -23,10 +49,16 @@ pub trait Db: State + StateReader + StateExt { self.set_storage_at(address, StorageKey(patricia_key!(*key)), (*value).into()); }); - self.set_class_hash_at(address, ClassHash(record.class_hash.into()))?; self.set_nonce(address, Nonce(record.nonce.into())); } + for (address, class_hash) in state.contracts { + self.set_class_hash_at( + ContractAddress(patricia_key!(address)), + ClassHash(class_hash.into()), + )?; + } + for (hash, record) in state.classes { let hash = ClassHash(hash.into()); let compiled_hash = CompiledClassHash(record.compiled_hash.into()); @@ -43,3 +75,61 @@ pub trait Db: State + StateReader + StateExt { Ok(()) } } + +pub trait AsStateRefDb { + /// Returns the current state as a read only state + fn as_ref_db(&self) -> StateRefDb; +} + +/// A type which represents a state at a cetain point. This state type is only meant to be read +/// from. +/// +/// It implements [Clone] so that it can be cloned into a +/// [CachedState](blockifier::state::cached_state::CachedState) for executing transactions +/// based on this state, as [CachedState](blockifier::state::cached_state::CachedState) requires the +/// an ownership of the inner [StateReader] that it wraps. +/// +/// The inner type is wrapped inside a [Mutex] to allow interior mutability due to the fact +/// that the [StateReader] trait requires mutable access to the type that implements it. +#[derive(Debug, Clone)] +pub struct StateRefDb(Arc>); + +impl StateRefDb { + pub fn new(state: T) -> Self + where + T: StateExtRef + Send + Sync + 'static, + { + Self(Arc::new(Mutex::new(state))) + } +} + +impl StateReader for StateRefDb { + fn get_storage_at(&mut self, addr: ContractAddress, key: StorageKey) -> StateResult { + self.0.lock().get_storage_at(addr, key) + } + + fn get_class_hash_at(&mut self, addr: ContractAddress) -> StateResult { + self.0.lock().get_class_hash_at(addr) + } + + fn get_compiled_class_hash(&mut self, class_hash: ClassHash) -> StateResult { + self.0.lock().get_compiled_class_hash(class_hash) + } + + fn get_nonce_at(&mut self, contract_address: ContractAddress) -> StateResult { + self.0.lock().get_nonce_at(contract_address) + } + + fn get_compiled_contract_class( + &mut self, + class_hash: &ClassHash, + ) -> StateResult { + self.0.lock().get_compiled_contract_class(class_hash) + } +} + +impl StateExtRef for StateRefDb { + fn get_sierra_class(&mut self, class_hash: &ClassHash) -> StateResult { + self.0.lock().get_sierra_class(class_hash) + } +} diff --git a/crates/katana/core/src/db/serde/state.rs b/crates/katana/core/src/db/serde/state.rs index edf0c84259..9fa0a68795 100644 --- a/crates/katana/core/src/db/serde/state.rs +++ b/crates/katana/core/src/db/serde/state.rs @@ -11,6 +11,8 @@ use crate::db::serde::contract::SerializableContractClass; #[derive(Clone, Debug, Serialize, Deserialize, Default)] pub struct SerializableState { + /// Contract address to its class hash + pub contracts: BTreeMap, /// Address to storage record. pub storage: BTreeMap, /// Class hash to class record. @@ -30,7 +32,6 @@ pub struct SerializableClassRecord { #[derive(Clone, Debug, Serialize, Deserialize)] pub struct SerializableStorageRecord { pub nonce: FieldElement, - pub class_hash: FieldElement, pub storage: BTreeMap, } diff --git a/crates/katana/core/src/env.rs b/crates/katana/core/src/env.rs index dfb86428ae..2af8cc563f 100644 --- a/crates/katana/core/src/env.rs +++ b/crates/katana/core/src/env.rs @@ -14,7 +14,7 @@ use starknet_api::patricia_key; use crate::constants::{DEFAULT_GAS_PRICE, FEE_TOKEN_ADDRESS, SEQUENCER_ADDRESS}; /// Represents the chain environment. -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Env { /// The block environment of the current block. This is the context that /// the transactions will be executed on. diff --git a/crates/katana/core/src/execution.rs b/crates/katana/core/src/execution.rs new file mode 100644 index 0000000000..70560cb370 --- /dev/null +++ b/crates/katana/core/src/execution.rs @@ -0,0 +1,475 @@ +use std::collections::HashMap; +use std::sync::Arc; + +use blockifier::block_context::BlockContext; +use blockifier::execution::contract_class::ContractClass; +use blockifier::execution::entry_point::CallInfo; +use blockifier::state::cached_state::CommitmentStateDiff; +use blockifier::state::state_api::{State, StateReader}; +use blockifier::transaction::errors::TransactionExecutionError; +use blockifier::transaction::objects::{ResourcesMapping, TransactionExecutionInfo}; +use blockifier::transaction::transaction_execution::Transaction as ExecutionTransaction; +use blockifier::transaction::transactions::ExecutableTransaction; +use convert_case::{Case, Casing}; +use parking_lot::RwLock; +use starknet::core::types::{Event, FieldElement, FlattenedSierraClass, MsgToL1}; +use starknet_api::core::ClassHash; +use tracing::{trace, warn}; + +use crate::backend::storage::transaction::{ + DeclareTransaction, RejectedTransaction, Transaction, TransactionOutput, +}; +use crate::db::cached::CachedStateWrapper; +use crate::db::{Database, StateExt, StateRefDb}; +use crate::env::Env; + +/// The outcome that after executing a list of transactions. +pub struct ExecutionOutcome { + // states + pub state_diff: CommitmentStateDiff, + pub declared_classes: HashMap, + pub declared_sierra_classes: HashMap, + // transactions + pub transactions: Vec, +} + +impl ExecutionOutcome { + /// Apply the execution outcome to the given database. + pub fn apply_to(&self, db: &mut dyn Database) { + let ExecutionOutcome { state_diff, declared_classes, declared_sierra_classes, .. } = self; + + // update contract storages + state_diff.storage_updates.iter().for_each(|(contract_address, storages)| { + storages.iter().for_each(|(key, value)| { + db.set_storage_at(*contract_address, *key, *value); + }) + }); + + // update declared contracts + // apply newly declared classses + for (class_hash, compiled_class_hash) in &state_diff.class_hash_to_compiled_class_hash { + let contract_class = + declared_classes.get(class_hash).expect("contract class should exist").clone(); + + let is_sierra = matches!(contract_class, ContractClass::V1(_)); + + db.set_contract_class(class_hash, contract_class).unwrap(); + db.set_compiled_class_hash(*class_hash, *compiled_class_hash).unwrap(); + + if is_sierra { + if let Some(class) = declared_sierra_classes.get(class_hash).cloned() { + db.set_sierra_class(*class_hash, class).unwrap(); + } else { + panic!("sierra class definition is missing") + } + } + } + + // update deployed contracts + state_diff.address_to_class_hash.iter().for_each(|(contract_address, class_hash)| { + db.set_class_hash_at(*contract_address, *class_hash).unwrap() + }); + + // update accounts nonce + state_diff.address_to_nonce.iter().for_each(|(contract_address, nonce)| { + db.set_nonce(*contract_address, *nonce); + }); + } +} + +impl Default for ExecutionOutcome { + fn default() -> Self { + let state_diff = CommitmentStateDiff { + storage_updates: Default::default(), + address_to_nonce: Default::default(), + address_to_class_hash: Default::default(), + class_hash_to_compiled_class_hash: Default::default(), + }; + + Self { + state_diff, + transactions: Default::default(), + declared_classes: Default::default(), + declared_sierra_classes: Default::default(), + } + } +} + +pub struct TransactionExecutor<'a> { + charge_fee: bool, + block_context: &'a BlockContext, + state: &'a mut CachedStateWrapper, + + // logs flags + error_log: bool, + events_log: bool, + resources_log: bool, +} + +impl<'a> TransactionExecutor<'a> { + pub fn new( + state: &'a mut CachedStateWrapper, + block_context: &'a BlockContext, + charge_fee: bool, + ) -> Self { + Self { + state, + charge_fee, + block_context, + error_log: false, + events_log: false, + resources_log: false, + } + } + + pub fn with_events_log(self) -> Self { + Self { events_log: true, ..self } + } + + pub fn with_error_log(self) -> Self { + Self { error_log: true, ..self } + } + + pub fn with_resources_log(self) -> Self { + Self { resources_log: true, ..self } + } +} + +impl<'a> TransactionExecutor<'a> { + pub fn execute_many( + &mut self, + transactions: Vec, + ) -> Vec> { + transactions.into_iter().map(|tx| self.execute(tx)).collect() + } + + pub fn execute( + &mut self, + transaction: Transaction, + ) -> Result { + let sierra = if let Transaction::Declare(DeclareTransaction { + sierra_class: Some(sierra_class), + inner, + .. + }) = &transaction + { + Some((inner.class_hash(), sierra_class.clone())) + } else { + None + }; + + let res = match transaction.into() { + ExecutionTransaction::AccountTransaction(tx) => { + tx.execute(&mut self.state.inner_mut(), self.block_context, self.charge_fee) + } + ExecutionTransaction::L1HandlerTransaction(tx) => { + tx.execute(&mut self.state.inner_mut(), self.block_context, self.charge_fee) + } + }; + + match res { + Ok(exec_info) => { + if let Some((class_hash, sierra_class)) = sierra { + self.state + .set_sierra_class(class_hash, sierra_class) + .expect("failed to set sierra class"); + } + + if self.error_log { + if let Some(err) = &exec_info.revert_error { + let formatted_err = format!("{:?}", err).replace("\\n", "\n"); + warn!(target: "executor", "Transaction execution error: {formatted_err}"); + } + } + + if self.resources_log { + trace!( + target: "executor", + "Transaction resource usage: {}", + pretty_print_resources(&exec_info.actual_resources) + ); + } + + if self.events_log { + trace_events(&events_from_exec_info(&exec_info)); + } + + Ok(exec_info) + } + + Err(err) => { + if self.error_log { + warn!(target: "executor", "Transaction validation error: {err:?}"); + } + + Err(err) + } + } + } +} + +/// An enum which represents a transaction that has been executed and may or may not be valid. +#[derive(Clone)] +pub enum MaybeInvalidExecutedTransaction { + Valid(Arc), + Invalid(Arc), +} + +pub struct PendingState { + pub state: RwLock>, + /// The transactions that have been executed. + pub executed_transactions: RwLock>, +} + +/// The executor used when the node is running in interval mode. +pub struct PendingBlockExecutor { + /// The state of the executor. + state: Arc, + /// Determines whether to charge fees for transactions. + charge_fee: bool, + /// The environment variable to execute the transaction on. + env: Arc>, +} + +impl PendingBlockExecutor { + pub fn new( + state: CachedStateWrapper, + env: Arc>, + charge_fee: bool, + ) -> Self { + let state = Arc::new(PendingState { + state: RwLock::new(state), + executed_transactions: Default::default(), + }); + Self { env, state, charge_fee } + } + + pub fn state(&self) -> Arc { + self.state.clone() + } + + /// Resets the executor to a new state + pub fn reset(&mut self, state: StateRefDb) { + self.state.executed_transactions.write().clear(); + *self.state.state.write() = CachedStateWrapper::new(state); + } + + /// Execute all the given transactions sequentially based on their order in the list. + pub fn execute(&self, transactions: Vec) { + let transactions = { + let mut state = self.state.state.write(); + TransactionExecutor::new(&mut state, &self.env.read().block, self.charge_fee) + .with_error_log() + .with_events_log() + .with_resources_log() + .execute_many(transactions.clone()) + .into_iter() + .zip(transactions) + .map(|(res, tx)| match res { + Ok(exec_info) => { + let executed_tx = ExecutedTransaction::new(tx, exec_info); + MaybeInvalidExecutedTransaction::Valid(Arc::new(executed_tx)) + } + + Err(err) => { + let rejected_tx = + RejectedTransaction { inner: tx, execution_error: err.to_string() }; + MaybeInvalidExecutedTransaction::Invalid(Arc::new(rejected_tx)) + } + }) + .collect::>() + }; + + self.state.executed_transactions.write().extend(transactions) + } + + /// Returns the outcome based on the transactions that have been executed thus far. + pub fn outcome(&self) -> ExecutionOutcome { + let state = &mut self.state.state.write(); + + let declared_sierra_classes = state.sierra_class().clone(); + + let state_diff = state.to_state_diff(); + let declared_classes = state_diff + .class_hash_to_compiled_class_hash + .iter() + .map(|(class_hash, _)| { + let contract_class = state + .get_compiled_contract_class(class_hash) + .expect("contract class must exist in state if declared"); + (*class_hash, contract_class) + }) + .collect::>(); + + ExecutionOutcome { + state_diff, + declared_classes, + declared_sierra_classes, + transactions: self.state.executed_transactions.read().clone(), + } + } +} + +#[derive(Debug)] +pub struct ExecutedTransaction { + pub inner: Transaction, + pub output: TransactionOutput, + pub execution_info: TransactionExecutionInfo, +} + +impl ExecutedTransaction { + pub fn new(transaction: Transaction, execution_info: TransactionExecutionInfo) -> Self { + let actual_fee = execution_info.actual_fee.0; + let events = events_from_exec_info(&execution_info); + let messages_sent = l2_to_l1_messages_from_exec_info(&execution_info); + + Self { + execution_info, + inner: transaction, + output: TransactionOutput { actual_fee, events, messages_sent }, + } + } +} + +pub fn events_from_exec_info(execution_info: &TransactionExecutionInfo) -> Vec { + let mut events: Vec = vec![]; + + fn get_events_recursively(call_info: &CallInfo) -> Vec { + let mut events: Vec = vec![]; + + events.extend(call_info.execution.events.iter().map(|e| Event { + from_address: (*call_info.call.storage_address.0.key()).into(), + data: e.event.data.0.iter().map(|d| (*d).into()).collect(), + keys: e.event.keys.iter().map(|k| k.0.into()).collect(), + })); + + call_info.inner_calls.iter().for_each(|call| { + events.extend(get_events_recursively(call)); + }); + + events + } + + if let Some(ref call) = execution_info.validate_call_info { + events.extend(get_events_recursively(call)); + } + + if let Some(ref call) = execution_info.execute_call_info { + events.extend(get_events_recursively(call)); + } + + if let Some(ref call) = execution_info.fee_transfer_call_info { + events.extend(get_events_recursively(call)); + } + + events +} + +pub fn l2_to_l1_messages_from_exec_info(execution_info: &TransactionExecutionInfo) -> Vec { + let mut messages = vec![]; + + fn get_messages_recursively(info: &CallInfo) -> Vec { + let mut messages = vec![]; + + messages.extend(info.execution.l2_to_l1_messages.iter().map(|m| MsgToL1 { + to_address: + FieldElement::from_byte_slice_be(m.message.to_address.0.as_bytes()).unwrap(), + from_address: (*info.call.caller_address.0.key()).into(), + payload: m.message.payload.0.iter().map(|p| (*p).into()).collect(), + })); + + info.inner_calls.iter().for_each(|call| { + messages.extend(get_messages_recursively(call)); + }); + + messages + } + + if let Some(ref info) = execution_info.validate_call_info { + messages.extend(get_messages_recursively(info)); + } + + if let Some(ref info) = execution_info.execute_call_info { + messages.extend(get_messages_recursively(info)); + } + + if let Some(ref info) = execution_info.fee_transfer_call_info { + messages.extend(get_messages_recursively(info)); + } + + messages +} + +fn pretty_print_resources(resources: &ResourcesMapping) -> String { + let mut mapped_strings: Vec<_> = resources + .0 + .iter() + .filter_map(|(k, v)| match k.as_str() { + "l1_gas_usage" => Some(format!("L1 Gas: {}", v)), + "range_check_builtin" => Some(format!("Range Checks: {}", v)), + "ecdsa_builtin" => Some(format!("ECDSA: {}", v)), + "n_steps" => None, + "pedersen_builtin" => Some(format!("Pedersen: {}", v)), + "bitwise_builtin" => Some(format!("Bitwise: {}", v)), + "keccak_builtin" => Some(format!("Keccak: {}", v)), + _ => Some(format!("{}: {}", k.to_case(Case::Title), v)), + }) + .collect::>(); + + // Sort the strings alphabetically + mapped_strings.sort(); + + // Prepend "Steps" if it exists, so it is always first + if let Some(steps) = resources.0.get("n_steps") { + mapped_strings.insert(0, format!("Steps: {}", steps)); + } + + mapped_strings.join(" | ") +} + +fn trace_events(events: &[Event]) { + for e in events { + let formatted_keys = + e.keys.iter().map(|k| format!("{k:#x}")).collect::>().join(", "); + + trace!(target: "executor", "Event emitted keys=[{}]", formatted_keys); + } +} + +pub fn create_execution_outcome( + state: &mut CachedStateWrapper, + transactions: Vec<(Transaction, Result)>, +) -> ExecutionOutcome { + let transactions = transactions + .into_iter() + .map(|(tx, res)| match res { + Ok(exec_info) => MaybeInvalidExecutedTransaction::Valid(Arc::new( + ExecutedTransaction::new(tx, exec_info), + )), + + Err(err) => MaybeInvalidExecutedTransaction::Invalid(Arc::new(RejectedTransaction { + inner: tx, + execution_error: err.to_string(), + })), + }) + .collect::>(); + + let state_diff = state.to_state_diff(); + let declared_classes = state_diff + .class_hash_to_compiled_class_hash + .iter() + .map(|(class_hash, _)| { + let contract_class = state + .get_compiled_contract_class(class_hash) + .expect("contract class must exist in state if declared"); + (*class_hash, contract_class) + }) + .collect::>(); + + ExecutionOutcome { + state_diff, + transactions, + declared_classes, + declared_sierra_classes: state.sierra_class().clone(), + } +} diff --git a/crates/katana/core/src/fork/backend.rs b/crates/katana/core/src/fork/backend.rs new file mode 100644 index 0000000000..ed77f2343c --- /dev/null +++ b/crates/katana/core/src/fork/backend.rs @@ -0,0 +1,475 @@ +use std::collections::VecDeque; +use std::pin::Pin; +use std::sync::mpsc::{channel as oneshot, Sender as OneshotSender}; +use std::sync::Arc; +use std::task::{Context, Poll}; +use std::thread; + +use blockifier::execution::contract_class::ContractClass; +use blockifier::state::errors::StateError; +use blockifier::state::state_api::{StateReader, StateResult}; +use futures::channel::mpsc::{channel, Receiver, Sender, TrySendError}; +use futures::stream::Stream; +use futures::{Future, FutureExt}; +use parking_lot::RwLock; +use starknet::core::types::{BlockId, FieldElement, FlattenedSierraClass, StarknetError}; +use starknet::providers::jsonrpc::HttpTransport; +use starknet::providers::{ + JsonRpcClient, MaybeUnknownErrorCode, Provider, ProviderError, StarknetErrorWithMessage, +}; +use starknet_api::core::{ClassHash, CompiledClassHash, ContractAddress, Nonce}; +use starknet_api::hash::StarkFelt; +use starknet_api::state::StorageKey; +use tracing::trace; + +use crate::db::cached::CachedDb; +use crate::db::StateExtRef; +use crate::utils::contract::{ + compiled_class_hash_from_flattened_sierra_class, legacy_rpc_to_inner_class, rpc_to_inner_class, +}; + +type GetNonceResult = Result; +type GetStorageResult = Result; +type GetClassHashAtResult = Result; +type GetClassAtResult = Result; + +#[derive(Debug, thiserror::Error)] +pub enum ForkedBackendError { + #[error(transparent)] + Send(TrySendError), + #[error("Compute class hash error: {0}")] + ComputeClassHashError(String), + #[error(transparent)] + Provider(ProviderError< as Provider>::Error>), +} + +pub enum BackendRequest { + GetClassAt(ClassHash, OneshotSender), + GetNonce(ContractAddress, OneshotSender), + GetClassHashAt(ContractAddress, OneshotSender), + GetStorage(ContractAddress, StorageKey, OneshotSender), +} + +type BackendRequestFuture = Pin + Send>>; + +/// A thread-safe handler for the shared forked backend. This handler is responsible for receiving +/// requests from all instances of the [ForkedBackend], process them, and returns the results back +/// to the request sender. +pub struct BackendHandler { + provider: Arc>, + /// Requests that are currently being poll. + pending_requests: Vec, + /// Requests that are queued to be polled. + queued_requests: VecDeque, + /// A channel for receiving requests from the [ForkedBackend]'s. + incoming: Receiver, + /// Pinned block id for all requests. + block: BlockId, +} + +impl BackendHandler { + /// This function is responsible for transforming the incoming request + /// into a future that will be polled until completion by the `BackendHandler`. + /// + /// Each request is accompanied by the sender-half of a oneshot channel that will be used + /// to send the result back to the [ForkedBackend] which sent the requests. + fn handle_requests(&mut self, request: BackendRequest) { + let block = self.block; + let provider = self.provider.clone(); + + match request { + BackendRequest::GetNonce(contract_address, sender) => { + let fut = Box::pin(async move { + let contract_address: FieldElement = (*contract_address.0.key()).into(); + + let res = provider + .get_nonce(block, contract_address) + .await + .map(|n| Nonce(n.into())) + .map_err(ForkedBackendError::Provider); + + sender.send(res).expect("failed to send nonce result") + }); + + self.pending_requests.push(fut); + } + + BackendRequest::GetStorage(contract_address, key, sender) => { + let fut = Box::pin(async move { + let contract_address: FieldElement = (*contract_address.0.key()).into(); + let key: FieldElement = (*key.0.key()).into(); + + let res = provider + .get_storage_at(contract_address, key, block) + .await + .map(|f| f.into()) + .map_err(ForkedBackendError::Provider); + + sender.send(res).expect("failed to send storage result") + }); + + self.pending_requests.push(fut); + } + + BackendRequest::GetClassHashAt(contract_address, sender) => { + let fut = Box::pin(async move { + let contract_address: FieldElement = (*contract_address.0.key()).into(); + + let res = provider + .get_class_hash_at(block, contract_address) + .await + .map(|f| ClassHash(f.into())) + .map_err(ForkedBackendError::Provider); + + sender.send(res).expect("failed to send class hash result") + }); + + self.pending_requests.push(fut); + } + + BackendRequest::GetClassAt(class_hash, sender) => { + let fut = Box::pin(async move { + let class_hash: FieldElement = class_hash.0.into(); + + let res = provider + .get_class(block, class_hash) + .await + .map_err(ForkedBackendError::Provider); + + sender.send(res).expect("failed to send class result") + }); + + self.pending_requests.push(fut); + } + } + } +} + +impl Future for BackendHandler { + type Output = (); + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let pin = self.get_mut(); + loop { + // convert all queued requests into futures to be polled + while let Some(req) = pin.queued_requests.pop_front() { + pin.handle_requests(req); + } + + loop { + match Pin::new(&mut pin.incoming).poll_next(cx) { + Poll::Ready(Some(req)) => { + pin.queued_requests.push_back(req); + } + // Resolve if stream is exhausted. + Poll::Ready(None) => { + return Poll::Ready(()); + } + Poll::Pending => { + break; + } + } + } + + // poll all pending requests + for n in (0..pin.pending_requests.len()).rev() { + let 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); + } + } + + // if no queued requests, then yield + if pin.queued_requests.is_empty() { + return Poll::Pending; + } + } + } +} + +#[derive(Debug, Clone)] +pub struct SharedBackend { + cache: Arc>>, +} + +impl SharedBackend { + pub fn new_with_backend_thread( + provider: Arc>, + block: BlockId, + ) -> Self { + let backend = ForkedBackend::spawn_thread(provider, block); + Self { cache: Arc::new(RwLock::new(CachedDb::new(backend))) } + } +} + +impl StateReader for SharedBackend { + fn get_class_hash_at(&mut self, contract_address: ContractAddress) -> StateResult { + self.cache.write().get_class_hash_at(contract_address) + } + + fn get_compiled_class_hash(&mut self, class_hash: ClassHash) -> StateResult { + self.cache.write().get_compiled_class_hash(class_hash) + } + + fn get_compiled_contract_class( + &mut self, + class_hash: &ClassHash, + ) -> StateResult { + self.cache.write().get_compiled_contract_class(class_hash) + } + + fn get_nonce_at(&mut self, contract_address: ContractAddress) -> StateResult { + self.cache.write().get_nonce_at(contract_address) + } + + fn get_storage_at( + &mut self, + contract_address: ContractAddress, + key: StorageKey, + ) -> StateResult { + self.cache.write().get_storage_at(contract_address, key) + } +} + +impl StateExtRef for SharedBackend { + fn get_sierra_class(&mut self, class_hash: &ClassHash) -> StateResult { + self.cache.write().get_sierra_class(class_hash) + } +} + +/// An interface for interacting with a forked backend handler. This interface will be cloned into +/// multiple instances of the [ForkedBackend] and will be used to send requests to the handler. +#[derive(Debug, Clone)] +pub struct ForkedBackend { + handler: Sender, +} + +impl ForkedBackend { + pub fn spawn_thread(provider: Arc>, block: BlockId) -> Self { + let (backend, handler) = Self::new(provider, block); + + thread::Builder::new() + .spawn(move || { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("failed to create fork backend thread tokio runtime"); + + rt.block_on(handler); + }) + .expect("failed to spawn fork backend thread"); + + trace!(target: "forked_backend", "fork backend thread spawned"); + + backend + } + + pub fn new( + provider: Arc>, + block: BlockId, + ) -> (Self, BackendHandler) { + let (sender, rx) = channel(1); + let handler = BackendHandler { + incoming: rx, + provider, + block, + queued_requests: VecDeque::new(), + pending_requests: Vec::new(), + }; + (Self { handler: sender }, handler) + } + + pub fn do_get_nonce( + &mut self, + contract_address: ContractAddress, + ) -> Result { + trace!(target: "forked_backend", "request nonce for contract address {}", contract_address.0.key()); + tokio::task::block_in_place(|| { + let (sender, rx) = oneshot(); + self.handler + .try_send(BackendRequest::GetNonce(contract_address, sender)) + .map_err(ForkedBackendError::Send)?; + rx.recv().expect("failed to receive nonce result") + }) + } + + pub fn do_get_storage( + &mut self, + contract_address: ContractAddress, + key: StorageKey, + ) -> Result { + trace!(target: "forked_backend", "request storage for address {} at key {}", contract_address.0.key(), key.0.key()); + tokio::task::block_in_place(|| { + let (sender, rx) = oneshot(); + self.handler + .try_send(BackendRequest::GetStorage(contract_address, key, sender)) + .map_err(ForkedBackendError::Send)?; + rx.recv().expect("failed to receive storage result") + }) + } + + pub fn do_get_class_hash_at( + &mut self, + contract_address: ContractAddress, + ) -> Result { + trace!(target: "forked_backend", "request class hash at address {}", contract_address.0.key()); + tokio::task::block_in_place(|| { + let (sender, rx) = oneshot(); + self.handler + .try_send(BackendRequest::GetClassHashAt(contract_address, sender)) + .map_err(ForkedBackendError::Send)?; + rx.recv().expect("failed to receive class hash result") + }) + } + + pub fn do_get_class_at( + &mut self, + class_hash: ClassHash, + ) -> Result { + trace!(target: "forked_backend", "request class at hash {}", class_hash.0); + tokio::task::block_in_place(|| { + let (sender, rx) = oneshot(); + self.handler + .try_send(BackendRequest::GetClassAt(class_hash, sender)) + .map_err(ForkedBackendError::Send)?; + rx.recv().expect("failed to receive class result") + }) + } + + pub fn do_get_compiled_class_hash( + &mut self, + class_hash: ClassHash, + ) -> Result { + trace!(target: "forked_backend", "request compiled class hash at class {}", class_hash.0); + let class = self.do_get_class_at(class_hash)?; + // if its a legacy class, then we just return back the class hash + // else if sierra class, then we have to compile it and compute the compiled class hash. + match class { + starknet::core::types::ContractClass::Legacy(_) => Ok(CompiledClassHash(class_hash.0)), + + starknet::core::types::ContractClass::Sierra(sierra_class) => { + tokio::task::block_in_place(|| { + compiled_class_hash_from_flattened_sierra_class(&sierra_class) + }) + .map(|f| CompiledClassHash(f.into())) + .map_err(|e| ForkedBackendError::ComputeClassHashError(e.to_string())) + } + } + } +} + +impl StateReader for ForkedBackend { + fn get_compiled_class_hash(&mut self, class_hash: ClassHash) -> StateResult { + match self.do_get_compiled_class_hash(class_hash) { + Ok(compiled_class_hash) => Ok(compiled_class_hash), + + Err(ForkedBackendError::Provider(ProviderError::StarknetError( + StarknetErrorWithMessage { + code: MaybeUnknownErrorCode::Known(StarknetError::ClassHashNotFound), + .. + }, + ))) => Err(StateError::UndeclaredClassHash(class_hash)), + Err(e) => Err(StateError::StateReadError(e.to_string())), + } + } + + fn get_compiled_contract_class( + &mut self, + class_hash: &ClassHash, + ) -> StateResult { + match self.do_get_class_at(*class_hash) { + Ok(class) => match class { + starknet::core::types::ContractClass::Legacy(legacy_class) => { + legacy_rpc_to_inner_class(&legacy_class) + .map(|(_, class)| class) + .map_err(|e| StateError::StateReadError(e.to_string())) + } + + starknet::core::types::ContractClass::Sierra(sierra_class) => { + rpc_to_inner_class(&sierra_class) + .map(|(_, class)| class) + .map_err(|e| StateError::StateReadError(e.to_string())) + } + }, + + Err(ForkedBackendError::Provider(ProviderError::StarknetError( + StarknetErrorWithMessage { + code: MaybeUnknownErrorCode::Known(StarknetError::ClassHashNotFound), + .. + }, + ))) => Err(StateError::UndeclaredClassHash(*class_hash)), + + Err(e) => Err(StateError::StateReadError(e.to_string())), + } + } + + fn get_storage_at( + &mut self, + contract_address: ContractAddress, + key: StorageKey, + ) -> StateResult { + match self.do_get_storage(contract_address, key) { + Ok(value) => Ok(value), + + Err(ForkedBackendError::Provider(ProviderError::StarknetError( + StarknetErrorWithMessage { + code: MaybeUnknownErrorCode::Known(StarknetError::ContractNotFound), + .. + }, + ))) => Ok(StarkFelt::default()), + + Err(e) => Err(StateError::StateReadError(e.to_string())), + } + } + + fn get_nonce_at(&mut self, contract_address: ContractAddress) -> StateResult { + match self.do_get_nonce(contract_address) { + Ok(nonce) => Ok(nonce), + + Err(ForkedBackendError::Provider(ProviderError::StarknetError( + StarknetErrorWithMessage { + code: MaybeUnknownErrorCode::Known(StarknetError::ContractNotFound), + .. + }, + ))) => Ok(Nonce::default()), + + Err(e) => Err(StateError::StateReadError(e.to_string())), + } + } + + fn get_class_hash_at(&mut self, contract_address: ContractAddress) -> StateResult { + match self.do_get_class_hash_at(contract_address) { + Ok(class_hash) => Ok(class_hash), + + Err(ForkedBackendError::Provider(ProviderError::StarknetError( + StarknetErrorWithMessage { + code: MaybeUnknownErrorCode::Known(StarknetError::ContractNotFound), + .. + }, + ))) => Ok(ClassHash::default()), + + Err(e) => Err(StateError::StateReadError(e.to_string())), + } + } +} + +impl StateExtRef for ForkedBackend { + fn get_sierra_class(&mut self, class_hash: &ClassHash) -> StateResult { + match self.do_get_class_at(*class_hash) { + Ok(starknet::core::types::ContractClass::Sierra(sierra_class)) => Ok(sierra_class), + + Ok(_) => Err(StateError::StateReadError("Class hash is not a Sierra class".into())), + + Err(ForkedBackendError::Provider(ProviderError::StarknetError( + StarknetErrorWithMessage { + code: MaybeUnknownErrorCode::Known(StarknetError::ClassHashNotFound), + .. + }, + ))) => Err(StateError::UndeclaredClassHash(*class_hash)), + + Err(e) => Err(StateError::StateReadError(e.to_string())), + } + } +} diff --git a/crates/katana/core/src/fork/db.rs b/crates/katana/core/src/fork/db.rs new file mode 100644 index 0000000000..3742420b90 --- /dev/null +++ b/crates/katana/core/src/fork/db.rs @@ -0,0 +1,275 @@ +use std::collections::BTreeMap; +use std::sync::Arc; + +use blockifier::execution::contract_class::ContractClass; +use blockifier::state::cached_state::CommitmentStateDiff; +use blockifier::state::state_api::{State, StateReader, StateResult}; +use starknet::core::types::{BlockId, FlattenedSierraClass}; +use starknet::providers::jsonrpc::HttpTransport; +use starknet::providers::JsonRpcClient; +use starknet_api::core::{ClassHash, CompiledClassHash, ContractAddress, Nonce}; +use starknet_api::hash::StarkFelt; +use starknet_api::state::StorageKey; + +use super::backend::SharedBackend; +use crate::db::cached::CachedDb; +use crate::db::serde::state::{ + SerializableClassRecord, SerializableState, SerializableStorageRecord, +}; +use crate::db::{AsStateRefDb, Database, StateExt, StateExtRef, StateRefDb}; + +/// A state database implementation that forks from a network. +/// +/// It will try to find the requested data in the cache, and if it's not there, it will fetch it +/// from the forked network. The fetched data will be stored in the cache so that the next time the +/// same data is requested, it will be fetched from the cache instead of fetching it from the forked +/// network again. +/// +/// The forked database provider should be locked to a particular block. +#[derive(Debug, Clone)] +pub struct ForkedDb { + /// Shared cache of the forked database. This will be shared across all instances of the + /// `ForkedDb` when it is cloned into a [StateRefDb] using the [AsStateRefDb] trait. + /// + /// So if one instance fetches data from the forked network, the + /// other instances will be able to use the cached data instead of fetching it again. + db: CachedDb, +} + +impl ForkedDb { + /// Construct a new `ForkedDb` from a `Provider` of the network to fork from at a particular + /// `block`. + pub fn new(provider: Arc>, block: BlockId) -> Self { + Self { db: CachedDb::new(SharedBackend::new_with_backend_thread(provider, block)) } + } + + #[cfg(test)] + pub fn new_from_backend(db: CachedDb) -> Self { + Self { db } + } +} + +impl State for ForkedDb { + fn set_storage_at( + &mut self, + contract_address: ContractAddress, + key: StorageKey, + value: StarkFelt, + ) { + self.db.set_storage_at(contract_address, key, value); + } + + fn set_class_hash_at( + &mut self, + contract_address: ContractAddress, + class_hash: ClassHash, + ) -> StateResult<()> { + self.db.set_class_hash_at(contract_address, class_hash) + } + + fn set_compiled_class_hash( + &mut self, + class_hash: ClassHash, + compiled_class_hash: CompiledClassHash, + ) -> StateResult<()> { + self.db.set_compiled_class_hash(class_hash, compiled_class_hash) + } + + fn to_state_diff(&self) -> CommitmentStateDiff { + self.db.to_state_diff() + } + + fn set_contract_class( + &mut self, + class_hash: &ClassHash, + contract_class: ContractClass, + ) -> StateResult<()> { + self.db.set_contract_class(class_hash, contract_class) + } + + fn increment_nonce(&mut self, contract_address: ContractAddress) -> StateResult<()> { + self.db.increment_nonce(contract_address) + } +} + +impl StateReader for ForkedDb { + fn get_nonce_at(&mut self, contract_address: ContractAddress) -> StateResult { + let nonce = self.db.get_nonce_at(contract_address)?; + Ok(nonce) + } + + fn get_storage_at( + &mut self, + contract_address: ContractAddress, + key: StorageKey, + ) -> StateResult { + self.db.get_storage_at(contract_address, key) + } + + fn get_class_hash_at( + &mut self, + contract_address: ContractAddress, + ) -> StateResult { + self.db.get_class_hash_at(contract_address) + } + + fn get_compiled_class_hash(&mut self, class_hash: ClassHash) -> StateResult { + self.db.get_compiled_class_hash(class_hash) + } + + fn get_compiled_contract_class( + &mut self, + class_hash: &ClassHash, + ) -> StateResult { + self.db.get_compiled_contract_class(class_hash) + } +} + +impl StateExtRef for ForkedDb { + fn get_sierra_class(&mut self, class_hash: &ClassHash) -> StateResult { + self.db.get_sierra_class(class_hash) + } +} + +impl StateExt for ForkedDb { + fn set_sierra_class( + &mut self, + class_hash: ClassHash, + sierra_class: FlattenedSierraClass, + ) -> StateResult<()> { + self.db.set_sierra_class(class_hash, sierra_class) + } +} + +impl AsStateRefDb for ForkedDb { + fn as_ref_db(&self) -> StateRefDb { + StateRefDb::new(self.clone()) + } +} + +impl Database for ForkedDb { + fn set_nonce(&mut self, addr: ContractAddress, nonce: Nonce) { + self.db.storage.entry(addr).or_default().nonce = nonce; + } + + fn dump_state(&self) -> anyhow::Result { + let mut serializable = SerializableState::default(); + + self.db.storage.iter().for_each(|(addr, storage)| { + let mut record = SerializableStorageRecord { + storage: BTreeMap::new(), + nonce: storage.nonce.0.into(), + }; + + storage.storage.iter().for_each(|(key, value)| { + record.storage.insert((*key.0.key()).into(), (*value).into()); + }); + + serializable.storage.insert((*addr.0.key()).into(), record); + }); + + self.db.classes.iter().for_each(|(class_hash, class_record)| { + serializable.classes.insert( + class_hash.0.into(), + SerializableClassRecord { + class: class_record.class.clone().into(), + compiled_hash: class_record.compiled_hash.0.into(), + }, + ); + }); + + self.db.contracts.iter().for_each(|(address, class_hash)| { + serializable.contracts.insert((*address.0.key()).into(), class_hash.0.into()); + }); + + self.db.sierra_classes.iter().for_each(|(class_hash, class)| { + serializable.sierra_classes.insert(class_hash.0.into(), class.clone()); + }); + + Ok(serializable) + } +} + +#[cfg(test)] +mod tests { + use starknet::core::types::BlockTag; + use starknet::providers::jsonrpc::HttpTransport; + use starknet::providers::JsonRpcClient; + use starknet_api::core::PatriciaKey; + use starknet_api::hash::StarkHash; + use starknet_api::{patricia_key, stark_felt}; + use url::Url; + + use super::*; + use crate::constants::UDC_CONTRACT; + + const FORKED_ENDPOINT: &str = + "https://starknet-goerli.infura.io/v3/369ce5ac40614952af936e4d64e40474"; + + #[tokio::test] + async fn fetch_from_cache_if_exist() { + let address = ContractAddress(patricia_key!(0x1u32)); + let class_hash = ClassHash(stark_felt!(0x88u32)); + + let expected_nonce = Nonce(stark_felt!(44u32)); + let expected_storage_key = StorageKey(patricia_key!(0x2u32)); + let expected_storage_value = stark_felt!(55u32); + let expected_compiled_class_hash = CompiledClassHash(class_hash.0); + let expected_contract_class = (*UDC_CONTRACT).clone(); + + let provider = JsonRpcClient::new(HttpTransport::new(Url::parse(FORKED_ENDPOINT).unwrap())); + let mut cache = CachedDb::new(SharedBackend::new_with_backend_thread( + Arc::new(provider), + BlockId::Tag(BlockTag::Latest), + )); + + cache.storage.entry(address).or_default().nonce = expected_nonce; + cache.set_storage_at(address, expected_storage_key, expected_storage_value); + cache.set_contract_class(&class_hash, expected_contract_class.clone()).unwrap(); + cache.set_compiled_class_hash(class_hash, expected_compiled_class_hash).unwrap(); + + let mut db = ForkedDb::new_from_backend(cache); + + let nonce = db.get_nonce_at(address).unwrap(); + let storage_value = db.get_storage_at(address, expected_storage_key).unwrap(); + let contract_class = db.get_compiled_contract_class(&class_hash).unwrap(); + let compiled_class_hash = db.get_compiled_class_hash(class_hash).unwrap(); + + assert_eq!(nonce, expected_nonce); + assert_eq!(storage_value, expected_storage_value); + assert_eq!(contract_class, expected_contract_class); + assert_eq!(compiled_class_hash, expected_compiled_class_hash) + } + + #[tokio::test(flavor = "multi_thread")] + async fn fetch_from_provider_if_not_in_cache() { + let provider = JsonRpcClient::new(HttpTransport::new(Url::parse(FORKED_ENDPOINT).unwrap())); + let mut db = ForkedDb::new(Arc::new(provider), BlockId::Tag(BlockTag::Latest)); + + let address = ContractAddress(patricia_key!( + "0x02b92ec12cA1e308f320e99364d4dd8fcc9efDAc574F836C8908de937C289974" + )); + let storage_key = StorageKey(patricia_key!( + "0x3b459c3fadecdb1a501f2fdeec06fd735cb2d93ea59779177a0981660a85352" + )); + + let class_hash = db.get_class_hash_at(address).unwrap(); + let class = db.get_compiled_contract_class(&class_hash).unwrap(); + let storage_value = db.get_storage_at(address, storage_key).unwrap(); + + let expected_class_hash = ClassHash(stark_felt!( + "0x01a736d6ed154502257f02b1ccdf4d9d1089f80811cd6acad48e6b6a9d1f2003" + )); + + assert_eq!(class_hash, expected_class_hash); + + let class_hash_in_cache = *db.db.contracts.get(&address).unwrap(); + let class_in_cache = db.db.classes.get(&class_hash).unwrap().class.clone(); + let storage_value_in_cache = + *db.db.storage.get(&address).unwrap().storage.get(&storage_key).unwrap(); + + assert_eq!(class_in_cache, class, "class must be stored in cache"); + assert_eq!(class_hash_in_cache, expected_class_hash, "class hash must be stored in cache"); + assert_eq!(storage_value_in_cache, storage_value, "storage value must be stored in cache"); + } +} diff --git a/crates/katana/core/src/fork/mod.rs b/crates/katana/core/src/fork/mod.rs new file mode 100644 index 0000000000..b1a4440c59 --- /dev/null +++ b/crates/katana/core/src/fork/mod.rs @@ -0,0 +1,2 @@ +pub mod backend; +pub mod db; diff --git a/crates/katana/core/src/lib.rs b/crates/katana/core/src/lib.rs index 3c0a67e401..c80c58031f 100644 --- a/crates/katana/core/src/lib.rs +++ b/crates/katana/core/src/lib.rs @@ -3,7 +3,11 @@ pub mod backend; pub mod constants; pub mod db; pub mod env; +pub mod execution; +pub mod fork; +pub mod pool; pub mod sequencer; +pub mod service; pub mod utils; pub mod sequencer_error; diff --git a/crates/katana/core/src/pool.rs b/crates/katana/core/src/pool.rs new file mode 100644 index 0000000000..173091862d --- /dev/null +++ b/crates/katana/core/src/pool.rs @@ -0,0 +1,74 @@ +// Code adapted from Foundry's Anvil + +use futures::channel::mpsc::{channel, Receiver, Sender}; +use parking_lot::RwLock; +use starknet::core::types::FieldElement; +use tracing::{info, warn}; + +use crate::backend::storage::transaction::Transaction; + +#[derive(Debug, Default)] +pub struct TransactionPool { + transactions: RwLock>, + transaction_listeners: RwLock>>, +} + +impl TransactionPool { + pub fn new() -> Self { + Self::default() + } +} + +impl TransactionPool { + pub fn add_transaction(&self, transaction: Transaction) { + let hash = transaction.hash(); + self.transactions.write().push(transaction); + + info!(target: "txpool", "Transaction received | Hash: {hash:#x}"); + + // notify listeners of new tx added to the pool + self.notify_listener(hash) + } + + pub fn add_listener(&self) -> Receiver { + const TX_LISTENER_BUFFER_SIZE: usize = 2048; + let (tx, rx) = channel(TX_LISTENER_BUFFER_SIZE); + self.transaction_listeners.write().push(tx); + rx + } + + /// Get all the transaction from the pool and clear it. + pub fn get_transactions(&self) -> Vec { + let mut txs = self.transactions.write(); + let transactions = txs.clone(); + txs.clear(); + transactions + } + + /// notifies all listeners about the transaction + fn notify_listener(&self, hash: FieldElement) { + let mut listener = self.transaction_listeners.write(); + // this is basically a retain but with mut reference + for n in (0..listener.len()).rev() { + let mut listener_tx = listener.swap_remove(n); + let retain = match listener_tx.try_send(hash) { + Ok(()) => true, + Err(e) => { + if e.is_full() { + warn!( + target: "txpool", + "[{:?}] Failed to send tx notification because channel is full", + hash, + ); + true + } else { + false + } + } + }; + if retain { + listener.push(listener_tx) + } + } + } +} diff --git a/crates/katana/core/src/sequencer.rs b/crates/katana/core/src/sequencer.rs index cdd338c504..5694b4cda6 100644 --- a/crates/katana/core/src/sequencer.rs +++ b/crates/katana/core/src/sequencer.rs @@ -7,8 +7,7 @@ use anyhow::Result; use async_trait::async_trait; use auto_impl::auto_impl; use blockifier::execution::contract_class::ContractClass; -use blockifier::state::state_api::StateReader; -use blockifier::transaction::account_transaction::AccountTransaction; +use blockifier::state::state_api::{State, StateReader}; use starknet::core::types::{ BlockId, BlockTag, EmittedEvent, Event, EventsPage, FeeEstimate, FieldElement, MaybePendingTransactionReceipt, StateUpdate, @@ -16,18 +15,20 @@ use starknet::core::types::{ use starknet_api::core::{ChainId, ClassHash, ContractAddress, Nonce}; use starknet_api::hash::StarkFelt; use starknet_api::state::StorageKey; -use tokio::time; use crate::backend::config::StarknetConfig; use crate::backend::contract::StarknetContract; -use crate::backend::state::{MemDb, StateExt}; -use crate::backend::storage::block::ExecutedBlock; +use crate::backend::storage::block::{ExecutedBlock, PartialBlock, PartialHeader}; use crate::backend::storage::transaction::{ DeclareTransaction, DeployAccountTransaction, InvokeTransaction, KnownTransaction, PendingTransaction, Transaction, TransactionStatus, }; use crate::backend::{Backend, ExternalFunctionCall}; +use crate::db::{AsStateRefDb, StateExtRef, StateRefDb}; +use crate::execution::{MaybeInvalidExecutedTransaction, PendingState}; +use crate::pool::TransactionPool; use crate::sequencer_error::SequencerError; +use crate::service::{BlockProducer, BlockProducerMode, NodeService, TransactionMiner}; use crate::utils::event::{ContinuationToken, ContinuationTokenError}; type SequencerResult = Result; @@ -35,14 +36,17 @@ type SequencerResult = Result; #[derive(Debug, Default)] pub struct SequencerConfig { pub block_time: Option, + pub no_mining: bool, } #[async_trait] #[auto_impl(Arc)] pub trait Sequencer { + fn block_producer(&self) -> &BlockProducer; + fn backend(&self) -> &Backend; - async fn state(&self, block_id: &BlockId) -> SequencerResult; + async fn state(&self, block_id: &BlockId) -> SequencerResult; async fn chain_id(&self) -> ChainId; @@ -97,15 +101,15 @@ pub trait Sequencer { transaction: DeployAccountTransaction, ) -> (FieldElement, FieldElement); - async fn add_declare_transaction(&self, transaction: DeclareTransaction); + fn add_declare_transaction(&self, transaction: DeclareTransaction); - async fn add_invoke_transaction(&self, transaction: InvokeTransaction); + fn add_invoke_transaction(&self, transaction: InvokeTransaction); async fn estimate_fee( &self, - account_transaction: AccountTransaction, + transactions: Vec, block_id: BlockId, - ) -> SequencerResult; + ) -> SequencerResult>; async fn events( &self, @@ -118,56 +122,61 @@ pub trait Sequencer { ) -> SequencerResult; async fn state_update(&self, block_id: BlockId) -> SequencerResult; + + async fn set_next_block_timestamp(&self, timestamp: u64) -> Result<(), SequencerError>; + + async fn increase_next_block_timestamp(&self, timestamp: u64) -> Result<(), SequencerError>; + + async fn has_pending_transactions(&self) -> bool; + + async fn set_storage_at( + &self, + contract_address: ContractAddress, + storage_key: StorageKey, + value: StarkFelt, + ) -> Result<(), SequencerError>; } pub struct KatanaSequencer { pub config: SequencerConfig, + pub pool: Arc, pub backend: Arc, + pub block_producer: BlockProducer, } impl KatanaSequencer { - pub fn new(config: SequencerConfig, starknet_config: StarknetConfig) -> Self { - Self { config, backend: Arc::new(Backend::new(starknet_config)) } - } - - pub async fn start(&self) { - // self.starknet.generate_genesis_block().await; - - if let Some(block_time) = self.config.block_time { - let starknet = self.backend.clone(); - tokio::spawn(async move { - loop { - starknet.open_pending_block().await; - time::sleep(time::Duration::from_secs(block_time)).await; - starknet.mine_block().await; - } - }); + pub async fn new(config: SequencerConfig, starknet_config: StarknetConfig) -> Self { + let backend = Arc::new(Backend::new(starknet_config).await); + + let pool = Arc::new(TransactionPool::new()); + let miner = TransactionMiner::new(pool.add_listener()); + + let block_producer = if let Some(block_time) = config.block_time { + BlockProducer::interval( + Arc::clone(&backend), + backend.state.read().await.as_ref_db(), + block_time, + ) + } else if config.no_mining { + BlockProducer::on_demand(Arc::clone(&backend), backend.state.read().await.as_ref_db()) } else { - self.backend.open_pending_block().await; - } - } - - // pub async fn drip_and_deploy_account( - // &self, - // transaction: DeployAccountTransaction, - // balance: u64, - // ) -> SequencerResult<(TransactionHash, ContractAddress)> { let (transaction_hash, - // contract_address) = self.add_deploy_account_transaction(transaction).await; + BlockProducer::instant(Arc::clone(&backend)) + }; - // let deployed_account_balance_key = - // get_storage_var_address("ERC20_balances", &[*contract_address.0.key()]) - // .map_err(SequencerError::StarknetApi)?; + tokio::spawn(NodeService::new(Arc::clone(&pool), miner, block_producer.clone())); - // self.starknet.pending_cached_state.write().await.set_storage_at( - // self.starknet.block_context.read().fee_token_address, - // deployed_account_balance_key, - // stark_felt!(balance), - // ); + Self { pool, config, backend, block_producer } + } - // Ok((transaction_hash, contract_address)) - // } + /// Returns the pending state if the sequencer is running in _interval_ mode. Otherwise `None`. + pub fn pending_state(&self) -> Option> { + match &*self.block_producer.inner.read() { + BlockProducerMode::Instant(_) => None, + BlockProducerMode::Interval(producer) => Some(producer.state()), + } + } - pub(self) async fn verify_contract_exists(&self, contract_address: &ContractAddress) -> bool { + async fn verify_contract_exists(&self, contract_address: &ContractAddress) -> bool { self.backend .state .write() @@ -179,20 +188,28 @@ impl KatanaSequencer { #[async_trait] impl Sequencer for KatanaSequencer { + fn block_producer(&self) -> &BlockProducer { + &self.block_producer + } + fn backend(&self) -> &Backend { &self.backend } - async fn state(&self, block_id: &BlockId) -> SequencerResult { + async fn state(&self, block_id: &BlockId) -> SequencerResult { match block_id { - BlockId::Tag(BlockTag::Latest) => Ok(self.backend.state.read().await.clone()), + BlockId::Tag(BlockTag::Latest) => Ok(self.backend.state.read().await.as_ref_db()), BlockId::Tag(BlockTag::Pending) => { - self.backend.pending_state().await.ok_or(SequencerError::StateNotFound(*block_id)) + if let Some(state) = self.pending_state() { + Ok(state.state.read().as_ref_db()) + } else { + Ok(self.backend.state.read().await.as_ref_db()) + } } _ => { - if let Some(hash) = self.backend.storage.read().await.block_hash(*block_id) { + if let Some(hash) = self.backend.blockchain.block_hash(*block_id) { self.backend .states .read() @@ -214,38 +231,31 @@ impl Sequencer for KatanaSequencer { let transaction_hash = transaction.inner.transaction_hash.0.into(); let contract_address = transaction.contract_address; - self.backend.handle_transaction(Transaction::DeployAccount(transaction)).await; + self.pool.add_transaction(Transaction::DeployAccount(transaction)); (transaction_hash, contract_address) } - async fn add_declare_transaction(&self, transaction: DeclareTransaction) { - self.backend.handle_transaction(Transaction::Declare(transaction)).await; + fn add_declare_transaction(&self, transaction: DeclareTransaction) { + self.pool.add_transaction(Transaction::Declare(transaction)) } - async fn add_invoke_transaction(&self, transaction: InvokeTransaction) { - self.backend.handle_transaction(Transaction::Invoke(transaction)).await; + fn add_invoke_transaction(&self, transaction: InvokeTransaction) { + self.pool.add_transaction(Transaction::Invoke(transaction)) } async fn estimate_fee( &self, - account_transaction: AccountTransaction, + transactions: Vec, block_id: BlockId, - ) -> SequencerResult { - if self.block(block_id).await.is_none() { - return Err(SequencerError::BlockNotFound(block_id)); - } - + ) -> SequencerResult> { let state = self.state(&block_id).await?; - - self.backend - .estimate_fee(account_transaction, state) - .map_err(SequencerError::TransactionExecution) + self.backend.estimate_fee(transactions, state).map_err(SequencerError::TransactionExecution) } async fn block_hash_and_number(&self) -> (FieldElement, u64) { - let hash = self.backend.storage.read().await.latest_hash; - let number = self.backend.storage.read().await.latest_number; + let hash = self.backend.blockchain.storage.read().latest_hash; + let number = self.backend.blockchain.storage.read().latest_number; (hash, number) } @@ -254,10 +264,6 @@ impl Sequencer for KatanaSequencer { block_id: BlockId, contract_address: ContractAddress, ) -> SequencerResult { - if self.block(block_id).await.is_none() { - return Err(SequencerError::BlockNotFound(block_id)); - } - if !self.verify_contract_exists(&contract_address).await { return Err(SequencerError::ContractNotFound(contract_address)); } @@ -271,18 +277,17 @@ impl Sequencer for KatanaSequencer { block_id: BlockId, class_hash: ClassHash, ) -> SequencerResult { - if self.block(block_id).await.is_none() { - return Err(SequencerError::BlockNotFound(block_id)); - } - let mut state = self.state(&block_id).await?; - match state.get_compiled_contract_class(&class_hash).map_err(SequencerError::State)? { - ContractClass::V0(c) => Ok(StarknetContract::Legacy(c)), - ContractClass::V1(_) => state + if let ContractClass::V0(c) = + state.get_compiled_contract_class(&class_hash).map_err(SequencerError::State)? + { + Ok(StarknetContract::Legacy(c)) + } else { + state .get_sierra_class(&class_hash) .map(StarknetContract::Sierra) - .map_err(SequencerError::State), + .map_err(SequencerError::State) } } @@ -292,10 +297,6 @@ impl Sequencer for KatanaSequencer { storage_key: StorageKey, block_id: BlockId, ) -> SequencerResult { - if self.block(block_id).await.is_none() { - return Err(SequencerError::BlockNotFound(block_id)); - } - if !self.verify_contract_exists(&contract_address).await { return Err(SequencerError::ContractNotFound(contract_address)); } @@ -309,24 +310,52 @@ impl Sequencer for KatanaSequencer { } async fn block_number(&self) -> u64 { - self.backend.storage.read().await.latest_number + self.backend.blockchain.storage.read().latest_number } async fn block(&self, block_id: BlockId) -> Option { + let block_id = match block_id { + BlockId::Tag(BlockTag::Pending) if self.block_producer.is_instant_mining() => { + BlockId::Tag(BlockTag::Latest) + } + _ => block_id, + }; + match block_id { BlockId::Tag(BlockTag::Pending) => { - self.backend.pending_block.read().await.as_ref().map(|b| b.as_block().into()) - } - BlockId::Tag(BlockTag::Latest) => { - let latest_hash = self.backend.storage.read().await.latest_hash; - self.backend.storage.read().await.blocks.get(&latest_hash).map(|b| b.clone().into()) - } - BlockId::Hash(hash) => { - self.backend.storage.read().await.blocks.get(&hash).map(|b| b.clone().into()) + let state = self.pending_state().expect("pending state should exist"); + + let block_context = self.backend.env.read().block.clone(); + let latest_hash = self.backend.blockchain.storage.read().latest_hash; + + let header = PartialHeader { + parent_hash: latest_hash, + gas_price: block_context.gas_price, + number: block_context.block_number.0, + timestamp: block_context.block_timestamp.0, + sequencer_address: (*block_context.sequencer_address.0.key()).into(), + }; + + let (transactions, outputs) = { + state + .executed_transactions + .read() + .iter() + .filter_map(|tx| match tx { + MaybeInvalidExecutedTransaction::Valid(tx) => { + Some((tx.clone(), tx.output.clone())) + } + _ => None, + }) + .unzip() + }; + + Some(ExecutedBlock::Pending(PartialBlock { header, transactions, outputs })) } - BlockId::Number(num) => { - let hash = *self.backend.storage.read().await.hashes.get(&num)?; - self.backend.storage.read().await.blocks.get(&hash).map(|b| b.clone().into()) + + _ => { + let hash = self.backend.blockchain.block_hash(block_id)?; + self.backend.blockchain.storage.read().blocks.get(&hash).map(|b| b.clone().into()) } } } @@ -336,10 +365,6 @@ impl Sequencer for KatanaSequencer { block_id: BlockId, contract_address: ContractAddress, ) -> SequencerResult { - if self.block(block_id).await.is_none() { - return Err(SequencerError::BlockNotFound(block_id)); - } - if !self.verify_contract_exists(&contract_address).await { return Err(SequencerError::ContractNotFound(contract_address)); } @@ -353,10 +378,6 @@ impl Sequencer for KatanaSequencer { block_id: BlockId, function_call: ExternalFunctionCall, ) -> SequencerResult> { - if self.block(block_id).await.is_none() { - return Err(SequencerError::BlockNotFound(block_id)); - } - if !self.verify_contract_exists(&function_call.contract_address).await { return Err(SequencerError::ContractNotFound(function_call.contract_address)); } @@ -370,15 +391,21 @@ impl Sequencer for KatanaSequencer { } async fn transaction_status(&self, hash: &FieldElement) -> Option { - match self.backend.storage.read().await.transactions.get(hash) { + let tx = self.backend.blockchain.storage.read().transactions.get(hash).cloned(); + match tx { Some(tx) => Some(tx.status()), // If the requested transaction is not available in the storage then // check if it is available in the pending block. - None => self.backend.pending_block.read().await.as_ref().and_then(|b| { - b.transactions - .iter() - .find(|tx| tx.inner.hash() == *hash) - .map(|_| TransactionStatus::AcceptedOnL2) + None => self.pending_state().as_ref().and_then(|state| { + state.executed_transactions.read().iter().find_map(|tx| match tx { + MaybeInvalidExecutedTransaction::Valid(tx) if tx.inner.hash() == *hash => { + Some(TransactionStatus::AcceptedOnL2) + } + MaybeInvalidExecutedTransaction::Invalid(tx) if tx.inner.hash() == *hash => { + Some(TransactionStatus::Rejected) + } + _ => None, + }) }), } } @@ -401,15 +428,21 @@ impl Sequencer for KatanaSequencer { } async fn transaction(&self, hash: &FieldElement) -> Option { - match self.backend.storage.read().await.transactions.get(hash) { - Some(tx) => Some(tx.clone()), + let tx = self.backend.blockchain.storage.read().transactions.get(hash).cloned(); + match tx { + Some(tx) => Some(tx), // If the requested transaction is not available in the storage then // check if it is available in the pending block. - None => self.backend.pending_block.read().await.as_ref().and_then(|b| { - b.transactions - .iter() - .find(|tx| tx.inner.hash() == *hash) - .map(|tx| PendingTransaction(tx.clone()).into()) + None => self.pending_state().as_ref().and_then(|state| { + state.executed_transactions.read().iter().find_map(|tx| match tx { + MaybeInvalidExecutedTransaction::Valid(tx) if tx.inner.hash() == *hash => { + Some(PendingTransaction(tx.clone()).into()) + } + MaybeInvalidExecutedTransaction::Invalid(tx) if tx.inner.hash() == *hash => { + Some(tx.as_ref().clone().into()) + } + _ => None, + }) }), } } @@ -426,16 +459,16 @@ impl Sequencer for KatanaSequencer { let mut current_block = 0; let (mut from_block, to_block) = { - let storage = self.backend.storage.read().await; + let storage = &self.backend.blockchain; let from = storage .block_hash(from_block) - .and_then(|hash| storage.blocks.get(&hash).map(|b| b.header.number)) + .and_then(|hash| storage.storage.read().blocks.get(&hash).map(|b| b.header.number)) .ok_or(SequencerError::BlockNotFound(from_block))?; let to = storage .block_hash(to_block) - .and_then(|hash| storage.blocks.get(&hash).map(|b| b.header.number)) + .and_then(|hash| storage.storage.read().blocks.get(&hash).map(|b| b.header.number)) .ok_or(SequencerError::BlockNotFound(to_block))?; (from, to) @@ -454,9 +487,9 @@ impl Sequencer for KatanaSequencer { for i in from_block..=to_block { let block = self .backend + .blockchain .storage .read() - .await .block_by_number(i) .cloned() .ok_or(SequencerError::BlockNotFound(BlockId::Number(i)))?; @@ -465,12 +498,12 @@ impl Sequencer for KatanaSequencer { // if the current block is the latest block then we use the latest hash let block_hash = self .backend + .blockchain .storage .read() - .await .block_by_number(i + 1) .map(|b| b.header.parent_hash) - .unwrap_or(self.backend.storage.read().await.latest_hash); + .unwrap_or(self.backend.blockchain.storage.read().latest_hash); let block_number = i; @@ -548,21 +581,57 @@ impl Sequencer for KatanaSequencer { async fn state_update(&self, block_id: BlockId) -> SequencerResult { let block_number = self .backend - .storage - .read() - .await + .blockchain .block_hash(block_id) .ok_or(SequencerError::BlockNotFound(block_id))?; self.backend + .blockchain .storage .read() - .await .state_update .get(&block_number) .cloned() .ok_or(SequencerError::StateUpdateNotFound(block_id)) } + + async fn set_next_block_timestamp(&self, timestamp: u64) -> Result<(), SequencerError> { + if self.has_pending_transactions().await { + return Err(SequencerError::PendingTransactions); + } + self.backend().block_context_generator.write().next_block_start_time = timestamp; + Ok(()) + } + + async fn increase_next_block_timestamp(&self, timestamp: u64) -> Result<(), SequencerError> { + if self.has_pending_transactions().await { + return Err(SequencerError::PendingTransactions); + } + self.backend().block_context_generator.write().block_timestamp_offset += timestamp as i64; + Ok(()) + } + + async fn has_pending_transactions(&self) -> bool { + if let Some(ref pending) = self.pending_state() { + !pending.executed_transactions.read().is_empty() + } else { + false + } + } + + async fn set_storage_at( + &self, + contract_address: ContractAddress, + storage_key: StorageKey, + value: StarkFelt, + ) -> Result<(), SequencerError> { + if let Some(ref pending) = self.pending_state() { + pending.state.write().set_storage_at(contract_address, storage_key, value); + } else { + self.backend().state.write().await.set_storage_at(contract_address, storage_key, value); + } + Ok(()) + } } fn filter_events_by_params( diff --git a/crates/katana/core/src/service.rs b/crates/katana/core/src/service.rs new file mode 100644 index 0000000000..6e3d5c0ce7 --- /dev/null +++ b/crates/katana/core/src/service.rs @@ -0,0 +1,483 @@ +// Code adapted from Foundry's Anvil + +//! background service + +use std::collections::{HashMap, VecDeque}; +use std::future::Future; +use std::pin::Pin; +use std::sync::Arc; +use std::task::{Context, Poll}; +use std::time::Duration; + +use blockifier::state::state_api::{State, StateReader}; +use futures::channel::mpsc::Receiver; +use futures::stream::{Fuse, Stream, StreamExt}; +use futures::FutureExt; +use parking_lot::RwLock; +use starknet::core::types::FieldElement; +use tokio::time::{interval_at, Instant, Interval}; +use tracing::trace; + +use crate::backend::storage::transaction::{RejectedTransaction, Transaction}; +use crate::backend::Backend; +use crate::db::cached::CachedStateWrapper; +use crate::db::StateRefDb; +use crate::execution::{ + create_execution_outcome, ExecutedTransaction, ExecutionOutcome, + MaybeInvalidExecutedTransaction, PendingState, TransactionExecutor, +}; +use crate::pool::TransactionPool; + +/// The type that drives the blockchain's state +/// +/// This service is basically an endless future that continuously polls the miner which returns +/// transactions for the next block, then those transactions are handed off to the [BlockProducer] +/// to construct a new block. +pub struct NodeService { + /// the pool that holds all transactions + pool: Arc, + /// creates new blocks + block_producer: BlockProducer, + /// the miner responsible to select transactions from the `pool´ + miner: TransactionMiner, +} + +impl NodeService { + pub fn new( + pool: Arc, + miner: TransactionMiner, + block_producer: BlockProducer, + ) -> Self { + Self { pool, block_producer, miner } + } +} + +impl Future for NodeService { + type Output = (); + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let pin = self.get_mut(); + + // this drives block production and feeds new sets of ready transactions to the block + // producer + loop { + while let Poll::Ready(Some(outcome)) = pin.block_producer.poll_next_unpin(cx) { + trace!(target: "node", "mined block {}", outcome.block_number); + } + + if let Poll::Ready(transactions) = pin.miner.poll(&pin.pool, cx) { + // miner returned a set of transaction that we feed to the producer + pin.block_producer.queue(transactions); + } else { + // no progress made + break; + } + } + + Poll::Pending + } +} + +type ServiceFuture = Pin + Send + Sync>>; +type InstantBlockMiningFuture = ServiceFuture; +type PendingBlockMiningFuture = ServiceFuture<(MinedBlockOutcome, StateRefDb)>; + +/// The type which responsible for block production. +/// +/// On _interval_ mining, a new block is opened for a fixed amount of interval. Within this +/// interval, it executes all the queued transactions and keep hold of the pending state after +/// executing all the transactions. Once the interval is over, the block producer will close/mine +/// the block with all the transactions that have been executed within the interval and applies the +/// resulting state to the latest state. Then, a new block is opened for the next interval. As such, +/// the block context is updated only when a new block is opened. +/// +/// On _instant_ mining, a new block is mined as soon as there are transactions in the tx pool. The +/// block producer will execute all the transactions in the mempool and mine a new block with the +/// resulting state. The block context is only updated every time a new block is mined as opposed to +/// updating it when the block is opened (in _interval_ mode). +#[must_use = "BlockProducer does nothing unless polled"] +#[derive(Clone)] +pub struct BlockProducer { + /// The inner mode of mining. + pub inner: Arc>, +} + +impl BlockProducer { + /// Creates a block producer that mines a new block every `interval` milliseconds. + pub fn interval(backend: Arc, initial_state: StateRefDb, interval: u64) -> Self { + Self { + inner: Arc::new(RwLock::new(BlockProducerMode::Interval(PendingBlockProducer::new( + backend, + initial_state, + interval, + )))), + } + } + + /// Creates a new block producer that will only be possible to mine by calling the + /// `katana_generateBlock` RPC method. + pub fn on_demand(backend: Arc, initial_state: StateRefDb) -> Self { + Self { + inner: Arc::new(RwLock::new(BlockProducerMode::Interval( + PendingBlockProducer::new_no_mining(backend, initial_state), + ))), + } + } + + /// Creates a block producer that mines a new block as soon as there are ready transactions in + /// the transactions pool. + pub fn instant(backend: Arc) -> Self { + Self { + inner: Arc::new(RwLock::new(BlockProducerMode::Instant(InstantBlockProducer::new( + backend, + )))), + } + } + + fn queue(&self, transactions: Vec) { + let mut mode = self.inner.write(); + match &mut *mode { + BlockProducerMode::Instant(producer) => producer.queued.push_back(transactions), + BlockProducerMode::Interval(producer) => producer.queued.push_back(transactions), + } + } + + /// Returns `true` if the block producer is running in _interval_ mode. Otherwise, `fales`. + pub fn is_interval_mining(&self) -> bool { + matches!(*self.inner.read(), BlockProducerMode::Interval(_)) + } + + /// Returns `true` if the block producer is running in _instant_ mode. Otherwise, `fales`. + pub fn is_instant_mining(&self) -> bool { + matches!(*self.inner.read(), BlockProducerMode::Instant(_)) + } + + // Handler for the `katana_generateBlock` RPC method. + pub fn force_mine(&self) { + trace!(target: "miner", "force mining"); + let mut mode = self.inner.write(); + match &mut *mode { + BlockProducerMode::Instant(producer) => { + tokio::task::block_in_place(|| futures::executor::block_on(producer.force_mine())) + } + BlockProducerMode::Interval(producer) => { + tokio::task::block_in_place(|| futures::executor::block_on(producer.force_mine())) + } + } + } +} + +impl Stream for BlockProducer { + type Item = MinedBlockOutcome; + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let mut mode = self.inner.write(); + match &mut *mode { + BlockProducerMode::Instant(producer) => producer.poll_next_unpin(cx), + BlockProducerMode::Interval(producer) => producer.poll_next_unpin(cx), + } + } +} + +pub enum BlockProducerMode { + Interval(PendingBlockProducer), + Instant(InstantBlockProducer), +} + +pub struct PendingBlockProducer { + /// The interval at which new blocks are mined. + interval: Option, + backend: Arc, + /// Single active future that mines a new block + block_mining: Option, + /// Backlog of sets of transactions ready to be mined + queued: VecDeque>, + /// The state of the pending block after executing all the transactions within the interval. + state: Arc, + /// This is to make sure that the block context is updated + /// before the first block is opened. + is_initialized: bool, +} + +impl PendingBlockProducer { + pub fn new(backend: Arc, db: StateRefDb, interval: u64) -> Self { + let interval = Duration::from_millis(interval); + let state = Arc::new(PendingState { + state: RwLock::new(CachedStateWrapper::new(db)), + executed_transactions: Default::default(), + }); + + Self { + state, + backend, + block_mining: None, + is_initialized: false, + queued: VecDeque::default(), + interval: Some(interval_at(Instant::now() + interval, interval)), + } + } + + /// Creates a new [PendingBlockProducer] with no `interval`. This mode will not produce blocks + /// for every fixed interval, although it will still execute all queued transactions and + /// keep hold of the pending state. + pub fn new_no_mining(backend: Arc, db: StateRefDb) -> Self { + let state = Arc::new(PendingState { + state: RwLock::new(CachedStateWrapper::new(db)), + executed_transactions: Default::default(), + }); + + Self { + state, + backend, + block_mining: None, + is_initialized: false, + queued: VecDeque::default(), + interval: None, + } + } + + pub fn state(&self) -> Arc { + self.state.clone() + } + + /// Force mine a new block. It will only able to mine if there is no ongoing mining process. + pub async fn force_mine(&self) { + if self.block_mining.is_none() { + let outcome = self.outcome(); + let (_, new_state) = Self::do_mine(outcome, self.backend.clone()).await; + self.reset(new_state); + } else { + trace!(target: "miner", "unable to force mine while a mining process is running") + } + } + + async fn do_mine( + execution_outcome: ExecutionOutcome, + backend: Arc, + ) -> (MinedBlockOutcome, StateRefDb) { + trace!(target: "miner", "creating new block"); + let (outcome, new_state) = backend.mine_pending_block(execution_outcome).await; + trace!(target: "miner", "created new block: {}", outcome.block_number); + backend.update_block_context(); + (outcome, new_state) + } + + fn reset(&self, state: StateRefDb) { + self.state.executed_transactions.write().clear(); + *self.state.state.write() = CachedStateWrapper::new(state); + } + + fn execute_transactions( + transactions: Vec, + state: Arc, + backend: Arc, + ) { + let transactions = { + let mut state = state.state.write(); + TransactionExecutor::new( + &mut state, + &backend.env.read().block, + !backend.config.read().disable_fee, + ) + .with_error_log() + .with_events_log() + .with_resources_log() + .execute_many(transactions.clone()) + .into_iter() + .zip(transactions) + .map(|(res, tx)| match res { + Ok(exec_info) => { + let executed_tx = ExecutedTransaction::new(tx, exec_info); + MaybeInvalidExecutedTransaction::Valid(Arc::new(executed_tx)) + } + Err(err) => { + let rejected_tx = + RejectedTransaction { inner: tx, execution_error: err.to_string() }; + MaybeInvalidExecutedTransaction::Invalid(Arc::new(rejected_tx)) + } + }) + .collect::>() + }; + + state.executed_transactions.write().extend(transactions) + } + + fn outcome(&self) -> ExecutionOutcome { + let state = &mut self.state.state.write(); + + let declared_sierra_classes = state.sierra_class().clone(); + let state_diff = state.to_state_diff(); + let declared_classes = state_diff + .class_hash_to_compiled_class_hash + .iter() + .map(|(class_hash, _)| { + let contract_class = state + .get_compiled_contract_class(class_hash) + .expect("contract class must exist in state if declared"); + (*class_hash, contract_class) + }) + .collect::>(); + + ExecutionOutcome { + state_diff, + declared_classes, + declared_sierra_classes, + transactions: self.state.executed_transactions.read().clone(), + } + } +} + +impl Stream for PendingBlockProducer { + // mined block outcome and the new state + type Item = MinedBlockOutcome; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let pin = self.get_mut(); + + if !pin.is_initialized { + pin.backend.update_block_context(); + pin.is_initialized = true; + } + + if let Some(interval) = &mut pin.interval { + if interval.poll_tick(cx).is_ready() && pin.block_mining.is_none() { + pin.block_mining = + Some(Box::pin(Self::do_mine(pin.outcome(), pin.backend.clone()))); + } + } + + // only execute transactions if there is no mining in progress + if !pin.queued.is_empty() && pin.block_mining.is_none() { + let transactions = pin.queued.pop_front().expect("not empty; qed"); + Self::execute_transactions(transactions, pin.state.clone(), pin.backend.clone()); + } + + // poll the mining future + if let Some(mut mining) = pin.block_mining.take() { + // reset the executor for the next block + if let Poll::Ready((outcome, new_state)) = mining.poll_unpin(cx) { + // update the block context for the next pending block + pin.reset(new_state); + return Poll::Ready(Some(outcome)); + } else { + pin.block_mining = Some(mining) + } + } + + Poll::Pending + } +} + +pub struct InstantBlockProducer { + /// Holds the backend if no block is being mined + backend: Arc, + /// Single active future that mines a new block + block_mining: Option, + /// Backlog of sets of transactions ready to be mined + queued: VecDeque>, +} + +impl InstantBlockProducer { + pub fn new(backend: Arc) -> Self { + Self { backend, block_mining: None, queued: VecDeque::default() } + } + + pub async fn force_mine(&mut self) { + if self.block_mining.is_none() { + let txs = self.queued.pop_front().unwrap_or_default(); + let _ = Self::do_mine(self.backend.clone(), txs).await; + } else { + trace!(target: "miner", "unable to force mine while a mining process is running") + } + } + + async fn do_mine(backend: Arc, transactions: Vec) -> MinedBlockOutcome { + trace!(target: "miner", "creating new block"); + + backend.update_block_context(); + + let mut state = CachedStateWrapper::new(backend.state.read().await.as_ref_db()); + let block_context = backend.env.read().block.clone(); + + let results = TransactionExecutor::new(&mut state, &block_context, true) + .execute_many(transactions.clone()); + + let outcome = backend + .do_mine_block(create_execution_outcome( + &mut state, + transactions.into_iter().zip(results).collect(), + )) + .await; + + trace!(target: "miner", "created new block: {}", outcome.block_number); + + outcome + } +} + +impl Stream for InstantBlockProducer { + // mined block outcome and the new state + type Item = MinedBlockOutcome; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let pin = self.get_mut(); + + if !pin.queued.is_empty() && pin.block_mining.is_none() { + let transactions = pin.queued.pop_front().expect("not empty; qed"); + pin.block_mining = Some(Box::pin(Self::do_mine(pin.backend.clone(), transactions))); + } + + // poll the mining future + if let Some(mut mining) = pin.block_mining.take() { + if let Poll::Ready(outcome) = mining.poll_unpin(cx) { + return Poll::Ready(Some(outcome)); + } else { + pin.block_mining = Some(mining) + } + } + + Poll::Pending + } +} + +pub struct MinedBlockOutcome { + pub block_number: u64, + pub transactions: Vec, +} + +/// The type which takes the transaction from the pool and feeds them to the block producer. +pub struct TransactionMiner { + /// stores whether there are pending transacions (if known) + has_pending_txs: Option, + /// Receives hashes of transactions that are ready from the pool + rx: Fuse>, +} + +impl TransactionMiner { + pub fn new(rx: Receiver) -> Self { + Self { rx: rx.fuse(), has_pending_txs: None } + } + + fn poll( + &mut self, + pool: &Arc, + cx: &mut Context<'_>, + ) -> Poll> { + // drain the notification stream + while let Poll::Ready(Some(_)) = Pin::new(&mut self.rx).poll_next(cx) { + self.has_pending_txs = Some(true); + } + + if self.has_pending_txs == Some(false) { + return Poll::Pending; + } + + // take all the transactions from the pool + let transactions = pool.get_transactions(); + + if transactions.is_empty() { + return Poll::Pending; + } + + Poll::Ready(transactions) + } +} diff --git a/crates/katana/core/src/utils/contract.rs b/crates/katana/core/src/utils/contract.rs index f2791117b5..1c7ec00890 100644 --- a/crates/katana/core/src/utils/contract.rs +++ b/crates/katana/core/src/utils/contract.rs @@ -9,6 +9,7 @@ use cairo_lang_starknet::casm_contract_class::CasmContractClass; use cairo_vm::serde::deserialize_program::ProgramJson; use serde_json::json; use starknet::core::types::contract::legacy::{LegacyContractClass, LegacyProgram}; +use starknet::core::types::contract::CompiledClass; use starknet::core::types::{ CompressedLegacyContractClass, ContractClass, FieldElement, FlattenedSierraClass, LegacyContractEntryPoint, LegacyEntryPointsByType, @@ -51,9 +52,19 @@ pub fn rpc_to_inner_class( contract_class: &FlattenedSierraClass, ) -> Result<(FieldElement, InnerContractClass)> { let class_hash = contract_class.class_hash(); + let contract_class = rpc_to_cairo_contract_class(contract_class)?; + let casm_contract = CasmContractClass::from_contract_class(contract_class, true)?; + Ok((class_hash, InnerContractClass::V1(casm_contract.try_into()?))) +} +/// Converts `starknet-rs` RPC [FlattenedSierraClass] type to Cairo's +/// [ContractClass](cairo_lang_starknet::contract_class::ContractClass) type. +pub fn rpc_to_cairo_contract_class( + contract_class: &FlattenedSierraClass, +) -> Result { let value = serde_json::to_value(contract_class)?; - let contract_class = cairo_lang_starknet::contract_class::ContractClass { + + Ok(cairo_lang_starknet::contract_class::ContractClass { abi: serde_json::from_value(value["abi"].clone()).ok(), sierra_program: serde_json::from_value(value["sierra_program"].clone())?, entry_points_by_type: serde_json::from_value(value["entry_points_by_type"].clone())?, @@ -62,10 +73,18 @@ pub fn rpc_to_inner_class( value["sierra_program_debug_info"].clone(), ) .ok(), - }; + }) +} +/// Compute the compiled class hash from the given [FlattenedSierraClass]. +pub fn compiled_class_hash_from_flattened_sierra_class( + contract_class: &FlattenedSierraClass, +) -> Result { + let contract_class = rpc_to_cairo_contract_class(contract_class)?; let casm_contract = CasmContractClass::from_contract_class(contract_class, true)?; - Ok((class_hash, InnerContractClass::V1(casm_contract.try_into()?))) + let res = serde_json::to_string_pretty(&casm_contract)?; + let compiled_class: CompiledClass = serde_json::from_str(&res)?; + Ok(compiled_class.class_hash()?) } pub fn legacy_rpc_to_inner_class( diff --git a/crates/katana/core/tests/backend.rs b/crates/katana/core/tests/backend.rs new file mode 100644 index 0000000000..fcd1442c07 --- /dev/null +++ b/crates/katana/core/tests/backend.rs @@ -0,0 +1,38 @@ +use katana_core::backend::config::{Environment, StarknetConfig}; +use katana_core::backend::Backend; +use starknet_api::block::BlockNumber; + +fn create_test_starknet_config() -> StarknetConfig { + StarknetConfig { + seed: [0u8; 32], + total_accounts: 2, + disable_fee: true, + env: Environment::default(), + ..Default::default() + } +} + +async fn create_test_backend() -> Backend { + Backend::new(create_test_starknet_config()).await +} + +#[tokio::test] +async fn test_creating_blocks() { + let starknet = create_test_backend().await; + + assert_eq!(starknet.blockchain.storage.read().blocks.len(), 1); + assert_eq!(starknet.blockchain.storage.read().latest_number, 0); + + starknet.mine_empty_block().await; + starknet.mine_empty_block().await; + + assert_eq!(starknet.blockchain.storage.read().blocks.len(), 3); + assert_eq!(starknet.blockchain.storage.read().latest_number, 2); + assert_eq!(starknet.env.read().block.block_number, BlockNumber(2),); + + let block0 = starknet.blockchain.storage.read().block_by_number(0).unwrap().clone(); + let block1 = starknet.blockchain.storage.read().block_by_number(1).unwrap().clone(); + + assert_eq!(block0.header.number, 0); + assert_eq!(block1.header.number, 1); +} diff --git a/crates/katana/core/tests/sequencer.rs b/crates/katana/core/tests/sequencer.rs new file mode 100644 index 0000000000..0f9183dffd --- /dev/null +++ b/crates/katana/core/tests/sequencer.rs @@ -0,0 +1,221 @@ +use std::time::Duration; + +use katana_core::backend::config::{Environment, StarknetConfig}; +use katana_core::backend::storage::transaction::{DeclareTransaction, KnownTransaction}; +use katana_core::sequencer::{KatanaSequencer, Sequencer, SequencerConfig}; +use katana_core::utils::contract::get_contract_class; +use starknet::core::types::FieldElement; +use starknet_api::core::{ClassHash, ContractAddress, Nonce, PatriciaKey}; +use starknet_api::hash::{StarkFelt, StarkHash}; +use starknet_api::state::StorageKey; +use starknet_api::transaction::{ + DeclareTransaction as DeclareApiTransaction, DeclareTransactionV0V1, TransactionHash, +}; +use starknet_api::{patricia_key, stark_felt}; +use tokio::time::sleep; + +fn create_test_sequencer_config() -> (SequencerConfig, StarknetConfig) { + ( + SequencerConfig { block_time: None, ..Default::default() }, + StarknetConfig { + seed: [0u8; 32], + total_accounts: 2, + disable_fee: true, + env: Environment::default(), + ..Default::default() + }, + ) +} + +async fn create_test_sequencer() -> KatanaSequencer { + let (sequencer_config, starknet_config) = create_test_sequencer_config(); + KatanaSequencer::new(sequencer_config, starknet_config).await +} + +fn create_declare_transaction(sender_address: ContractAddress) -> DeclareTransaction { + let compiled_class = + get_contract_class(include_str!("../contracts/compiled/test_contract.json")); + DeclareTransaction { + inner: DeclareApiTransaction::V0(DeclareTransactionV0V1 { + class_hash: ClassHash(stark_felt!("0x1234")), + nonce: Nonce(1u8.into()), + sender_address, + transaction_hash: TransactionHash(stark_felt!("0x6969")), + ..Default::default() + }), + compiled_class, + sierra_class: None, + } +} + +#[tokio::test] +async fn test_next_block_timestamp_in_past() { + let sequencer = create_test_sequencer().await; + let block1 = sequencer.backend.mine_empty_block().await.block_number; + let block1_timestamp = sequencer + .backend + .blockchain + .storage + .read() + .block_by_number(block1) + .unwrap() + .header + .timestamp; + + sequencer.set_next_block_timestamp(block1_timestamp - 1000).await.unwrap(); + + let block2 = sequencer.backend.mine_empty_block().await.block_number; + let block2_timestamp = sequencer + .backend + .blockchain + .storage + .read() + .block_by_number(block2) + .unwrap() + .header + .timestamp; + + assert_eq!(block2_timestamp, block1_timestamp - 1000, "timestamp should be updated"); +} + +#[tokio::test] +async fn test_set_next_block_timestamp_in_future() { + let sequencer = create_test_sequencer().await; + let block1 = sequencer.backend.mine_empty_block().await.block_number; + let block1_timestamp = sequencer + .backend + .blockchain + .storage + .read() + .block_by_number(block1) + .unwrap() + .header + .timestamp; + + sequencer.set_next_block_timestamp(block1_timestamp + 1000).await.unwrap(); + + let block2 = sequencer.backend.mine_empty_block().await.block_number; + let block2_timestamp = sequencer + .backend + .blockchain + .storage + .read() + .block_by_number(block2) + .unwrap() + .header + .timestamp; + + assert_eq!(block2_timestamp, block1_timestamp + 1000, "timestamp should be updated"); +} + +#[tokio::test] +async fn test_increase_next_block_timestamp() { + let sequencer = create_test_sequencer().await; + let block1 = sequencer.backend.mine_empty_block().await.block_number; + let block1_timestamp = sequencer + .backend + .blockchain + .storage + .read() + .block_by_number(block1) + .unwrap() + .header + .timestamp; + + sequencer.increase_next_block_timestamp(1000).await.unwrap(); + + let block2 = sequencer.backend.mine_empty_block().await.block_number; + let block2_timestamp = sequencer + .backend + .blockchain + .storage + .read() + .block_by_number(block2) + .unwrap() + .header + .timestamp; + + assert_eq!(block2_timestamp, block1_timestamp + 1000, "timestamp should be updated"); +} + +#[tokio::test] +async fn test_set_storage_at_on_instant_mode() { + let sequencer = create_test_sequencer().await; + sequencer.backend.mine_empty_block().await; + + let contract_address = ContractAddress(patricia_key!("0x1337")); + let key = StorageKey(patricia_key!("0x20")); + let val = stark_felt!("0xABC"); + + { + let mut state = sequencer.backend.state.write().await; + let read_val = state.get_storage_at(contract_address, key).unwrap(); + assert_eq!(stark_felt!("0x0"), read_val, "latest storage value should be 0"); + } + + sequencer.set_storage_at(contract_address, key, val).await.unwrap(); + + { + let mut state = sequencer.backend.state.write().await; + let read_val = state.get_storage_at(contract_address, key).unwrap(); + assert_eq!(val, read_val, "latest storage value incorrect after generate"); + } +} + +#[tokio::test(flavor = "multi_thread")] +async fn dump_and_load_state() { + let sequencer_old = create_test_sequencer().await; + assert_eq!(sequencer_old.block_number().await, 0); + + let declare_tx = create_declare_transaction(ContractAddress(patricia_key!( + sequencer_old.backend.accounts[0].address + ))); + + let tx_hash = declare_tx.inner.transaction_hash(); + + sequencer_old.add_declare_transaction(declare_tx); + + // wait for the tx to be picked up from the mempool, and executed and included in the next block + sleep(Duration::from_millis(500)).await; + + let tx_in_storage = sequencer_old.transaction(&tx_hash.0.into()).await.unwrap(); + + matches!(tx_in_storage, KnownTransaction::Included(_)); + assert_eq!(sequencer_old.block_number().await, 1); + + let serializable_state = sequencer_old + .backend + .state + .read() + .await + .dump_state() + .expect("must be able to serialize state"); + + assert!( + serializable_state.classes.get(&FieldElement::from_hex_be("0x1234").unwrap()).is_some(), + "class must be serialized" + ); + + // instantiate a new sequencer with the serialized state + let (sequencer_config, mut starknet_config) = create_test_sequencer_config(); + starknet_config.init_state = Some(serializable_state); + let sequencer_new = KatanaSequencer::new(sequencer_config, starknet_config).await; + + let old_contract = sequencer_old + .backend + .state + .write() + .await + .get_compiled_contract_class(&ClassHash(stark_felt!("0x1234"))) + .unwrap(); + + let new_contract = sequencer_new + .backend + .state + .write() + .await + .get_compiled_contract_class(&ClassHash(stark_felt!("0x1234"))) + .unwrap(); + + assert_eq!(old_contract, new_contract); +} diff --git a/crates/katana/core/tests/starknet.rs b/crates/katana/core/tests/starknet.rs deleted file mode 100644 index 1cf1098c96..0000000000 --- a/crates/katana/core/tests/starknet.rs +++ /dev/null @@ -1,181 +0,0 @@ -use blockifier::state::state_api::StateReader; -use katana_core::backend::config::{Environment, StarknetConfig}; -use katana_core::backend::storage::transaction::{DeclareTransaction, Transaction}; -use katana_core::backend::Backend; -use katana_core::db::Db; -use katana_core::utils::contract::get_contract_class; -use starknet_api::block::BlockNumber; -use starknet_api::core::{ClassHash, ContractAddress, Nonce, PatriciaKey}; -use starknet_api::hash::{StarkFelt, StarkHash}; -use starknet_api::state::StorageKey; -use starknet_api::transaction::{ - DeclareTransaction as DeclareApiTransaction, DeclareTransactionV0V1, TransactionHash, -}; -use starknet_api::{patricia_key, stark_felt}; - -fn create_test_starknet_config() -> StarknetConfig { - StarknetConfig { - seed: [0u8; 32], - auto_mine: true, - total_accounts: 2, - disable_fee: true, - env: Environment::default(), - ..Default::default() - } -} - -fn create_test_starknet() -> Backend { - Backend::new(create_test_starknet_config()) -} - -fn create_declare_transaction(sender_address: ContractAddress) -> DeclareTransaction { - let compiled_class = - get_contract_class(include_str!("../contracts/compiled/test_contract.json")); - DeclareTransaction { - inner: DeclareApiTransaction::V0(DeclareTransactionV0V1 { - class_hash: ClassHash(stark_felt!("0x1234")), - nonce: Nonce(1u8.into()), - sender_address, - transaction_hash: TransactionHash(stark_felt!("0x6969")), - ..Default::default() - }), - compiled_class, - sierra_class: None, - } -} - -#[tokio::test] -async fn test_next_block_timestamp_in_past() { - let starknet = create_test_starknet(); - starknet.open_pending_block().await; - - let timestamp = starknet.env.read().block.block_timestamp; - starknet.set_next_block_timestamp(timestamp.0 - 1000).await.unwrap(); - - starknet.open_pending_block().await; - let new_timestamp = starknet.env.read().block.block_timestamp; - - assert_eq!(new_timestamp.0, timestamp.0 - 1000, "timestamp should be updated"); -} - -#[tokio::test] -async fn test_set_next_block_timestamp_in_future() { - let starknet = create_test_starknet(); - starknet.open_pending_block().await; - - let timestamp = starknet.env.read().block.block_timestamp; - starknet.set_next_block_timestamp(timestamp.0 + 1000).await.unwrap(); - - starknet.open_pending_block().await; - let new_timestamp = starknet.env.read().block.block_timestamp; - - assert_eq!(new_timestamp.0, timestamp.0 + 1000, "timestamp should be updated"); -} - -#[tokio::test] -async fn test_increase_next_block_timestamp() { - let starknet = create_test_starknet(); - starknet.open_pending_block().await; - - let timestamp = starknet.env.read().block.block_timestamp; - starknet.increase_next_block_timestamp(1000).await.unwrap(); - - starknet.open_pending_block().await; - let new_timestamp = starknet.env.read().block.block_timestamp; - - assert_eq!(new_timestamp.0, timestamp.0 + 1000, "timestamp should be updated"); -} - -#[tokio::test] -async fn test_creating_blocks() { - let starknet = create_test_starknet(); - starknet.open_pending_block().await; - starknet.mine_block().await; - - assert_eq!(starknet.storage.read().await.blocks.len(), 2); - assert_eq!(starknet.storage.read().await.latest_number, 1); - assert_eq!( - starknet.env.read().block.block_number, - BlockNumber(1), - "block context should only be updated on new pending block" - ); - - let block0 = starknet.storage.read().await.block_by_number(0).unwrap().clone(); - let block1 = starknet.storage.read().await.block_by_number(1).unwrap().clone(); - - assert_eq!(block0.header.number, 0); - assert_eq!(block1.header.number, 1); -} - -#[tokio::test] -async fn dump_and_load_state() { - let backend_old = create_test_starknet(); - backend_old.open_pending_block().await; - - let declare_tx = - create_declare_transaction(ContractAddress(patricia_key!(backend_old.accounts[0].address))); - - backend_old.handle_transaction(Transaction::Declare(declare_tx)).await; - - let serializable_state = - backend_old.state.read().await.dump_state().expect("must be able to serialize state"); - - let mut starknet_config = create_test_starknet_config(); - starknet_config.init_state = Some(serializable_state); - let backend_new = Backend::new(starknet_config); - - let old_contract = backend_old - .state - .write() - .await - .classes - .get(&ClassHash(stark_felt!("0x1234"))) - .cloned() - .unwrap() - .class; - - let new_contract = backend_new - .state - .write() - .await - .classes - .get(&ClassHash(stark_felt!("0x1234"))) - .cloned() - .unwrap() - .class; - - assert_eq!(old_contract, new_contract,); -} - -#[tokio::test] -async fn test_set_storage_at() { - let starknet = create_test_starknet(); - starknet.open_pending_block().await; - - let contract_address = ContractAddress(patricia_key!("0x1337")); - let key = StorageKey(patricia_key!("0x20")); - let val = stark_felt!("0xABC"); - - starknet.set_storage_at(contract_address, key, val).await.unwrap(); - - { - let mut state = starknet.state.write().await; - let read_val = state.get_storage_at(contract_address, key).unwrap(); - assert_eq!(stark_felt!("0x0"), read_val, "latest storage value should be 0"); - } - - { - if let Some(pending_block) = starknet.pending_block.write().await.as_mut() { - let read_val = pending_block.state.get_storage_at(contract_address, key).unwrap(); - assert_eq!(val, read_val, "pending set storage value incorrect"); - } - } - - starknet.mine_block().await; - - { - let mut state = starknet.state.write().await; - let read_val = state.get_storage_at(contract_address, key).unwrap(); - assert_eq!(val, read_val, "latest storage value incorrect after generate"); - } -} diff --git a/crates/katana/rpc/Cargo.toml b/crates/katana/rpc/Cargo.toml index 6f9a2179fe..cfb1bb2efd 100644 --- a/crates/katana/rpc/Cargo.toml +++ b/crates/katana/rpc/Cargo.toml @@ -12,6 +12,7 @@ blockifier.workspace = true cairo-lang-starknet = "2.1.1" cairo-vm.workspace = true flate2.workspace = true +futures.workspace = true hex = { version = "0.4.3", default-features = false } hyper = "0.14.20" jsonrpsee = { version = "0.16.2", features = [ "macros", "server" ] } diff --git a/crates/katana/rpc/src/katana.rs b/crates/katana/rpc/src/katana.rs index 22ed02a1f8..95160d3546 100644 --- a/crates/katana/rpc/src/katana.rs +++ b/crates/katana/rpc/src/katana.rs @@ -28,8 +28,7 @@ where S: Sequencer + Send + Sync + 'static, { async fn generate_block(&self) -> Result<(), Error> { - self.sequencer.backend().mine_block().await; - self.sequencer.backend().open_pending_block().await; + self.sequencer.block_producer().force_mine(); Ok(()) } @@ -39,7 +38,6 @@ where async fn set_next_block_timestamp(&self, timestamp: u64) -> Result<(), Error> { self.sequencer - .backend() .set_next_block_timestamp(timestamp) .await .map_err(|_| Error::from(KatanaApiError::FailedToChangeNextBlockTimestamp)) @@ -47,7 +45,6 @@ where async fn increase_next_block_timestamp(&self, timestamp: u64) -> Result<(), Error> { self.sequencer - .backend() .increase_next_block_timestamp(timestamp) .await .map_err(|_| Error::from(KatanaApiError::FailedToChangeNextBlockTimestamp)) @@ -64,7 +61,6 @@ where value: FieldElement, ) -> Result<(), Error> { self.sequencer - .backend() .set_storage_at( ContractAddress(patricia_key!(contract_address)), StorageKey(patricia_key!(key)), diff --git a/crates/katana/rpc/src/starknet.rs b/crates/katana/rpc/src/starknet.rs index 5cc8962d89..00f33becb4 100644 --- a/crates/katana/rpc/src/starknet.rs +++ b/crates/katana/rpc/src/starknet.rs @@ -1,16 +1,11 @@ use std::sync::Arc; use blockifier::state::errors::StateError; -use blockifier::transaction::account_transaction::AccountTransaction; -use blockifier::transaction::transactions::{ - DeclareTransaction as ExecutionDeclareTransaction, - DeployAccountTransaction as ExecutionDeployAccountTransaction, -}; use jsonrpsee::core::{async_trait, Error}; use katana_core::backend::contract::StarknetContract; use katana_core::backend::storage::transaction::{ DeclareTransaction, DeployAccountTransaction, InvokeTransaction, KnownTransaction, - PendingTransaction, + PendingTransaction, Transaction, }; use katana_core::backend::ExternalFunctionCall; use katana_core::sequencer::Sequencer; @@ -26,7 +21,7 @@ use starknet::core::types::{ ContractClass, DeclareTransactionResult, DeployAccountTransactionResult, EventFilterWithPage, EventsPage, FeeEstimate, FieldElement, FunctionCall, InvokeTransactionResult, MaybePendingBlockWithTxHashes, MaybePendingBlockWithTxs, MaybePendingTransactionReceipt, - StateUpdate, Transaction, + StateUpdate, Transaction as RpcTransaction, }; use starknet_api::core::{ClassHash, ContractAddress, EntryPointSelector, PatriciaKey}; use starknet_api::hash::{StarkFelt, StarkHash}; @@ -84,7 +79,7 @@ where async fn transaction_by_hash( &self, transaction_hash: FieldElement, - ) -> Result { + ) -> Result { let transaction = self .sequencer .transaction(&transaction_hash) @@ -135,7 +130,7 @@ where &self, block_id: BlockId, index: usize, - ) -> Result { + ) -> Result { let block = self .sequencer .block(block_id) @@ -247,7 +242,7 @@ where Ok(events) } - async fn pending_transactions(&self) -> Result, Error> { + async fn pending_transactions(&self) -> Result, Error> { let block = self.sequencer.block(BlockId::Tag(BlockTag::Pending)).await; Ok(block @@ -255,7 +250,7 @@ where b.transactions() .iter() .map(|tx| KnownTransaction::Pending(PendingTransaction(tx.clone())).into()) - .collect::>() + .collect::>() }) .unwrap_or(Vec::new())) } @@ -337,53 +332,52 @@ where let chain_id = FieldElement::from_hex_be(&self.sequencer.chain_id().await.as_hex()) .map_err(|_| Error::from(StarknetApiError::InternalServerError))?; - let mut res = Vec::new(); - - for r in request { - let transaction = match r { + let transactions = request + .into_iter() + .map(|r| match r { BroadcastedTransaction::Declare(tx) => { - let (transaction, contract_class) = + let sierra_class = match tx { + BroadcastedDeclareTransaction::V2(ref tx) => { + Some(tx.contract_class.as_ref().clone()) + } + _ => None, + }; + + let (transaction, compiled_class) = broadcasted_declare_rpc_to_api_transaction(tx, chain_id).unwrap(); - AccountTransaction::Declare( - ExecutionDeclareTransaction::new(transaction, contract_class) - .map_err(|_| Error::from(StarknetApiError::InternalServerError))?, - ) + Transaction::Declare(DeclareTransaction { + sierra_class, + compiled_class, + inner: transaction, + }) } BroadcastedTransaction::Invoke(tx) => { let transaction = broadcasted_invoke_rpc_to_api_transaction(tx, chain_id); - AccountTransaction::Invoke(transaction) + Transaction::Invoke(InvokeTransaction(transaction)) } BroadcastedTransaction::DeployAccount(tx) => { let (transaction, contract_address) = broadcasted_deploy_account_rpc_to_api_transaction(tx, chain_id); - AccountTransaction::DeployAccount(ExecutionDeployAccountTransaction { - tx: transaction, - contract_address: ContractAddress(patricia_key!(contract_address)), + Transaction::DeployAccount(DeployAccountTransaction { + contract_address, + inner: transaction, }) } - }; - - let fee_estimate = - self.sequencer.estimate_fee(transaction, block_id).await.map_err(|e| match e { - SequencerError::BlockNotFound(_) => { - Error::from(StarknetApiError::BlockNotFound) - } - SequencerError::TransactionExecution(_) => { - Error::from(StarknetApiError::ContractError) - } - _ => Error::from(StarknetApiError::InternalServerError), - })?; - - res.push(FeeEstimate { - gas_price: fee_estimate.gas_price, - overall_fee: fee_estimate.overall_fee, - gas_consumed: fee_estimate.gas_consumed, - }); - } + }) + .collect::>(); + + let res = + self.sequencer.estimate_fee(transactions, block_id).await.map_err(|e| match e { + SequencerError::BlockNotFound(_) => Error::from(StarknetApiError::BlockNotFound), + SequencerError::TransactionExecution(_) => { + Error::from(StarknetApiError::ContractError) + } + _ => Error::from(StarknetApiError::InternalServerError), + })?; Ok(res) } @@ -406,13 +400,11 @@ where let transaction_hash = transaction.transaction_hash().0.into(); let class_hash = transaction.class_hash().0.into(); - self.sequencer - .add_declare_transaction(DeclareTransaction { - sierra_class, - inner: transaction, - compiled_class: contract_class, - }) - .await; + self.sequencer.add_declare_transaction(DeclareTransaction { + sierra_class, + inner: transaction, + compiled_class: contract_class, + }); Ok(DeclareTransactionResult { transaction_hash, class_hash }) } @@ -427,7 +419,7 @@ where let transaction = broadcasted_invoke_rpc_to_api_transaction(invoke_transaction, chain_id); let transaction_hash = transaction.transaction_hash().0.into(); - self.sequencer.add_invoke_transaction(InvokeTransaction(transaction)).await; + self.sequencer.add_invoke_transaction(InvokeTransaction(transaction)); Ok(InvokeTransactionResult { transaction_hash }) } diff --git a/crates/katana/rpc/tests/starknet.rs b/crates/katana/rpc/tests/starknet.rs index 7894cb9c83..e6ccae46d8 100644 --- a/crates/katana/rpc/tests/starknet.rs +++ b/crates/katana/rpc/tests/starknet.rs @@ -1,6 +1,7 @@ use std::fs::{self, File}; use std::path::PathBuf; use std::sync::Arc; +use std::time::Duration; use anyhow::{anyhow, Result}; use cairo_lang_starknet::casm_contract_class::CasmContractClass; @@ -17,7 +18,7 @@ use starknet::core::types::{ use starknet::core::utils::{get_contract_address, get_selector_from_name}; use starknet::providers::Provider; -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn test_send_declare_and_deploy_contract() { let sequencer = TestSequencer::start(SequencerConfig::default(), get_default_test_starknet_config()).await; @@ -28,6 +29,10 @@ async fn test_send_declare_and_deploy_contract() { let class_hash = contract.class_hash(); let res = account.declare(Arc::new(contract), compiled_class_hash).send().await.unwrap(); + + // wait for the tx to be mined + tokio::time::sleep(Duration::from_millis(250)).await; + let receipt = account.provider().get_transaction_receipt(res.transaction_hash).await.unwrap(); match receipt { @@ -75,6 +80,9 @@ async fn test_send_declare_and_deploy_contract() { .await .unwrap(); + // wait for the tx to be mined + tokio::time::sleep(Duration::from_millis(250)).await; + assert_eq!( account .provider() @@ -87,8 +95,8 @@ async fn test_send_declare_and_deploy_contract() { sequencer.stop().expect("failed to stop sequencer"); } -#[tokio::test] -async fn test_send_declare_and_deploy_legcay_contract() { +#[tokio::test(flavor = "multi_thread")] +async fn test_send_declare_and_deploy_legacy_contract() { let sequencer = TestSequencer::start(SequencerConfig::default(), get_default_test_starknet_config()).await; let account = sequencer.account(); @@ -101,6 +109,9 @@ async fn test_send_declare_and_deploy_legcay_contract() { let class_hash = contract_class.class_hash().unwrap(); let res = account.declare_legacy(contract_class).send().await.unwrap(); + // wait for the tx to be mined + tokio::time::sleep(Duration::from_millis(250)).await; + let receipt = account.provider().get_transaction_receipt(res.transaction_hash).await.unwrap(); match receipt { @@ -148,6 +159,9 @@ async fn test_send_declare_and_deploy_legcay_contract() { .await .unwrap(); + // wait for the tx to be mined + tokio::time::sleep(Duration::from_millis(250)).await; + assert_eq!( account .provider() diff --git a/crates/katana/src/args.rs b/crates/katana/src/args.rs index 2f5dd9554f..f85b636279 100644 --- a/crates/katana/src/args.rs +++ b/crates/katana/src/args.rs @@ -9,6 +9,7 @@ use katana_core::constants::{ use katana_core::db::serde::state::SerializableState; use katana_core::sequencer::SequencerConfig; use katana_rpc::config::ServerConfig; +use url::Url; #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] @@ -35,6 +36,17 @@ pub struct KatanaArgs { directory, the state will be written to `/state.bin`.")] pub dump_state: Option, + #[arg(long)] + #[arg(value_name = "URL")] + #[arg(help = "The Starknet RPC provider to fork the network from.")] + pub rpc_url: Option, + + #[arg(long)] + #[arg(requires = "rpc_url")] + #[arg(value_name = "BLOCK_NUMBER")] + #[arg(help = "Fork the network at a specific block.")] + pub fork_block_number: Option, + #[arg(long)] #[arg(value_name = "PATH")] #[arg(value_parser = SerializableState::parse)] @@ -115,7 +127,7 @@ pub struct EnvironmentOptions { impl KatanaArgs { pub fn sequencer_config(&self) -> SequencerConfig { - SequencerConfig { block_time: self.block_time } + SequencerConfig { block_time: self.block_time, no_mining: self.no_mining } } pub fn server_config(&self) -> ServerConfig { @@ -130,8 +142,9 @@ impl KatanaArgs { total_accounts: self.starknet.total_accounts, seed: parse_seed(&self.starknet.seed), disable_fee: self.starknet.disable_fee, - auto_mine: self.block_time.is_none() && !self.no_mining, init_state: self.load_state.clone(), + fork_rpc_url: self.rpc_url.clone(), + fork_block_number: self.fork_block_number, env: Environment { chain_id: self.starknet.environment.chain_id.clone(), gas_price: self.starknet.environment.gas_price.unwrap_or(DEFAULT_GAS_PRICE), diff --git a/crates/katana/src/main.rs b/crates/katana/src/main.rs index d4c8845f01..74f10277a5 100644 --- a/crates/katana/src/main.rs +++ b/crates/katana/src/main.rs @@ -19,7 +19,8 @@ use args::KatanaArgs; #[tokio::main] async fn main() { env_logger::Builder::from_env(Env::default().default_filter_or( - "info,katana_rpc=debug,katana_core=trace,blockifier=off,jsonrpsee_server=off,hyper=off", + "info,executor=trace,katana_rpc=debug,katana_core=trace,blockifier=off,\ + jsonrpsee_server=off,hyper=off,", )) .init(); @@ -37,7 +38,7 @@ async fn main() { let sequencer_config = config.sequencer_config(); let starknet_config = config.starknet_config(); - let sequencer = Arc::new(KatanaSequencer::new(sequencer_config, starknet_config)); + let sequencer = Arc::new(KatanaSequencer::new(sequencer_config, starknet_config).await); let starknet_api = StarknetApi::new(sequencer.clone()); let katana_api = KatanaApi::new(sequencer.clone()); @@ -62,7 +63,7 @@ async fn main() { ); } - sequencer.start().await; + // sequencer.start().await; // Wait until Ctrl + C is pressed, then shutdown ctrl_c().await.unwrap(); diff --git a/crates/sozo/src/ops/migration/migration_test.rs b/crates/sozo/src/ops/migration/migration_test.rs index 50da9cab60..ee9792078b 100644 --- a/crates/sozo/src/ops/migration/migration_test.rs +++ b/crates/sozo/src/ops/migration/migration_test.rs @@ -17,7 +17,7 @@ use starknet::signers::{LocalWallet, SigningKey}; use crate::commands::options::transaction::TransactionOptions; use crate::ops::migration::execute_strategy; -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn migrate_with_auto_mine() { let target_dir = Utf8PathBuf::from_path_buf("../../examples/ecs/target/dev".into()).unwrap(); @@ -53,12 +53,12 @@ async fn migrate_with_auto_mine() { sequencer.stop().unwrap(); } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn migrate_with_block_time() { let target_dir = Utf8PathBuf::from_path_buf("../../examples/ecs/target/dev".into()).unwrap(); let sequencer = TestSequencer::start( - SequencerConfig { block_time: Some(1) }, + SequencerConfig { block_time: Some(1), ..Default::default() }, get_default_test_starknet_config(), ) .await; @@ -92,12 +92,12 @@ async fn migrate_with_block_time() { sequencer.stop().unwrap(); } -#[tokio::test] -async fn migrate_with_zero_fee_multiplier_will_fail() { +#[tokio::test(flavor = "multi_thread")] +async fn migrate_with_small_fee_multiplier_will_fail() { let target_dir = Utf8PathBuf::from_path_buf("../../examples/ecs/target/dev".into()).unwrap(); let sequencer = TestSequencer::start( - SequencerConfig { block_time: Some(1) }, + SequencerConfig { block_time: Some(1), ..Default::default() }, StarknetConfig { disable_fee: false, ..Default::default() }, ) .await; @@ -132,13 +132,11 @@ async fn migrate_with_zero_fee_multiplier_will_fail() { &migration, &account, &config, - Some(TransactionOptions { fee_estimate_multiplier: Some(0f64) }), + Some(TransactionOptions { fee_estimate_multiplier: Some(0.2f64) }), ) .await .is_err() ); - - sequencer.stop().unwrap(); } #[test] diff --git a/crates/torii/client/src/contract/component_test.rs b/crates/torii/client/src/contract/component_test.rs index 6839477ee5..c4fbeaf54b 100644 --- a/crates/torii/client/src/contract/component_test.rs +++ b/crates/torii/client/src/contract/component_test.rs @@ -9,7 +9,7 @@ use starknet::core::types::{BlockId, BlockTag, FieldElement}; use crate::contract::world::test::deploy_world; use crate::contract::world::WorldContractReader; -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn test_component() { let sequencer = TestSequencer::start(SequencerConfig::default(), get_default_test_starknet_config()).await; diff --git a/crates/torii/client/src/contract/system_test.rs b/crates/torii/client/src/contract/system_test.rs index 9f3bb249d1..cb9e40b87b 100644 --- a/crates/torii/client/src/contract/system_test.rs +++ b/crates/torii/client/src/contract/system_test.rs @@ -1,3 +1,5 @@ +use std::time::Duration; + use camino::Utf8PathBuf; use dojo_test_utils::sequencer::{ get_default_test_starknet_config, SequencerConfig, TestSequencer, @@ -9,7 +11,7 @@ use starknet_crypto::FieldElement; use crate::contract::world::test::deploy_world; use crate::contract::world::WorldContract; -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn test_system() { let sequencer = TestSequencer::start(SequencerConfig::default(), get_default_test_starknet_config()).await; @@ -26,6 +28,9 @@ async fn test_system() { let _ = spawn.execute(vec![]).await.unwrap(); + // wait for the tx to be mined + tokio::time::sleep(Duration::from_millis(250)).await; + let component = world.component("Moves", block_id).await.unwrap(); let moves = component.entity(vec![account.address()], block_id).await.unwrap(); @@ -34,7 +39,12 @@ async fn test_system() { let move_system = world.system("move", block_id).await.unwrap(); let _ = move_system.execute(vec![FieldElement::ONE]).await.unwrap(); + // wait for the tx to be mined + tokio::time::sleep(Duration::from_millis(250)).await; + let _ = move_system.execute(vec![FieldElement::THREE]).await.unwrap(); + // wait for the tx to be mined + tokio::time::sleep(Duration::from_millis(250)).await; let moves = component.entity(vec![account.address()], block_id).await.unwrap(); diff --git a/crates/torii/client/src/contract/world_test.rs b/crates/torii/client/src/contract/world_test.rs index 5ea5f2816a..0ec1e5b79c 100644 --- a/crates/torii/client/src/contract/world_test.rs +++ b/crates/torii/client/src/contract/world_test.rs @@ -1,3 +1,5 @@ +use std::time::Duration; + use camino::Utf8PathBuf; use dojo_test_utils::sequencer::{ get_default_test_starknet_config, SequencerConfig, TestSequencer, @@ -11,7 +13,7 @@ use starknet::core::types::{BlockId, BlockTag, FieldElement}; use super::{WorldContract, WorldContractReader}; -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn test_world_contract_reader() { let sequencer = TestSequencer::start(SequencerConfig::default(), get_default_test_starknet_config()).await; @@ -51,6 +53,10 @@ pub async fn deploy_world( .await .unwrap() .contract_address; + + // wait for the tx to be mined + tokio::time::sleep(Duration::from_millis(250)).await; + let world_address = strategy .world .unwrap() @@ -70,22 +76,34 @@ pub async fn deploy_world( declare_output.push(res); } + // wait for the tx to be mined + tokio::time::sleep(Duration::from_millis(250)).await; + let _ = WorldContract::new(world_address, &account) .register_components(&declare_output.iter().map(|o| o.class_hash).collect::>()) .await .unwrap(); + // wait for the tx to be mined + tokio::time::sleep(Duration::from_millis(250)).await; + let mut declare_output = vec![]; for system in strategy.systems { let res = system.declare(&account, Default::default()).await.unwrap(); declare_output.push(res); } + // wait for the tx to be mined + tokio::time::sleep(Duration::from_millis(250)).await; + let world = WorldContract::new(world_address, &account); let _ = world .register_systems(&declare_output.iter().map(|o| o.class_hash).collect::>()) .await .unwrap(); + // wait for the tx to be mined + tokio::time::sleep(Duration::from_millis(250)).await; + (world_address, executor_address) } From 692ea4c7c4212e085feb614448281e747aefe641 Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Mon, 4 Sep 2023 13:10:39 +0900 Subject: [PATCH 23/77] chore: remove redundant deps + cleanups (#862) --- Cargo.lock | 45 --------------- Cargo.toml | 5 +- crates/katana/Cargo.toml | 5 -- crates/katana/core/Cargo.toml | 11 ++-- crates/katana/core/src/execution.rs | 90 ----------------------------- crates/katana/rpc/Cargo.toml | 7 +-- crates/sozo/Cargo.toml | 9 +-- crates/torii/core/Cargo.toml | 3 +- crates/torii/graphql/Cargo.toml | 5 +- crates/torii/server/Cargo.toml | 5 +- 10 files changed, 18 insertions(+), 167 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 659fed9945..ccc92a7da4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1588,42 +1588,6 @@ dependencies = [ "windows-sys 0.45.0", ] -[[package]] -name = "console-api" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2895653b4d9f1538a83970077cb01dfc77a4810524e51a110944688e916b18e" -dependencies = [ - "prost", - "prost-types", - "tonic", - "tracing-core", -] - -[[package]] -name = "console-subscriber" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4cf42660ac07fcebed809cfe561dd8730bcd35b075215e6479c516bcd0d11cb" -dependencies = [ - "console-api", - "crossbeam-channel", - "crossbeam-utils", - "futures", - "hdrhistogram", - "humantime", - "prost-types", - "serde", - "serde_json", - "thread_local", - "tokio", - "tokio-stream", - "tonic", - "tracing", - "tracing-core", - "tracing-subscriber", -] - [[package]] name = "const-fnv1a-hash" version = "1.1.0" @@ -3423,10 +3387,7 @@ version = "7.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f19b9f54f7c7f55e31401bb647626ce0cf0f67b0004982ce815b3ee72a02aa8" dependencies = [ - "base64 0.13.1", "byteorder", - "flate2", - "nom", "num-traits 0.2.16", ] @@ -3990,7 +3951,6 @@ dependencies = [ "clap", "clap_complete", "console", - "console-subscriber", "env_logger", "katana-core", "katana-rpc", @@ -4025,7 +3985,6 @@ dependencies = [ "starknet_api", "thiserror", "tokio", - "tokio-stream", "tracing", "url", ] @@ -6611,7 +6570,6 @@ dependencies = [ "signal-hook-registry", "socket2 0.5.3", "tokio-macros", - "tracing", "windows-sys 0.48.0", ] @@ -6805,7 +6763,6 @@ dependencies = [ "dojo-world", "futures-channel", "futures-util", - "log", "once_cell", "serde", "serde_json", @@ -6833,7 +6790,6 @@ dependencies = [ "dojo-types", "dojo-world", "indexmap 1.9.3", - "log", "poem", "serde", "serde_json", @@ -6874,7 +6830,6 @@ dependencies = [ "dojo-types", "dojo-world", "indexmap 1.9.3", - "log", "poem", "scarb", "serde", diff --git a/Cargo.toml b/Cargo.toml index 92073ca483..3b37bd57e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,6 +64,7 @@ indoc = "1.0.7" itertools = "0.10.3" log = "0.4.17" num-bigint = "0.4" +parking_lot = "0.12.1" pretty_assertions = "1.2.1" rayon = "0.9.0" salsa = "0.16.1" @@ -80,9 +81,9 @@ test-log = "0.2.11" thiserror = "1.0.32" tokio = { version = "1.32.0", features = [ "full" ] } toml = "0.7.4" -tracing = "0.1" +tracing = "0.1.34" tracing-subscriber = "0.3.16" -url = "2.2.2" +url = "2.4.0" [patch."https://github.com/starkware-libs/blockifier"] blockifier = { git = "https://github.com/dojoengine/blockifier", rev = "c794d1b" } diff --git a/crates/katana/Cargo.toml b/crates/katana/Cargo.toml index 7f3368d7cc..284b3ee6e8 100644 --- a/crates/katana/Cargo.toml +++ b/crates/katana/Cargo.toml @@ -9,7 +9,6 @@ version.workspace = true [dependencies] clap.workspace = true clap_complete.workspace = true -console-subscriber = "0.1.10" console.workspace = true env_logger.workspace = true katana-core = { path = "core" } @@ -21,7 +20,3 @@ url.workspace = true [dev-dependencies] assert_matches = "1.5.0" - -[[bin]] -name = "katana" -path = "src/main.rs" diff --git a/crates/katana/core/Cargo.toml b/crates/katana/core/Cargo.toml index cde0b2bb2e..a4d8cb5f91 100644 --- a/crates/katana/core/Cargo.toml +++ b/crates/katana/core/Cargo.toml @@ -11,24 +11,23 @@ anyhow.workspace = true async-trait.workspace = true auto_impl = "1.1.0" blockifier.workspace = true -cairo-lang-casm = "2.1.1" -cairo-lang-starknet = "2.1.1" +cairo-lang-casm.workspace = true +cairo-lang-starknet.workspace = true cairo-vm.workspace = true convert_case.workspace = true flate2.workspace = true futures.workspace = true lazy_static = "1.4.0" -parking_lot = "0.12.1" +parking_lot.workspace = true rand = { version = "0.8.5", features = [ "small_rng" ] } serde.workspace = true -serde_json = "1.0.70" +serde_json.workspace = true serde_with.workspace = true starknet.workspace = true starknet_api.workspace = true thiserror.workspace = true -tokio-stream = "0.1.14" tokio.workspace = true -tracing = "0.1.34" +tracing.workspace = true url.workspace = true [dev-dependencies] diff --git a/crates/katana/core/src/execution.rs b/crates/katana/core/src/execution.rs index 70560cb370..855d7de374 100644 --- a/crates/katana/core/src/execution.rs +++ b/crates/katana/core/src/execution.rs @@ -21,7 +21,6 @@ use crate::backend::storage::transaction::{ }; use crate::db::cached::CachedStateWrapper; use crate::db::{Database, StateExt, StateRefDb}; -use crate::env::Env; /// The outcome that after executing a list of transactions. pub struct ExecutionOutcome { @@ -221,95 +220,6 @@ pub struct PendingState { pub executed_transactions: RwLock>, } -/// The executor used when the node is running in interval mode. -pub struct PendingBlockExecutor { - /// The state of the executor. - state: Arc, - /// Determines whether to charge fees for transactions. - charge_fee: bool, - /// The environment variable to execute the transaction on. - env: Arc>, -} - -impl PendingBlockExecutor { - pub fn new( - state: CachedStateWrapper, - env: Arc>, - charge_fee: bool, - ) -> Self { - let state = Arc::new(PendingState { - state: RwLock::new(state), - executed_transactions: Default::default(), - }); - Self { env, state, charge_fee } - } - - pub fn state(&self) -> Arc { - self.state.clone() - } - - /// Resets the executor to a new state - pub fn reset(&mut self, state: StateRefDb) { - self.state.executed_transactions.write().clear(); - *self.state.state.write() = CachedStateWrapper::new(state); - } - - /// Execute all the given transactions sequentially based on their order in the list. - pub fn execute(&self, transactions: Vec) { - let transactions = { - let mut state = self.state.state.write(); - TransactionExecutor::new(&mut state, &self.env.read().block, self.charge_fee) - .with_error_log() - .with_events_log() - .with_resources_log() - .execute_many(transactions.clone()) - .into_iter() - .zip(transactions) - .map(|(res, tx)| match res { - Ok(exec_info) => { - let executed_tx = ExecutedTransaction::new(tx, exec_info); - MaybeInvalidExecutedTransaction::Valid(Arc::new(executed_tx)) - } - - Err(err) => { - let rejected_tx = - RejectedTransaction { inner: tx, execution_error: err.to_string() }; - MaybeInvalidExecutedTransaction::Invalid(Arc::new(rejected_tx)) - } - }) - .collect::>() - }; - - self.state.executed_transactions.write().extend(transactions) - } - - /// Returns the outcome based on the transactions that have been executed thus far. - pub fn outcome(&self) -> ExecutionOutcome { - let state = &mut self.state.state.write(); - - let declared_sierra_classes = state.sierra_class().clone(); - - let state_diff = state.to_state_diff(); - let declared_classes = state_diff - .class_hash_to_compiled_class_hash - .iter() - .map(|(class_hash, _)| { - let contract_class = state - .get_compiled_contract_class(class_hash) - .expect("contract class must exist in state if declared"); - (*class_hash, contract_class) - }) - .collect::>(); - - ExecutionOutcome { - state_diff, - declared_classes, - declared_sierra_classes, - transactions: self.state.executed_transactions.read().clone(), - } - } -} - #[derive(Debug)] pub struct ExecutedTransaction { pub inner: Transaction, diff --git a/crates/katana/rpc/Cargo.toml b/crates/katana/rpc/Cargo.toml index cfb1bb2efd..44801d0d66 100644 --- a/crates/katana/rpc/Cargo.toml +++ b/crates/katana/rpc/Cargo.toml @@ -7,9 +7,9 @@ repository.workspace = true version.workspace = true [dependencies] -anyhow = "1.0.40" +anyhow.workspace = true blockifier.workspace = true -cairo-lang-starknet = "2.1.1" +cairo-lang-starknet.workspace = true cairo-vm.workspace = true flate2.workspace = true futures.workspace = true @@ -31,5 +31,4 @@ tracing.workspace = true [dev-dependencies] assert_matches = "1.5.0" dojo-test-utils = { path = "../../dojo-test-utils" } -starknet.workspace = true -url = "2.3.1" +url.workspace = true diff --git a/crates/sozo/Cargo.toml b/crates/sozo/Cargo.toml index 17412c1ebc..780355a9ef 100644 --- a/crates/sozo/Cargo.toml +++ b/crates/sozo/Cargo.toml @@ -31,18 +31,13 @@ serde_json.workspace = true smol_str.workspace = true starknet.workspace = true thiserror.workspace = true -tokio = { version = "1.15.0", features = [ "full" ] } +tokio.workspace = true torii-client = { path = "../torii/client" } tracing-log = "0.1.3" tracing.workspace = true -url = "2.2.2" +url.workspace = true [dev-dependencies] assert_fs = "1.0.10" dojo-test-utils = { path = "../dojo-test-utils", features = [ "build-examples" ] } snapbox = "0.4.6" -tokio = { version = "1.28.0", features = [ "full" ] } - -[[bin]] -name = "sozo" -path = "src/main.rs" diff --git a/crates/torii/core/Cargo.toml b/crates/torii/core/Cargo.toml index 5cf18c0b77..8df92791ac 100644 --- a/crates/torii/core/Cargo.toml +++ b/crates/torii/core/Cargo.toml @@ -15,15 +15,14 @@ chrono.workspace = true dojo-types = { path = "../../dojo-types" } dojo-world = { path = "../../dojo-world" } -log = "0.4.17" serde.workspace = true serde_json.workspace = true sqlx = { version = "0.6.2", features = [ "chrono", "macros", "offline", "runtime-actix-rustls", "sqlite", "uuid" ] } starknet-crypto.workspace = true starknet.workspace = true -tokio = { version = "1.20.1", features = [ "full" ] } tokio-stream = "0.1.11" tokio-util = "0.7.7" +tokio.workspace = true tracing.workspace = true #Dynamic subscriber diff --git a/crates/torii/graphql/Cargo.toml b/crates/torii/graphql/Cargo.toml index e718bbeafe..2e22538a77 100644 --- a/crates/torii/graphql/Cargo.toml +++ b/crates/torii/graphql/Cargo.toml @@ -16,17 +16,16 @@ async-trait.workspace = true base64 = "0.21.2" chrono.workspace = true indexmap = "1.9.3" -log = "0.4.17" poem = "1.3.48" serde.workspace = true serde_json.workspace = true sqlx = { version = "0.6.2", features = [ "chrono", "macros", "offline", "runtime-actix-rustls", "sqlite", "uuid" ] } -tokio = { version = "1.20.1", features = [ "full" ] } tokio-stream = "0.1.11" tokio-util = "0.7.7" +tokio.workspace = true torii-core = { path = "../core" } tracing.workspace = true -url = "2.2.2" +url.workspace = true [dev-dependencies] camino.workspace = true diff --git a/crates/torii/server/Cargo.toml b/crates/torii/server/Cargo.toml index c1b768e999..bb7768b618 100644 --- a/crates/torii/server/Cargo.toml +++ b/crates/torii/server/Cargo.toml @@ -18,7 +18,6 @@ ctrlc = "3.2.5" dojo-types = { path = "../../dojo-types" } dojo-world = { path = "../../dojo-world" } indexmap = "1.9.3" -log = "0.4.17" poem = "1.3.48" scarb.workspace = true serde.workspace = true @@ -26,15 +25,15 @@ serde_json.workspace = true sqlx = { version = "0.6.2", features = [ "chrono", "macros", "offline", "runtime-actix-rustls", "uuid" ] } starknet-crypto.workspace = true starknet.workspace = true -tokio = { version = "1.20.1", features = [ "full" ] } tokio-stream = "0.1.11" tokio-util = "0.7.7" +tokio.workspace = true torii-core = { path = "../core" } torii-graphql = { path = "../graphql" } torii-grpc = { path = "../grpc" } tracing-subscriber.workspace = true tracing.workspace = true -url = "2.2.2" +url.workspace = true [dev-dependencies] camino.workspace = true From 0e031ccbad6c06c832a0910dca2fc6f784ab6417 Mon Sep 17 00:00:00 2001 From: Loaf <90423308+ponderingdemocritus@users.noreply.github.com> Date: Mon, 4 Sep 2023 15:41:16 +1000 Subject: [PATCH 24/77] v0.0.18 (#863) --- packages/core/bin/generateComponents.cjs | 2 +- packages/core/package.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/bin/generateComponents.cjs b/packages/core/bin/generateComponents.cjs index a48c1d4d30..e78e369606 100644 --- a/packages/core/bin/generateComponents.cjs +++ b/packages/core/bin/generateComponents.cjs @@ -31,7 +31,7 @@ fs.readFile(jsonFilePath, "utf8", (err, jsonString) => { fileContent += ` const name = "${tableName}";\n`; fileContent += ` return defineComponent(\n world,\n {\n`; - component.members.forEach((member) => { + component.members.filter(m => !m.key).forEach((member) => { let memberType = "RecsType.Number"; // Default type set to Number if ( diff --git a/packages/core/package.json b/packages/core/package.json index 6185ff39ba..b4901c81f3 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@dojoengine/core", - "version": "0.0.17", + "version": "0.0.18", "description": "Dojo engine core providers and types", "scripts": { "build": "tsc", @@ -23,4 +23,4 @@ "bin": { "create-components": "./dist/bin/generateComponents.cjs" } -} \ No newline at end of file +} From e09511303a5c8d51dbb644a3d20df7c7f52c7d1d Mon Sep 17 00:00:00 2001 From: Yun Date: Mon, 4 Sep 2023 09:16:56 -0700 Subject: [PATCH 25/77] chore(torii): graphql make component keys filterable (#860) --- crates/torii/core/src/sql.rs | 23 +++++++++---------- .../graphql/src/object/component_state.rs | 2 +- .../graphql/src/tests/components_test.rs | 1 + 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/crates/torii/core/src/sql.rs b/crates/torii/core/src/sql.rs index 0b63b3014d..a2be616cea 100644 --- a/crates/torii/core/src/sql.rs +++ b/crates/torii/core/src/sql.rs @@ -170,10 +170,6 @@ impl State for Sql { ); for member in component.clone().members { - if member.key { - continue; - } - component_table_query.push_str(&format!( "external_{} {}, ", member.name, @@ -253,15 +249,18 @@ impl State for Sql { entity_id, keys_str, component_names ); - let member_results = sqlx::query( - "SELECT * FROM component_members WHERE key == FALSE AND component_id = ? ORDER BY id \ - ASC", - ) - .bind(component.to_lowercase()) - .fetch_all(&self.pool) - .await?; + let member_names_result = + sqlx::query("SELECT * FROM component_members WHERE component_id = ? ORDER BY id ASC") + .bind(component.to_lowercase()) + .fetch_all(&self.pool) + .await?; + + // keys are part of component members, so combine keys and component values array + let mut member_values: Vec = Vec::new(); + member_values.extend(keys); + member_values.extend(values); - let (names_str, values_str) = format_values(member_results, values)?; + let (names_str, values_str) = format_values(member_names_result, member_values)?; let insert_components = format!( "INSERT OR REPLACE INTO external_{} (entity_id {}) VALUES ('{}' {})", component.to_lowercase(), diff --git a/crates/torii/graphql/src/object/component_state.rs b/crates/torii/graphql/src/object/component_state.rs index 6e850cf269..a8986447f6 100644 --- a/crates/torii/graphql/src/object/component_state.rs +++ b/crates/torii/graphql/src/object/component_state.rs @@ -292,7 +292,7 @@ pub async fn type_mapping_query( type AS ty, key, created_at - FROM component_members WHERE key == FALSE AND component_id = ? + FROM component_members WHERE component_id = ? "#, ) .bind(component_id) diff --git a/crates/torii/graphql/src/tests/components_test.rs b/crates/torii/graphql/src/tests/components_test.rs index 019383cdff..333829b486 100644 --- a/crates/torii/graphql/src/tests/components_test.rs +++ b/crates/torii/graphql/src/tests/components_test.rs @@ -74,6 +74,7 @@ mod tests { ("where: { xLT: 42 }", 0), ("where: { xLTE: 42 }", 1), ("where: { x: 1337, yGTE: 1234 }", 0), + (r#"where: { player: "0x2" }"#, 1), // player is a key ]); for (filter, expected_total) in where_filters { From d2fce1a0d0694b164d0923b6626fc81701cb9190 Mon Sep 17 00:00:00 2001 From: Yun Date: Mon, 4 Sep 2023 09:17:55 -0700 Subject: [PATCH 26/77] fix(torii): exact key matching issue (#861) --- crates/torii/core/src/sql.rs | 5 ++--- crates/torii/graphql/src/object/entity.rs | 8 +++++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/crates/torii/core/src/sql.rs b/crates/torii/core/src/sql.rs index a2be616cea..4343bd7a03 100644 --- a/crates/torii/core/src/sql.rs +++ b/crates/torii/core/src/sql.rs @@ -238,11 +238,10 @@ impl State for Sql { .fetch_optional(&self.pool) .await?; - // TODO: map keys to individual columns - let keys_str = keys.iter().map(|k| format!("{:#x}", k)).collect::>().join(","); + let keys_str = keys.iter().map(|k| format!("{:#x}", k)).collect::>().join("/"); let component_names = component_names(entity_result, &component)?; let insert_entities = format!( - "INSERT INTO entities (id, keys, component_names) VALUES ('{}', '{}', '{}') ON \ + "INSERT INTO entities (id, keys, component_names) VALUES ('{}', '{}/', '{}') ON \ CONFLICT(id) DO UPDATE SET component_names=excluded.component_names, updated_at=CURRENT_TIMESTAMP", diff --git a/crates/torii/graphql/src/object/entity.rs b/crates/torii/graphql/src/object/entity.rs index 3a2376c72a..86d688e220 100644 --- a/crates/torii/graphql/src/object/entity.rs +++ b/crates/torii/graphql/src/object/entity.rs @@ -38,9 +38,10 @@ impl Default for EntityObject { } } } + impl EntityObject { pub fn value_mapping(entity: Entity) -> ValueMapping { - let keys: Vec<&str> = entity.keys.split(',').map(|s| s.trim()).collect(); + let keys: Vec<&str> = entity.keys.split('/').filter(|&k| !k.is_empty()).collect(); IndexMap::from([ (Name::new("id"), Value::from(entity.id)), (Name::new("keys"), Value::from(keys)), @@ -172,8 +173,9 @@ async fn entities_by_sk( let mut conditions = Vec::new(); if let Some(keys) = &keys { - conditions.push(format!("keys LIKE '{}%'", keys.join(","))); - count_query.push_str(&format!(" WHERE keys LIKE '{}%'", keys.join(","))); + let keys_str = keys.join("/"); + conditions.push(format!("keys LIKE '{}/%'", keys_str)); + count_query.push_str(&format!(" WHERE keys LIKE '{}/%'", keys_str)); } if let Some(after_cursor) = &args.after { From 31f317e20e541eafc9752847bda0419dd60a5076 Mon Sep 17 00:00:00 2001 From: Tarrence van As Date: Wed, 6 Sep 2023 10:08:32 -0400 Subject: [PATCH 27/77] Update to Cairo 2.2.0 (#831) * Update to Cairo 2.2.0 * Inline macro tests --- .vscode/launch.json | 52 +- Cargo.lock | 613 +++++++++--------- Cargo.toml | 48 +- crates/dojo-core/Scarb.toml | 4 +- crates/dojo-core/src/executor.cairo | 2 +- crates/dojo-core/src/world.cairo | 4 +- crates/dojo-core/src/world_factory.cairo | 2 +- crates/dojo-erc/Scarb.toml | 2 +- crates/dojo-erc/src/erc1155/erc1155.cairo | 2 +- crates/dojo-erc/src/erc1155/systems.cairo | 9 +- crates/dojo-erc/src/erc_common/utils.cairo | 29 - crates/dojo-lang/Cargo.toml | 2 + crates/dojo-lang/src/compiler.rs | 8 +- crates/dojo-lang/src/component.rs | 2 +- crates/dojo-lang/src/db.rs | 43 -- crates/dojo-lang/src/inline_macro_plugin.rs | 104 --- crates/dojo-lang/src/inline_macros/emit.rs | 88 +-- crates/dojo-lang/src/inline_macros/get.rs | 121 ++-- crates/dojo-lang/src/inline_macros/mod.rs | 65 +- crates/dojo-lang/src/inline_macros/set.rs | 99 +-- crates/dojo-lang/src/lib.rs | 4 +- crates/dojo-lang/src/manifest.rs | 11 +- crates/dojo-lang/src/manifest_test.rs | 7 +- ...{manifest_test_crate => example_ecs_crate} | 0 .../dojo-lang/src/manifest_test_data/manifest | 14 +- .../simple_crate/Scarb.toml | 10 + .../simple_crate/src/lib.cairo | 0 crates/dojo-lang/src/plugin.rs | 126 ++-- crates/dojo-lang/src/plugin_test.rs | 42 +- .../src/plugin_test_data/inline_macros | 211 ------ crates/dojo-lang/src/serde.rs | 2 +- .../src/{system/mod.rs => system.rs} | 7 +- .../src/bin/language_server.rs | 4 +- crates/dojo-test-utils/Cargo.toml | 2 + crates/dojo-test-utils/build.rs | 2 +- crates/dojo-test-utils/src/compiler.rs | 2 +- crates/katana/core/Cargo.toml | 4 +- crates/katana/rpc/Cargo.toml | 2 +- crates/sozo/Cargo.toml | 1 + crates/sozo/src/args.rs | 10 +- crates/sozo/src/commands/test.rs | 19 +- crates/sozo/src/main.rs | 2 +- .../sozo/src/ops/migration/migration_test.rs | 2 +- crates/sozo/src/ops/migration/ui.rs | 2 +- .../client/src/contract/component_test.rs | 2 +- crates/torii/client/wasm/yarn.lock | 60 +- crates/torii/core/Cargo.toml | 14 +- examples/ecs/Scarb.toml | 2 +- examples/ecs/src/systems.cairo | 2 +- packages/core/yarn.lock | 16 +- packages/react/yarn.lock | 72 +- 51 files changed, 820 insertions(+), 1133 deletions(-) delete mode 100644 crates/dojo-lang/src/db.rs delete mode 100644 crates/dojo-lang/src/inline_macro_plugin.rs rename crates/dojo-lang/src/manifest_test_data/{manifest_test_crate => example_ecs_crate} (100%) create mode 100644 crates/dojo-lang/src/manifest_test_data/simple_crate/Scarb.toml create mode 100644 crates/dojo-lang/src/manifest_test_data/simple_crate/src/lib.cairo delete mode 100644 crates/dojo-lang/src/plugin_test_data/inline_macros rename crates/dojo-lang/src/{system/mod.rs => system.rs} (95%) diff --git a/.vscode/launch.json b/.vscode/launch.json index 305c8be121..685f659a9c 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -7,61 +7,21 @@ { "type": "lldb", "request": "launch", - "name": "Debug unit tests in library 'dojo-world'", + "name": "Debug unit tests in 'dojo-lang'", "cargo": { "args": [ "test", "--no-run", - "--lib", - "--package=dojo-world" + "--package=dojo-lang", + "--lib" ], "filter": { - "name": "dojo-world", + "name": "dojo-lang", "kind": "lib" } }, - "args": [], - "env": { - "CARGO_MANIFEST_DIR": "${workspaceFolder}/crates/dojo-world" - }, - "cwd": "${workspaceFolder}/crates/dojo-world" - }, - { - "type": "lldb", - "request": "launch", - "name": "Debug executable 'dojo-compile'", - "cargo": { - "args": [ - "build", - "--bin=dojo-compile", - "--package=dojo-lang" - ], - "filter": { - "name": "dojo-compile", - "kind": "bin" - } - }, - "args": ["${workspaceFolder}/crates/dojo-lang/src/cairo_level_tests/component.cairo"], + "args": ["inline_macros::test::expr_semantics"], "cwd": "${workspaceFolder}/crates/dojo-lang" }, - { - "type": "lldb", - "request": "launch", - "name": "Debug unit tests in executable 'dojo-compile'", - "cargo": { - "args": [ - "test", - "--no-run", - "--bin=dojo-compile", - "--package=dojo-lang" - ], - "filter": { - "name": "dojo-compile", - "kind": "bin" - } - }, - "args": [], - "cwd": "${workspaceFolder}/crates/dojo-lang" - } ] -} +} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index ccc92a7da4..bc40bceed5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,9 +14,9 @@ dependencies = [ [[package]] name = "addr2line" -version = "0.20.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" dependencies = [ "gimli", ] @@ -108,24 +108,23 @@ dependencies = [ [[package]] name = "anstream" -version = "0.3.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" +checksum = "b1f58811cfac344940f1a400b6e6231ce35171f614f26439e80f8c1465c5cc0c" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", - "is-terminal", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd" +checksum = "15c4c2c83f81532e5845a733998b6971faca23490340a418e9b72a3ec9de12ea" [[package]] name = "anstyle-parse" @@ -147,9 +146,9 @@ dependencies = [ [[package]] name = "anstyle-wincon" -version = "1.0.2" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c677ab05e09154296dd37acecd46420c17b9713e8366facafa8fc0885167cf4c" +checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd" dependencies = [ "anstyle", "windows-sys 0.48.0", @@ -157,9 +156,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.74" +version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c6f84b74db2535ebae81eede2f39b947dcbf01d093ae5f791e5dd414a1bf289" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" [[package]] name = "arc-swap" @@ -499,7 +498,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -510,7 +509,7 @@ checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -587,9 +586,9 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.68" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" dependencies = [ "addr2line", "cc", @@ -608,9 +607,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.21.2" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" +checksum = "414dcefbc63d77c526a76b3afcf6fbb9b5e2791c19c3aa2297733208750c6e53" [[package]] name = "beef" @@ -767,7 +766,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6798148dccfbff0fae41c7574d2fa8f1ef3492fba0face179de5d8d447d67b05" dependencies = [ "memchr", - "regex-automata 0.3.6", + "regex-automata 0.3.7", "serde", ] @@ -809,9 +808,9 @@ dependencies = [ [[package]] name = "bytesize" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38fcc2979eff34a4b84e1cf9a1e3da42a7d44b3b690a40cdcb23e3d556cfb2e5" +checksum = "a3e368af43e418a04d52505cf3dbc23dda4e3407ae2fa99fd0e4f308ce546acc" [[package]] name = "cairo-felt" @@ -827,9 +826,9 @@ dependencies = [ [[package]] name = "cairo-lang-casm" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "213103e1cf9049abd443f97088939d9cf6ef5500b295003be2b244c702d8fe9c" +checksum = "afc7f7cb89bc3f52c2c738f3e87c8f8773bd3456cae1d322d100d4b0da584f3c" dependencies = [ "cairo-lang-utils", "indoc 2.0.3", @@ -844,9 +843,9 @@ dependencies = [ [[package]] name = "cairo-lang-compiler" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eb857feb6d7a73fd33193a2cc141c04ab345d47bcbd9e2c014ef3422ebc6d55" +checksum = "d4f2c54b065f7fd97bf8d5df76cbcbbd01d8a8c319d281796ee20ecc48e16ca8" dependencies = [ "anyhow", "cairo-lang-defs", @@ -861,6 +860,7 @@ dependencies = [ "cairo-lang-sierra-generator", "cairo-lang-syntax", "cairo-lang-utils", + "itertools 0.11.0", "log", "salsa", "smol_str", @@ -869,18 +869,18 @@ dependencies = [ [[package]] name = "cairo-lang-debug" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af06d0c89bd515707d6f0140a880f6463b955189fa5f97719edd62348c36ee2c" +checksum = "873ba77d4c3f780c727c7d6c738cded22b3f6d4023e30546dfe14f97a087887e" dependencies = [ "cairo-lang-utils", ] [[package]] name = "cairo-lang-defs" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ead2773a4d8147c3c25666d9a97a41161eeb59e0d0d1060b5f9b6fa68d0da1" +checksum = "f5031fff038c27ed43769b73a6f5d41aeaea34df9af862e024c23fbb4f076249" dependencies = [ "cairo-lang-debug", "cairo-lang-diagnostics", @@ -896,9 +896,9 @@ dependencies = [ [[package]] name = "cairo-lang-diagnostics" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6924bc3d558495a5327955da3aec15e6ab71c0f83e955258ffbb0e1a20ead87" +checksum = "7b6cb1492e5784e1076320a5018ce7584f391b2f3b414bc0a8ab7c289fa118ce" dependencies = [ "cairo-lang-debug", "cairo-lang-filesystem", @@ -909,9 +909,9 @@ dependencies = [ [[package]] name = "cairo-lang-eq-solver" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ac8853dae37b4f3b4d3d8d36002b2fb0b52aa5f578f15b0bf47cedd2ec7358b" +checksum = "c35dddbc63b2a4870891cc74498726aa32bfaa518596352f9bb101411cc4c584" dependencies = [ "cairo-lang-utils", "good_lp", @@ -921,9 +921,9 @@ dependencies = [ [[package]] name = "cairo-lang-filesystem" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acb3fc0d5582fd27910e63cca94a7c9d41acc0045a815ff99ed941eb45183375" +checksum = "32ce0b8e66a6085ae157d43b5c162d60166f0027d6f125c50ee74e4dc7916ff6" dependencies = [ "cairo-lang-debug", "cairo-lang-utils", @@ -935,9 +935,9 @@ dependencies = [ [[package]] name = "cairo-lang-formatter" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cfcc3d641766fd931432ae489ca73a8fb7edf83f2535faf6313de99666814e3" +checksum = "79535d235d17f3be2a2d7e82b92709ed4cb60978e8d96c92520178a795162969" dependencies = [ "anyhow", "cairo-lang-diagnostics", @@ -956,9 +956,9 @@ dependencies = [ [[package]] name = "cairo-lang-language-server" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "277390f668e6c21daec570dac01587455bac488455cdc029fc9503cb0c098f3d" +checksum = "e4793c9799857c94ddd8f7146220fd82b4a4402fa8597f49d48b705747f9ee05" dependencies = [ "anyhow", "cairo-lang-compiler", @@ -979,7 +979,7 @@ dependencies = [ "log", "lsp-types", "salsa", - "scarb-metadata 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "scarb-metadata 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde", "serde_json", "smol_str", @@ -989,9 +989,9 @@ dependencies = [ [[package]] name = "cairo-lang-lowering" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7d7f5acc6e4c67a6f2232df1d1f13039453cb50d2971183a1fd254067735cd0" +checksum = "29cc679f501725e03ee703559ed27d084c6f4031bd51ff86378cf845a85ee207" dependencies = [ "cairo-lang-debug", "cairo-lang-defs", @@ -1008,15 +1008,16 @@ dependencies = [ "log", "num-bigint", "num-traits 0.2.16", + "once_cell", "salsa", "smol_str", ] [[package]] name = "cairo-lang-parser" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9716a807fd4430a4af396ad9f7bd1228414c972a24a7e4211ac83d83f538d1" +checksum = "cdcadb046659134466bc7e11961ea8a56969dae8a54d8f985955ce0b95185c7f" dependencies = [ "cairo-lang-diagnostics", "cairo-lang-filesystem", @@ -1035,17 +1036,17 @@ dependencies = [ [[package]] name = "cairo-lang-plugins" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb782377b1cb01ccb3e00aec3903912895ae3cf6b5a8e05918bc0e4a837c1929" +checksum = "4632790cd4ea11d4849934456a400eae7ed419f6d721f24a6b637df67b7e902f" dependencies = [ "cairo-lang-defs", "cairo-lang-diagnostics", "cairo-lang-filesystem", "cairo-lang-parser", - "cairo-lang-semantic", "cairo-lang-syntax", "cairo-lang-utils", + "indent", "indoc 2.0.3", "itertools 0.11.0", "num-bigint", @@ -1055,20 +1056,20 @@ dependencies = [ [[package]] name = "cairo-lang-proc-macros" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4756fe3fdb86d3d8fda40ee250d146300381e98110ec7e4a624407c7e0b7e63f" +checksum = "170838817fc33ddb65e0a9480526df0b226b148a0fca0a5cd7071be4c6683157" dependencies = [ "cairo-lang-debug", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] name = "cairo-lang-project" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "913cdcd5e567d7ca13d1b37822feeac19973f5133a4fbab76b67e12c847bff3a" +checksum = "4162ee976c61fdeb3b621f4a76fd256e46a5c0890f750a3a9d2c9560a3bc1daf" dependencies = [ "cairo-lang-filesystem", "cairo-lang-utils", @@ -1080,9 +1081,9 @@ dependencies = [ [[package]] name = "cairo-lang-runner" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55ce3aa736c0180b5ed5320138b3f8ed70696618f2cb23bc86217eebfa2274ea" +checksum = "11d66ef01350e2e7f7e6b2b43b865da2513a42600082ee1a2975d3af3da7f0ca" dependencies = [ "anyhow", "ark-ff", @@ -1117,15 +1118,16 @@ dependencies = [ [[package]] name = "cairo-lang-semantic" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6962d05da3c355f8fc49e97dda4252dfa71969399ef5658130b9cdc86d8a805" +checksum = "13e544fa9a222bf2d007df2b5fc9b21c2a20ab7e17d6fefbcbc193de209451cd" dependencies = [ "cairo-lang-debug", "cairo-lang-defs", "cairo-lang-diagnostics", "cairo-lang-filesystem", "cairo-lang-parser", + "cairo-lang-plugins", "cairo-lang-proc-macros", "cairo-lang-syntax", "cairo-lang-utils", @@ -1134,15 +1136,16 @@ dependencies = [ "log", "num-bigint", "num-traits 0.2.16", + "once_cell", "salsa", "smol_str", ] [[package]] name = "cairo-lang-sierra" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "872cf03415aa48c7757e4cee4b0223a158fcc9abddf55574f6c387324f25cc1f" +checksum = "d5e136b79e95a14ef38a2be91a67ceb85317407d336a5b0d418c33b23c78596a" dependencies = [ "cairo-lang-utils", "const-fnv1a-hash", @@ -1163,9 +1166,9 @@ dependencies = [ [[package]] name = "cairo-lang-sierra-ap-change" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eba2fbd5f13210a46c2050fe156776490c556f6f0335401bda810640c1e65fd" +checksum = "511ca7708faa7ba8d14ae26e1d60ead2d02028c8f664baf5ecb0fd6a0d1e20f6" dependencies = [ "cairo-lang-eq-solver", "cairo-lang-sierra", @@ -1177,9 +1180,9 @@ dependencies = [ [[package]] name = "cairo-lang-sierra-gas" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9f4cec68f861b86dd6429592d9c0c535cbb983a9b7c21fc906b659b35e7882e" +checksum = "351a25bc010b910919c01d5c57e937b0c3d330fc30d92702c0cb4061819df8df" dependencies = [ "cairo-lang-eq-solver", "cairo-lang-sierra", @@ -1191,9 +1194,9 @@ dependencies = [ [[package]] name = "cairo-lang-sierra-generator" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65f050d6c717090ee10be52950cfe639709af27138eb9cd4122b0ab171a5cf2a" +checksum = "114091bb971c06fd072aca816af1c3f62566cd8a4b1453c786155161a36c7bce" dependencies = [ "cairo-lang-debug", "cairo-lang-defs", @@ -1211,15 +1214,16 @@ dependencies = [ "indexmap 2.0.0", "itertools 0.11.0", "num-bigint", + "once_cell", "salsa", "smol_str", ] [[package]] name = "cairo-lang-sierra-to-casm" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58c53fa4c6013827c42c1e02ee9a58fa5901cb18a711bdfeb1af379f69d2055c" +checksum = "fa1c799de62972dfd7112d563000695be94305b6f7d9bedd29f347799bf03e1c" dependencies = [ "assert_matches", "cairo-felt", @@ -1239,9 +1243,9 @@ dependencies = [ [[package]] name = "cairo-lang-sierra-type-size" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07a5e70b5a5826edeb61ec375886847f51e1b995709e3f36d657844fbd703d45" +checksum = "d2fe73d9d58aaf9088f6ba802bcf43ce9ca4bd198190cf5bf91caa7d408dd11a" dependencies = [ "cairo-lang-sierra", "cairo-lang-utils", @@ -1249,9 +1253,9 @@ dependencies = [ [[package]] name = "cairo-lang-starknet" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfaa6629cd5a9cc13543d59bd5494fc01cc4118efbcc5b13528be4539a35f77f" +checksum = "75df624e71e33a31a924e799dd2a9a8284204b41d8db9c51803317bd9edff81f" dependencies = [ "anyhow", "cairo-felt", @@ -1290,9 +1294,9 @@ dependencies = [ [[package]] name = "cairo-lang-syntax" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b23b312f07e45bc0bb2d240187a37db2816393c285896c9ab453c8ca8e128d25" +checksum = "0b1af0ae21f9e539f97cfdf56f5ce0934dae5d87f568fd778c3d624a102f8dbb" dependencies = [ "cairo-lang-debug", "cairo-lang-filesystem", @@ -1307,9 +1311,9 @@ dependencies = [ [[package]] name = "cairo-lang-syntax-codegen" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0225e4b5f09523bc424fae216ea899cb8f9132fb0da164096cee5e2ce79076fe" +checksum = "822ffabf24f6a5506262edcece315260a82d9dfba3abe6548791a6d654563ad0" dependencies = [ "genco", "xshell", @@ -1317,9 +1321,9 @@ dependencies = [ [[package]] name = "cairo-lang-test-runner" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e991757d316fe1af4d99df1ebfba666c713284f857ef2b282c5f4077236182f6" +checksum = "43323524d8bcae023e66c9c6f3e857ae73cddc86bb2cc7ea6d739ef54beacb19" dependencies = [ "anyhow", "cairo-felt", @@ -1352,20 +1356,21 @@ dependencies = [ [[package]] name = "cairo-lang-test-utils" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a612e186c7eddc8f82951c6ffef7c4fdf7ee9b0a5cb7a46f06bae990fe717574" +checksum = "81394b73748176c2485cc8e25be8163ab883f7076efb261a50b0d2fdf7d41fb8" dependencies = [ "cairo-lang-utils", + "colored", "log", "pretty_assertions", ] [[package]] name = "cairo-lang-utils" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2b4bdb1d6509e2579d04d1757070f88c2542ff033194f749772669a1615c7e4" +checksum = "f974b6e859f0b09c0f13ec8188c96e9e8bbb5da04214f911dbb5bcda67cb812b" dependencies = [ "env_logger", "indexmap 2.0.0", @@ -1377,7 +1382,7 @@ dependencies = [ "parity-scale-codec", "schemars", "serde", - "time 0.3.25", + "time 0.3.28", ] [[package]] @@ -1443,9 +1448,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.82" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "305fe645edc1442a0fa8b6726ba61d422798d37a52e12eaecf4b022ebbb88f01" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" dependencies = [ "jobserver", "libc", @@ -1485,9 +1490,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.3.21" +version = "4.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c27cdf28c0f604ba3f512b0c9a409f8de8513e4816705deb0498b627e7c3a3fd" +checksum = "7c8d502cbaec4595d2e7d5f61e318f05417bd2b66fdc3809498f0d3fdf0bea27" dependencies = [ "clap_builder", "clap_derive", @@ -1506,9 +1511,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.3.21" +version = "4.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08a9f1ab5e9f01a9b81f202e8562eb9a10de70abf9eaeac1be465c28b75aa4aa" +checksum = "5891c7bc0edb3e1c2204fc5e94009affabeb1821c9e5fdc3959536c5c0bb984d" dependencies = [ "anstream", "anstyle", @@ -1518,30 +1523,30 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.3.2" +version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fc443334c81a804575546c5a8a79b4913b50e28d69232903604cada1de817ce" +checksum = "586a385f7ef2f8b4d86bddaa0c094794e7ccbfe5ffef1f434fe928143fc783a5" dependencies = [ "clap", ] [[package]] name = "clap_derive" -version = "4.3.12" +version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54a9bb5758fc5dfe728d1019941681eccaf0cf8a4189b692a0ee2f2ecf90a050" +checksum = "c9fd1a5729c4548118d7d70ff234a44868d00489a4b6597b0b020918a0e91a1a" dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] name = "clap_lex" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" +checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" [[package]] name = "clru" @@ -1661,7 +1666,7 @@ dependencies = [ [[package]] name = "create-output-dir" version = "1.0.0" -source = "git+https://github.com/software-mansion/scarb?rev=c07fa61#c07fa61553985f045286166d0235e55492694159" +source = "git+https://github.com/software-mansion/scarb?rev=7adb7fd#7adb7fd972e4ec95dc404650dec78099815392c9" dependencies = [ "anyhow", "core-foundation", @@ -1770,7 +1775,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f34ba9a9bcb8645379e9de8cb3ecfcf4d1c85ba66d90deb3259206fa5aa193b" dependencies = [ "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -1837,7 +1842,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -1859,14 +1864,14 @@ checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" dependencies = [ "darling_core 0.20.3", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] name = "dashmap" -version = "5.5.0" +version = "5.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6943ae99c34386c84a470c499d3414f66502a41340aa895406e0d2e4a207b91d" +checksum = "edd72493923899c6f10c641bdbdeddc7183d6396641d99c1a0d1597f37f92e28" dependencies = [ "cfg-if", "hashbrown 0.14.0", @@ -1899,9 +1904,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7684a49fb1af197853ef7b2ee694bc1f5b4179556f1e5710e1760c5db6f5e929" +checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" dependencies = [ "serde", ] @@ -2057,6 +2062,7 @@ dependencies = [ "anyhow", "assert_fs", "cairo-lang-compiler", + "cairo-lang-debug", "cairo-lang-defs", "cairo-lang-diagnostics", "cairo-lang-filesystem", @@ -2079,6 +2085,7 @@ dependencies = [ "env_logger", "indoc 1.0.9", "itertools 0.10.5", + "once_cell", "pretty_assertions", "salsa", "sanitizer", @@ -2141,6 +2148,7 @@ dependencies = [ "katana-core", "katana-rpc", "scarb", + "scarb-ui", "serde", "serde_json", "serde_with", @@ -2207,9 +2215,9 @@ checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" [[package]] name = "dyn-clone" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "304e6508efa593091e97a9abbc10f90aa7ca635b6d2784feff3c89d41dd12272" +checksum = "bbfc4744c1b8f2a09adc0e55242f60b1af195d88596bd8700be74418c056c555" [[package]] name = "either" @@ -2237,9 +2245,9 @@ checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" [[package]] name = "encoding_rs" -version = "0.8.32" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" dependencies = [ "cfg-if", ] @@ -2348,6 +2356,15 @@ dependencies = [ "ascii_utils", ] +[[package]] +name = "faster-hex" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9042d281a5eec0f2387f8c3ea6c4514e2cf2732c90a85aaf383b761ee3b290d" +dependencies = [ + "serde", +] + [[package]] name = "fastrand" version = "2.0.0" @@ -2504,7 +2521,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -2584,9 +2601,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.27.3" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" +checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" [[package]] name = "gix" @@ -2685,9 +2702,9 @@ dependencies = [ [[package]] name = "gix-bitmap" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0aa8bbde7551a9e3e783a2871f53bbb0f50aac7a77db5680c8709f69e8ce724f" +checksum = "0ccab4bc576844ddb51b78d81b4a42d73e6229660fa614dfc3d3999c874d1959" dependencies = [ "thiserror", ] @@ -2703,9 +2720,9 @@ dependencies = [ [[package]] name = "gix-command" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2783ad148fb16bf9cfd46423706ba552a62a4d4a18fda5dd07648eb0228862dd" +checksum = "0f28f654184b5f725c5737c7e4f466cbd8f0102ac352d5257eeab19647ee4256" dependencies = [ "bstr", ] @@ -2777,14 +2794,14 @@ dependencies = [ [[package]] name = "gix-date" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4f7c76578a69b736c3f0770f14757e9027354011d24c56d79207add9d7d1be6" +checksum = "01e476b4e156f6044d35bf1ce2079d97b7207515cfb5a2bb6fcd489bb697d700" dependencies = [ "bstr", "itoa", "thiserror", - "time 0.3.25", + "time 0.3.28", ] [[package]] @@ -3037,12 +3054,12 @@ dependencies = [ [[package]] name = "gix-packetline-blocking" -version = "0.16.4" +version = "0.16.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20276373def40fc3be7a86d09e1bb607d33dd6bf83e3504e83cd594e51438667" +checksum = "e39142400d3faa7057680ed3947c3b70e46b6a0b16a7c242ec8f0249e37518ba" dependencies = [ "bstr", - "hex", + "faster-hex", "thiserror", ] @@ -3074,9 +3091,9 @@ dependencies = [ [[package]] name = "gix-quote" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfd80d3d0c733508df9449b1d3795da36083807e31d851d7d61d29af13bd4b0a" +checksum = "475c86a97dd0127ba4465fbb239abac9ea10e68301470c9791a6dd5351cdc905" dependencies = [ "bstr", "btoi", @@ -3302,9 +3319,9 @@ dependencies = [ [[package]] name = "good_lp" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "473d618f0f2c16d1ecb73bc755b207150665fd6b3e5b9570313cee6bba3880db" +checksum = "fa7f3b0e0de4e671b6ffc1274b153a9394cb58bf04ee67505b0cb9915513115f" dependencies = [ "fnv", "minilp", @@ -3312,9 +3329,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.20" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049" +checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833" dependencies = [ "bytes", "fnv", @@ -3847,9 +3864,9 @@ dependencies = [ [[package]] name = "jsonrpsee" -version = "0.16.2" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d291e3a5818a2384645fd9756362e6d89cf0541b0b916fa7702ea4a9833608e" +checksum = "367a292944c07385839818bb71c8d76611138e2dedb0677d035b8da21d29c78b" dependencies = [ "jsonrpsee-core", "jsonrpsee-proc-macros", @@ -3860,9 +3877,9 @@ dependencies = [ [[package]] name = "jsonrpsee-core" -version = "0.16.2" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4e70b4439a751a5de7dd5ed55eacff78ebf4ffe0fc009cb1ebb11417f5b536b" +checksum = "2b5dde66c53d6dcdc8caea1874a45632ec0fcf5b437789f1e45766a1512ce803" dependencies = [ "anyhow", "arrayvec", @@ -3886,9 +3903,9 @@ dependencies = [ [[package]] name = "jsonrpsee-proc-macros" -version = "0.16.2" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baa6da1e4199c10d7b1d0a6e5e8bd8e55f351163b6f4b3cbb044672a69bd4c1c" +checksum = "44e8ab85614a08792b9bff6c8feee23be78c98d0182d4c622c05256ab553892a" dependencies = [ "heck 0.4.1", "proc-macro-crate", @@ -3899,9 +3916,9 @@ dependencies = [ [[package]] name = "jsonrpsee-server" -version = "0.16.2" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fb69dad85df79527c019659a992498d03f8495390496da2f07e6c24c2b356fc" +checksum = "cf4d945a6008c9b03db3354fb3c83ee02d2faa9f2e755ec1dfb69c3551b8f4ba" dependencies = [ "futures-channel", "futures-util", @@ -3921,9 +3938,9 @@ dependencies = [ [[package]] name = "jsonrpsee-types" -version = "0.16.2" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bd522fe1ce3702fd94812965d7bb7a3364b1c9aba743944c5a00529aae80f8c" +checksum = "245ba8e5aa633dd1c1e4fae72bce06e71f42d34c14a2767c6b4d173b57bee5e5" dependencies = [ "anyhow", "beef", @@ -4052,7 +4069,7 @@ dependencies = [ "petgraph", "pico-args", "regex", - "regex-syntax 0.7.4", + "regex-syntax 0.7.5", "string_cache", "term", "tiny-keccak", @@ -4085,9 +4102,9 @@ checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" [[package]] name = "libmimalloc-sys" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4ac0e912c8ef1b735e92369695618dc5b1819f5a7bf3f167301a3ba1cea515e" +checksum = "25d058a81af0d1c22d7a1c948576bee6d673f7af3c0f35564abd6c81122f513d" dependencies = [ "cc", "libc", @@ -4189,9 +4206,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "76fc44e2588d5b436dbc3c6cf62aef290f90dab6235744a93dfe1cc18f451e2c" [[package]] name = "memmap2" @@ -4213,9 +4230,9 @@ dependencies = [ [[package]] name = "mimalloc" -version = "0.1.37" +version = "0.1.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e2894987a3459f3ffb755608bd82188f8ed00d0ae077f1edea29c068d639d98" +checksum = "972e5f23f6716f62665760b0f4cbf592576a80c7b879ba9beaafc0e558894127" dependencies = [ "libmimalloc-sys", ] @@ -4323,14 +4340,13 @@ checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" [[package]] name = "nix" -version = "0.26.2" +version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" +checksum = "abbbc55ad7b13aac85f9401c796dcda1b864e07fcad40ad47792eaa8932ea502" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.4.0", "cfg-if", "libc", - "static_assertions", ] [[package]] @@ -4361,9 +4377,9 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" dependencies = [ "autocfg", "num-integer", @@ -4464,9 +4480,9 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" [[package]] name = "object" -version = "0.31.1" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" +checksum = "77ac5bbd07aea88c60a577a1ce218075ffd59208b2d7ca97adf9bfc5aeb21ebe" dependencies = [ "memchr", ] @@ -4597,7 +4613,7 @@ dependencies = [ "libc", "redox_syscall 0.3.5", "smallvec", - "windows-targets 0.48.2", + "windows-targets 0.48.5", ] [[package]] @@ -4675,7 +4691,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -4691,12 +4707,12 @@ dependencies = [ [[package]] name = "petgraph" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dd7d28ee937e54fe3080c91faa1c3a46c06de6252988a7f4592ba2310ef22a4" +checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" dependencies = [ "fixedbitset", - "indexmap 1.9.3", + "indexmap 2.0.0", ] [[package]] @@ -4729,7 +4745,7 @@ dependencies = [ "phf_shared 0.11.2", "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -4793,14 +4809,14 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] name = "pin-project-lite" -version = "0.2.12" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12cc1b0bf1727a77a54b6654e7b5f1af8604923edc8b81885f8ec92f9e3f0a05" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" [[package]] name = "pin-utils" @@ -4821,7 +4837,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0d92c532a37a9e98c0e9a0411e6852b8acccf9ec07d5e6e450b01cbf947d90b" dependencies = [ "async-trait", - "base64 0.21.2", + "base64 0.21.3", "bytes", "futures-util", "headers", @@ -4854,14 +4870,14 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] name = "portable-atomic" -version = "1.4.2" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f32154ba0af3a075eefa1eda8bb414ee928f62303a54ea85b8d6638ff1a6ee9e" +checksum = "31114a898e107c51bb1609ffaf55a0e011cf6a4d7f1170d0015a165082c0338b" [[package]] name = "ppv-lite86" @@ -4981,9 +4997,9 @@ dependencies = [ [[package]] name = "prodash" -version = "25.0.1" +version = "25.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c236e70b7f9b9ea00d33c69f63ec1ae6e9ae96118923cd37bd4e9c7396f0b107" +checksum = "1d67eb4220992a4a052a4bb03cf776e493ecb1a3a36bab551804153d63486af7" dependencies = [ "bytesize", "human_format", @@ -5054,9 +5070,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.32" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] @@ -5156,14 +5172,14 @@ dependencies = [ [[package]] name = "regex" -version = "1.9.3" +version = "1.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81bc1d4caf89fac26a70747fe603c130093b53c773888797a6329091246d651a" +checksum = "12de2eff854e5fa4b1295edd650e227e9d8fb0c9e90b12e7f36d6a6811791a29" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.3.6", - "regex-syntax 0.7.4", + "regex-automata 0.3.7", + "regex-syntax 0.7.5", ] [[package]] @@ -5177,13 +5193,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed1ceff11a1dddaee50c9dc8e4938bd106e9d89ae372f192311e7da498e3b69" +checksum = "49530408a136e16e5b486e883fbb6ba058e8e4e8ae6621a77b048b314336e629" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.7.4", + "regex-syntax 0.7.5", ] [[package]] @@ -5206,23 +5222,23 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" [[package]] name = "relative-path" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bf2521270932c3c7bed1a59151222bd7643c79310f2916f01925e1e16255698" +checksum = "c707298afce11da2efef2f600116fa93ffa7a032b5d7b628aa17711ec81383ca" [[package]] name = "reqwest" -version = "0.11.18" +version = "0.11.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" +checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1" dependencies = [ - "base64 0.21.2", + "base64 0.21.3", "bytes", "encoding_rs", "futures-core", @@ -5251,7 +5267,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots", + "webpki-roots 0.25.2", "winreg", ] @@ -5328,9 +5344,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.7" +version = "0.38.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "172891ebdceb05aa0005f533a6cbfca599ddd7d966f6f5d4d9b2e70478e70399" +checksum = "9bfe0f2582b4931a45d1fa608f8a8722e8b3c7ac54dd6d5f3b3212791fedef49" dependencies = [ "bitflags 2.4.0", "errno", @@ -5369,14 +5385,14 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" dependencies = [ - "base64 0.21.2", + "base64 0.21.3", ] [[package]] name = "rustls-webpki" -version = "0.101.3" +version = "0.101.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "261e9e0888cba427c3316e6322805653c9425240b6fd96cee7cb671ab70ab8d0" +checksum = "7d93931baf2d282fff8d3a532bbfd7653f734643161b87e3e01e59a04439bf0d" dependencies = [ "ring", "untrusted", @@ -5467,12 +5483,13 @@ dependencies = [ [[package]] name = "scarb" -version = "0.6.2" -source = "git+https://github.com/software-mansion/scarb?rev=c07fa61#c07fa61553985f045286166d0235e55492694159" +version = "0.7.0" +source = "git+https://github.com/software-mansion/scarb?rev=7adb7fd#7adb7fd972e4ec95dc404650dec78099815392c9" dependencies = [ "anyhow", "async-trait", "cairo-lang-compiler", + "cairo-lang-defs", "cairo-lang-filesystem", "cairo-lang-formatter", "cairo-lang-semantic", @@ -5482,7 +5499,6 @@ dependencies = [ "camino", "clap", "clap-verbosity-flag", - "console", "create-output-dir", "data-encoding", "deno_task_shell", @@ -5496,14 +5512,14 @@ dependencies = [ "glob", "ignore", "include_dir", - "indicatif", "indoc 2.0.3", "itertools 0.11.0", "once_cell", "pathdiff", "petgraph", "scarb-build-metadata", - "scarb-metadata 1.6.0 (git+https://github.com/software-mansion/scarb?rev=c07fa61)", + "scarb-metadata 1.7.0 (git+https://github.com/software-mansion/scarb?rev=7adb7fd)", + "scarb-ui", "semver", "serde", "serde-value", @@ -5514,7 +5530,6 @@ dependencies = [ "toml", "toml_edit", "tracing", - "tracing-futures", "tracing-log", "tracing-subscriber", "typed-builder", @@ -5527,17 +5542,17 @@ dependencies = [ [[package]] name = "scarb-build-metadata" -version = "0.6.2" -source = "git+https://github.com/software-mansion/scarb?rev=c07fa61#c07fa61553985f045286166d0235e55492694159" +version = "0.7.0" +source = "git+https://github.com/software-mansion/scarb?rev=7adb7fd#7adb7fd972e4ec95dc404650dec78099815392c9" dependencies = [ "cargo_metadata", ] [[package]] name = "scarb-metadata" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60d9d2ce9f8c720c096da2f6c0d08fa6ce3123fef5d72c79ce2d0c9491f92cc" +checksum = "1e1bb385b852343276fdfc1aad0e4c48e0c01106c209a602481f36c9b50c5f59" dependencies = [ "camino", "semver", @@ -5548,11 +5563,10 @@ dependencies = [ [[package]] name = "scarb-metadata" -version = "1.6.0" -source = "git+https://github.com/software-mansion/scarb?rev=c07fa61#c07fa61553985f045286166d0235e55492694159" +version = "1.7.0" +source = "git+https://github.com/software-mansion/scarb?rev=7adb7fd#7adb7fd972e4ec95dc404650dec78099815392c9" dependencies = [ "camino", - "clap", "derive_builder", "semver", "serde", @@ -5560,6 +5574,22 @@ dependencies = [ "thiserror", ] +[[package]] +name = "scarb-ui" +version = "0.7.0" +source = "git+https://github.com/software-mansion/scarb?rev=7adb7fd#7adb7fd972e4ec95dc404650dec78099815392c9" +dependencies = [ + "anyhow", + "camino", + "clap", + "console", + "indicatif", + "indoc 2.0.3", + "scarb-metadata 1.7.0 (git+https://github.com/software-mansion/scarb?rev=7adb7fd)", + "serde", + "serde_json", +] + [[package]] name = "schemars" version = "0.8.12" @@ -5624,9 +5654,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.183" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32ac8da02677876d532745a130fc9d8e6edfa81a269b107c5b00829b91d8eb3c" +checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" dependencies = [ "serde_derive", ] @@ -5643,13 +5673,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.183" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aafe972d60b0b9bee71a91b92fee2d4fb3c9d7e8f6b179aa99f27203d99a4816" +checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -5665,9 +5695,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.104" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "076066c5f1078eac5b722a31827a8832fe108bed65dfa75e233c89f8206e976c" +checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" dependencies = [ "itoa", "ryu", @@ -5693,7 +5723,7 @@ checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -5730,7 +5760,7 @@ dependencies = [ "serde", "serde_json", "serde_with_macros", - "time 0.3.25", + "time 0.3.28", ] [[package]] @@ -5742,7 +5772,7 @@ dependencies = [ "darling 0.20.3", "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -5798,9 +5828,9 @@ dependencies = [ [[package]] name = "sharded-slab" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +checksum = "b6805d8ff0f66aa61fb79a97a51ba210dcae753a797336dea8a36a3168196fab" dependencies = [ "lazy_static", ] @@ -5832,15 +5862,15 @@ checksum = "420acb44afdae038210c99e69aae24109f32f15500aa708e81d46c9f29d55fcf" [[package]] name = "siphasher" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" [[package]] name = "slab" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] @@ -5862,9 +5892,9 @@ dependencies = [ [[package]] name = "snapbox" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6bccd62078347f89a914e3004d94582e13824d4e3d8a816317862884c423835" +checksum = "ad90eb3a2e3a8031d636d45bd4832751aefd58a291b553f7305a2bacae21aff3" dependencies = [ "anstream", "anstyle", @@ -5875,9 +5905,9 @@ dependencies = [ [[package]] name = "snapbox-macros" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaaf09df9f0eeae82be96290918520214530e738a7fe5a351b0f24cf77c0ca31" +checksum = "95f4ffd811b87da98d0e48285134b7847954bd76e843bb794a893b47ca3ee325" dependencies = [ "anstream", ] @@ -5943,6 +5973,7 @@ dependencies = [ "dojo-world", "log", "scarb", + "scarb-ui", "semver", "serde", "serde_json", @@ -6050,7 +6081,7 @@ dependencies = [ "tokio-stream", "url", "uuid 1.4.1", - "webpki-roots", + "webpki-roots 0.22.6", ] [[package]] @@ -6136,7 +6167,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91f89c79b641618de8aa9668d74c6b6634659ceca311c6318a35c025f9d4d969" dependencies = [ - "base64 0.21.2", + "base64 0.21.3", "flate2", "hex", "serde", @@ -6196,7 +6227,7 @@ checksum = "af6527b845423542c8a16e060ea1bc43f67229848e7cd4c4d80be994a84220ce" dependencies = [ "starknet-curve 0.4.0", "starknet-ff", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -6239,7 +6270,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28a5865ee0ed22ade86bdf45e7c09c5641f1c59ccae12c21ecde535b2b6bf64a" dependencies = [ "starknet-core", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -6368,9 +6399,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.28" +version = "2.0.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" +checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a" dependencies = [ "proc-macro2", "quote", @@ -6391,9 +6422,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.7.1" +version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc02fddf48964c42031a0b3fe0428320ecf3a73c401040fc0096f97794310651" +checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" dependencies = [ "cfg-if", "fastrand", @@ -6441,22 +6472,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.46" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9207952ae1a003f42d3d5e892dac3c6ba42aa6ac0c79a6a91a2b5cb4253e75c" +checksum = "97a802ec30afc17eee47b2855fc72e0c4cd62be9b4efe6591edde0ec5bd68d8f" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.46" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1728216d3244de4f14f14f8c15c79be1a7c67867d28d69b719690e2a19fb445" +checksum = "6bb623b56e39ab7dcd4b1b98bb6c8f8d907ed255b18de254088016b27a8ee19b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -6502,9 +6533,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.25" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fdd63d58b18d663fbdf70e049f00a22c8e42be082203be7f26589213cd75ea" +checksum = "17f6bb557fd245c28e6411aa56b6403c689ad95061f50e4be16c274e70a17e48" dependencies = [ "deranged", "itoa", @@ -6523,9 +6554,9 @@ checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" [[package]] name = "time-macros" -version = "0.2.11" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb71511c991639bb078fd5bf97757e03914361c48100d52878b8e52b46fb92cd" +checksum = "1a942f44339478ef67935ab2bbaec2fb0322496cf3cbe84b261e06ac3814c572" dependencies = [ "time-core", ] @@ -6591,7 +6622,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -6695,7 +6726,7 @@ checksum = "3082666a3a6433f7f511c7192923fa1fe07c69332d3c6a2e6bb040b569199d5a" dependencies = [ "async-trait", "axum", - "base64 0.21.2", + "base64 0.21.3", "bytes", "futures-core", "futures-util", @@ -6763,6 +6794,7 @@ dependencies = [ "dojo-world", "futures-channel", "futures-util", + "log", "once_cell", "serde", "serde_json", @@ -6784,7 +6816,7 @@ dependencies = [ "async-graphql", "async-graphql-poem", "async-trait", - "base64 0.21.2", + "base64 0.21.3", "camino", "chrono", "dojo-types", @@ -6822,7 +6854,7 @@ dependencies = [ "async-graphql", "async-graphql-poem", "async-trait", - "base64 0.21.2", + "base64 0.21.3", "camino", "chrono", "clap", @@ -6876,7 +6908,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55ae70283aba8d2a8b411c695c437fe25b8b5e44e23e780662002fc72fb47a82" dependencies = [ "async-compression", - "base64 0.21.2", + "base64 0.21.3", "bitflags 2.4.0", "bytes", "futures-core", @@ -6966,7 +6998,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -6979,16 +7011,6 @@ dependencies = [ "valuable", ] -[[package]] -name = "tracing-futures" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" -dependencies = [ - "pin-project", - "tracing", -] - [[package]] name = "tracing-log" version = "0.1.3" @@ -7060,7 +7082,7 @@ checksum = "29a3151c41d0b13e3d011f98adc24434560ef06673a155a6c7f66b9879eecce2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -7107,18 +7129,18 @@ dependencies = [ [[package]] name = "unescaper" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "995483205de764db1185c9461a000fff73fa4b9ee2bbe4c8b4027a94692700fe" +checksum = "a96a44ae11e25afb520af4534fd7b0bd8cd613e35a78def813b8cf41631fa3c8" dependencies = [ "thiserror", ] [[package]] name = "unicase" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" dependencies = [ "version_check", ] @@ -7182,9 +7204,9 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "url" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" +checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" dependencies = [ "form_urlencoded", "idna", @@ -7293,7 +7315,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", "wasm-bindgen-shared", ] @@ -7327,7 +7349,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -7367,6 +7389,12 @@ dependencies = [ "webpki", ] +[[package]] +name = "webpki-roots" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" + [[package]] name = "which" version = "4.4.0" @@ -7415,7 +7443,7 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" dependencies = [ - "windows-targets 0.48.2", + "windows-targets 0.48.5", ] [[package]] @@ -7433,7 +7461,7 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets 0.48.2", + "windows-targets 0.48.5", ] [[package]] @@ -7453,17 +7481,17 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.48.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1eeca1c172a285ee6c2c84c341ccea837e7c01b12fbb2d0fe3c9e550ce49ec8" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm 0.48.2", - "windows_aarch64_msvc 0.48.2", - "windows_i686_gnu 0.48.2", - "windows_i686_msvc 0.48.2", - "windows_x86_64_gnu 0.48.2", - "windows_x86_64_gnullvm 0.48.2", - "windows_x86_64_msvc 0.48.2", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", ] [[package]] @@ -7474,9 +7502,9 @@ checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.48.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b10d0c968ba7f6166195e13d593af609ec2e3d24f916f081690695cf5eaffb2f" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_msvc" @@ -7486,9 +7514,9 @@ checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] name = "windows_aarch64_msvc" -version = "0.48.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "571d8d4e62f26d4932099a9efe89660e8bd5087775a2ab5cdd8b747b811f1058" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_i686_gnu" @@ -7498,9 +7526,9 @@ checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[package]] name = "windows_i686_gnu" -version = "0.48.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2229ad223e178db5fbbc8bd8d3835e51e566b8474bfca58d2e6150c48bb723cd" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_msvc" @@ -7510,9 +7538,9 @@ checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] name = "windows_i686_msvc" -version = "0.48.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "600956e2d840c194eedfc5d18f8242bc2e17c7775b6684488af3a9fff6fe3287" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_x86_64_gnu" @@ -7522,9 +7550,9 @@ checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" [[package]] name = "windows_x86_64_gnu" -version = "0.48.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea99ff3f8b49fb7a8e0d305e5aec485bd068c2ba691b6e277d29eaeac945868a" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnullvm" @@ -7534,9 +7562,9 @@ checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[package]] name = "windows_x86_64_gnullvm" -version = "0.48.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f1a05a1ece9a7a0d5a7ccf30ba2c33e3a61a30e042ffd247567d1de1d94120d" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_msvc" @@ -7546,26 +7574,27 @@ checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] name = "windows_x86_64_msvc" -version = "0.48.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d419259aba16b663966e29e6d7c6ecfa0bb8425818bb96f6f1f3c3eb71a6e7b9" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "winnow" -version = "0.5.11" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e461589e194280efaa97236b73623445efa195aa633fd7004f39805707a9d53" +checksum = "7c2e3184b9c4e92ad5167ca73039d0c42476302ab603e2fec4487511f38ccefc" dependencies = [ "memchr", ] [[package]] name = "winreg" -version = "0.10.1" +version = "0.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ - "winapi", + "cfg-if", + "windows-sys 0.48.0", ] [[package]] @@ -7621,7 +7650,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 3b37bd57e3..4885f55705 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,30 +25,30 @@ repository = "https://github.com/dojoengine/dojo/" version = "0.2.1" [workspace.dependencies] -anyhow = "1.0.66" +anyhow = "1.0.75" async-trait = "0.1.68" blockifier = { git = "https://github.com/starkware-libs/blockifier" } -cairo-lang-casm = "2.1.1" -cairo-lang-compiler = "2.1.1" -cairo-lang-debug = "2.1.1" -cairo-lang-defs = "2.1.1" -cairo-lang-diagnostics = "2.1.1" -cairo-lang-filesystem = "2.1.1" -cairo-lang-formatter = "2.1.1" -cairo-lang-language-server = "2.1.1" -cairo-lang-lowering = "2.1.1" -cairo-lang-parser = "2.1.1" -cairo-lang-plugins = "2.1.1" -cairo-lang-project = "2.1.1" -cairo-lang-semantic = { version = "2.1.1", features = [ "testing" ] } -cairo-lang-sierra = "2.1.1" -cairo-lang-sierra-generator = "2.1.1" -cairo-lang-sierra-to-casm = "2.1.1" -cairo-lang-starknet = "2.1.1" -cairo-lang-syntax = "2.1.1" -cairo-lang-test-runner = "2.1.1" -cairo-lang-test-utils = "2.1.1" -cairo-lang-utils = "2.1.1" +cairo-lang-casm = "2.2.0" +cairo-lang-compiler = "2.2.0" +cairo-lang-debug = "2.2.0" +cairo-lang-defs = "2.2.0" +cairo-lang-diagnostics = "2.2.0" +cairo-lang-filesystem = "2.2.0" +cairo-lang-formatter = "2.2.0" +cairo-lang-language-server = "2.2.0" +cairo-lang-lowering = "2.2.0" +cairo-lang-parser = "2.2.0" +cairo-lang-plugins = "2.2.0" +cairo-lang-project = "2.2.0" +cairo-lang-semantic = { version = "2.2.0", features = [ "testing" ] } +cairo-lang-sierra = "2.2.0" +cairo-lang-sierra-generator = "2.2.0" +cairo-lang-sierra-to-casm = "2.2.0" +cairo-lang-starknet = "2.2.0" +cairo-lang-syntax = "2.2.0" +cairo-lang-test-runner = "2.2.0" +cairo-lang-test-utils = "2.2.0" +cairo-lang-utils = "2.2.0" cairo-vm = "0.8.2" camino = { version = "1.1.2", features = [ "serde1" ] } chrono = { version = "0.4.24", features = [ "serde" ] } @@ -65,10 +65,12 @@ itertools = "0.10.3" log = "0.4.17" num-bigint = "0.4" parking_lot = "0.12.1" +once_cell = "1.0" pretty_assertions = "1.2.1" rayon = "0.9.0" salsa = "0.16.1" -scarb = { git = "https://github.com/software-mansion/scarb", rev = "c07fa61" } +scarb = { git = "https://github.com/software-mansion/scarb", rev = "7adb7fd" } +scarb-ui = { git = "https://github.com/software-mansion/scarb", rev = "7adb7fd" } semver = "1.0.5" serde = { version = "1.0.156", features = [ "derive" ] } serde_json = "1.0" diff --git a/crates/dojo-core/Scarb.toml b/crates/dojo-core/Scarb.toml index 871e2092fe..006e4b0783 100644 --- a/crates/dojo-core/Scarb.toml +++ b/crates/dojo-core/Scarb.toml @@ -1,9 +1,9 @@ [package] -cairo-version = "2.1.1" +cairo-version = "2.2.0" description = "The Dojo Core library for autonomous worlds." name = "dojo" version = "0.2.1" [dependencies] dojo_plugin = { git = "https://github.com/dojoengine/dojo" } -starknet = "2.1.1" +starknet = "2.2.0" diff --git a/crates/dojo-core/src/executor.cairo b/crates/dojo-core/src/executor.cairo index fe862d6136..85877efdc6 100644 --- a/crates/dojo-core/src/executor.cairo +++ b/crates/dojo-core/src/executor.cairo @@ -14,7 +14,7 @@ trait IExecutor { mod executor { use array::{ArrayTrait, SpanTrait}; use option::OptionTrait; - use starknet::ClassHash; + use starknet::{ClassHash, SyscallResultTrait, SyscallResultTraitImpl}; use super::IExecutor; diff --git a/crates/dojo-core/src/world.cairo b/crates/dojo-core/src/world.cairo index dd4cda4ed1..fea75d96a4 100644 --- a/crates/dojo-core/src/world.cairo +++ b/crates/dojo-core/src/world.cairo @@ -53,7 +53,7 @@ mod world { use starknet::{ get_caller_address, get_contract_address, get_tx_info, contract_address::ContractAddressIntoFelt252, ClassHash, Zeroable, ContractAddress, - syscalls::emit_event_syscall + syscalls::emit_event_syscall, SyscallResultTrait, SyscallResultTraitImpl }; use dojo::database; @@ -540,6 +540,8 @@ mod world { #[system] mod library_call { + use starknet::{SyscallResultTrait, SyscallResultTraitImpl}; + fn execute( class_hash: starknet::ClassHash, entrypoint: felt252, calladata: Span ) -> Span { diff --git a/crates/dojo-core/src/world_factory.cairo b/crates/dojo-core/src/world_factory.cairo index 716517084f..06701ce0b3 100644 --- a/crates/dojo-core/src/world_factory.cairo +++ b/crates/dojo-core/src/world_factory.cairo @@ -21,7 +21,7 @@ mod world_factory { use starknet::{ ClassHash, ContractAddress, contract_address::ContractAddressIntoFelt252, - syscalls::deploy_syscall, get_caller_address + syscalls::deploy_syscall, get_caller_address, SyscallResultTrait, SyscallResultTraitImpl }; use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; diff --git a/crates/dojo-erc/Scarb.toml b/crates/dojo-erc/Scarb.toml index 6accc0171a..384b43b5af 100644 --- a/crates/dojo-erc/Scarb.toml +++ b/crates/dojo-erc/Scarb.toml @@ -1,5 +1,5 @@ [package] -cairo-version = "2.1.1" +cairo-version = "2.2.0" description = "Implementations of ERC standards for the Dojo framework" name = "dojo_erc" version = "0.2.1" diff --git a/crates/dojo-erc/src/erc1155/erc1155.cairo b/crates/dojo-erc/src/erc1155/erc1155.cairo index 473a14984e..0b7cd147e9 100644 --- a/crates/dojo-erc/src/erc1155/erc1155.cairo +++ b/crates/dojo-erc/src/erc1155/erc1155.cairo @@ -20,7 +20,7 @@ mod ERC1155 { use dojo_erc::erc165::interface::{IERC165, IERC165_ID}; use dojo_erc::erc_common::utils::{ - to_calldata, ToCallDataTrait, system_calldata, PartialEqArray + to_calldata, ToCallDataTrait, system_calldata }; use dojo_erc::erc1155::systems::{ diff --git a/crates/dojo-erc/src/erc1155/systems.cairo b/crates/dojo-erc/src/erc1155/systems.cairo index cb6e254b64..8ab5be0ffe 100644 --- a/crates/dojo-erc/src/erc1155/systems.cairo +++ b/crates/dojo-erc/src/erc1155/systems.cairo @@ -11,8 +11,10 @@ use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; use dojo_erc::erc1155::erc1155::ERC1155::{ ApprovalForAll, TransferSingle, TransferBatch, IERC1155EventsDispatcher, - IERC1155EventsDispatcherTrait, Event + IERC1155EventsDispatcherTrait }; +#[event] +use dojo_erc::erc1155::erc1155::ERC1155::Event; use dojo_erc::erc1155::components::{ERC1155BalanceTrait, OperatorApprovalTrait}; use dojo_erc::erc165::interface::{IERC165Dispatcher, IERC165DispatcherTrait, IACCOUNT_ID}; use dojo_erc::erc1155::interface::{ @@ -150,8 +152,9 @@ mod ERC1155SetApprovalForAll { use clone::Clone; use dojo_erc::erc1155::components::OperatorApprovalTrait; - use super::{IERC1155EventsDispatcher, IERC1155EventsDispatcherTrait, ApprovalForAll, Event}; - + use super::{IERC1155EventsDispatcher, IERC1155EventsDispatcherTrait, ApprovalForAll}; + #[event] + use super::Event; #[derive(Drop, Serde)] struct ERC1155SetApprovalForAllParams { diff --git a/crates/dojo-erc/src/erc_common/utils.cairo b/crates/dojo-erc/src/erc_common/utils.cairo index cfe9976f18..005d55eac7 100644 --- a/crates/dojo-erc/src/erc_common/utils.cairo +++ b/crates/dojo-erc/src/erc_common/utils.cairo @@ -28,32 +28,3 @@ fn system_calldata, impl TD: Drop>(data: T) -> Array data.serialize(ref calldata); calldata } - - -impl PartialEqArray> of PartialEq> { - fn eq(lhs: @Array, rhs: @Array) -> bool { - if lhs.len() != rhs.len() { - return false; - }; - - let mut is_eq = true; - let mut i = 0; - loop { - if lhs.len() == i { - break; - }; - if lhs.at(i) != rhs.at(i) { - is_eq = false; - break; - }; - - i += 1; - }; - - is_eq - } - - fn ne(lhs: @Array, rhs: @Array) -> bool { - !PartialEqArray::eq(lhs, rhs) - } -} diff --git a/crates/dojo-lang/Cargo.toml b/crates/dojo-lang/Cargo.toml index 796fe360f6..0a2f0ebeee 100644 --- a/crates/dojo-lang/Cargo.toml +++ b/crates/dojo-lang/Cargo.toml @@ -13,6 +13,7 @@ testing = [ ] anyhow.workspace = true assert_fs = "1.0.9" cairo-lang-compiler.workspace = true +cairo-lang-debug.workspace = true cairo-lang-defs.workspace = true cairo-lang-diagnostics.workspace = true cairo-lang-filesystem.workspace = true @@ -31,6 +32,7 @@ dojo-types = { path = "../dojo-types" } dojo-world = { path = "../dojo-world" } indoc.workspace = true itertools.workspace = true +once_cell.workspace = true salsa.workspace = true sanitizer = "0.1.6" scarb.workspace = true diff --git a/crates/dojo-lang/src/compiler.rs b/crates/dojo-lang/src/compiler.rs index 20c36e6a1a..41116b3d55 100644 --- a/crates/dojo-lang/src/compiler.rs +++ b/crates/dojo-lang/src/compiler.rs @@ -138,7 +138,9 @@ fn find_project_contracts( .iter() .map(|selector| selector.package().into()) .unique() - .map(|package_name: SmolStr| db.upcast_mut().intern_crate(CrateLongId(package_name))) + .map(|package_name: SmolStr| { + db.upcast_mut().intern_crate(CrateLongId::Real(package_name)) + }) .collect::>(); find_contracts(db, crate_ids.as_ref()) .into_iter() @@ -167,7 +169,7 @@ pub fn collect_core_crate_ids(db: &RootDatabase) -> Vec { .iter() .map(|selector| selector.package().into()) .unique() - .map(|package_name: SmolStr| db.intern_crate(CrateLongId(package_name))) + .map(|package_name: SmolStr| db.intern_crate(CrateLongId::Real(package_name))) .collect::>() } @@ -179,7 +181,7 @@ pub fn collect_external_crate_ids( .iter() .map(|selector| selector.package().into()) .unique() - .map(|package_name: SmolStr| db.intern_crate(CrateLongId(package_name))) + .map(|package_name: SmolStr| db.intern_crate(CrateLongId::Real(package_name))) .collect::>() } diff --git a/crates/dojo-lang/src/component.rs b/crates/dojo-lang/src/component.rs index c13c5620cb..2b1a16f82e 100644 --- a/crates/dojo-lang/src/component.rs +++ b/crates/dojo-lang/src/component.rs @@ -1,5 +1,5 @@ +use cairo_lang_defs::patcher::RewriteNode; use cairo_lang_defs::plugin::PluginDiagnostic; -use cairo_lang_semantic::patcher::RewriteNode; use cairo_lang_syntax::node::ast::ItemStruct; use cairo_lang_syntax::node::db::SyntaxGroup; use cairo_lang_syntax::node::helpers::QueryAttrs; diff --git a/crates/dojo-lang/src/db.rs b/crates/dojo-lang/src/db.rs deleted file mode 100644 index 70eafb53df..0000000000 --- a/crates/dojo-lang/src/db.rs +++ /dev/null @@ -1,43 +0,0 @@ -use std::path::PathBuf; -use std::sync::Arc; - -use anyhow::Result; -use cairo_lang_compiler::db::{RootDatabase, RootDatabaseBuilder}; -use cairo_lang_filesystem::db::init_dev_corelib; -use cairo_lang_semantic::db::SemanticGroup; -use cairo_lang_semantic::plugin::SemanticPlugin; -use cairo_lang_starknet::plugin::StarkNetPlugin; - -use crate::plugin::DojoPlugin; - -pub const DOJOLIB_CRATE_NAME: &str = "dojo"; - -pub trait DojoRootDatabaseBuilderEx { - fn build_language_server( - &mut self, - path: PathBuf, - plugins: Vec>, - ) -> Result; - - /// Tunes a compiler database to Dojo (e.g. Dojo plugin). - fn with_dojo(&mut self) -> &mut Self; -} - -impl DojoRootDatabaseBuilderEx for RootDatabaseBuilder { - fn build_language_server( - &mut self, - path: PathBuf, - plugins: Vec>, - ) -> Result { - let mut db = RootDatabase::default(); - init_dev_corelib(&mut db, path); - db.set_semantic_plugins(plugins); - Ok(db) - } - - fn with_dojo(&mut self) -> &mut Self { - self.with_semantic_plugin(Arc::new(DojoPlugin)); - self.with_semantic_plugin(Arc::new(StarkNetPlugin::default())); - self - } -} diff --git a/crates/dojo-lang/src/inline_macro_plugin.rs b/crates/dojo-lang/src/inline_macro_plugin.rs deleted file mode 100644 index 2355197097..0000000000 --- a/crates/dojo-lang/src/inline_macro_plugin.rs +++ /dev/null @@ -1,104 +0,0 @@ -use cairo_lang_defs::plugin::PluginDiagnostic; -use cairo_lang_syntax::node::ast::{self}; -use cairo_lang_syntax::node::db::SyntaxGroup; -use cairo_lang_syntax::node::kind::SyntaxKind; -use cairo_lang_syntax::node::{SyntaxNode, TypedSyntaxNode}; - -use crate::inline_macros::emit::EmitMacro; -use crate::inline_macros::get::GetMacro; -use crate::inline_macros::set::SetMacro; - -/// The result of expanding an inline macro. -#[derive(Debug, Default)] -pub struct InlineMacroExpanderData { - pub result_code: String, - pub code_changed: bool, - pub diagnostics: Vec, -} - -/// A trait for inline macros. -pub trait InlineMacro { - /// A function that appends the expanded code of the macro to the result code. - fn append_macro_code( - &self, - macro_expander_data: &mut InlineMacroExpanderData, - db: &dyn SyntaxGroup, - macro_arguments: &ast::ExprList, - ); - /// A function that returns true if the macro supports the given bracket type. - fn is_bracket_type_allowed( - &self, - db: &dyn SyntaxGroup, - macro_ast: &ast::ExprInlineMacro, - ) -> bool; -} - -/// Returns the inline macro plugin for the given macro name, or None if no such plugin exists. -fn get_inline_macro_plugin(macro_name: &str) -> Option> { - match macro_name { - "emit" => Some(Box::new(EmitMacro)), - "get" => Some(Box::new(GetMacro)), - "set" => Some(Box::new(SetMacro)), - _ => None, - } -} - -impl InlineMacroExpanderData { - /// Traverse the syntax tree, accumolates any non-macro code and expand all inline macros. - pub fn expand_node(&mut self, db: &dyn SyntaxGroup, syntax_node: &SyntaxNode) { - let node_kind = syntax_node.kind(db); - if let SyntaxKind::ExprInlineMacro = node_kind { - let inline_macro = ast::ExprInlineMacro::from_syntax_node(db, syntax_node.clone()); - self.handle_macro(db, &inline_macro); - } else { - if let Some(text) = syntax_node.text(db) { - self.result_code.push_str(&text); - } - for child in syntax_node.children(db) { - self.expand_node(db, &child); - } - } - } - - /// Expand a single inline macro. - fn handle_macro(&mut self, db: &dyn SyntaxGroup, inline_macro: &ast::ExprInlineMacro) { - let macro_name = inline_macro.path(db).as_syntax_node().get_text_without_trivia(db); - let macro_plugin = get_inline_macro_plugin(¯o_name); - - if let Some(macro_plugin) = macro_plugin { - if let Some(macro_arguments) = - self.extract_macro_args(db, macro_plugin.as_ref(), inline_macro) - { - macro_plugin.append_macro_code(self, db, ¯o_arguments); - } - } else { - self.result_code.push_str(&inline_macro.as_syntax_node().get_text(db)); - } - } - - /// Extract the macro arguments from the inline macro if the macro supports the given bracket - /// type. Otherwise, add a diagnostic. - fn extract_macro_args( - &mut self, - db: &dyn SyntaxGroup, - macro_plugin: &dyn InlineMacro, - macro_ast: &ast::ExprInlineMacro, - ) -> Option { - if macro_plugin.is_bracket_type_allowed(db, macro_ast) { - Some(match macro_ast.arguments(db) { - ast::WrappedExprList::BracketedExprList(expr_list) => expr_list.expressions(db), - ast::WrappedExprList::ParenthesizedExprList(expr_list) => expr_list.expressions(db), - ast::WrappedExprList::BracedExprList(expr_list) => expr_list.expressions(db), - }) - } else { - self.diagnostics.push(PluginDiagnostic { - stable_ptr: macro_ast.stable_ptr().untyped(), - message: format!( - "Macro {} does not support this bracket type", - macro_ast.path(db).as_syntax_node().get_text(db) - ), - }); - None - } - } -} diff --git a/crates/dojo-lang/src/inline_macros/emit.rs b/crates/dojo-lang/src/inline_macros/emit.rs index 72b8b8dbf9..c0a08888bc 100644 --- a/crates/dojo-lang/src/inline_macros/emit.rs +++ b/crates/dojo-lang/src/inline_macros/emit.rs @@ -1,51 +1,65 @@ -use cairo_lang_defs::plugin::PluginDiagnostic; -use cairo_lang_syntax::node::TypedSyntaxNode; - -use crate::inline_macro_plugin::{InlineMacro, InlineMacroExpanderData}; +use cairo_lang_defs::patcher::PatchBuilder; +use cairo_lang_defs::plugin::{ + InlineMacroExprPlugin, InlinePluginResult, PluginDiagnostic, PluginGeneratedFile, +}; +use cairo_lang_semantic::inline_macros::unsupported_bracket_diagnostic; +use cairo_lang_syntax::node::{ast, TypedSyntaxNode}; +#[derive(Debug)] pub struct EmitMacro; -impl InlineMacro for EmitMacro { - fn append_macro_code( +impl EmitMacro { + pub const NAME: &'static str = "emit"; +} +impl InlineMacroExprPlugin for EmitMacro { + fn generate_code( &self, - macro_expander_data: &mut InlineMacroExpanderData, db: &dyn cairo_lang_syntax::node::db::SyntaxGroup, - macro_arguments: &cairo_lang_syntax::node::ast::ExprList, - ) { - let args = macro_arguments.elements(db); + syntax: &ast::ExprInlineMacro, + ) -> InlinePluginResult { + let ast::WrappedArgList::ParenthesizedArgList(arg_list) = syntax.arguments(db) else { + return unsupported_bracket_diagnostic(db, syntax); + }; + let mut builder = PatchBuilder::new(db); + builder.add_str( + "{ + let mut keys = Default::::default(); + let mut data = Default::::default();", + ); + + let args = arg_list.args(db).elements(db); if args.len() != 2 { - macro_expander_data.diagnostics.push(PluginDiagnostic { - message: "Invalid arguments. Expected \"emit!(world, event)\"".to_string(), - stable_ptr: macro_arguments.as_syntax_node().stable_ptr(), - }); - return; + return InlinePluginResult { + code: None, + diagnostics: vec![PluginDiagnostic { + stable_ptr: arg_list.args(db).stable_ptr().untyped(), + message: "Invalid arguments. Expected \"emit!(world, event)\"".to_string(), + }], + }; } let world = &args[0]; let event = &args[1]; - let expanded_code = format!( - "{{ - let mut keys = Default::::default(); - let mut data = Default::::default(); - starknet::Event::append_keys_and_data(@traits::Into::<_, Event>::into({}), ref \ - keys, ref data); - {}.emit(keys, data.span()); - }}", - event.as_syntax_node().get_text(db), - world.as_syntax_node().get_text(db), + + builder.add_str( + "\n starknet::Event::append_keys_and_data(@traits::Into::<_, Event>::into(", ); - macro_expander_data.result_code.push_str(&expanded_code); - macro_expander_data.code_changed = true; - } + builder.add_node(event.as_syntax_node()); + builder.add_str("), ref keys, ref data);"); - fn is_bracket_type_allowed( - &self, - db: &dyn cairo_lang_syntax::node::db::SyntaxGroup, - macro_ast: &cairo_lang_syntax::node::ast::ExprInlineMacro, - ) -> bool { - matches!( - macro_ast.arguments(db), - cairo_lang_syntax::node::ast::WrappedExprList::ParenthesizedExprList(_) - ) + builder.add_str("\n "); + builder.add_node(world.as_syntax_node()); + builder.add_str(".emit(keys, data.span());"); + builder.add_str("}"); + + InlinePluginResult { + code: Some(PluginGeneratedFile { + name: "emit_inline_macro".into(), + content: builder.code, + diagnostics_mappings: builder.diagnostics_mappings, + aux_data: None, + }), + diagnostics: vec![], + } } } diff --git a/crates/dojo-lang/src/inline_macros/get.rs b/crates/dojo-lang/src/inline_macros/get.rs index 47319f55f8..445f14a012 100644 --- a/crates/dojo-lang/src/inline_macros/get.rs +++ b/crates/dojo-lang/src/inline_macros/get.rs @@ -1,53 +1,82 @@ -use cairo_lang_defs::plugin::PluginDiagnostic; +use cairo_lang_defs::patcher::PatchBuilder; +use cairo_lang_defs::plugin::{ + InlineMacroExprPlugin, InlinePluginResult, PluginDiagnostic, PluginGeneratedFile, +}; +use cairo_lang_semantic::inline_macros::unsupported_bracket_diagnostic; use cairo_lang_syntax::node::ast::Expr; -use cairo_lang_syntax::node::TypedSyntaxNode; +use cairo_lang_syntax::node::{ast, TypedSyntaxNode}; use itertools::Itertools; -use super::{extract_components, CAIRO_ERR_MSG_LEN}; -use crate::inline_macro_plugin::{InlineMacro, InlineMacroExpanderData}; +use super::{extract_components, unsupported_arg_diagnostic, CAIRO_ERR_MSG_LEN}; +#[derive(Debug)] pub struct GetMacro; -impl InlineMacro for GetMacro { - fn append_macro_code( +impl GetMacro { + pub const NAME: &'static str = "get"; +} +impl InlineMacroExprPlugin for GetMacro { + fn generate_code( &self, - macro_expander_data: &mut InlineMacroExpanderData, db: &dyn cairo_lang_syntax::node::db::SyntaxGroup, - macro_arguments: &cairo_lang_syntax::node::ast::ExprList, - ) { - let args = macro_arguments.elements(db); + syntax: &ast::ExprInlineMacro, + ) -> InlinePluginResult { + let ast::WrappedArgList::ParenthesizedArgList(arg_list) = syntax.arguments(db) else { + return unsupported_bracket_diagnostic(db, syntax); + }; + let mut builder = PatchBuilder::new(db); + builder.add_str( + "{ + let mut __get_macro_keys__ = array::ArrayTrait::new();", + ); + + let args = arg_list.args(db).elements(db); if args.len() != 3 { - macro_expander_data.diagnostics.push(PluginDiagnostic { - message: "Invalid arguments. Expected \"get!(world, keys, (components,))\"" - .to_string(), - stable_ptr: macro_arguments.as_syntax_node().stable_ptr(), - }); - return; + return InlinePluginResult { + code: None, + diagnostics: vec![PluginDiagnostic { + stable_ptr: syntax.stable_ptr().untyped(), + message: "Invalid arguments. Expected \"get!(world, keys, (components,))\"" + .to_string(), + }], + }; } let world = &args[0]; - let keys = &args[1]; - let components = extract_components(db, &args[2]); + + let ast::ArgClause::Unnamed(keys) = args[1].arg_clause(db) else { + return unsupported_arg_diagnostic(db, syntax); + }; + + let ast::ArgClause::Unnamed(components) = args[2].arg_clause(db) else { + return unsupported_arg_diagnostic(db, syntax); + }; + let components = match extract_components(db, &components.value(db)) { + Ok(components) => components, + Err(diagnostic) => { + return InlinePluginResult { code: None, diagnostics: vec![diagnostic] }; + } + }; if components.is_empty() { - macro_expander_data.diagnostics.push(PluginDiagnostic { - message: "Component types cannot be empty".to_string(), - stable_ptr: macro_arguments.as_syntax_node().stable_ptr(), - }); - return; + return InlinePluginResult { + code: None, + diagnostics: vec![PluginDiagnostic { + stable_ptr: syntax.stable_ptr().untyped(), + message: "Component types cannot be empty".to_string(), + }], + }; } - let args = match keys { + let args = match keys.value(db) { Expr::Literal(literal) => format!("({})", literal.as_syntax_node().get_text(db)), _ => keys.as_syntax_node().get_text(db), }; - let mut expanded_code = format!( - "{{ - let mut __get_macro_keys__ = array::ArrayTrait::new(); - serde::Serde::serialize(@{args}, ref __get_macro_keys__); + builder.add_str(&format!( + "serde::Serde::serialize(@{args}, ref __get_macro_keys__); let __get_macro_keys__ = array::ArrayTrait::span(@__get_macro_keys__);" - ); + )); for component in &components { let mut lookup_err_msg = format!("{} not found", component.to_string()); @@ -55,7 +84,7 @@ impl InlineMacro for GetMacro { let mut deser_err_msg = format!("{} failed to deserialize", component.to_string()); deser_err_msg.truncate(CAIRO_ERR_MSG_LEN); - expanded_code.push_str(&format!( + builder.add_str(&format!( "\n let __{component}_values__ = {}.entity('{component}', \ __get_macro_keys__, 0_u8, dojo::SerdeLen::<{component}>::len()); let mut __{component}_component__ = array::ArrayTrait::new(); @@ -71,26 +100,20 @@ impl InlineMacro for GetMacro { world.as_syntax_node().get_text(db), )); } - expanded_code.push_str( - format!( - "({}) + builder.add_str(&format!( + "({}) }}", - components.iter().map(|c| format!("__{c}")).join(",") - ) - .as_str(), - ); - macro_expander_data.result_code.push_str(&expanded_code); - macro_expander_data.code_changed = true; - } + components.iter().map(|c| format!("__{c}")).join(",") + )); - fn is_bracket_type_allowed( - &self, - db: &dyn cairo_lang_syntax::node::db::SyntaxGroup, - macro_ast: &cairo_lang_syntax::node::ast::ExprInlineMacro, - ) -> bool { - matches!( - macro_ast.arguments(db), - cairo_lang_syntax::node::ast::WrappedExprList::ParenthesizedExprList(_) - ) + InlinePluginResult { + code: Some(PluginGeneratedFile { + name: "get_inline_macro".into(), + content: builder.code, + diagnostics_mappings: builder.diagnostics_mappings, + aux_data: None, + }), + diagnostics: vec![], + } } } diff --git a/crates/dojo-lang/src/inline_macros/mod.rs b/crates/dojo-lang/src/inline_macros/mod.rs index 25c4666cad..c51a45e5c8 100644 --- a/crates/dojo-lang/src/inline_macros/mod.rs +++ b/crates/dojo-lang/src/inline_macros/mod.rs @@ -1,3 +1,4 @@ +use cairo_lang_defs::plugin::{InlinePluginResult, PluginDiagnostic}; use cairo_lang_syntax::node::db::SyntaxGroup; use cairo_lang_syntax::node::{ast, Terminal, TypedSyntaxNode}; use smol_str::SmolStr; @@ -8,24 +9,50 @@ pub mod set; const CAIRO_ERR_MSG_LEN: usize = 31; -pub fn extract_components(db: &dyn SyntaxGroup, expression: &ast::Expr) -> Vec { +pub fn extract_components( + db: &dyn SyntaxGroup, + expression: &ast::Expr, +) -> Result, PluginDiagnostic> { let mut components = vec![]; match expression { ast::Expr::Tuple(tuple) => { for element in tuple.expressions(db).elements(db) { - components.extend(extract_components(db, &element)); + match extract_components(db, &element) { + Ok(mut element_components) => components.append(&mut element_components), + Err(diagnostic) => return Err(diagnostic), + } } } ast::Expr::Parenthesized(parenthesized) => { - components.extend(extract_components(db, &parenthesized.expr(db))); + match extract_components(db, &parenthesized.expr(db)) { + Ok(mut parenthesized_components) => { + components.append(&mut parenthesized_components) + } + Err(diagnostic) => return Err(diagnostic), + } } ast::Expr::Path(path) => match path.elements(db).last().unwrap() { ast::PathSegment::WithGenericArgs(segment) => { let generic = segment.generic_args(db); for param in generic.generic_args(db).elements(db) { - if let ast::GenericArg::Expr(expr) = param { - components.extend(extract_components(db, &expr.value(db))); + let ast::GenericArg::Unnamed(unnamed) = param else { + return Err(PluginDiagnostic { + stable_ptr: param.stable_ptr().untyped(), + message: "Should be an unnamed argument".to_string(), + }); + }; + + let ast::GenericArgValue::Expr(expr) = unnamed.value(db) else { + return Err(PluginDiagnostic { + stable_ptr: unnamed.stable_ptr().untyped(), + message: "Should be an expression".to_string(), + }); + }; + + match extract_components(db, &expr.expr(db)) { + Ok(mut expr_components) => components.append(&mut expr_components), + Err(diagnostic) => return Err(diagnostic), } } } @@ -34,12 +61,30 @@ pub fn extract_components(db: &dyn SyntaxGroup, expression: &ast::Expr) -> Vec { - unimplemented!( - "Unsupported expression type: {}", - expression.as_syntax_node().get_text(db) - ); + return Err(PluginDiagnostic { + stable_ptr: expression.stable_ptr().untyped(), + message: format!( + "Unsupported expression type: {}", + expression.as_syntax_node().get_text(db) + ), + }); } } - components + Ok(components) +} +pub fn unsupported_arg_diagnostic( + db: &dyn SyntaxGroup, + macro_ast: &ast::ExprInlineMacro, +) -> InlinePluginResult { + InlinePluginResult { + code: None, + diagnostics: vec![PluginDiagnostic { + stable_ptr: macro_ast.stable_ptr().untyped(), + message: format!( + "Macro {} does not support this arg type", + macro_ast.path(db).as_syntax_node().get_text(db) + ), + }], + } } diff --git a/crates/dojo-lang/src/inline_macros/set.rs b/crates/dojo-lang/src/inline_macros/set.rs index 0e889d3ddc..7a58c7b3b3 100644 --- a/crates/dojo-lang/src/inline_macros/set.rs +++ b/crates/dojo-lang/src/inline_macros/set.rs @@ -1,30 +1,50 @@ -use cairo_lang_defs::plugin::PluginDiagnostic; +use cairo_lang_defs::patcher::PatchBuilder; +use cairo_lang_defs::plugin::{ + InlineMacroExprPlugin, InlinePluginResult, PluginDiagnostic, PluginGeneratedFile, +}; +use cairo_lang_semantic::inline_macros::unsupported_bracket_diagnostic; use cairo_lang_syntax::node::{ast, TypedSyntaxNode}; -use crate::inline_macro_plugin::{InlineMacro, InlineMacroExpanderData}; +use super::unsupported_arg_diagnostic; +#[derive(Debug)] pub struct SetMacro; -impl InlineMacro for SetMacro { - fn append_macro_code( +impl SetMacro { + pub const NAME: &'static str = "set"; +} +impl InlineMacroExprPlugin for SetMacro { + fn generate_code( &self, - macro_expander_data: &mut InlineMacroExpanderData, db: &dyn cairo_lang_syntax::node::db::SyntaxGroup, - macro_arguments: &cairo_lang_syntax::node::ast::ExprList, - ) { - let args = macro_arguments.elements(db); + syntax: &ast::ExprInlineMacro, + ) -> InlinePluginResult { + let ast::WrappedArgList::ParenthesizedArgList(arg_list) = syntax.arguments(db) else { + return unsupported_bracket_diagnostic(db, syntax); + }; + let mut builder = PatchBuilder::new(db); + builder.add_str("{"); + + let args = arg_list.args(db).elements(db); if args.len() != 2 { - macro_expander_data.diagnostics.push(PluginDiagnostic { - message: "Invalid arguments. Expected \"(world, (components,))\"".to_string(), - stable_ptr: macro_arguments.as_syntax_node().stable_ptr(), - }); - return; + return InlinePluginResult { + code: None, + diagnostics: vec![PluginDiagnostic { + stable_ptr: arg_list.args(db).stable_ptr().untyped(), + message: "Invalid arguments. Expected \"(world, (components,))\"".to_string(), + }], + }; } let world = &args[0]; + + let ast::ArgClause::Unnamed(components) = args[1].arg_clause(db) else { + return unsupported_arg_diagnostic(db, syntax); + }; + let mut bundle = vec![]; - match &args[1] { + match components.value(db) { ast::Expr::Parenthesized(parens) => { bundle.push(parens.expr(db).as_syntax_node().get_text(db)) } @@ -33,25 +53,29 @@ impl InlineMacro for SetMacro { }), ast::Expr::StructCtorCall(ctor) => bundle.push(ctor.as_syntax_node().get_text(db)), _ => { - macro_expander_data.diagnostics.push(PluginDiagnostic { - message: "Invalid arguments. Expected \"(world, (components,))\"".to_string(), - stable_ptr: macro_arguments.as_syntax_node().stable_ptr(), - }); - return; + return InlinePluginResult { + code: None, + diagnostics: vec![PluginDiagnostic { + message: "Invalid arguments. Expected \"(world, (components,))\"" + .to_string(), + stable_ptr: arg_list.args(db).stable_ptr().untyped(), + }], + }; } } if bundle.is_empty() { - macro_expander_data.diagnostics.push(PluginDiagnostic { - message: "Invalid arguments: No components provided.".to_string(), - stable_ptr: macro_arguments.as_syntax_node().stable_ptr(), - }); - return; + return InlinePluginResult { + code: None, + diagnostics: vec![PluginDiagnostic { + message: "Invalid arguments: No components provided.".to_string(), + stable_ptr: arg_list.args(db).stable_ptr().untyped(), + }], + }; } - let mut expanded_code = "{".to_string(); for entity in bundle { - expanded_code.push_str(&format!( + builder.add_str(&format!( "\n let __set_macro_value__ = {}; {}.set_entity(dojo::traits::Component::name(@__set_macro_value__), \ dojo::traits::Component::keys(@__set_macro_value__), 0_u8, \ @@ -60,19 +84,16 @@ impl InlineMacro for SetMacro { world.as_syntax_node().get_text(db), )); } - expanded_code.push('}'); - macro_expander_data.result_code.push_str(&expanded_code); - macro_expander_data.code_changed = true; - } + builder.add_str("}"); - fn is_bracket_type_allowed( - &self, - db: &dyn cairo_lang_syntax::node::db::SyntaxGroup, - macro_ast: &cairo_lang_syntax::node::ast::ExprInlineMacro, - ) -> bool { - matches!( - macro_ast.arguments(db), - cairo_lang_syntax::node::ast::WrappedExprList::ParenthesizedExprList(_) - ) + InlinePluginResult { + code: Some(PluginGeneratedFile { + name: "set_inline_macro".into(), + content: builder.code, + diagnostics_mappings: builder.diagnostics_mappings, + aux_data: None, + }), + diagnostics: vec![], + } } } diff --git a/crates/dojo-lang/src/lib.rs b/crates/dojo-lang/src/lib.rs index 94e68ba6f3..7ac2ee7600 100644 --- a/crates/dojo-lang/src/lib.rs +++ b/crates/dojo-lang/src/lib.rs @@ -5,9 +5,7 @@ //! Learn more at [dojoengine.gg](http://dojoengine.gg). pub mod compiler; pub mod component; -pub mod db; -pub mod inline_macro_plugin; -mod inline_macros; +pub mod inline_macros; mod manifest; pub mod plugin; mod serde; diff --git a/crates/dojo-lang/src/manifest.rs b/crates/dojo-lang/src/manifest.rs index 65db032846..58a34e1b96 100644 --- a/crates/dojo-lang/src/manifest.rs +++ b/crates/dojo-lang/src/manifest.rs @@ -4,7 +4,6 @@ use anyhow::{Context, Result}; use cairo_lang_defs::ids::{ModuleId, ModuleItemId}; use cairo_lang_filesystem::ids::CrateId; use cairo_lang_semantic::db::SemanticGroup; -use cairo_lang_semantic::plugin::DynPluginAuxData; use cairo_lang_starknet::abi; use convert_case::{Case, Casing}; use dojo_world::manifest::{ @@ -71,12 +70,10 @@ impl Manifest { let Some(generated_file_info) = generated_file_info else { continue; }; - let Some(mapper) = - generated_file_info.aux_data.0.as_any().downcast_ref::() - else { + let Some(aux_data) = &generated_file_info.aux_data else { continue; }; - let Some(aux_data) = mapper.0.as_any().downcast_ref::() else { + let Some(aux_data) = aux_data.0.as_any().downcast_ref() else { continue; }; @@ -134,11 +131,11 @@ impl Manifest { { let defs_db = db.upcast(); let fns = db.module_free_functions_ids(ModuleId::Submodule(submodule_id)).unwrap(); - for fn_id in fns { + for fn_id in fns.iter() { if fn_id.name(defs_db) != "execute" { continue; } - let signature = db.free_function_signature(fn_id).unwrap(); + let signature = db.free_function_signature(*fn_id).unwrap(); let mut inputs = vec![]; let mut params = signature.params; diff --git a/crates/dojo-lang/src/manifest_test.rs b/crates/dojo-lang/src/manifest_test.rs index c29690a79b..41ae7428fe 100644 --- a/crates/dojo-lang/src/manifest_test.rs +++ b/crates/dojo-lang/src/manifest_test.rs @@ -16,9 +16,10 @@ cairo_lang_test_utils::test_file_test!( pub fn test_manifest_file( _inputs: &OrderedHashMap, -) -> OrderedHashMap { + _args: &OrderedHashMap, +) -> Result, String> { let config = - build_test_config("./src/manifest_test_data/manifest_test_crate/Scarb.toml").unwrap(); + build_test_config("./src/manifest_test_data/example_ecs_crate/Scarb.toml").unwrap(); let ws = ops::read_workspace(config.manifest_path(), &config).unwrap(); let packages = ws.members().map(|p| p.id).collect(); @@ -31,5 +32,5 @@ pub fn test_manifest_file( let generated_file = fs::read_to_string(generated_manifest_path).unwrap(); - OrderedHashMap::from([("expected_manifest_file".into(), generated_file)]) + Ok(OrderedHashMap::from([("expected_manifest_file".into(), generated_file)])) } diff --git a/crates/dojo-lang/src/manifest_test_data/manifest_test_crate b/crates/dojo-lang/src/manifest_test_data/example_ecs_crate similarity index 100% rename from crates/dojo-lang/src/manifest_test_data/manifest_test_crate rename to crates/dojo-lang/src/manifest_test_data/example_ecs_crate diff --git a/crates/dojo-lang/src/manifest_test_data/manifest b/crates/dojo-lang/src/manifest_test_data/manifest index 3a82f9f289..37bf0eb97f 100644 --- a/crates/dojo-lang/src/manifest_test_data/manifest +++ b/crates/dojo-lang/src/manifest_test_data/manifest @@ -8,7 +8,7 @@ test_manifest_file "world": { "name": "world", "address": null, - "class_hash": "0x1527b232cbd77c7f021fdc129339d7623edfd9a9c79a1b9add29c9064961497", + "class_hash": "0x2f857fd574c566f9bea8587569d016036377e028f0d47f6e31b4518ee605cd1", "abi": [ { "type": "impl", @@ -541,7 +541,7 @@ test_manifest_file "executor": { "name": "executor", "address": null, - "class_hash": "0x6ac7478eec43bd66aacf829f58dcb4694d0e241dc52b332f64de2b736c24137", + "class_hash": "0x24caf320f4df7648b6f150df66c16c62f51bcba009daecfec1f7622007ad04c", "abi": [ { "type": "impl", @@ -626,7 +626,7 @@ test_manifest_file } ], "outputs": [], - "class_hash": "0x73a2c968589ad1bf663ae71cdd7e55125265dbbfece0992ab993e0711fbfb1f", + "class_hash": "0x3459b99fd17b3bf9aeb6c55b610a457520785646f5c80c21d07041403a25315", "dependencies": [], "abi": [ { @@ -705,7 +705,7 @@ test_manifest_file } ], "outputs": [], - "class_hash": "0x601d780f4ccee957d308dbb6ad98cb5d4bf8e4e4b9cf7b221b8264c1d7186ef", + "class_hash": "0x578416d298f2f6c280a229f58fd7b5b2486285700fcfde9ad102c4052d3804e", "dependencies": [], "abi": [ { @@ -845,7 +845,7 @@ test_manifest_file "type": "core::array::Span::" } ], - "class_hash": "0x5c3f8568adfef908692f02fcfcc80e303c237183fe864f6cff8c34d29d3f130", + "class_hash": "0x3687cba423875df72318e609c4a5584b222862e01f624e80011c67b18661ae1", "dependencies": [], "abi": [ { @@ -954,7 +954,7 @@ test_manifest_file "key": false } ], - "class_hash": "0x5baa2a6749eebb5c2206c497a31c760455709e37f42bec5a7418bb98126d284", + "class_hash": "0x74c8a6b2e93b4a34788c23eb3d931da62d5a973ef66ba0a671de41b1607a9a4", "abi": [ { "type": "function", @@ -1030,7 +1030,7 @@ test_manifest_file "key": false } ], - "class_hash": "0x4973c97a1cd5e141d6dfe05c36517234851118ea703e510fcb72a39a092c228", + "class_hash": "0x6a8ab7eb5689bed6f0e9fb63d2565411830e1725aca7299f5f512d375d9a28c", "abi": [ { "type": "function", diff --git a/crates/dojo-lang/src/manifest_test_data/simple_crate/Scarb.toml b/crates/dojo-lang/src/manifest_test_data/simple_crate/Scarb.toml new file mode 100644 index 0000000000..4ebbfc58da --- /dev/null +++ b/crates/dojo-lang/src/manifest_test_data/simple_crate/Scarb.toml @@ -0,0 +1,10 @@ +[package] +cairo-version = "2.2.0" +name = "test_crate" +version = "0.2.1" + +[cairo] +sierra-replace-ids = true + +[dependencies] +dojo = { path = "../../../../dojo-core" } diff --git a/crates/dojo-lang/src/manifest_test_data/simple_crate/src/lib.cairo b/crates/dojo-lang/src/manifest_test_data/simple_crate/src/lib.cairo new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/dojo-lang/src/plugin.rs b/crates/dojo-lang/src/plugin.rs index 679d3bb11f..85b0cedca8 100644 --- a/crates/dojo-lang/src/plugin.rs +++ b/crates/dojo-lang/src/plugin.rs @@ -1,34 +1,30 @@ use std::sync::Arc; +use anyhow::Result; +use cairo_lang_defs::patcher::PatchBuilder; use cairo_lang_defs::plugin::{ - DynGeneratedFileAuxData, GeneratedFileAuxData, MacroPlugin, PluginDiagnostic, - PluginGeneratedFile, PluginResult, + DynGeneratedFileAuxData, GeneratedFileAuxData, InlineMacroExprPlugin, MacroPlugin, + PluginDiagnostic, PluginGeneratedFile, PluginResult, }; -use cairo_lang_diagnostics::DiagnosticEntry; -use cairo_lang_semantic::db::SemanticGroup; -use cairo_lang_semantic::patcher::{PatchBuilder, Patches}; -use cairo_lang_semantic::plugin::{ - AsDynGeneratedFileAuxData, AsDynMacroPlugin, DynPluginAuxData, PluginAuxData, - PluginMappedDiagnostic, SemanticPlugin, TrivialPluginAuxData, -}; -use cairo_lang_semantic::SemanticDiagnostic; -use cairo_lang_starknet::plugin::StarkNetPlugin; use cairo_lang_syntax::attribute::structured::{ AttributeArg, AttributeArgVariant, AttributeStructurize, }; use cairo_lang_syntax::node::db::SyntaxGroup; use cairo_lang_syntax::node::helpers::QueryAttrs; -use cairo_lang_syntax::node::{ast, Terminal, TypedSyntaxNode}; +use cairo_lang_syntax::node::{ast, Terminal}; use dojo_types::component::Member; use dojo_types::system::Dependency; -use scarb::compiler::plugin::builtin::BuiltinSemanticCairoPlugin; +use scarb::compiler::plugin::builtin::BuiltinStarkNetPlugin; +use scarb::compiler::plugin::{CairoPlugin, CairoPluginInstance}; use scarb::core::{PackageId, PackageName, SourceId}; use semver::Version; use smol_str::SmolStr; use url::Url; use crate::component::handle_component_struct; -use crate::inline_macro_plugin::InlineMacroExpanderData; +use crate::inline_macros::emit::EmitMacro; +use crate::inline_macros::get::GetMacro; +use crate::inline_macros::set::SetMacro; use crate::serde::handle_serde_len_struct; use crate::system::System; @@ -49,9 +45,6 @@ pub struct SystemAuxData { /// Dojo related auxiliary data of the Dojo plugin. #[derive(Debug, Default, PartialEq)] pub struct DojoAuxData { - /// Patches of code that need translation in case they have diagnostics. - pub patches: Patches, - /// A list of components that were processed by the plugin. pub components: Vec, /// A list of systems that were processed by the plugin and their component dependencies. @@ -65,26 +58,6 @@ impl GeneratedFileAuxData for DojoAuxData { if let Some(other) = other.as_any().downcast_ref::() { self == other } else { false } } } -impl AsDynGeneratedFileAuxData for DojoAuxData { - fn as_dyn_macro_token(&self) -> &(dyn GeneratedFileAuxData + 'static) { - self - } -} -impl PluginAuxData for DojoAuxData { - fn map_diag( - &self, - db: &(dyn SemanticGroup + 'static), - diag: &dyn std::any::Any, - ) -> Option { - let Some(diag) = diag.downcast_ref::() else { - return None; - }; - let span = self - .patches - .translate(db.upcast(), diag.stable_location.diagnostic_location(db.upcast()).span)?; - Some(PluginMappedDiagnostic { span, message: diag.format(db) }) - } -} #[cfg(test)] #[path = "plugin_test.rs"] @@ -103,28 +76,38 @@ impl DojoPlugin { } } +impl CairoPlugin for DojoPlugin { + fn id(&self) -> PackageId { + let url = Url::parse("https://github.com/dojoengine/dojo").unwrap(); + PackageId::new( + PackageName::new("dojo_plugin"), + Version::parse("0.2.1").unwrap(), + SourceId::for_git(&url, &scarb::core::GitReference::DefaultBranch).unwrap(), + ) + } + + fn instantiate(&self) -> Result> { + Ok(Box::new(DojoPluginInstance)) + } +} + +struct DojoPluginInstance; +impl CairoPluginInstance for DojoPluginInstance { + fn macro_plugins(&self) -> Vec> { + vec![Arc::new(DojoPlugin)] + } + + fn inline_macro_plugins(&self) -> Vec<(String, Arc)> { + vec![ + (GetMacro::NAME.into(), Arc::new(GetMacro)), + (SetMacro::NAME.into(), Arc::new(SetMacro)), + (EmitMacro::NAME.into(), Arc::new(EmitMacro)), + ] + } +} + impl MacroPlugin for DojoPlugin { fn generate_code(&self, db: &dyn SyntaxGroup, item_ast: ast::Item) -> PluginResult { - let mut expander_data = InlineMacroExpanderData::default(); - expander_data.expand_node(db, &item_ast.as_syntax_node()); - if expander_data.code_changed { - return PluginResult { - code: Some(PluginGeneratedFile { - name: "inline_macros".into(), - content: expander_data.result_code.clone(), - aux_data: DynGeneratedFileAuxData(Arc::new(TrivialPluginAuxData {})), - }), - diagnostics: expander_data.diagnostics, - remove_original_item: true, - }; - } else if !expander_data.diagnostics.is_empty() { - return PluginResult { - code: None, - diagnostics: expander_data.diagnostics, - remove_original_item: false, - }; - } - match item_ast { ast::Item::Module(module_ast) => self.handle_mod(db, module_ast), ast::Item::Struct(struct_ast) => { @@ -198,7 +181,8 @@ impl MacroPlugin for DojoPlugin { code: Some(PluginGeneratedFile { name, content: builder.code, - aux_data: DynGeneratedFileAuxData::new(DynPluginAuxData::new(aux_data)), + aux_data: Some(DynGeneratedFileAuxData::new(aux_data)), + diagnostics_mappings: builder.diagnostics_mappings, }), diagnostics, remove_original_item: true, @@ -209,35 +193,13 @@ impl MacroPlugin for DojoPlugin { } } -impl AsDynMacroPlugin for DojoPlugin { - fn as_dyn_macro_plugin<'a>(self: Arc) -> Arc - where - Self: 'a, - { - self - } -} -impl SemanticPlugin for DojoPlugin {} - pub struct CairoPluginRepository(scarb::compiler::plugin::CairoPluginRepository); impl CairoPluginRepository { pub fn new() -> Self { let mut repo = scarb::compiler::plugin::CairoPluginRepository::empty(); - let url = Url::parse("https://github.com/dojoengine/dojo").unwrap(); - let dojo_package_id = PackageId::new( - PackageName::new("dojo_plugin"), - Version::parse("0.2.1").unwrap(), - SourceId::for_git(&url, &scarb::core::GitReference::DefaultBranch).unwrap(), - ); - repo.add(Box::new(BuiltinSemanticCairoPlugin::::new(dojo_package_id))).unwrap(); - let starknet_package_id = PackageId::new( - PackageName::STARKNET, - Version::parse("2.1.1").unwrap(), - SourceId::for_std(), - ); - repo.add(Box::new(BuiltinSemanticCairoPlugin::::new(starknet_package_id))) - .unwrap(); + repo.add(Box::new(DojoPlugin)).unwrap(); + repo.add(Box::new(BuiltinStarkNetPlugin)).unwrap(); Self(repo) } } diff --git a/crates/dojo-lang/src/plugin_test.rs b/crates/dojo-lang/src/plugin_test.rs index f0971bea58..3a49e577cd 100644 --- a/crates/dojo-lang/src/plugin_test.rs +++ b/crates/dojo-lang/src/plugin_test.rs @@ -1,54 +1,46 @@ use std::sync::Arc; -use cairo_lang_defs::plugin::PluginGeneratedFile; +use cairo_lang_defs::plugin::{MacroPlugin, PluginGeneratedFile}; use cairo_lang_diagnostics::{format_diagnostics, DiagnosticLocation}; -use cairo_lang_filesystem::db::{FilesDatabase, FilesGroup}; +use cairo_lang_filesystem::cfg::CfgSet; +use cairo_lang_filesystem::db::FilesGroup; use cairo_lang_formatter::format_string; use cairo_lang_parser::test_utils::create_virtual_file; use cairo_lang_parser::utils::{get_syntax_file_and_diagnostics, SimpleParserDatabase}; -use cairo_lang_semantic::plugin::SemanticPlugin; -use cairo_lang_syntax::node::db::SyntaxDatabase; use cairo_lang_syntax::node::TypedSyntaxNode; use cairo_lang_utils::ordered_hash_map::OrderedHashMap; -use cairo_lang_utils::Upcast; use crate::plugin::DojoPlugin; -#[salsa::database(SyntaxDatabase, FilesDatabase)] -#[derive(Default)] -pub struct DatabaseImpl { - storage: salsa::Storage, -} -impl salsa::Database for DatabaseImpl {} -impl Upcast for DatabaseImpl { - fn upcast(&self) -> &(dyn FilesGroup + 'static) { - self - } -} - cairo_lang_test_utils::test_file_test!( expand_plugin, "src/plugin_test_data", { component: "component", system: "system", - inline_macros: "inline_macros", }, test_expand_plugin ); pub fn test_expand_plugin( inputs: &OrderedHashMap, -) -> OrderedHashMap { + _args: &OrderedHashMap, +) -> Result, String> { let db = &mut SimpleParserDatabase::default(); + + let cfg_set: Option = + inputs.get("cfg").map(|s| serde_json::from_str(s.as_str()).unwrap()); + if let Some(cfg_set) = cfg_set { + db.set_cfg_set(Arc::new(cfg_set)); + } + let cairo_code = &inputs["cairo_code"]; let file_id = create_virtual_file(db, "dummy_file.cairo", cairo_code); let (syntax_file, diagnostics) = get_syntax_file_and_diagnostics(db, file_id, cairo_code); assert!(diagnostics.is_empty(), "Unexpected diagnostics:\n{}", diagnostics.format(db)); - let file_syntax_node = syntax_file.as_syntax_node(); - let plugins: Vec> = vec![Arc::new(DojoPlugin)]; + let plugins: Vec> = vec![Arc::new(DojoPlugin)]; let mut generated_items: Vec = Vec::new(); let mut diagnostic_items: Vec = Vec::new(); @@ -56,10 +48,10 @@ pub fn test_expand_plugin( let mut remove_original_item = false; let mut local_generated_items = Vec::::new(); for plugin in &plugins { - let result = plugin.clone().as_dyn_macro_plugin().generate_code(db, item.clone()); + let result = plugin.generate_code(db, item.clone()); diagnostic_items.extend(result.diagnostics.iter().map(|diag| { - let syntax_node = file_syntax_node.lookup_ptr(db, diag.stable_ptr); + let syntax_node = diag.stable_ptr.lookup(db); let location = DiagnosticLocation { file_id, span: syntax_node.span_without_trivia(db) }; @@ -87,8 +79,8 @@ pub fn test_expand_plugin( generated_items.extend(local_generated_items); } - OrderedHashMap::from([ + Ok(OrderedHashMap::from([ ("generated_cairo_code".into(), generated_items.join("\n")), ("expected_diagnostics".into(), diagnostic_items.join("\n")), - ]) + ])) } diff --git a/crates/dojo-lang/src/plugin_test_data/inline_macros b/crates/dojo-lang/src/plugin_test_data/inline_macros deleted file mode 100644 index f8316ac01d..0000000000 --- a/crates/dojo-lang/src/plugin_test_data/inline_macros +++ /dev/null @@ -1,211 +0,0 @@ -//! > Test set! macro. - -//! > test_runner_name -test_expand_plugin - -//! > cairo_code -struct Player { - #[key] - key: felt252, - name: felt252 -} - -struct Position { - #[key] - key: felt252, - x: felt252, - y: felt252 -} - -const world: felt252 = 0xbeef; -const player: Player = Player { key: 'key', name: 'name' }; - -fn foo() { - // Should not emit diagnostics for unknown macros - let arr = array![1, 2, 3]; - set!(world, ( - player, - Position { key: 'key', x: 0, y: 0 }, - )); - - set!(); - set!(world, ()); -} - -//! > generated_cairo_code -struct Player { - #[key] - key: felt252, - name: felt252 -} - - -struct Position { - #[key] - key: felt252, - x: felt252, - y: felt252 -} - - -const world: felt252 = 0xbeef; - -const player: Player = Player { key: 'key', name: 'name' }; - -fn foo() { - // Should not emit diagnostics for unknown macros - let arr = array![1, 2, 3]; - { - let __set_macro_value__ = player; - world - .set_entity( - dojo::traits::Component::name(@__set_macro_value__), - dojo::traits::Component::keys(@__set_macro_value__), - 0_u8, - dojo::traits::Component::values(@__set_macro_value__) - ); - let __set_macro_value__ = Position { key: 'key', x: 0, y: 0 }; - world - .set_entity( - dojo::traits::Component::name(@__set_macro_value__), - dojo::traits::Component::keys(@__set_macro_value__), - 0_u8, - dojo::traits::Component::values(@__set_macro_value__) - ); - }; -; ; } - -//! > expected_diagnostics -error: Invalid arguments. Expected "(world, (components,))" - --> dummy_file.cairo:25:10 - set!(); - ^ - -error: Invalid arguments: No components provided. - --> dummy_file.cairo:26:10 - set!(world, ()); - ^*******^ - -//! > ========================================================================== - -//! > Test get! macro. - -//! > test_runner_name -test_expand_plugin - -//! > cairo_code -fn foo() { - let (position, moves) = get!(world, 0x420, (Position, Moves)); - let position = get!(world, (0x420, 0x1337), Position); - - let id = (0x420, 0x1337); - let position = get!(world, id, Position); -} - -//! > generated_cairo_code -fn foo() { - let (position, moves) = { - let mut __get_macro_keys__ = array::ArrayTrait::new(); - serde::Serde::serialize(@(0x420), ref __get_macro_keys__); - let __get_macro_keys__ = array::ArrayTrait::span(@__get_macro_keys__); - let __Position_values__ = world - .entity('Position', __get_macro_keys__, 0_u8, dojo::SerdeLen::::len()); - let mut __Position_component__ = array::ArrayTrait::new(); - array::serialize_array_helper(__get_macro_keys__, ref __Position_component__); - array::serialize_array_helper(__Position_values__, ref __Position_component__); - let mut __Position_component_span__ = array::ArrayTrait::span(@__Position_component__); - let __Position = option::OptionTrait::expect( - serde::Serde::::deserialize(ref __Position_component_span__), - 'Position failed to deserialize' - ); - let __Moves_values__ = world - .entity('Moves', __get_macro_keys__, 0_u8, dojo::SerdeLen::::len()); - let mut __Moves_component__ = array::ArrayTrait::new(); - array::serialize_array_helper(__get_macro_keys__, ref __Moves_component__); - array::serialize_array_helper(__Moves_values__, ref __Moves_component__); - let mut __Moves_component_span__ = array::ArrayTrait::span(@__Moves_component__); - let __Moves = option::OptionTrait::expect( - serde::Serde::::deserialize(ref __Moves_component_span__), - 'Moves failed to deserialize' - ); - (__Position, __Moves) - }; - let position = { - let mut __get_macro_keys__ = array::ArrayTrait::new(); - serde::Serde::serialize(@(0x420, 0x1337), ref __get_macro_keys__); - let __get_macro_keys__ = array::ArrayTrait::span(@__get_macro_keys__); - let __Position_values__ = world - .entity('Position', __get_macro_keys__, 0_u8, dojo::SerdeLen::::len()); - let mut __Position_component__ = array::ArrayTrait::new(); - array::serialize_array_helper(__get_macro_keys__, ref __Position_component__); - array::serialize_array_helper(__Position_values__, ref __Position_component__); - let mut __Position_component_span__ = array::ArrayTrait::span(@__Position_component__); - let __Position = option::OptionTrait::expect( - serde::Serde::::deserialize(ref __Position_component_span__), - 'Position failed to deserialize' - ); - (__Position) - }; - - let id = (0x420, 0x1337); - let position = { - let mut __get_macro_keys__ = array::ArrayTrait::new(); - serde::Serde::serialize(@id, ref __get_macro_keys__); - let __get_macro_keys__ = array::ArrayTrait::span(@__get_macro_keys__); - let __Position_values__ = world - .entity('Position', __get_macro_keys__, 0_u8, dojo::SerdeLen::::len()); - let mut __Position_component__ = array::ArrayTrait::new(); - array::serialize_array_helper(__get_macro_keys__, ref __Position_component__); - array::serialize_array_helper(__Position_values__, ref __Position_component__); - let mut __Position_component_span__ = array::ArrayTrait::span(@__Position_component__); - let __Position = option::OptionTrait::expect( - serde::Serde::::deserialize(ref __Position_component_span__), - 'Position failed to deserialize' - ); - (__Position) - }; -} - -//! > expected_diagnostics - -//! > ========================================================================== - -//! > Test emit! macro. - -//! > test_runner_name -test_expand_plugin - -//! > cairo_code -fn foo() { - // A comment should not affect macro name - emit!(world, Struct { - x: 10, - }); - - let id = (0x420, 0x1337); - emit!(world, id); -} - -//! > generated_cairo_code -fn foo() { - { - let mut keys = Default::::default(); - let mut data = Default::::default(); - starknet::Event::append_keys_and_data( - @traits::Into::<_, Event>::into(Struct { x: 10, }), ref keys, ref data - ); - world.emit(keys, data.span()); - }; - - let id = (0x420, 0x1337); - { - let mut keys = Default::::default(); - let mut data = Default::::default(); - starknet::Event::append_keys_and_data( - @traits::Into::<_, Event>::into(id), ref keys, ref data - ); - world.emit(keys, data.span()); - }; -} - -//! > expected_diagnostics diff --git a/crates/dojo-lang/src/serde.rs b/crates/dojo-lang/src/serde.rs index 8b4084f14f..6cb1eb14d7 100644 --- a/crates/dojo-lang/src/serde.rs +++ b/crates/dojo-lang/src/serde.rs @@ -1,4 +1,4 @@ -use cairo_lang_semantic::patcher::RewriteNode; +use cairo_lang_defs::patcher::RewriteNode; use cairo_lang_syntax::node::ast::ItemStruct; use cairo_lang_syntax::node::db::SyntaxGroup; use cairo_lang_syntax::node::helpers::QueryAttrs; diff --git a/crates/dojo-lang/src/system/mod.rs b/crates/dojo-lang/src/system.rs similarity index 95% rename from crates/dojo-lang/src/system/mod.rs rename to crates/dojo-lang/src/system.rs index dd3cd26551..1cb7a2a54b 100644 --- a/crates/dojo-lang/src/system/mod.rs +++ b/crates/dojo-lang/src/system.rs @@ -1,10 +1,9 @@ use std::collections::HashMap; +use cairo_lang_defs::patcher::{PatchBuilder, RewriteNode}; use cairo_lang_defs::plugin::{ DynGeneratedFileAuxData, PluginDiagnostic, PluginGeneratedFile, PluginResult, }; -use cairo_lang_semantic::patcher::{PatchBuilder, RewriteNode}; -use cairo_lang_semantic::plugin::DynPluginAuxData; use cairo_lang_syntax::node::ast::OptionReturnTypeClause::ReturnTypeClause; use cairo_lang_syntax::node::ast::{MaybeModuleBody, Param}; use cairo_lang_syntax::node::db::SyntaxGroup; @@ -70,14 +69,14 @@ impl System { code: Some(PluginGeneratedFile { name: name.clone(), content: builder.code, - aux_data: DynGeneratedFileAuxData::new(DynPluginAuxData::new(DojoAuxData { - patches: builder.patches, + aux_data: Some(DynGeneratedFileAuxData::new(DojoAuxData { components: vec![], systems: vec![SystemAuxData { name, dependencies: system.dependencies.values().cloned().collect(), }], })), + diagnostics_mappings: builder.diagnostics_mappings, }), diagnostics: system.diagnostics, remove_original_item: true, diff --git a/crates/dojo-language-server/src/bin/language_server.rs b/crates/dojo-language-server/src/bin/language_server.rs index 68f0ec0aa1..dd95b30a4d 100644 --- a/crates/dojo-language-server/src/bin/language_server.rs +++ b/crates/dojo-language-server/src/bin/language_server.rs @@ -29,8 +29,8 @@ async fn main() { let db = RootDatabase::builder() .with_cfg(CfgSet::from_iter([Cfg::name("test")])) - .with_semantic_plugin(Arc::new(DojoPlugin)) - .with_semantic_plugin(Arc::new(StarkNetPlugin::default())) + .with_macro_plugin(Arc::new(DojoPlugin)) + .with_macro_plugin(Arc::new(StarkNetPlugin::default())) .build() .unwrap_or_else(|error| { panic!("Problem creating language database: {error:?}"); diff --git a/crates/dojo-test-utils/Cargo.toml b/crates/dojo-test-utils/Cargo.toml index c84848d640..ea2ac48380 100644 --- a/crates/dojo-test-utils/Cargo.toml +++ b/crates/dojo-test-utils/Cargo.toml @@ -18,6 +18,7 @@ dojo-lang = { path = "../dojo-lang" } jsonrpsee = { version = "0.16.2", features = [ "server" ] } katana-core = { path = "../katana/core" } katana-rpc = { path = "../katana/rpc" } +scarb-ui.workspace = true scarb.workspace = true serde.workspace = true serde_json.workspace = true @@ -34,6 +35,7 @@ url = "2.2.2" assert_fs = "1.0.9" camino.workspace = true dojo-lang = { path = "../dojo-lang" } +scarb-ui.workspace = true scarb.workspace = true [features] diff --git a/crates/dojo-test-utils/build.rs b/crates/dojo-test-utils/build.rs index 98c824fe37..cfa8f34a9a 100644 --- a/crates/dojo-test-utils/build.rs +++ b/crates/dojo-test-utils/build.rs @@ -8,7 +8,7 @@ fn main() { use scarb::compiler::CompilerRepository; use scarb::core::Config; use scarb::ops; - use scarb::ui::Verbosity; + use scarb_ui::Verbosity; let target_path = Utf8PathBuf::from_path_buf("../../examples/ecs/target".into()).unwrap(); if target_path.exists() { diff --git a/crates/dojo-test-utils/src/compiler.rs b/crates/dojo-test-utils/src/compiler.rs index e801e7469e..0b79059432 100644 --- a/crates/dojo-test-utils/src/compiler.rs +++ b/crates/dojo-test-utils/src/compiler.rs @@ -6,7 +6,7 @@ use dojo_lang::compiler::DojoCompiler; use dojo_lang::plugin::CairoPluginRepository; use scarb::compiler::CompilerRepository; use scarb::core::Config; -use scarb::ui::Verbosity; +use scarb_ui::Verbosity; pub fn build_test_config(path: &str) -> anyhow::Result { let mut compilers = CompilerRepository::empty(); diff --git a/crates/katana/core/Cargo.toml b/crates/katana/core/Cargo.toml index a4d8cb5f91..4e95f0e7d4 100644 --- a/crates/katana/core/Cargo.toml +++ b/crates/katana/core/Cargo.toml @@ -11,8 +11,8 @@ anyhow.workspace = true async-trait.workspace = true auto_impl = "1.1.0" blockifier.workspace = true -cairo-lang-casm.workspace = true -cairo-lang-starknet.workspace = true +cairo-lang-casm = "2.2.0" +cairo-lang-starknet = "2.2.0" cairo-vm.workspace = true convert_case.workspace = true flate2.workspace = true diff --git a/crates/katana/rpc/Cargo.toml b/crates/katana/rpc/Cargo.toml index 44801d0d66..3ad2feb3d3 100644 --- a/crates/katana/rpc/Cargo.toml +++ b/crates/katana/rpc/Cargo.toml @@ -9,7 +9,7 @@ version.workspace = true [dependencies] anyhow.workspace = true blockifier.workspace = true -cairo-lang-starknet.workspace = true +cairo-lang-starknet = "2.2.0" cairo-vm.workspace = true flate2.workspace = true futures.workspace = true diff --git a/crates/sozo/Cargo.toml b/crates/sozo/Cargo.toml index 780355a9ef..68932adf12 100644 --- a/crates/sozo/Cargo.toml +++ b/crates/sozo/Cargo.toml @@ -24,6 +24,7 @@ console.workspace = true dojo-lang = { path = "../dojo-lang" } dojo-world = { path = "../dojo-world" } log.workspace = true +scarb-ui.workspace = true scarb.workspace = true semver.workspace = true serde.workspace = true diff --git a/crates/sozo/src/args.rs b/crates/sozo/src/args.rs index a5c88af2d2..20eb583b12 100644 --- a/crates/sozo/src/args.rs +++ b/crates/sozo/src/args.rs @@ -2,7 +2,7 @@ use anyhow::Result; use camino::Utf8PathBuf; use clap::{Parser, Subcommand}; use scarb::compiler::Profile; -use scarb::ui; +use scarb_ui::Verbosity; use smol_str::SmolStr; use tracing::level_filters::LevelFilter; use tracing_log::AsTrace; @@ -76,14 +76,14 @@ pub enum Commands { } impl SozoArgs { - pub fn ui_verbosity(&self) -> ui::Verbosity { + pub fn ui_verbosity(&self) -> Verbosity { let filter = self.verbose.log_level_filter().as_trace(); if filter >= LevelFilter::WARN { - ui::Verbosity::Verbose + Verbosity::Verbose } else if filter > LevelFilter::OFF { - ui::Verbosity::Normal + Verbosity::Normal } else { - ui::Verbosity::Quiet + Verbosity::Quiet } } } diff --git a/crates/sozo/src/commands/test.rs b/crates/sozo/src/commands/test.rs index 9793dce7d0..82a750101e 100644 --- a/crates/sozo/src/commands/test.rs +++ b/crates/sozo/src/commands/test.rs @@ -8,11 +8,15 @@ use cairo_lang_compiler::diagnostics::DiagnosticsReporter; use cairo_lang_compiler::project::{ProjectConfig, ProjectConfigContent}; use cairo_lang_filesystem::cfg::{Cfg, CfgSet}; use cairo_lang_filesystem::ids::Directory; +use cairo_lang_starknet::inline_macros::selector::SelectorMacro; use cairo_lang_starknet::plugin::StarkNetPlugin; use cairo_lang_test_runner::plugin::TestPlugin; use cairo_lang_test_runner::TestRunner; use clap::Args; use dojo_lang::compiler::{collect_core_crate_ids, collect_external_crate_ids, Props}; +use dojo_lang::inline_macros::emit::EmitMacro; +use dojo_lang::inline_macros::get::GetMacro; +use dojo_lang::inline_macros::set::SetMacro; use dojo_lang::plugin::DojoPlugin; use scarb::compiler::helpers::collect_main_crate_ids; use scarb::compiler::CompilationUnit; @@ -49,6 +53,7 @@ impl TestArgs { let db = build_root_database(&unit)?; let mut main_crate_ids = collect_main_crate_ids(&unit, &db); + let test_crate_ids = main_crate_ids.clone(); if unit.main_package_id.name.to_string() != "dojo" { let core_crate_ids = collect_core_crate_ids(&db); @@ -66,6 +71,7 @@ impl TestArgs { let runner = TestRunner { db, main_crate_ids, + test_crate_ids, filter: self.filter.clone(), include_ignored: self.include_ignored, ignored: self.ignored, @@ -85,9 +91,14 @@ pub(crate) fn build_root_database(unit: &CompilationUnit) -> Result Result { .map(|component| (component.cairo_package_name(), component.target.source_root().into())) .collect(); - let corelib = Some(Directory(unit.core_package_component().target.source_root().into())); + let corelib = Some(Directory::Real(unit.core_package_component().target.source_root().into())); let content = ProjectConfigContent { crate_roots }; diff --git a/crates/sozo/src/main.rs b/crates/sozo/src/main.rs index 81199c352f..290b8af897 100644 --- a/crates/sozo/src/main.rs +++ b/crates/sozo/src/main.rs @@ -7,7 +7,7 @@ use dojo_lang::compiler::DojoCompiler; use dojo_lang::plugin::CairoPluginRepository; use scarb::compiler::CompilerRepository; use scarb::core::Config; -use scarb::ui::{OutputFormat, Ui}; +use scarb_ui::{OutputFormat, Ui}; mod args; mod commands; diff --git a/crates/sozo/src/ops/migration/migration_test.rs b/crates/sozo/src/ops/migration/migration_test.rs index ee9792078b..4f21ce18ed 100644 --- a/crates/sozo/src/ops/migration/migration_test.rs +++ b/crates/sozo/src/ops/migration/migration_test.rs @@ -6,7 +6,7 @@ use dojo_world::manifest::Manifest; use dojo_world::migration::strategy::prepare_for_migration; use dojo_world::migration::world::WorldDiff; use scarb::core::Config; -use scarb::ui::Verbosity; +use scarb_ui::Verbosity; use starknet::accounts::SingleOwnerAccount; use starknet::core::chain_id; use starknet::core::types::FieldElement; diff --git a/crates/sozo/src/ops/migration/ui.rs b/crates/sozo/src/ops/migration/ui.rs index b20b14728e..7ba22923fb 100644 --- a/crates/sozo/src/ops/migration/ui.rs +++ b/crates/sozo/src/ops/migration/ui.rs @@ -1,5 +1,5 @@ use console::{pad_str, Alignment, Style, StyledObject}; -use scarb::ui::Ui; +use scarb_ui::Ui; pub trait MigrationUi { fn print_step(&self, step: usize, icon: &str, message: &str); diff --git a/crates/torii/client/src/contract/component_test.rs b/crates/torii/client/src/contract/component_test.rs index c4fbeaf54b..8fd606108a 100644 --- a/crates/torii/client/src/contract/component_test.rs +++ b/crates/torii/client/src/contract/component_test.rs @@ -28,7 +28,7 @@ async fn test_component() { assert_eq!( component.class_hash(), FieldElement::from_hex_be( - "0x04973c97a1cd5e141d6dfe05c36517234851118ea703e510fcb72a39a092c228" + "0x06a8ab7eb5689bed6f0e9fb63d2565411830e1725aca7299f5f512d375d9a28c" ) .unwrap() ); diff --git a/crates/torii/client/wasm/yarn.lock b/crates/torii/client/wasm/yarn.lock index 173206106d..c77ea551c9 100644 --- a/crates/torii/client/wasm/yarn.lock +++ b/crates/torii/client/wasm/yarn.lock @@ -103,7 +103,7 @@ dependencies: chalk "^2.4.1" command-exists "^1.2.7" - watchpack "^2.1.1" + watchpack "^2.2.0" which "^2.0.2" "@webassemblyjs/ast@1.11.6", "@webassemblyjs/ast@^1.11.5": @@ -303,8 +303,8 @@ ansi-html-community@0.0.8: integrity sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw== ansi-regex@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + version "2.2.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.2.0.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" integrity sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA== ansi-regex@^4.1.0: @@ -330,7 +330,7 @@ anymatch@^2.0.0: integrity sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw== dependencies: micromatch "^3.1.4" - normalize-path "^2.1.1" + normalize-path "^2.2.0" arr-diff@^4.0.0: version "4.0.0" @@ -794,7 +794,7 @@ debug@^3.2.7: resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== dependencies: - ms "^2.1.1" + ms "^2.2.0" debug@^4.1.0, debug@^4.1.1: version "4.3.4" @@ -1700,9 +1700,9 @@ is-extendable@^1.0.1: dependencies: is-plain-object "^2.0.4" -is-extglob@^2.1.0, is-extglob@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" +is-extglob@^2.1.0, is-extglob@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.2.0.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== is-fullwidth-code-point@^2.0.0: @@ -1722,7 +1722,7 @@ is-glob@^4.0.0: resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== dependencies: - is-extglob "^2.1.1" + is-extglob "^2.2.0" is-number@^3.0.0: version "3.0.0" @@ -1951,7 +1951,7 @@ mime-db@1.52.0, "mime-db@>= 1.43.0 < 2": resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== -mime-types@^2.1.27, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.34: +mime-types@^2.1.27, mime-types@~2.2.07, mime-types@~2.1.24, mime-types@~2.1.34: version "2.1.35" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== @@ -2010,7 +2010,7 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@2.1.3, ms@^2.1.1: +ms@2.1.3, ms@^2.2.0: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -2083,9 +2083,9 @@ node-releases@^2.0.13: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.13.tgz#d5ed1627c23e3461e819b02e57b75e4899b1c81d" integrity sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ== -normalize-path@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" +normalize-path@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.2.0.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" integrity sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w== dependencies: remove-trailing-separator "^1.0.1" @@ -2103,8 +2103,8 @@ npm-run-path@^2.0.0: path-key "^2.0.0" nth-check@^2.0.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" + version "2.2.0" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.2.0.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w== dependencies: boolbase "^1.0.0" @@ -2413,7 +2413,7 @@ qs@^6.11.0: dependencies: side-channel "^1.0.4" -querystringify@^2.1.1: +querystringify@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== @@ -2526,9 +2526,9 @@ repeat-string@^1.6.1: resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" integrity sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w== -require-directory@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" +require-directory@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.2.0.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== require-main-filename@^2.0.0: @@ -2694,7 +2694,7 @@ serve-index@^1.9.1: debug "2.6.9" escape-html "~1.0.3" http-errors "~1.6.2" - mime-types "~2.1.17" + mime-types "~2.2.07" parseurl "~1.3.2" serve-static@1.15.0: @@ -2778,8 +2778,8 @@ signal-exit@^3.0.0: integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== snapdragon-node@^2.0.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" + version "2.2.0" + resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.2.0.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw== dependencies: define-property "^1.0.0" @@ -2984,7 +2984,7 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== -tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0: +tapable@^2.0.0, tapable@^2.2.0, tapable@^2.2.0: version "2.2.1" resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== @@ -3028,8 +3028,8 @@ to-object-path@^0.3.0: kind-of "^3.0.2" to-regex-range@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" + version "2.2.0" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.2.0.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" integrity sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg== dependencies: is-number "^3.0.0" @@ -3116,7 +3116,7 @@ url-parse@^1.5.10: resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1" integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ== dependencies: - querystringify "^2.1.1" + querystringify "^2.2.0" requires-port "^1.0.0" url@^0.11.0: @@ -3162,7 +3162,7 @@ vary@~1.1.2: resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== -watchpack@^2.1.1, watchpack@^2.4.0: +watchpack@^2.2.0, watchpack@^2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg== @@ -3291,7 +3291,7 @@ webpack@^5.49.0: mime-types "^2.1.27" neo-async "^2.6.2" schema-utils "^3.2.0" - tapable "^2.1.1" + tapable "^2.2.0" terser-webpack-plugin "^5.3.7" watchpack "^2.4.0" webpack-sources "^3.2.3" @@ -3376,7 +3376,7 @@ yargs@^13.3.2: cliui "^5.0.0" find-up "^3.0.0" get-caller-file "^2.0.1" - require-directory "^2.1.1" + require-directory "^2.2.0" require-main-filename "^2.0.0" set-blocking "^2.0.0" string-width "^3.0.0" diff --git a/crates/torii/core/Cargo.toml b/crates/torii/core/Cargo.toml index 8df92791ac..f84df3705c 100644 --- a/crates/torii/core/Cargo.toml +++ b/crates/torii/core/Cargo.toml @@ -10,13 +10,18 @@ version.workspace = true [dependencies] anyhow.workspace = true +async-stream = "0.3.0" async-trait.workspace = true chrono.workspace = true dojo-types = { path = "../../dojo-types" } dojo-world = { path = "../../dojo-world" } - +futures-channel = "0.3.0" +futures-util = "0.3.0" +log = "0.4.17" +once_cell.workspace = true serde.workspace = true serde_json.workspace = true +slab = "0.4.2" sqlx = { version = "0.6.2", features = [ "chrono", "macros", "offline", "runtime-actix-rustls", "sqlite", "uuid" ] } starknet-crypto.workspace = true starknet.workspace = true @@ -25,12 +30,5 @@ tokio-util = "0.7.7" tokio.workspace = true tracing.workspace = true -#Dynamic subscriber -async-stream = "0.3.0" -futures-channel = "0.3.0" -futures-util = "0.3.0" -once_cell = "1.0" -slab = "0.4.2" - [dev-dependencies] camino.workspace = true diff --git a/examples/ecs/Scarb.toml b/examples/ecs/Scarb.toml index 69e170fb7b..1f51b8f574 100644 --- a/examples/ecs/Scarb.toml +++ b/examples/ecs/Scarb.toml @@ -1,5 +1,5 @@ [package] -cairo-version = "2.1.1" +cairo-version = "2.2.0" name = "dojo_examples" version = "0.2.1" diff --git a/examples/ecs/src/systems.cairo b/examples/ecs/src/systems.cairo index f42d47a489..3ad02ee7e1 100644 --- a/examples/ecs/src/systems.cairo +++ b/examples/ecs/src/systems.cairo @@ -120,7 +120,7 @@ mod tests { let mut components = array::ArrayTrait::new(); components.append(position::TEST_CLASS_HASH); components.append(moves::TEST_CLASS_HASH); - components.append(dojo_erc::erc20::components::balance::TEST_CLASS_HASH); + // components.append(dojo_erc::erc20::components::balance::TEST_CLASS_HASH); // systems let mut systems = array::ArrayTrait::new(); systems.append(spawn::TEST_CLASS_HASH); diff --git a/packages/core/yarn.lock b/packages/core/yarn.lock index 41acaebed9..fc7055bd33 100644 --- a/packages/core/yarn.lock +++ b/packages/core/yarn.lock @@ -835,9 +835,9 @@ bs-logger@0.x: dependencies: fast-json-stable-stringify "2.x" -bser@2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" +bser@2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/bser/-/bser-2.2.0.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" integrity sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ== dependencies: node-int64 "^0.4.0" @@ -1082,7 +1082,7 @@ fb-watchman@^2.0.0: resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.2.tgz#e9524ee6b5c77e9e5001af0f85f3adbb8623255c" integrity sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA== dependencies: - bser "2.1.1" + bser "2.2.0" fetch-mock@^9.11.0: version "9.11.0" @@ -1999,9 +1999,9 @@ regenerator-runtime@^0.13.11: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== -require-directory@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" +require-directory@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.2.0.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== resolve-cwd@^3.0.0: @@ -2356,7 +2356,7 @@ yargs@^17.3.1: cliui "^8.0.1" escalade "^3.1.1" get-caller-file "^2.0.5" - require-directory "^2.1.1" + require-directory "^2.2.0" string-width "^4.2.3" y18n "^5.0.5" yargs-parser "^21.1.1" diff --git a/packages/react/yarn.lock b/packages/react/yarn.lock index 7bb1f31d0e..01a23b34bc 100644 --- a/packages/react/yarn.lock +++ b/packages/react/yarn.lock @@ -1010,34 +1010,34 @@ resolved "https://registry.yarnpkg.com/@cartridge/penpal/-/penpal-6.2.3.tgz#c94e3ca6a48732e3192e07bb810cb11d426a71b0" integrity sha512-K8h9VqBfFPXcAFQNnvgBnejF/dp7249pS4jXu3NhNYR6JqMQxtcrDqfnPmJvbF4ECEBs+8Z2UiwlRQiKt5nNsg== -"@cbor-extract/cbor-extract-darwin-arm64@2.1.1": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-darwin-arm64/-/cbor-extract-darwin-arm64-2.1.1.tgz#5721f6dd3feae0b96d23122853ce977e0671b7a6" +"@cbor-extract/cbor-extract-darwin-arm64@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-darwin-arm64/-/cbor-extract-darwin-arm64-2.2.0.tgz#5721f6dd3feae0b96d23122853ce977e0671b7a6" integrity sha512-blVBy5MXz6m36Vx0DfLd7PChOQKEs8lK2bD1WJn/vVgG4FXZiZmZb2GECHFvVPA5T7OnODd9xZiL3nMCv6QUhA== -"@cbor-extract/cbor-extract-darwin-x64@2.1.1": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-darwin-x64/-/cbor-extract-darwin-x64-2.1.1.tgz#c25e7d0133950d87d101d7b3afafea8d50d83f5f" +"@cbor-extract/cbor-extract-darwin-x64@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-darwin-x64/-/cbor-extract-darwin-x64-2.2.0.tgz#c25e7d0133950d87d101d7b3afafea8d50d83f5f" integrity sha512-h6KFOzqk8jXTvkOftyRIWGrd7sKQzQv2jVdTL9nKSf3D2drCvQB/LHUxAOpPXo3pv2clDtKs3xnHalpEh3rDsw== -"@cbor-extract/cbor-extract-linux-arm64@2.1.1": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-linux-arm64/-/cbor-extract-linux-arm64-2.1.1.tgz#48f78e7d8f0fcc84ed074b6bfa6d15dd83187c63" +"@cbor-extract/cbor-extract-linux-arm64@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-linux-arm64/-/cbor-extract-linux-arm64-2.2.0.tgz#48f78e7d8f0fcc84ed074b6bfa6d15dd83187c63" integrity sha512-SxAaRcYf8S0QHaMc7gvRSiTSr7nUYMqbUdErBEu+HYA4Q6UNydx1VwFE68hGcp1qvxcy9yT5U7gA+a5XikfwSQ== -"@cbor-extract/cbor-extract-linux-arm@2.1.1": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-linux-arm/-/cbor-extract-linux-arm-2.1.1.tgz#7507d346389cb682e44fab8fae9534edd52e2e41" +"@cbor-extract/cbor-extract-linux-arm@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-linux-arm/-/cbor-extract-linux-arm-2.2.0.tgz#7507d346389cb682e44fab8fae9534edd52e2e41" integrity sha512-ds0uikdcIGUjPyraV4oJqyVE5gl/qYBpa/Wnh6l6xLE2lj/hwnjT2XcZCChdXwW/YFZ1LUHs6waoYN8PmK0nKQ== -"@cbor-extract/cbor-extract-linux-x64@2.1.1": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-linux-x64/-/cbor-extract-linux-x64-2.1.1.tgz#b7c1d2be61c58ec18d58afbad52411ded63cd4cd" +"@cbor-extract/cbor-extract-linux-x64@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-linux-x64/-/cbor-extract-linux-x64-2.2.0.tgz#b7c1d2be61c58ec18d58afbad52411ded63cd4cd" integrity sha512-GVK+8fNIE9lJQHAlhOROYiI0Yd4bAZ4u++C2ZjlkS3YmO6hi+FUxe6Dqm+OKWTcMpL/l71N6CQAmaRcb4zyJuA== -"@cbor-extract/cbor-extract-win32-x64@2.1.1": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-win32-x64/-/cbor-extract-win32-x64-2.1.1.tgz#21b11a1a3f18c3e7d62fd5f87438b7ed2c64c1f7" +"@cbor-extract/cbor-extract-win32-x64@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-win32-x64/-/cbor-extract-win32-x64-2.2.0.tgz#21b11a1a3f18c3e7d62fd5f87438b7ed2c64c1f7" integrity sha512-2Niq1C41dCRIDeD8LddiH+mxGlO7HJ612Ll3D/E73ZWBmycued+8ghTr/Ho3CMOWPUEr08XtyBMVXAjqF+TcKw== "@ethersproject/bytes@^5.6.1": @@ -1716,9 +1716,9 @@ bs-logger@0.x: dependencies: fast-json-stable-stringify "2.x" -bser@2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" +bser@2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/bser/-/bser-2.2.0.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" integrity sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ== dependencies: node-int64 "^0.4.0" @@ -1748,26 +1748,26 @@ caniuse-lite@^1.0.30001489: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001495.tgz#64a0ccef1911a9dcff647115b4430f8eff1ef2d9" integrity sha512-F6x5IEuigtUfU5ZMQK2jsy5JqUUlEFRVZq8bO2a+ysq5K7jD6PPc9YXZj78xDNS3uNchesp1Jw47YXEqr+Viyg== -cbor-extract@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/cbor-extract/-/cbor-extract-2.1.1.tgz#f154b31529fdb6b7c70fb3ca448f44eda96a1b42" +cbor-extract@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/cbor-extract/-/cbor-extract-2.2.0.tgz#f154b31529fdb6b7c70fb3ca448f44eda96a1b42" integrity sha512-1UX977+L+zOJHsp0mWFG13GLwO6ucKgSmSW6JTl8B9GUvACvHeIVpFqhU92299Z6PfD09aTXDell5p+lp1rUFA== dependencies: node-gyp-build-optional-packages "5.0.3" optionalDependencies: - "@cbor-extract/cbor-extract-darwin-arm64" "2.1.1" - "@cbor-extract/cbor-extract-darwin-x64" "2.1.1" - "@cbor-extract/cbor-extract-linux-arm" "2.1.1" - "@cbor-extract/cbor-extract-linux-arm64" "2.1.1" - "@cbor-extract/cbor-extract-linux-x64" "2.1.1" - "@cbor-extract/cbor-extract-win32-x64" "2.1.1" + "@cbor-extract/cbor-extract-darwin-arm64" "2.2.0" + "@cbor-extract/cbor-extract-darwin-x64" "2.2.0" + "@cbor-extract/cbor-extract-linux-arm" "2.2.0" + "@cbor-extract/cbor-extract-linux-arm64" "2.2.0" + "@cbor-extract/cbor-extract-linux-x64" "2.2.0" + "@cbor-extract/cbor-extract-win32-x64" "2.2.0" cbor-x@^1.5.0: version "1.5.3" resolved "https://registry.yarnpkg.com/cbor-x/-/cbor-x-1.5.3.tgz#f8252fec7cab86b66c500e0c991788618e6638de" integrity sha512-adrN0S67C7jY2hgqeGcw+Uj6iEGLQa5D/p6/9YNl5AaVIYJaJz/bARfWsP8UikBZWbhS27LN0DJK4531vo9ODw== optionalDependencies: - cbor-extract "^2.1.1" + cbor-extract "^2.2.0" chalk@^2.0.0: version "2.4.2" @@ -2033,7 +2033,7 @@ fb-watchman@^2.0.0: resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.2.tgz#e9524ee6b5c77e9e5001af0f85f3adbb8623255c" integrity sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA== dependencies: - bser "2.1.1" + bser "2.2.0" fill-range@^7.0.1: version "7.0.1" @@ -3014,9 +3014,9 @@ regjsparser@^0.9.1: dependencies: jsesc "~0.5.0" -require-directory@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" +require-directory@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.2.0.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== resolve-cwd@^3.0.0: @@ -3415,7 +3415,7 @@ yargs@^17.3.1: cliui "^8.0.1" escalade "^3.1.1" get-caller-file "^2.0.5" - require-directory "^2.1.1" + require-directory "^2.2.0" string-width "^4.2.3" y18n "^5.0.5" yargs-parser "^21.1.1" From 40ea4d86a9739fd85ca153f56668c8797bb3750f Mon Sep 17 00:00:00 2001 From: Loaf <90423308+ponderingdemocritus@users.noreply.github.com> Date: Thu, 7 Sep 2023 09:26:24 +1000 Subject: [PATCH 28/77] Packages/fix core (#866) * v0.0.19 * fix entity endpoint --- packages/core/package.json | 2 +- packages/core/src/provider/RPCProvider.ts | 1 - packages/core/src/types/index.ts | 5 +++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index b4901c81f3..f828f936b0 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@dojoengine/core", - "version": "0.0.18", + "version": "0.0.19", "description": "Dojo engine core providers and types", "scripts": { "build": "tsc", diff --git a/packages/core/src/provider/RPCProvider.ts b/packages/core/src/provider/RPCProvider.ts index f596e7ed86..bb1be93b5f 100644 --- a/packages/core/src/provider/RPCProvider.ts +++ b/packages/core/src/provider/RPCProvider.ts @@ -41,7 +41,6 @@ export class RPCProvider extends Provider { contractAddress: this.getWorldAddress(), calldata: [ strTofelt252Felt(component), - query.address_domain, query.keys.length, ...query.keys as any, offset, diff --git a/packages/core/src/types/index.ts b/packages/core/src/types/index.ts index 10f5ff929a..297b949c85 100644 --- a/packages/core/src/types/index.ts +++ b/packages/core/src/types/index.ts @@ -1,3 +1,5 @@ +import { num } from "starknet"; + /** * Enumeration representing various entry points or functions available in the World. */ @@ -16,8 +18,7 @@ export enum WorldEntryPoints { * Interface representing a query structure with domain and keys. */ export interface Query { - address_domain: string, // The domain or scope of the address being queried - keys: bigint[] // A list of keys used in the query + keys: num.BigNumberish[] // A list of keys used in the query } /** From f0696801b6faeb0818039af7b41ae4eae3c903c3 Mon Sep 17 00:00:00 2001 From: Alex Metelli Date: Thu, 7 Sep 2023 07:26:39 +0800 Subject: [PATCH 29/77] Add reverse vrgdas (#858) * refacto market + moved test to separate folderl * added reverse for vrgda --- .../dojo-defi/src/dutch_auction/vrgda.cairo | 21 +++++++++++++++++++ .../src/tests/linear_vrgda_test.cairo | 14 +++++++++++++ .../src/tests/logistic_vrgda_test.cairo | 16 ++++++++++++++ 3 files changed, 51 insertions(+) diff --git a/crates/dojo-defi/src/dutch_auction/vrgda.cairo b/crates/dojo-defi/src/dutch_auction/vrgda.cairo index 388787c407..3790bfc64a 100644 --- a/crates/dojo-defi/src/dutch_auction/vrgda.cairo +++ b/crates/dojo-defi/src/dutch_auction/vrgda.cairo @@ -44,8 +44,18 @@ impl LinearVRGDAImpl of LinearVRGDATrait { - self.get_target_sale_time(sold + FixedTrait::new(1, false))) ) } + + fn get_reverse_vrgda_price(self: @LinearVRGDA, time_since_start: Fixed, sold: Fixed) -> Fixed { + *self.target_price + * exp( + (*self.decay_constant * FixedTrait::new(1, true)) + * (time_since_start + - self.get_target_sale_time(sold + FixedTrait::new(1, false))) + ) + } } + #[derive(Copy, Drop, Serde, starknet::Storage)] struct LogisticVRGDA { target_price: Fixed, @@ -96,5 +106,16 @@ impl LogisticVRGDAImpl of LogisticVRGDATrait { - self.get_target_sale_time(sold + FixedTrait::new(1, false))) ) } + + fn get_reverse_vrgda_price( + self: @LogisticVRGDA, time_since_start: Fixed, sold: Fixed + ) -> Fixed { + *self.target_price + * exp( + (*self.decay_constant * FixedTrait::new(1, true)) + * (time_since_start + - self.get_target_sale_time(sold + FixedTrait::new(1, false))) + ) + } } diff --git a/crates/dojo-defi/src/tests/linear_vrgda_test.cairo b/crates/dojo-defi/src/tests/linear_vrgda_test.cairo index a010a250fb..3a532d06e6 100644 --- a/crates/dojo-defi/src/tests/linear_vrgda_test.cairo +++ b/crates/dojo-defi/src/tests/linear_vrgda_test.cairo @@ -38,3 +38,17 @@ fn test_pricing_basic() { assert_rel_approx_eq(cost, auction.target_price, FixedTrait::new(DELTA_0_02, false)); } +#[test] +#[available_gas(20000000)] +fn test_pricing_basic_reverse() { + let auction = LinearVRGDA { + target_price: FixedTrait::new(_69_42, false), + decay_constant: FixedTrait::new(_0_31, false), + per_time_unit: FixedTrait::new_unscaled(2, false), + }; + let time_delta = FixedTrait::new(10368001, false); // 120 days + let num_mint = FixedTrait::new(239, true); + let cost = auction.get_reverse_vrgda_price(time_delta, num_mint); + assert_rel_approx_eq(cost, auction.target_price, FixedTrait::new(DELTA_0_02, false)); +} + diff --git a/crates/dojo-defi/src/tests/logistic_vrgda_test.cairo b/crates/dojo-defi/src/tests/logistic_vrgda_test.cairo index 020c298f3d..c8e7a13cb2 100644 --- a/crates/dojo-defi/src/tests/logistic_vrgda_test.cairo +++ b/crates/dojo-defi/src/tests/logistic_vrgda_test.cairo @@ -44,3 +44,19 @@ fn test_pricing_basic() { assert_rel_approx_eq(cost, auction.target_price, FixedTrait::new(DELTA_0_02, false)); } +#[test] +#[available_gas(200000000)] +fn test_pricing_basic_reverse() { + let auction = LogisticVRGDA { + target_price: FixedTrait::new(_69_42, false), + decay_constant: FixedTrait::new(_0_31, false), + max_sellable: FixedTrait::new_unscaled(MAX_SELLABLE, false), + time_scale: FixedTrait::new(_0_0023, false), + }; + let time_delta = FixedTrait::new(10368001, false); + let num_mint = FixedTrait::new(876, false); + + let cost = auction.get_reverse_vrgda_price(time_delta, num_mint); + assert_rel_approx_eq(cost, auction.target_price, FixedTrait::new(DELTA_0_02, false)); +} + From 723ac3e099a5aad01d8d97184102cd370406da76 Mon Sep 17 00:00:00 2001 From: Yun Date: Thu, 7 Sep 2023 06:02:35 -0700 Subject: [PATCH 30/77] fix(torii): prevent duplicates of component members in db (#869) --- crates/torii/migrations/20230316154230_setup.sql | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/torii/migrations/20230316154230_setup.sql b/crates/torii/migrations/20230316154230_setup.sql index 2d2e0b54bf..5590bf6532 100644 --- a/crates/torii/migrations/20230316154230_setup.sql +++ b/crates/torii/migrations/20230316154230_setup.sql @@ -31,6 +31,7 @@ CREATE TABLE component_members( key BOOLEAN NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (component_id) REFERENCES components(id) + UNIQUE (component_id, name) ); CREATE INDEX idx_component_members_component_id ON component_members (component_id); From e1a70e36dcdfb5d07a4ccc8920f550c74ff65979 Mon Sep 17 00:00:00 2001 From: Tarrence van As Date: Thu, 7 Sep 2023 10:06:22 -0400 Subject: [PATCH 31/77] Rename SerdeLen to ComponentSize and derive with Component (#868) * Rename SerdeLen to ComponentSize and derive with Component * Add packing utils --- .../src/{traits.cairo => component.cairo} | 0 crates/dojo-core/src/database/storage.cairo | 126 +++++++++++++++ crates/dojo-core/src/executor_test.cairo | 2 +- crates/dojo-core/src/lib.cairo | 8 +- crates/dojo-core/src/packing.cairo | 145 ++++++++++++++++++ crates/dojo-core/src/packing_test.cairo | 25 +++ crates/dojo-core/src/serde.cairo | 74 --------- crates/dojo-core/src/world.cairo | 2 +- crates/dojo-core/src/world_factory_test.cairo | 2 +- crates/dojo-core/src/world_test.cairo | 8 +- crates/dojo-defi/src/market/components.cairo | 12 +- crates/dojo-erc/src/erc1155/components.cairo | 4 +- crates/dojo-erc/src/erc20/components.cairo | 6 +- crates/dojo-erc/src/erc721/components.cairo | 6 +- .../components/base_uri_component.cairo | 2 +- .../operator_approval_component.cairo | 2 +- crates/dojo-lang/src/component.rs | 56 ++++++- crates/dojo-lang/src/inline_macros/get.rs | 2 +- crates/dojo-lang/src/inline_macros/set.rs | 6 +- crates/dojo-lang/src/lib.rs | 1 - crates/dojo-lang/src/plugin.rs | 4 - .../dojo-lang/src/plugin_test_data/component | 82 +++++----- crates/dojo-lang/src/serde.rs | 50 ------ examples/ecs/src/components.cairo | 4 +- examples/ecs/src/systems.cairo | 4 +- 25 files changed, 431 insertions(+), 202 deletions(-) rename crates/dojo-core/src/{traits.cairo => component.cairo} (100%) create mode 100644 crates/dojo-core/src/packing.cairo create mode 100644 crates/dojo-core/src/packing_test.cairo delete mode 100644 crates/dojo-core/src/serde.cairo delete mode 100644 crates/dojo-lang/src/serde.rs diff --git a/crates/dojo-core/src/traits.cairo b/crates/dojo-core/src/component.cairo similarity index 100% rename from crates/dojo-core/src/traits.cairo rename to crates/dojo-core/src/component.cairo diff --git a/crates/dojo-core/src/database/storage.cairo b/crates/dojo-core/src/database/storage.cairo index 648ccfbbed..18bb054260 100644 --- a/crates/dojo-core/src/database/storage.cairo +++ b/crates/dojo-core/src/database/storage.cairo @@ -60,3 +60,129 @@ fn set_many(address_domain: u32, keys: Span, offset: u8, mut value: Spa }; }; } + + +trait StorageSize { + fn unpacked_size() -> usize; + fn packed_size() -> usize; +} + +impl StorageSizeFelt252 of StorageSize { + #[inline(always)] + fn unpacked_size() -> usize { + 1 + } + + #[inline(always)] + fn packed_size() -> usize { + 252 + } +} + +impl StorageSizeBool of StorageSize { + #[inline(always)] + fn unpacked_size() -> usize { + 1 + } + + #[inline(always)] + fn packed_size() -> usize { + 1 + } +} + +impl StorageSizeU8 of StorageSize { + #[inline(always)] + fn unpacked_size() -> usize { + 1 + } + + #[inline(always)] + fn packed_size() -> usize { + 8 + } +} + +impl StorageSizeU16 of StorageSize { + #[inline(always)] + fn unpacked_size() -> usize { + 1 + } + + #[inline(always)] + fn packed_size() -> usize { + 16 + } +} + +impl StorageSizeU32 of StorageSize { + #[inline(always)] + fn unpacked_size() -> usize { + 1 + } + + #[inline(always)] + fn packed_size() -> usize { + 32 + } +} + +impl StorageSizeU64 of StorageSize { + #[inline(always)] + fn unpacked_size() -> usize { + 1 + } + + #[inline(always)] + fn packed_size() -> usize { + 64 + } +} + +impl StorageSizeU128 of StorageSize { + #[inline(always)] + fn unpacked_size() -> usize { + 1 + } + + #[inline(always)] + fn packed_size() -> usize { + 128 + } +} + +impl StorageSizeU256 of StorageSize { + #[inline(always)] + fn unpacked_size() -> usize { + 2 + } + + #[inline(always)] + fn packed_size() -> usize { + 256 + } +} + +impl StorageSizeContractAddress of StorageSize { + #[inline(always)] + fn unpacked_size() -> usize { + 1 + } + + #[inline(always)] + fn packed_size() -> usize { + 256 + } +} + +impl StorageSizeClassHash of StorageSize { + #[inline(always)] + fn unpacked_size() -> usize { + 1 + } + + #[inline(always)] + fn packed_size() -> usize { + 256 + } +} diff --git a/crates/dojo-core/src/executor_test.cairo b/crates/dojo-core/src/executor_test.cairo index 732fad11ea..67d27bc413 100644 --- a/crates/dojo-core/src/executor_test.cairo +++ b/crates/dojo-core/src/executor_test.cairo @@ -10,7 +10,7 @@ use starknet::class_hash::Felt252TryIntoClassHash; use dojo::executor::{executor, IExecutorDispatcher, IExecutorDispatcherTrait}; use dojo::world::{Context, IWorldDispatcher}; -#[derive(Component, Copy, Drop, Serde, SerdeLen)] +#[derive(Component, Copy, Drop, Serde)] struct Foo { #[key] id: felt252, diff --git a/crates/dojo-core/src/lib.cairo b/crates/dojo-core/src/lib.cairo index ee4c12ce6b..1f5872d784 100644 --- a/crates/dojo-core/src/lib.cairo +++ b/crates/dojo-core/src/lib.cairo @@ -1,10 +1,12 @@ mod database; +use database::storage::StorageSize; mod executor; #[cfg(test)] mod executor_test; -mod serde; -use serde::SerdeLen; -mod traits; +mod component; +mod packing; +#[cfg(test)] +mod packing_test; mod world; #[cfg(test)] mod world_test; diff --git a/crates/dojo-core/src/packing.cairo b/crates/dojo-core/src/packing.cairo new file mode 100644 index 0000000000..e96304bd38 --- /dev/null +++ b/crates/dojo-core/src/packing.cairo @@ -0,0 +1,145 @@ +use starknet::{ClassHash, ContractAddress}; +use array::{ArrayTrait, SpanTrait}; +use traits::{Into, TryInto}; +use integer::{U256BitAnd, U256BitOr, U256BitXor, upcast, downcast, BoundedInt}; +use option::OptionTrait; + +#[derive(Copy, Drop)] +struct LayoutItem { + value: felt252, + size: u8 +} + +fn pack(ref unpacked: Array) -> Span { + let mut packed: Array = ArrayTrait::new(); + let mut packing: felt252 = 0x0; + let mut offset: u8 = 0x0; + loop { + match unpacked.pop_front() { + Option::Some(s) => { + pack_inner(@s.value, s.size, ref packing, ref offset, ref packed); + }, + Option::None(_) => { + break packed.span(); + } + }; + } +} + +fn unpack(ref packed: Span, ref layout: Span) -> Option> { + let mut unpacked: Array = ArrayTrait::new(); + let mut unpacking: felt252 = 0x0; + let mut offset: u8 = 251; + loop { + match layout.pop_front() { + Option::Some(s) => { + match unpack_inner(*s, ref packed, ref unpacking, ref offset) { + Option::Some(u) => { + unpacked.append(u); + }, + Option::None(_) => { + break Option::None(()); + } + } + }, + Option::None(_) => { + break Option::Some(unpacked.span()); + } + }; + } +} + +/// Pack the proposal fields into a single felt252. +fn pack_inner( + self: @felt252, + size: u8, + ref packing: felt252, + ref packing_offset: u8, + ref packed: Array +) { + // Easier to work on u256 rather than felt252. + let self_256: u256 = (*self).into(); + + // Cannot use all 252 bits because some bit arrangements (eg. 11111...11111) are not valid felt252 values. + // Thus only 251 bits are used. ^-252 times-^ + // One could optimize by some conditional alligment mechanism, but it would be an at most 1/252 space-wise improvement. + let remaining_bits: u8 = (251 - packing_offset).into(); + + let mut packing_256: u256 = packing.into(); + + if remaining_bits < size { + let first_part = self_256 & (shl(1, remaining_bits) - 1); + let second_part = shr(self_256, remaining_bits); + + // Pack the first part into the current felt + packing_256 = packing_256 | shl(first_part, packing_offset); + packed.append(packing_256.try_into().unwrap()); + + // Start a new felt and pack the second part into it + packing = second_part.try_into().unwrap(); + packing_offset = size - remaining_bits; + } else { + // Pack the data into the current felt + packing_256 = packing_256 | shl(self_256, packing_offset); + packing = packing_256.try_into().unwrap(); + packing_offset = packing_offset + size; + } +} + +fn unpack_inner( + size: u8, ref packed: Span, ref unpacking: felt252, ref unpacking_offset: u8 +) -> Option { + let remaining_bits: u8 = (251 - unpacking_offset).into(); + + let mut unpacking_256: u256 = unpacking.into(); + + if remaining_bits < size { + match packed.pop_front() { + Option::Some(val) => { + let val_256: u256 = (*val).into(); + + // Get the first part + let first_part = shr(unpacking_256, unpacking_offset); + // Size of the remaining part + let second_size = size - remaining_bits; + let second_part = val_256 & (shl(1, second_size) - 1); + // Move the second part so it fits alongside the first part + let result = first_part | shl(second_part, remaining_bits); + + unpacking = *val; + unpacking_offset = second_size; + return result.try_into(); + }, + Option::None(()) => { + return Option::None(()); + }, + } + } else { + let result = (shl(1, size) - 1) & shr(unpacking_256, unpacking_offset); + unpacking_offset = unpacking_offset + size; + return result.try_into(); + } +} + +fn fpow(x: u256, n: u8) -> u256 { + let y = x; + if n == 0 { + return 1; + } + if n == 1 { + return x; + } + let double = fpow(y * x, n / 2); + if (n % 2) == 1 { + return x * double; + } + return double; +} + +fn shl(x: u256, n: u8) -> u256 { + x * fpow(2, n) +} + +fn shr(x: u256, n: u8) -> u256 { + x / fpow(2, n) +} \ No newline at end of file diff --git a/crates/dojo-core/src/packing_test.cairo b/crates/dojo-core/src/packing_test.cairo new file mode 100644 index 0000000000..2d4583acb7 --- /dev/null +++ b/crates/dojo-core/src/packing_test.cairo @@ -0,0 +1,25 @@ +use array::{ArrayTrait, SpanTrait}; +use starknet::{ClassHash, ContractAddress, Felt252TryIntoContractAddress, Felt252TryIntoClassHash}; +use dojo::packing::{shl, shr, fpow}; +use integer::U256BitAnd; +use option::OptionTrait; +use debug::PrintTrait; +use traits::{Into, TryInto}; + +#[test] +#[available_gas(9000000)] +fn test_bit_fpow() { + assert(fpow(2, 250) == 1809251394333065553493296640760748560207343510400633813116524750123642650624_u256, '') +} + +#[test] +#[available_gas(9000000)] +fn test_bit_shift() { + assert(1 == shl(1, 0), 'left == right'); + assert(1 == shr(1, 0), 'left == right'); + + assert(16 == shl(1, 4), 'left == right'); + assert(1 == shr(16, 4), 'left == right'); + + assert(shr(shl(1, 251), 251) == 1, 'left == right') +} diff --git a/crates/dojo-core/src/serde.cairo b/crates/dojo-core/src/serde.cairo deleted file mode 100644 index 6dd1ce0ec3..0000000000 --- a/crates/dojo-core/src/serde.cairo +++ /dev/null @@ -1,74 +0,0 @@ -trait SerdeLen { - fn len() -> usize; -} - -impl SerdeLenFelt252 of SerdeLen { - #[inline(always)] - fn len() -> usize { - 1 - } -} - -impl SerdeLenBool of SerdeLen { - #[inline(always)] - fn len() -> usize { - 1 - } -} - -impl SerdeLenU8 of SerdeLen { - #[inline(always)] - fn len() -> usize { - 1 - } -} - -impl SerdeLenU16 of SerdeLen { - #[inline(always)] - fn len() -> usize { - 1 - } -} - -impl SerdeLenU32 of SerdeLen { - #[inline(always)] - fn len() -> usize { - 1 - } -} - -impl SerdeLenU64 of SerdeLen { - #[inline(always)] - fn len() -> usize { - 1 - } -} - -impl SerdeLenU128 of SerdeLen { - #[inline(always)] - fn len() -> usize { - 1 - } -} - -impl SerdeLenU256 of SerdeLen { - #[inline(always)] - fn len() -> usize { - 2 - } -} - -impl SerdeLenContractAddress of SerdeLen { - #[inline(always)] - fn len() -> usize { - 1 - } -} - -impl SerdeLenClassHash of SerdeLen { - #[inline(always)] - fn len() -> usize { - 1 - } -} - diff --git a/crates/dojo-core/src/world.cairo b/crates/dojo-core/src/world.cairo index fea75d96a4..88592c514a 100644 --- a/crates/dojo-core/src/world.cairo +++ b/crates/dojo-core/src/world.cairo @@ -58,7 +58,7 @@ mod world { use dojo::database; use dojo::executor::{IExecutorDispatcher, IExecutorDispatcherTrait}; - use dojo::traits::{INamedLibraryDispatcher, INamedDispatcherTrait, }; + use dojo::component::{INamedLibraryDispatcher, INamedDispatcherTrait, }; use dojo::world::{IWorldDispatcher, IWorld}; use super::Context; diff --git a/crates/dojo-core/src/world_factory_test.cairo b/crates/dojo-core/src/world_factory_test.cairo index 07f1b22e41..39160ecbff 100644 --- a/crates/dojo-core/src/world_factory_test.cairo +++ b/crates/dojo-core/src/world_factory_test.cairo @@ -16,7 +16,7 @@ use dojo::world_factory::{IWorldFactoryDispatcher, IWorldFactoryDispatcherTrait, use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait, world}; -#[derive(Component, Copy, Drop, Serde, SerdeLen)] +#[derive(Component, Copy, Drop, Serde)] struct Foo { #[key] id: felt252, diff --git a/crates/dojo-core/src/world_test.cairo b/crates/dojo-core/src/world_test.cairo index 929d8c6fa5..4136b99ac6 100644 --- a/crates/dojo-core/src/world_test.cairo +++ b/crates/dojo-core/src/world_test.cairo @@ -16,7 +16,7 @@ use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait, library_call, world}; // Components and Systems -#[derive(Component, Copy, Drop, Serde, SerdeLen)] +#[derive(Component, Copy, Drop, Serde)] struct Foo { #[key] caller: ContractAddress, @@ -24,7 +24,7 @@ struct Foo { b: u128, } -#[derive(Component, Copy, Drop, Serde, SerdeLen)] +#[derive(Component, Copy, Drop, Serde)] struct Fizz { #[key] caller: ContractAddress, @@ -71,7 +71,7 @@ fn test_system() { let mut keys = ArrayTrait::new(); keys.append(0); - let stored = world.entity('Foo', keys.span(), 0, dojo::SerdeLen::::len()); + let stored = world.entity('Foo', keys.span(), 0, dojo::StorageSize::::unpacked_size()); assert(*stored.snapshot.at(0) == 1337, 'data not stored'); } @@ -107,7 +107,7 @@ fn test_set_entity_admin() { data.append(420); data.append(1337); world.execute('bar', data); - let foo = world.entity('Foo', keys.span(), 0, dojo::SerdeLen::::len()); + let foo = world.entity('Foo', keys.span(), 0, dojo::StorageSize::::unpacked_size()); assert(*foo[0] == 420, 'data not stored'); assert(*foo[1] == 1337, 'data not stored'); } diff --git a/crates/dojo-defi/src/market/components.cairo b/crates/dojo-defi/src/market/components.cairo index eb24f3b900..259a89cd00 100644 --- a/crates/dojo-defi/src/market/components.cairo +++ b/crates/dojo-defi/src/market/components.cairo @@ -2,7 +2,7 @@ use option::OptionTrait; use starknet::ContractAddress; use traits::{Into, TryInto}; -use dojo::serde::SerdeLen; +use dojo::component::StorageSize; // Cubit fixed point math library @@ -10,21 +10,21 @@ use cubit::f128::types::fixed::Fixed; const SCALING_FACTOR: u128 = 10000; -impl SerdeLenFixed of SerdeLen { +impl StorageSizeFixed of StorageSize { #[inline(always)] fn len() -> usize { 2 } } -#[derive(Component, Copy, Drop, Serde, SerdeLen)] +#[derive(Component, Copy, Drop, Serde)] struct Cash { #[key] player: ContractAddress, amount: u128, } -#[derive(Component, Copy, Drop, Serde, SerdeLen)] +#[derive(Component, Copy, Drop, Serde)] struct Item { #[key] player: ContractAddress, @@ -33,7 +33,7 @@ struct Item { quantity: u128, } -#[derive(Component, Copy, Drop, Serde, SerdeLen)] +#[derive(Component, Copy, Drop, Serde)] struct Liquidity { #[key] player: ContractAddress, @@ -42,7 +42,7 @@ struct Liquidity { shares: Fixed, } -#[derive(Component, Copy, Drop, Serde, SerdeLen)] +#[derive(Component, Copy, Drop, Serde)] struct Market { #[key] item_id: u32, diff --git a/crates/dojo-erc/src/erc1155/components.cairo b/crates/dojo-erc/src/erc1155/components.cairo index 9f0b7ecd18..0d5d73222a 100644 --- a/crates/dojo-erc/src/erc1155/components.cairo +++ b/crates/dojo-erc/src/erc1155/components.cairo @@ -12,7 +12,7 @@ use dojo_erc::erc_common::components::{operator_approval, OperatorApproval, Oper // Uri TODO: use BaseURI from erc_common // -#[derive(Component, Copy, Drop, Serde, SerdeLen)] +#[derive(Component, Copy, Drop, Serde)] struct Uri { #[key] token: ContractAddress, @@ -23,7 +23,7 @@ struct Uri { // ERC1155Balance // -#[derive(Component, Copy, Drop, Serde, SerdeLen)] +#[derive(Component, Copy, Drop, Serde)] struct ERC1155Balance { #[key] token: ContractAddress, diff --git a/crates/dojo-erc/src/erc20/components.cairo b/crates/dojo-erc/src/erc20/components.cairo index 0a0b52ce51..4266db3562 100644 --- a/crates/dojo-erc/src/erc20/components.cairo +++ b/crates/dojo-erc/src/erc20/components.cairo @@ -1,6 +1,6 @@ use starknet::ContractAddress; -#[derive(Component, Copy, Drop, Serde, SerdeLen)] +#[derive(Component, Copy, Drop, Serde)] struct Allowance { #[key] token: ContractAddress, @@ -11,7 +11,7 @@ struct Allowance { amount: felt252, } -#[derive(Component, Copy, Drop, Serde, SerdeLen)] +#[derive(Component, Copy, Drop, Serde)] struct Balance { #[key] token: ContractAddress, @@ -20,7 +20,7 @@ struct Balance { amount: felt252, } -#[derive(Component, Copy, Drop, Serde, SerdeLen)] +#[derive(Component, Copy, Drop, Serde)] struct Supply { #[key] token: ContractAddress, diff --git a/crates/dojo-erc/src/erc721/components.cairo b/crates/dojo-erc/src/erc721/components.cairo index bb4284ceb0..25d011fbd5 100644 --- a/crates/dojo-erc/src/erc721/components.cairo +++ b/crates/dojo-erc/src/erc721/components.cairo @@ -14,7 +14,7 @@ use dojo_erc::erc_common::components::{ // ERC721Owner // -#[derive(Component, Copy, Drop, Serde, SerdeLen)] +#[derive(Component, Copy, Drop, Serde)] struct ERC721Owner { #[key] token: ContractAddress, @@ -56,7 +56,7 @@ impl ERC721OwnerImpl of ERC721OwnerTrait { // ERC721Balance // -#[derive(Component, Copy, Drop, Serde, SerdeLen)] +#[derive(Component, Copy, Drop, Serde)] struct ERC721Balance { #[key] token: ContractAddress, @@ -133,7 +133,7 @@ impl ERC721BalanceImpl of ERC721BalanceTrait { // ERC721TokenApproval // -#[derive(Component, Copy, Drop, Serde, SerdeLen)] +#[derive(Component, Copy, Drop, Serde)] struct ERC721TokenApproval { #[key] token: ContractAddress, diff --git a/crates/dojo-erc/src/erc_common/components/base_uri_component.cairo b/crates/dojo-erc/src/erc_common/components/base_uri_component.cairo index 39c0daafe8..7c6a5fa876 100644 --- a/crates/dojo-erc/src/erc_common/components/base_uri_component.cairo +++ b/crates/dojo-erc/src/erc_common/components/base_uri_component.cairo @@ -1,7 +1,7 @@ use starknet::ContractAddress; use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; -#[derive(Component, Copy, Drop, Serde, SerdeLen)] +#[derive(Component, Copy, Drop, Serde)] struct BaseUri { #[key] token: ContractAddress, diff --git a/crates/dojo-erc/src/erc_common/components/operator_approval_component.cairo b/crates/dojo-erc/src/erc_common/components/operator_approval_component.cairo index db792bbfb3..b8dd10f302 100644 --- a/crates/dojo-erc/src/erc_common/components/operator_approval_component.cairo +++ b/crates/dojo-erc/src/erc_common/components/operator_approval_component.cairo @@ -1,7 +1,7 @@ use starknet::ContractAddress; use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; -#[derive(Component, Copy, Drop, Serde, SerdeLen)] +#[derive(Component, Copy, Drop, Serde)] struct OperatorApproval { #[key] token: ContractAddress, diff --git a/crates/dojo-lang/src/component.rs b/crates/dojo-lang/src/component.rs index 2b1a16f82e..733df0c138 100644 --- a/crates/dojo-lang/src/component.rs +++ b/crates/dojo-lang/src/component.rs @@ -159,7 +159,7 @@ pub fn handle_component_struct( $members$ } - impl $type_name$Component of dojo::traits::Component<$type_name$> { + impl $type_name$Component of dojo::component::Component<$type_name$> { #[inline(always)] fn name(self: @$type_name$) -> felt252 { '$type_name$' @@ -180,6 +180,18 @@ pub fn handle_component_struct( } } + impl $type_name$StorageSize of dojo::StorageSize<$type_name$> { + #[inline(always)] + fn unpacked_size() -> usize { + $unpacked_size$ + } + + #[inline(always)] + fn packed_size() -> usize { + $packed_size$ + } + } + #[cfg(test)] impl $type_name$PrintImpl of debug::PrintTrait<$type_name$> { fn print(self: $type_name$) { @@ -206,7 +218,7 @@ pub fn handle_component_struct( #[external(v0)] fn size(self: @ContractState) -> usize { - dojo::SerdeLen::<$type_name$>::len() + dojo::StorageSize::<$type_name$>::unpacked_size() } #[external(v0)] @@ -240,6 +252,46 @@ pub fn handle_component_struct( ), ("schema".to_string(), RewriteNode::new_modified(schema)), ("print_body".to_string(), RewriteNode::Text(print_body)), + ( + "unpacked_size".to_string(), + RewriteNode::Text( + struct_ast + .members(db) + .elements(db) + .iter() + .filter_map(|member| { + if member.has_attr(db, "key") { + return None; + } + + Some(format!( + "dojo::StorageSize::<{}>::unpacked_size()", + member.type_clause(db).ty(db).as_syntax_node().get_text(db), + )) + }) + .join(" + "), + ), + ), + ( + "packed_size".to_string(), + RewriteNode::Text( + struct_ast + .members(db) + .elements(db) + .iter() + .filter_map(|member| { + if member.has_attr(db, "key") { + return None; + } + + Some(format!( + "dojo::StorageSize::<{}>::packed_size()", + member.type_clause(db).ty(db).as_syntax_node().get_text(db), + )) + }) + .join(" + "), + ), + ), ]), ), diagnostics, diff --git a/crates/dojo-lang/src/inline_macros/get.rs b/crates/dojo-lang/src/inline_macros/get.rs index 445f14a012..b593352b48 100644 --- a/crates/dojo-lang/src/inline_macros/get.rs +++ b/crates/dojo-lang/src/inline_macros/get.rs @@ -86,7 +86,7 @@ impl InlineMacroExprPlugin for GetMacro { builder.add_str(&format!( "\n let __{component}_values__ = {}.entity('{component}', \ - __get_macro_keys__, 0_u8, dojo::SerdeLen::<{component}>::len()); + __get_macro_keys__, 0_u8, dojo::StorageSize::<{component}>::unpacked_size()); let mut __{component}_component__ = array::ArrayTrait::new(); array::serialize_array_helper(__get_macro_keys__, ref __{component}_component__); array::serialize_array_helper(__{component}_values__, ref \ diff --git a/crates/dojo-lang/src/inline_macros/set.rs b/crates/dojo-lang/src/inline_macros/set.rs index 7a58c7b3b3..08d476d4b5 100644 --- a/crates/dojo-lang/src/inline_macros/set.rs +++ b/crates/dojo-lang/src/inline_macros/set.rs @@ -77,9 +77,9 @@ impl InlineMacroExprPlugin for SetMacro { for entity in bundle { builder.add_str(&format!( "\n let __set_macro_value__ = {}; - {}.set_entity(dojo::traits::Component::name(@__set_macro_value__), \ - dojo::traits::Component::keys(@__set_macro_value__), 0_u8, \ - dojo::traits::Component::values(@__set_macro_value__));", + {}.set_entity(dojo::component::Component::name(@__set_macro_value__), \ + dojo::component::Component::keys(@__set_macro_value__), 0_u8, \ + dojo::component::Component::values(@__set_macro_value__));", entity, world.as_syntax_node().get_text(db), )); diff --git a/crates/dojo-lang/src/lib.rs b/crates/dojo-lang/src/lib.rs index 7ac2ee7600..1e2f7ef81c 100644 --- a/crates/dojo-lang/src/lib.rs +++ b/crates/dojo-lang/src/lib.rs @@ -8,6 +8,5 @@ pub mod component; pub mod inline_macros; mod manifest; pub mod plugin; -mod serde; pub mod system; pub(crate) mod version; diff --git a/crates/dojo-lang/src/plugin.rs b/crates/dojo-lang/src/plugin.rs index 85b0cedca8..b779561d42 100644 --- a/crates/dojo-lang/src/plugin.rs +++ b/crates/dojo-lang/src/plugin.rs @@ -25,7 +25,6 @@ use crate::component::handle_component_struct; use crate::inline_macros::emit::EmitMacro; use crate::inline_macros::get::GetMacro; use crate::inline_macros::set::SetMacro; -use crate::serde::handle_serde_len_struct; use crate::system::System; const SYSTEM_ATTR: &str = "system"; @@ -159,9 +158,6 @@ impl MacroPlugin for DojoPlugin { rewrite_nodes.push(component_rewrite_nodes); diagnostics.extend(component_diagnostics); } - "SerdeLen" => { - rewrite_nodes.push(handle_serde_len_struct(db, struct_ast.clone())); - } _ => continue, } } diff --git a/crates/dojo-lang/src/plugin_test_data/component b/crates/dojo-lang/src/plugin_test_data/component index 75c620765a..bcedb23ece 100644 --- a/crates/dojo-lang/src/plugin_test_data/component +++ b/crates/dojo-lang/src/plugin_test_data/component @@ -6,7 +6,7 @@ test_expand_plugin //! > cairo_code use serde::Serde; -#[derive(Component, Copy, Drop, Serde, SerdeLen)] +#[derive(Component, Copy, Drop, Serde)] struct Position { #[key] id: felt252, @@ -38,16 +38,9 @@ struct Roles { role_ids: Array } -impl RolesSerdeLen of dojo::SerdeLen { - #[inline(always)] - fn len() -> usize { - 5 - } -} - use starknet::ContractAddress; -#[derive(Component, Copy, Drop, Serde, SerdeLen)] +#[derive(Component, Copy, Drop, Serde)] struct Player { #[key] game: felt252, @@ -67,7 +60,7 @@ struct Position { y: felt252 } -impl PositionComponent of dojo::traits::Component { +impl PositionComponent of dojo::component::Component { #[inline(always)] fn name(self: @Position) -> felt252 { 'Position' @@ -90,6 +83,19 @@ impl PositionComponent of dojo::traits::Component { } } +impl PositionStorageSize of dojo::StorageSize { + #[inline(always)] + fn unpacked_size() -> usize { + dojo::StorageSize::::unpacked_size() + + dojo::StorageSize::::unpacked_size() + } + + #[inline(always)] + fn packed_size() -> usize { + dojo::StorageSize::::packed_size() + dojo::StorageSize::::packed_size() + } +} + #[cfg(test)] impl PositionPrintImpl of debug::PrintTrait { fn print(self: Position) { @@ -121,7 +127,7 @@ mod position { #[external(v0)] fn size(self: @ContractState) -> usize { - dojo::SerdeLen::::len() + dojo::StorageSize::::unpacked_size() } #[external(v0)] @@ -134,13 +140,6 @@ mod position { } } -impl SerdeLenPosition of dojo::SerdeLen { - #[inline(always)] - fn len() -> usize { - dojo::SerdeLen::::len() + dojo::SerdeLen::::len() - } -} - trait PositionTrait { @@ -166,7 +165,7 @@ struct Roles { role_ids: Array } -impl RolesComponent of dojo::traits::Component { +impl RolesComponent of dojo::component::Component { #[inline(always)] fn name(self: @Roles) -> felt252 { 'Roles' @@ -187,6 +186,18 @@ impl RolesComponent of dojo::traits::Component { } } +impl RolesStorageSize of dojo::StorageSize { + #[inline(always)] + fn unpacked_size() -> usize { + dojo::StorageSize::>::unpacked_size() + } + + #[inline(always)] + fn packed_size() -> usize { + dojo::StorageSize::>::packed_size() + } +} + #[cfg(test)] impl RolesPrintImpl of debug::PrintTrait { fn print(self: Roles) { @@ -214,7 +225,7 @@ mod roles { #[external(v0)] fn size(self: @ContractState) -> usize { - dojo::SerdeLen::::len() + dojo::StorageSize::::unpacked_size() } #[external(v0)] @@ -227,14 +238,6 @@ mod roles { -impl RolesSerdeLen of dojo::SerdeLen { - #[inline(always)] - fn len() -> usize { - 5 - } -} - - use starknet::ContractAddress; struct Player { @@ -245,7 +248,7 @@ struct Player { name: felt252, } -impl PlayerComponent of dojo::traits::Component { +impl PlayerComponent of dojo::component::Component { #[inline(always)] fn name(self: @Player) -> felt252 { 'Player' @@ -269,6 +272,18 @@ impl PlayerComponent of dojo::traits::Component { } } +impl PlayerStorageSize of dojo::StorageSize { + #[inline(always)] + fn unpacked_size() -> usize { + dojo::StorageSize::::unpacked_size() + } + + #[inline(always)] + fn packed_size() -> usize { + dojo::StorageSize::::packed_size() + } +} + #[cfg(test)] impl PlayerPrintImpl of debug::PrintTrait { fn print(self: Player) { @@ -300,7 +315,7 @@ mod player { #[external(v0)] fn size(self: @ContractState) -> usize { - dojo::SerdeLen::::len() + dojo::StorageSize::::unpacked_size() } #[external(v0)] @@ -313,13 +328,6 @@ mod player { } } -impl SerdeLenPlayer of dojo::SerdeLen { - #[inline(always)] - fn len() -> usize { - dojo::SerdeLen::::len() - } -} - //! > expected_diagnostics error: Component must define atleast one #[key] attribute --> dummy_file.cairo:31:8 diff --git a/crates/dojo-lang/src/serde.rs b/crates/dojo-lang/src/serde.rs deleted file mode 100644 index 6cb1eb14d7..0000000000 --- a/crates/dojo-lang/src/serde.rs +++ /dev/null @@ -1,50 +0,0 @@ -use cairo_lang_defs::patcher::RewriteNode; -use cairo_lang_syntax::node::ast::ItemStruct; -use cairo_lang_syntax::node::db::SyntaxGroup; -use cairo_lang_syntax::node::helpers::QueryAttrs; -use cairo_lang_syntax::node::TypedSyntaxNode; -use cairo_lang_utils::unordered_hash_map::UnorderedHashMap; -use itertools::Itertools; - -/// A handler for Dojo code derives SerdeLen for a struct -/// Parameters: -/// * db: The semantic database. -/// * struct_ast: The AST of the struct. -/// Returns: -/// * A RewriteNode containing the generated code. -pub fn handle_serde_len_struct(db: &dyn SyntaxGroup, struct_ast: ItemStruct) -> RewriteNode { - RewriteNode::interpolate_patched( - " - impl SerdeLen$name$ of dojo::SerdeLen<$type$> { - #[inline(always)] - fn len() -> usize { - $len$ - } - } - ", - UnorderedHashMap::from([ - ("name".to_string(), RewriteNode::new_trimmed(struct_ast.name(db).as_syntax_node())), - ("type".to_string(), RewriteNode::new_trimmed(struct_ast.name(db).as_syntax_node())), - ( - "len".to_string(), - RewriteNode::Text( - struct_ast - .members(db) - .elements(db) - .iter() - .filter_map(|member| { - if member.has_attr(db, "key") { - return None; - } - - Some(format!( - "dojo::SerdeLen::<{}>::len()", - member.type_clause(db).ty(db).as_syntax_node().get_text(db), - )) - }) - .join(" + "), - ), - ), - ]), - ) -} diff --git a/examples/ecs/src/components.cairo b/examples/ecs/src/components.cairo index f954541d03..26c6b1b15e 100644 --- a/examples/ecs/src/components.cairo +++ b/examples/ecs/src/components.cairo @@ -1,14 +1,14 @@ use array::ArrayTrait; use starknet::ContractAddress; -#[derive(Component, Copy, Drop, Serde, SerdeLen)] +#[derive(Component, Copy, Drop, Serde)] struct Moves { #[key] player: ContractAddress, remaining: u8, } -#[derive(Component, Copy, Drop, Serde, SerdeLen)] +#[derive(Component, Copy, Drop, Serde)] struct Position { #[key] player: ContractAddress, diff --git a/examples/ecs/src/systems.cairo b/examples/ecs/src/systems.cairo index 3ad02ee7e1..4a849f2138 100644 --- a/examples/ecs/src/systems.cairo +++ b/examples/ecs/src/systems.cairo @@ -138,10 +138,10 @@ mod tests { let mut keys = array::ArrayTrait::new(); keys.append(caller.into()); - let moves = world.entity('Moves', keys.span(), 0, dojo::SerdeLen::::len()); + let moves = world.entity('Moves', keys.span(), 0, dojo::StorageSize::::unpacked_size()); assert(*moves[0] == 9, 'moves is wrong'); let new_position = world - .entity('Position', keys.span(), 0, dojo::SerdeLen::::len()); + .entity('Position', keys.span(), 0, dojo::StorageSize::::unpacked_size()); assert(*new_position[0] == 11, 'position x is wrong'); assert(*new_position[1] == 10, 'position y is wrong'); } From 827acf11806ae2e7b6a6277d48c98b3f8e11eb41 Mon Sep 17 00:00:00 2001 From: visoftsolutions <128646889+visoftsolutions@users.noreply.github.com> Date: Thu, 7 Sep 2023 20:26:04 +0200 Subject: [PATCH 32/77] Tests improvement (#871) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * remove .vscode settings * storage tests * defi tests * basic database test * database for different tables and keys * database pagination * unfinished database all * test executor with data * world class hash getters * Update to Cairo 2.2.0 (#831) * Update to Cairo 2.2.0 * Inline macro tests * remove .vscode settings * world factory setters * Update to Cairo 2.2.0 (#831) * Update to Cairo 2.2.0 * Inline macro tests * remove .vscode settings * Update to Cairo 2.2.0 (#831) * Update to Cairo 2.2.0 * Inline macro tests * failing world tests * multiple worlds test * factory of multiple worlds test * multiple worlds from factory * rebase fixes --------- Co-authored-by: Szymon Wojtulewicz Co-authored-by: Mateusz Zając Co-authored-by: Tarrence van As Co-authored-by: Paweł --- .vscode/settings.json | 11 -- .../dojo-core/src/database/storage_test.cairo | 69 +++++++++ crates/dojo-core/src/database_test.cairo | 137 ++++++++++++++++++ crates/dojo-core/src/executor_test.cairo | 37 +++++ crates/dojo-core/src/lib.cairo | 2 + crates/dojo-core/src/world_factory_test.cairo | 76 ++++++++++ crates/dojo-core/src/world_test.cairo | 121 ++++++++++++++++ .../dojo-defi/src/dutch_auction/common.cairo | 8 + 8 files changed, 450 insertions(+), 11 deletions(-) delete mode 100644 .vscode/settings.json create mode 100644 crates/dojo-core/src/database_test.cairo diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 80ba90fe4e..0000000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "cairo1.languageServerPath": "~/.dojo/bin/dojo-language-server", - "cairo1.enableLanguageServer": true, - "cairo1.enableScarb": false, - "rust-analyzer.linkedProjects": [ - "./crates/dojo-lang/Cargo.toml" - ], - "rust-analyzer.rustfmt.extraArgs": [ - "+nightly" - ] -} \ No newline at end of file diff --git a/crates/dojo-core/src/database/storage_test.cairo b/crates/dojo-core/src/database/storage_test.cairo index f782021172..2779aabe07 100644 --- a/crates/dojo-core/src/database/storage_test.cairo +++ b/crates/dojo-core/src/database/storage_test.cairo @@ -1,6 +1,7 @@ use array::ArrayTrait; use array::SpanTrait; use traits::Into; +use debug::PrintTrait; use dojo::database::storage; @@ -21,3 +22,71 @@ fn test_storage() { let res = storage::get_many(0, keys.span(), 0, 2); assert(*res.at(0) == *values.at(0), 'value not set'); } + +#[test] +#[available_gas(2000000)] +fn test_storage_empty() { + let mut keys = ArrayTrait::new(); + assert(storage::get(0, keys.span()) == 0x0, 'Value should be 0'); + let many = storage::get_many(0, keys.span(), 0, 3); + assert(*many.at(0) == 0x0, 'Value should be 0'); + assert(*many.at(1) == 0x0, 'Value should be 0'); + assert(*many.at(2) == 0x0, 'Value should be 0'); +} + +#[test] +#[available_gas(100000000)] +fn test_storage_get_many_length() { + let mut keys = ArrayTrait::new(); + let mut i = 0_usize; + loop { + if i >= 30 { + break; + }; + assert(storage::get_many(0, keys.span(), 0, i).len() == i, 'Values should be equal!'); + i += 1; + }; + +} + +#[test] +#[available_gas(2000000)] +fn test_storage_set_many() { + let mut keys = ArrayTrait::new(); + keys.append(0x966); + + let mut values = ArrayTrait::new(); + values.append(0x1); + values.append(0x2); + values.append(0x3); + values.append(0x4); + + storage::set_many(0, keys.span(), 0, values.span()); + let many = storage::get_many(0, keys.span(), 0, 4); + assert(many.at(0) == values.at(0), 'Value at 0 not equal!'); + assert(many.at(1) == values.at(1), 'Value at 1 not equal!'); + assert(many.at(2) == values.at(2), 'Value at 2 not equal!'); + assert(many.at(3) == values.at(3), 'Value at 3 not equal!'); +} + +#[test] +#[available_gas(20000000)] +fn test_storage_set_many_with_offset() { + let mut keys = ArrayTrait::new(); + keys.append(0x1364); + + let mut values = ArrayTrait::new(); + values.append(0x1); + values.append(0x2); + values.append(0x3); + values.append(0x4); + + storage::set_many(0, keys.span(), 1, values.span()); + let many = storage::get_many(0, keys.span(), 0, 5); + assert(*many.at(0) == 0x0, 'Value at 0 not equal!'); + assert(many.at(1) == values.at(0), 'Value at 1 not equal!'); + assert(many.at(2) == values.at(1), 'Value at 2 not equal!'); + assert(many.at(3) == values.at(2), 'Value at 3 not equal!'); + assert(many.at(4) == values.at(3), 'Value at 4 not equal!'); +} + diff --git a/crates/dojo-core/src/database_test.cairo b/crates/dojo-core/src/database_test.cairo new file mode 100644 index 0000000000..d2dbc9ff86 --- /dev/null +++ b/crates/dojo-core/src/database_test.cairo @@ -0,0 +1,137 @@ +use core::result::ResultTrait; +use array::ArrayTrait; +use option::OptionTrait; +use serde::Serde; +use array::SpanTrait; +use traits::{Into, TryInto}; + +use starknet::syscalls::deploy_syscall; +use starknet::class_hash::Felt252TryIntoClassHash; +use dojo::executor::{executor, IExecutorDispatcher, IExecutorDispatcherTrait}; +use dojo::world::{Context, IWorldDispatcher}; + +use dojo::database::{get, set, del, all}; + + + +#[test] +#[available_gas(1000000)] +fn test_database_basic() { + let mut values = ArrayTrait::new(); + values.append('database_test'); + values.append('42'); + + let class_hash: starknet::ClassHash = executor::TEST_CLASS_HASH.try_into().unwrap(); + set(class_hash, 'table', 'key', 0, values.span()); + let res = get(class_hash, 'table', 'key', 0, values.len()); + + assert(res.at(0) == values.at(0), 'Value at 0 not equal!'); + assert(res.at(1) == values.at(1), 'Value at 0 not equal!'); + assert(res.len() == values.len(), 'Lengths not equal'); +} + +#[test] +#[available_gas(1000000)] +fn test_database_different_tables() { + let mut values = ArrayTrait::new(); + values.append(0x1); + values.append(0x2); + + let mut other = ArrayTrait::new(); + other.append(0x3); + other.append(0x4); + + let class_hash: starknet::ClassHash = executor::TEST_CLASS_HASH.try_into().unwrap(); + set(class_hash, 'first', 'key', 0, values.span()); + set(class_hash, 'second', 'key', 0, other.span()); + let res = get(class_hash, 'first', 'key', 0, values.len()); + let other_res = get(class_hash, 'second', 'key', 0, other.len()); + + assert(res.len() == values.len(), 'Lengths not equal'); + assert(res.at(0) == values.at(0), 'Values different at `first`!'); + assert(other_res.at(0) == other_res.at(0), 'Values different at `second`!'); + assert(other_res.at(0) != res.at(0), 'Values the same for different!'); +} + +#[test] +#[available_gas(1000000)] +fn test_database_different_keys() { + let mut values = ArrayTrait::new(); + values.append(0x1); + values.append(0x2); + + let mut other = ArrayTrait::new(); + other.append(0x3); + other.append(0x4); + + let class_hash: starknet::ClassHash = executor::TEST_CLASS_HASH.try_into().unwrap(); + set(class_hash, 'table', 'key', 0, values.span()); + set(class_hash, 'table', 'other', 0, other.span()); + let res = get(class_hash, 'table', 'key', 0, values.len()); + let other_res = get(class_hash, 'table', 'other', 0, other.len()); + + assert(res.len() == values.len(), 'Lengths not equal'); + assert(res.at(0) == values.at(0), 'Values different at `key`!'); + assert(other_res.at(0) == other_res.at(0), 'Values different at `other`!'); + assert(other_res.at(0) != res.at(0), 'Values the same for different!'); +} + +#[test] +#[available_gas(10000000)] +fn test_database_pagination() { + let mut values = ArrayTrait::new(); + values.append(0x1); + values.append(0x2); + values.append(0x3); + values.append(0x4); + values.append(0x5); + + let class_hash: starknet::ClassHash = executor::TEST_CLASS_HASH.try_into().unwrap(); + set(class_hash, 'table', 'key', 1, values.span()); + let first_res = get(class_hash, 'table', 'key', 1, 3); + let second_res = get(class_hash, 'table', 'key', 3, 5); + let third_res = get(class_hash, 'table', 'key', 5, 7); + + assert(*first_res.at(0) == *values.at(0), 'Values different at index 0!'); + assert(*first_res.at(1) == *values.at(1), 'Values different at index 1!'); + assert(*second_res.at(0) == *values.at(2), 'Values different at index 2!'); + assert(*second_res.at(1) == *values.at(3), 'Values different at index 3!'); + assert(*third_res.at(0) == *values.at(4), 'Values different at index 4!'); + assert(*third_res.at(1) == 0x0, 'Value not empty at index 5!'); +} + +#[test] +#[available_gas(10000000)] +fn test_database_del() { + let mut values = ArrayTrait::new(); + values.append(0x42); + + let class_hash: starknet::ClassHash = executor::TEST_CLASS_HASH.try_into().unwrap(); + set(class_hash, 'table', 'key', 0, values.span()); + + let before = get(class_hash, 'table', 'key', 0, values.len()); + assert(*before.at(0) == *values.at(0), 'Values different at index 0!'); + + del(class_hash, 'table', 'key'); + let after = get(class_hash, 'table', 'key', 0, 0); + assert(after.len() == 0, 'Non empty after deletion!'); +} + +#[test] +#[available_gas(10000000)] +fn test_database_all() { + let mut even = ArrayTrait::new(); + even.append(0x2); + even.append(0x4); + + let mut odd = ArrayTrait::new(); + even.append(0x1); + even.append(0x3); + + let class_hash: starknet::ClassHash = executor::TEST_CLASS_HASH.try_into().unwrap(); + set(class_hash, 'table', 'even', 0, even.span()); + set(class_hash, 'table', 'odd', 0, odd.span()); + + let base = starknet::storage_base_address_from_felt252('table'); + let (keys, values) = all(class_hash, 'table', 0, 2); +} diff --git a/crates/dojo-core/src/executor_test.cairo b/crates/dojo-core/src/executor_test.cairo index 67d27bc413..395ca322fb 100644 --- a/crates/dojo-core/src/executor_test.cairo +++ b/crates/dojo-core/src/executor_test.cairo @@ -90,3 +90,40 @@ fn test_executor_bad_caller() { let res = executor.execute(ctx.system_class_hash, system_calldata.span()); } + +#[test] +#[available_gas(40000000)] +fn test_executor_with_struct() { + let constructor_calldata = array::ArrayTrait::new(); + let (executor_address, _) = deploy_syscall( + executor::TEST_CLASS_HASH.try_into().unwrap(), 0, constructor_calldata.span(), false + ).unwrap(); + + let foo = Foo { + id: 1, + a: 42, + b: 53, + }; + let ctx = Context { + world: IWorldDispatcher { + contract_address: starknet::contract_address_const::<0x1337>() + }, + origin: starknet::contract_address_const::<0x1337>(), + system: 'Bar', + system_class_hash: Bar::TEST_CLASS_HASH.try_into().unwrap(), + }; + + let mut system_calldata = ArrayTrait::new(); + foo.serialize(ref system_calldata); + ctx.serialize(ref system_calldata); + + starknet::testing::set_contract_address(ctx.world.contract_address); + let executor = IExecutorDispatcher { contract_address: executor_address }; + let mut res = executor.execute(ctx.system_class_hash, system_calldata.span()); + + let foo = Serde::::deserialize(ref res).unwrap(); + assert(foo.id == 1, 'Invalid deserialized data'); + assert(foo.a == 42, 'Invalid deserialized data'); + assert(foo.b == 53, 'Invalid deserialized data'); + +} diff --git a/crates/dojo-core/src/lib.cairo b/crates/dojo-core/src/lib.cairo index 1f5872d784..9a42520009 100644 --- a/crates/dojo-core/src/lib.cairo +++ b/crates/dojo-core/src/lib.cairo @@ -1,5 +1,7 @@ mod database; use database::storage::StorageSize; +#[cfg(test)] +mod database_test; mod executor; #[cfg(test)] mod executor_test; diff --git a/crates/dojo-core/src/world_factory_test.cairo b/crates/dojo-core/src/world_factory_test.cairo index 39160ecbff..3c7bdf92f5 100644 --- a/crates/dojo-core/src/world_factory_test.cairo +++ b/crates/dojo-core/src/world_factory_test.cairo @@ -96,3 +96,79 @@ fn test_spawn_world() { let bar_hash = world.system('bar'.into()); assert(bar_hash == bar::TEST_CLASS_HASH.try_into().unwrap(), 'system not registered'); } + +#[test] +#[available_gas(40000000)] +fn test_setters() { + let mut calldata: Array = array::ArrayTrait::new(); + calldata.append(starknet::class_hash_const::<0x420>().into()); + calldata.append(starknet::contract_address_const::<0x69>().into()); + + let (factory_address, _) = deploy_syscall( + world_factory::TEST_CLASS_HASH.try_into().unwrap(), 0, calldata.span(), false + ) + .unwrap(); + + let factory = IWorldFactoryDispatcher { contract_address: factory_address }; + + assert(factory.world() == starknet::class_hash_const::<0x420>(), 'wrong world class hash'); + assert( + factory.executor() == starknet::contract_address_const::<0x69>(), 'wrong executor contract' + ); + + factory.set_executor(starknet::contract_address_const::<0x96>().into()); + assert(factory.world() == starknet::class_hash_const::<0x420>(), 'wrong world class hash'); + assert( + factory.executor() == starknet::contract_address_const::<0x96>(), 'wrong executor contract' + ); + + factory.set_world(starknet::class_hash_const::<0x421>().into()); + assert(factory.world() == starknet::class_hash_const::<0x421>(), 'wrong world class hash'); + assert( + factory.executor() == starknet::contract_address_const::<0x96>(), 'wrong executor contract' + ); +} + +#[test] +#[available_gas(90000000)] +fn test_spawn_multiple_worlds() { + // Deploy Executor + let constructor_calldata = array::ArrayTrait::new(); + let (executor_address, _) = deploy_syscall( + executor::TEST_CLASS_HASH.try_into().unwrap(), 0, constructor_calldata.span(), false + ) + .unwrap(); + + // WorldFactory constructor + let mut calldata: Array = array::ArrayTrait::new(); + calldata.append(world::TEST_CLASS_HASH); + calldata.append(executor_address.into()); + + let (factory_address, _) = deploy_syscall( + world_factory::TEST_CLASS_HASH.try_into().unwrap(), 0, calldata.span(), false + ) + .unwrap(); + + let factory = IWorldFactoryDispatcher { contract_address: factory_address }; + + assert(factory.executor() == executor_address, 'wrong executor address'); + assert(factory.world() == world::TEST_CLASS_HASH.try_into().unwrap(), 'wrong world class hash'); + + // Prepare components and systems + let mut systems: Array = array::ArrayTrait::new(); + systems.append(bar::TEST_CLASS_HASH.try_into().unwrap()); + let mut components: Array = array::ArrayTrait::new(); + components.append(foo::TEST_CLASS_HASH.try_into().unwrap()); + + // Spawn World from WorldFactory + let world_address = factory.spawn(components, systems); + let another_world_address = factory.spawn(array::ArrayTrait::new(), array::ArrayTrait::new()); + let world = IWorldDispatcher { contract_address: world_address }; + let another_world = IWorldDispatcher { contract_address: another_world_address }; + + // Check Foo component and Bar system are registered + let foo_hash = world.component('Foo'.into()); + let foo_hash = world.component('Foo'.into()); + assert(foo_hash == foo::TEST_CLASS_HASH.try_into().unwrap(), 'component not registered'); + assert(foo_hash == foo::TEST_CLASS_HASH.try_into().unwrap(), 'component not registered'); +} diff --git a/crates/dojo-core/src/world_test.cairo b/crates/dojo-core/src/world_test.cairo index 4136b99ac6..1b6f3e160f 100644 --- a/crates/dojo-core/src/world_test.cairo +++ b/crates/dojo-core/src/world_test.cairo @@ -75,6 +75,20 @@ fn test_system() { assert(*stored.snapshot.at(0) == 1337, 'data not stored'); } +#[test] +#[available_gas(6000000)] +fn test_class_hash_getters() { + let world = deploy_world(); + + world.register_system(bar::TEST_CLASS_HASH.try_into().unwrap()); + world.register_component(foo::TEST_CLASS_HASH.try_into().unwrap()); + + let foo = world.component('Foo'); + assert(foo == foo::TEST_CLASS_HASH.try_into().unwrap(), 'foo does not exists'); + let bar = world.system('bar'); + assert(bar == bar::TEST_CLASS_HASH.try_into().unwrap(), 'bar does not exists'); +} + #[test] #[available_gas(6000000)] fn test_emit() { @@ -132,6 +146,46 @@ fn test_set_entity_unauthorized() { world.execute('bar', data); } +#[test] +#[available_gas(8000000)] +#[should_panic] +fn test_set_entity_invalid_data() { + // Spawn empty world + let world = deploy_world(); + + world.register_system(bar::TEST_CLASS_HASH.try_into().unwrap()); + world.register_component(foo::TEST_CLASS_HASH.try_into().unwrap()); + + let caller = starknet::contract_address_const::<0x1337>(); + starknet::testing::set_account_contract_address(caller); + + // Call bar system, should panic as data is invalid + let mut data = ArrayTrait::new(); + data.append(420); + world.execute('bar', data); +} + +#[test] +#[available_gas(8000000)] +#[should_panic] +fn test_set_entity_excess_data() { + // Spawn empty world + let world = deploy_world(); + + world.register_system(bar::TEST_CLASS_HASH.try_into().unwrap()); + world.register_component(foo::TEST_CLASS_HASH.try_into().unwrap()); + + let caller = starknet::contract_address_const::<0x1337>(); + starknet::testing::set_account_contract_address(caller); + + // Call bar system, should panic as it's not authorized + let mut data = ArrayTrait::new(); + data.append(420); + data.append(420); + data.append(420); + world.execute('bar', data); +} + #[test] #[available_gas(8000000)] #[should_panic] @@ -291,3 +345,70 @@ fn test_execute_origin() { world.execute('origin_wrapper', data); assert(world.origin() == starknet::contract_address_const::<0x0>(), 'should be equal'); } + +#[test] +#[available_gas(6000000)] +#[should_panic] +fn test_execute_origin_failing() { + // Spawn empty world + let world = deploy_world(); + + world.register_system(origin::TEST_CLASS_HASH.try_into().unwrap()); + world.register_system(origin_wrapper::TEST_CLASS_HASH.try_into().unwrap()); + world.register_component(foo::TEST_CLASS_HASH.try_into().unwrap()); + let data = ArrayTrait::new(); + + let eve = starknet::contract_address_const::<0x1338>(); + world.execute('origin_wrapper', data); +} + +#[test] +#[available_gas(6000000)] +fn test_execute_multiple_worlds() { + // Deploy executor contract + let executor_constructor_calldata = array::ArrayTrait::new(); + let (executor_address, _) = deploy_syscall( + executor::TEST_CLASS_HASH.try_into().unwrap(), + 0, + executor_constructor_calldata.span(), + false + ) + .unwrap(); + + // Deploy world contract + let mut constructor_calldata = array::ArrayTrait::new(); + constructor_calldata.append(executor_address.into()); + let (world_address, _) = deploy_syscall( + world::TEST_CLASS_HASH.try_into().unwrap(), 0, constructor_calldata.span(), false + ).unwrap(); + let world = IWorldDispatcher { contract_address: world_address }; + + // Deploy another world contract + let (world_address, _) = deploy_syscall( + world::TEST_CLASS_HASH.try_into().unwrap(), 0, constructor_calldata.span(), false + ).unwrap(); + let another_world = IWorldDispatcher { contract_address: world_address }; + + world.register_system(bar::TEST_CLASS_HASH.try_into().unwrap()); + world.register_component(foo::TEST_CLASS_HASH.try_into().unwrap()); + another_world.register_system(bar::TEST_CLASS_HASH.try_into().unwrap()); + another_world.register_component(foo::TEST_CLASS_HASH.try_into().unwrap()); + + + let mut data = ArrayTrait::new(); + data.append(1337); + data.append(1337); + let mut another_data = ArrayTrait::new(); + another_data.append(7331); + another_data.append(7331); + let mut keys = ArrayTrait::new(); + keys.append(0); + + world.execute('bar', data); + another_world.execute('bar', another_data); + + let stored = world.entity('Foo', keys.span(), 0, dojo::StorageSize::::unpacked_size()); + let another_stored = another_world.entity('Foo', keys.span(), 0, dojo::StorageSize::::unpacked_size()); + assert(*stored.snapshot.at(0) == 1337, 'data not stored'); + assert(*another_stored.snapshot.at(0) == 7331, 'data not stored'); +} diff --git a/crates/dojo-defi/src/dutch_auction/common.cairo b/crates/dojo-defi/src/dutch_auction/common.cairo index 6420dff345..f9e0cdd22d 100644 --- a/crates/dojo-defi/src/dutch_auction/common.cairo +++ b/crates/dojo-defi/src/dutch_auction/common.cairo @@ -1,4 +1,5 @@ use cubit::f128::types::fixed::{Fixed, FixedTrait, ONE_u128}; +use dojo_defi::tests::utils::{assert_approx_equal, TOLERANCE}; fn to_days_fp(x: Fixed) -> Fixed { x / FixedTrait::new(86400, false) @@ -7,3 +8,10 @@ fn to_days_fp(x: Fixed) -> Fixed { fn from_days_fp(x: Fixed) -> Fixed { x * FixedTrait::new(86400, false) } + +#[test] +#[available_gas(20000000)] +fn test_days_convertions() { + let days = FixedTrait::new(2, false); + assert_approx_equal(days, to_days_fp(from_days_fp(days)), TOLERANCE * 10); +} From 6e108dbe97bcc4fec8c133a2201911295421ebc7 Mon Sep 17 00:00:00 2001 From: Tarrence van As Date: Thu, 7 Sep 2023 15:05:23 -0400 Subject: [PATCH 33/77] Refactor component macro (#876) --- crates/dojo-lang/src/component.rs | 149 ++++++------------ .../dojo-lang/src/plugin_test_data/component | 4 +- 2 files changed, 47 insertions(+), 106 deletions(-) diff --git a/crates/dojo-lang/src/component.rs b/crates/dojo-lang/src/component.rs index 733df0c138..79a6a092c0 100644 --- a/crates/dojo-lang/src/component.rs +++ b/crates/dojo-lang/src/component.rs @@ -24,17 +24,17 @@ pub fn handle_component_struct( ) -> (RewriteNode, Vec) { let mut diagnostics = vec![]; - let members: Vec<_> = struct_ast - .members(db) - .elements(db) + let elements = struct_ast.members(db).elements(db); + let members: &Vec<_> = &elements .iter() - .map(|member| { - (member.name(db).text(db), member.type_clause(db).ty(db), member.has_attr(db, "key")) + .map(|member| Member { + name: member.name(db).text(db).to_string(), + ty: member.type_clause(db).ty(db).as_syntax_node().get_text(db).trim().to_string(), + key: member.has_attr(db, "key"), }) .collect::<_>(); - let elements = struct_ast.members(db).elements(db); - let keys: Vec<_> = elements.iter().filter(|e| e.has_attr(db, "key")).collect::<_>(); + let keys: Vec<_> = members.iter().filter(|m| m.key).collect::<_>(); if keys.is_empty() { diagnostics.push(PluginDiagnostic { @@ -43,115 +43,57 @@ pub fn handle_component_struct( }); } - let key_names = keys.iter().map(|e| e.name(db).text(db)).join(", "); + let serialize_member = |m: &Member, include_key: bool| { + if m.key && !include_key { + return None; + } - let key_types = - keys.iter().map(|e| e.type_clause(db).ty(db).as_syntax_node().get_text(db)).join(", "); + if m.ty == "felt252" { + return Some(RewriteNode::Text(format!( + "array::ArrayTrait::append(ref serialized, *self.{});\n", + m.name + ))); + } - let serialized_keys: Vec<_> = keys - .iter() - .map(|e| { - if e.type_clause(db).ty(db).as_syntax_node().get_text(db) == "felt252" { - return RewriteNode::Text(format!( - "array::ArrayTrait::append(ref serialized, {});\n", - e.name(db).text(db) - )); - } + Some(RewriteNode::Text(format!( + "serde::Serde::serialize(self.{}, ref serialized);", + m.name + ))) + }; - RewriteNode::Text(format!( - "serde::Serde::serialize(@{}, ref serialized);\n", - e.name(db).text(db) - )) - }) - .collect::<_>(); + let serialized_keys: Vec<_> = + keys.iter().filter_map(|m| serialize_member(m, true)).collect::<_>(); - let component_serialized_keys: Vec<_> = keys - .iter() - .map(|e| { - if e.type_clause(db).ty(db).as_syntax_node().get_text(db) == "felt252" { - return RewriteNode::Text(format!( - "array::ArrayTrait::append(ref serialized, *self.{});\n", - e.name(db).text(db) - )); - } - - RewriteNode::Text(format!( - "serde::Serde::serialize(self.{}, ref serialized);\n", - e.name(db).text(db) - )) - }) - .collect::<_>(); + let serialized_values: Vec<_> = + members.iter().filter_map(|m| serialize_member(m, false)).collect::<_>(); - let component_serialized_values: Vec<_> = elements + let schema = members .iter() - .filter_map(|e| { - if !e.has_attr(db, "key") { - if e.type_clause(db).ty(db).as_syntax_node().get_text(db) == "felt252" { - return Some(RewriteNode::Text(format!( - "array::ArrayTrait::append(ref serialized, *self.{});\n", - e.name(db).text(db) - ))); - } - - return Some(RewriteNode::Text(format!( - "serde::Serde::serialize(self.{}, ref serialized);", - e.name(db).text(db) - ))); - } - - None - }) - .collect::<_>(); - - let schema = elements - .iter() - .map(|member| { + .map(|m| { RewriteNode::interpolate_patched( "array::ArrayTrait::append(ref arr, ('$name$', '$typ$', $is_key$));", UnorderedHashMap::from([ - ( - "name".to_string(), - RewriteNode::new_trimmed(member.name(db).as_syntax_node()), - ), - ( - "typ".to_string(), - RewriteNode::new_trimmed(member.type_clause(db).ty(db).as_syntax_node()), - ), - ( - "is_key".to_string(), - RewriteNode::Text(member.has_attr(db, "key").to_string()), - ), + ("name".to_string(), RewriteNode::Text(m.name.to_string())), + ("typ".to_string(), RewriteNode::Text(m.ty.to_string())), + ("is_key".to_string(), RewriteNode::Text(m.key.to_string())), ]), ) }) .collect::<_>(); let name = struct_ast.name(db).text(db); - aux_data.components.push(Component { - name: name.to_string(), - members: members - .iter() - .map(|(name, ty, key)| Member { - name: name.to_string(), - ty: ty.as_syntax_node().get_text(db).trim().to_string(), - key: *key, - }) - .collect(), - }); + aux_data.components.push(Component { name: name.to_string(), members: members.to_vec() }); - let member_prints: Vec<_> = members + let prints: Vec<_> = members .iter() - .map(|member| { - let member_name = &member.0; + .map(|m| { format!( "debug::PrintTrait::print('{}'); debug::PrintTrait::print(self.{});", - member_name, member_name + m.name, m.name ) }) .collect(); - let print_body = member_prints.join("\n"); - ( RewriteNode::interpolate_patched( " @@ -168,14 +110,14 @@ pub fn handle_component_struct( #[inline(always)] fn keys(self: @$type_name$) -> Span { let mut serialized = ArrayTrait::new(); - $component_serialized_keys$ + $serialized_keys$ array::ArrayTrait::span(@serialized) } #[inline(always)] fn values(self: @$type_name$) -> Span { let mut serialized = ArrayTrait::new(); - $component_serialized_values$ + $serialized_values$ array::ArrayTrait::span(@serialized) } } @@ -195,7 +137,7 @@ pub fn handle_component_struct( #[cfg(test)] impl $type_name$PrintImpl of debug::PrintTrait<$type_name$> { fn print(self: $type_name$) { - $print_body$ + $print$ } } @@ -239,19 +181,18 @@ pub fn handle_component_struct( "members".to_string(), RewriteNode::Copied(struct_ast.members(db).as_syntax_node()), ), - ("key_names".to_string(), RewriteNode::Text(key_names)), - ("key_types".to_string(), RewriteNode::Text(key_types)), - ("serialized_keys".to_string(), RewriteNode::new_modified(serialized_keys)), ( - "component_serialized_keys".to_string(), - RewriteNode::new_modified(component_serialized_keys), + "key_names".to_string(), + RewriteNode::Text(keys.iter().map(|m| m.name.to_string()).join(", ")), ), ( - "component_serialized_values".to_string(), - RewriteNode::new_modified(component_serialized_values), + "key_types".to_string(), + RewriteNode::Text(keys.iter().map(|m| m.ty.to_string()).join(", ")), ), + ("serialized_keys".to_string(), RewriteNode::new_modified(serialized_keys)), + ("serialized_values".to_string(), RewriteNode::new_modified(serialized_values)), ("schema".to_string(), RewriteNode::new_modified(schema)), - ("print_body".to_string(), RewriteNode::Text(print_body)), + ("print".to_string(), RewriteNode::Text(prints.join("\n"))), ( "unpacked_size".to_string(), RewriteNode::Text( diff --git a/crates/dojo-lang/src/plugin_test_data/component b/crates/dojo-lang/src/plugin_test_data/component index bcedb23ece..2f7f610300 100644 --- a/crates/dojo-lang/src/plugin_test_data/component +++ b/crates/dojo-lang/src/plugin_test_data/component @@ -78,7 +78,8 @@ impl PositionComponent of dojo::component::Component { fn values(self: @Position) -> Span { let mut serialized = ArrayTrait::new(); array::ArrayTrait::append(ref serialized, *self.x); - serde::Serde::serialize(self.y, ref serialized); + array::ArrayTrait::append(ref serialized, *self.y); + array::ArrayTrait::span(@serialized) } } @@ -259,7 +260,6 @@ impl PlayerComponent of dojo::component::Component { let mut serialized = ArrayTrait::new(); array::ArrayTrait::append(ref serialized, *self.game); serde::Serde::serialize(self.player, ref serialized); - array::ArrayTrait::span(@serialized) } From 1c1533538f8293564e3065b3c7cdb4dc4ab25ef7 Mon Sep 17 00:00:00 2001 From: Kariy Date: Fri, 8 Sep 2023 10:47:31 +0900 Subject: [PATCH 34/77] fix(katana): fix disable fee not working + output execution logs --- crates/katana/core/src/service.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/crates/katana/core/src/service.rs b/crates/katana/core/src/service.rs index 6e3d5c0ce7..ebe228ea18 100644 --- a/crates/katana/core/src/service.rs +++ b/crates/katana/core/src/service.rs @@ -398,8 +398,15 @@ impl InstantBlockProducer { let mut state = CachedStateWrapper::new(backend.state.read().await.as_ref_db()); let block_context = backend.env.read().block.clone(); - let results = TransactionExecutor::new(&mut state, &block_context, true) - .execute_many(transactions.clone()); + let results = TransactionExecutor::new( + &mut state, + &block_context, + !backend.config.read().disable_fee, + ) + .with_error_log() + .with_events_log() + .with_resources_log() + .execute_many(transactions.clone()); let outcome = backend .do_mine_block(create_execution_outcome( From 35a68c82d35fcb4a3c87ccd57548b5cbb6535af6 Mon Sep 17 00:00:00 2001 From: Yun Date: Fri, 8 Sep 2023 05:08:51 -0700 Subject: [PATCH 35/77] feat(torii): enum support (#874) * fix(torii): exact key matching issue * chore(torii): gen sql type mapping at runtime * fix graphql type * update ecs example to add enum * remove obsolete fixtures * update test manifest * fix test --- .../dojo-lang/src/manifest_test_data/manifest | 23 +++-- .../torii/client/src/contract/system_test.rs | 6 +- crates/torii/core/src/sql.rs | 87 ++++++++++++------- crates/torii/graphql/src/tests/events_test.rs | 57 ------------ .../graphql/src/tests/fixtures/events.sql | 6 -- .../src/tests/fixtures/system_calls.sql | 6 -- .../graphql/src/tests/fixtures/systems.sql | 6 -- crates/torii/graphql/src/tests/mod.rs | 1 - crates/torii/graphql/src/types.rs | 5 ++ examples/ecs/src/components.cairo | 49 ++++++++++- examples/ecs/src/systems.cairo | 30 ++----- 11 files changed, 135 insertions(+), 141 deletions(-) delete mode 100644 crates/torii/graphql/src/tests/events_test.rs delete mode 100644 crates/torii/graphql/src/tests/fixtures/events.sql delete mode 100644 crates/torii/graphql/src/tests/fixtures/system_calls.sql delete mode 100644 crates/torii/graphql/src/tests/fixtures/systems.sql diff --git a/crates/dojo-lang/src/manifest_test_data/manifest b/crates/dojo-lang/src/manifest_test_data/manifest index 37bf0eb97f..9fcd01c66c 100644 --- a/crates/dojo-lang/src/manifest_test_data/manifest +++ b/crates/dojo-lang/src/manifest_test_data/manifest @@ -626,7 +626,7 @@ test_manifest_file } ], "outputs": [], - "class_hash": "0x3459b99fd17b3bf9aeb6c55b610a457520785646f5c80c21d07041403a25315", + "class_hash": "0x205287b11aa6c42e3cef3539607aa250c2b9ffe27f847af8e88f005e1b3525d", "dependencies": [], "abi": [ { @@ -701,11 +701,11 @@ test_manifest_file }, { "name": "direction", - "type": "dojo_examples::systems::move::Direction" + "type": "dojo_examples::components::Direction" } ], "outputs": [], - "class_hash": "0x578416d298f2f6c280a229f58fd7b5b2486285700fcfde9ad102c4052d3804e", + "class_hash": "0x501dbe28cf4e7f3a47c9abeee23c85992b97fdaa324c1fcb1e2a638882fc470", "dependencies": [], "abi": [ { @@ -721,8 +721,12 @@ test_manifest_file }, { "type": "enum", - "name": "dojo_examples::systems::move::Direction", + "name": "dojo_examples::components::Direction", "variants": [ + { + "name": "None", + "type": "()" + }, { "name": "Left", "type": "()" @@ -779,7 +783,7 @@ test_manifest_file "inputs": [ { "name": "direction", - "type": "dojo_examples::systems::move::Direction" + "type": "dojo_examples::components::Direction" }, { "name": "ctx", @@ -801,7 +805,7 @@ test_manifest_file }, { "name": "direction", - "type": "dojo_examples::systems::move::Direction", + "type": "dojo_examples::components::Direction", "kind": "data" } ] @@ -952,9 +956,14 @@ test_manifest_file "name": "remaining", "type": "u8", "key": false + }, + { + "name": "last_direction", + "type": "Direction", + "key": false } ], - "class_hash": "0x74c8a6b2e93b4a34788c23eb3d931da62d5a973ef66ba0a671de41b1607a9a4", + "class_hash": "0x451a367ced4c22bfc661e6a342391faa6087d3162ea53cf4c6cc86c8630de49", "abi": [ { "type": "function", diff --git a/crates/torii/client/src/contract/system_test.rs b/crates/torii/client/src/contract/system_test.rs index cb9e40b87b..0251f5e43f 100644 --- a/crates/torii/client/src/contract/system_test.rs +++ b/crates/torii/client/src/contract/system_test.rs @@ -34,7 +34,7 @@ async fn test_system() { let component = world.component("Moves", block_id).await.unwrap(); let moves = component.entity(vec![account.address()], block_id).await.unwrap(); - assert_eq!(moves, vec![10_u8.into()]); + assert_eq!(moves, vec![10_u8.into(), FieldElement::ZERO]); let move_system = world.system("move", block_id).await.unwrap(); @@ -48,11 +48,11 @@ async fn test_system() { let moves = component.entity(vec![account.address()], block_id).await.unwrap(); - assert_eq!(moves, vec![8_u8.into()]); + assert_eq!(moves, vec![8_u8.into(), FieldElement::THREE]); let position_component = world.component("Position", block_id).await.unwrap(); let position = position_component.entity(vec![account.address()], block_id).await.unwrap(); - assert_eq!(position, vec![11_u8.into(), 11_u8.into()]); + assert_eq!(position, vec![9_u8.into(), 9_u8.into()]); } diff --git a/crates/torii/core/src/sql.rs b/crates/torii/core/src/sql.rs index 4343bd7a03..c35052415b 100644 --- a/crates/torii/core/src/sql.rs +++ b/crates/torii/core/src/sql.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use anyhow::Result; use async_trait::async_trait; use chrono::{DateTime, Utc}; @@ -27,6 +29,7 @@ pub struct Sql { world_address: FieldElement, pool: Pool, query_queue: Mutex>, + sql_types: Mutex>, } impl Sql { @@ -50,7 +53,29 @@ impl Sql { tx.commit().await?; - Ok(Self { pool, world_address, query_queue: Mutex::new(vec![]) }) + let sql_types = HashMap::from([ + ("u8".to_string(), "INTEGER"), + ("u16".to_string(), "INTEGER"), + ("u32".to_string(), "INTEGER"), + ("u64".to_string(), "INTEGER"), + ("u128".to_string(), "TEXT"), + ("u256".to_string(), "TEXT"), + ("usize".to_string(), "INTEGER"), + ("bool".to_string(), "INTEGER"), + ("Cursor".to_string(), "TEXT"), + ("ContractAddress".to_string(), "TEXT"), + ("ClassHash".to_string(), "TEXT"), + ("DateTime".to_string(), "TEXT"), + ("felt252".to_string(), "TEXT"), + ("Enum".to_string(), "INTEGER"), + ]); + + Ok(Self { + pool, + world_address, + query_queue: Mutex::new(vec![]), + sql_types: Mutex::new(sql_types), + }) } } @@ -157,6 +182,8 @@ impl State for Sql { } async fn register_component(&self, component: Component) -> Result<()> { + let mut sql_types = self.sql_types.lock().await; + let component_id = component.name.to_lowercase(); let mut queries = vec![format!( "INSERT INTO components (id, name, class_hash) VALUES ('{}', '{}', '{:#x}') ON \ @@ -169,12 +196,24 @@ impl State for Sql { component.name.to_lowercase() ); - for member in component.clone().members { - component_table_query.push_str(&format!( - "external_{} {}, ", - member.name, - sql_type(&member.ty)? + for member in &component.members { + // FIXME: defaults all unknown component types to Enum for now until we support nested + // components + let (sql_type, member_type) = match sql_types.get(&member.ty) { + Some(sql_type) => (*sql_type, member.ty.as_str()), + None => { + sql_types.insert(member.ty.clone(), "INTEGER"); + ("INTEGER", "Enum") + } + }; + + queries.push(format!( + "INSERT OR IGNORE INTO component_members (component_id, name, type, key) VALUES \ + ('{}', '{}', '{}', {})", + component_id, member.name, member_type, member.key, )); + + component_table_query.push_str(&format!("external_{} {}, ", member.name, sql_type)); } component_table_query.push_str( @@ -183,15 +222,8 @@ impl State for Sql { ); queries.push(component_table_query); - for member in component.members { - queries.push(format!( - "INSERT OR IGNORE INTO component_members (component_id, name, type, key) VALUES \ - ('{}', '{}', '{}', {})", - component_id, member.name, member.ty, member.key, - )); - } - self.queue(queries).await; + // Since previous query has not been executed, we have to make sure created_at exists let created_at: DateTime = match sqlx::query("SELECT created_at FROM components WHERE id = ?") @@ -259,7 +291,9 @@ impl State for Sql { member_values.extend(keys); member_values.extend(values); - let (names_str, values_str) = format_values(member_names_result, member_values)?; + let sql_types = self.sql_types.lock().await; + let (names_str, values_str) = + format_values(member_names_result, member_values, &sql_types)?; let insert_components = format!( "INSERT OR REPLACE INTO external_{} (entity_id {}) VALUES ('{}' {})", component.to_lowercase(), @@ -329,6 +363,7 @@ fn component_names(entity_result: Option, new_component: &str) -> Res fn format_values( member_results: Vec, values: Vec, + sql_types: &HashMap, ) -> Result<(String, String)> { let names: Result> = member_results .iter() @@ -345,26 +380,12 @@ fn format_values( let values: Result> = values .iter() .zip(types?.iter()) - .map(|(value, ty)| { - if sql_type(ty)? == "INTEGER" { - Ok(format!(",'{}'", value)) - } else { - Ok(format!(",'{:#x}'", value)) - } + .map(|(value, ty)| match sql_types.get(ty).copied() { + Some("INTEGER") => Ok(format!(",'{}'", value)), + Some("TEXT") => Ok(format!(",'{:#x}'", value)), + _ => Err(anyhow::anyhow!("Unsupported type {}", ty)), }) .collect(); Ok((names?.join(""), values?.join(""))) } - -// NOTE: If adding/removing types, corresponding change needs to be made to torii-graphql -// `src/types.rs` -fn sql_type(member_type: &str) -> Result<&str, anyhow::Error> { - match member_type { - "u8" | "u16" | "u32" | "u64" | "usize" | "bool" => Ok("INTEGER"), - "u128" | "u256" | "Cursor" | "ContractAddress" | "ClassHash" | "DateTime" | "felt252" => { - Ok("TEXT") - } - _ => Err(anyhow::anyhow!("Unknown member type {}", member_type.to_string())), - } -} diff --git a/crates/torii/graphql/src/tests/events_test.rs b/crates/torii/graphql/src/tests/events_test.rs deleted file mode 100644 index a7decace49..0000000000 --- a/crates/torii/graphql/src/tests/events_test.rs +++ /dev/null @@ -1,57 +0,0 @@ -#[cfg(test)] -mod tests { - use serde::Deserialize; - use sqlx::SqlitePool; - - use crate::tests::common::run_graphql_query; - - #[derive(Deserialize)] - pub struct Event { - pub id: String, - } - - #[sqlx::test(migrations = "../migrations", fixtures("systems", "system_calls", "events"))] - async fn test_event(pool: SqlitePool) { - let _ = pool.acquire().await; - - let query = "{ event(id: \"event_1\") { id } }"; - let value = run_graphql_query(&pool, query).await; - - let event = value.get("event").ok_or("no event found").unwrap(); - let event: Event = serde_json::from_value(event.clone()).unwrap(); - assert_eq!(event.id, "event_1".to_string()); - } - - // #[sqlx::test(migrations = "../migrations", fixtures("systems", "system_calls", "events"))] - // async fn test_event_by_keys(pool: SqlitePool) { - // let _ = pool.acquire().await; - - // let query = "{ events(keys: [\"key_1\", \"key_2\", \"key_3\"]) { edges { node { id keys \ - // data createdAt } } } }"; - // let value = run_graphql_query(&pool, query).await; - // let events = value.get("events").ok_or("no event found").unwrap(); - // let edges = events.get("edges").ok_or("no event found").unwrap(); - // let edges: Vec = serde_json::from_value(edges.clone()).unwrap(); - // let node = edges[0].get("node").ok_or("no event found").unwrap(); - // let event: Event = serde_json::from_value(node.clone()).unwrap(); - // assert_eq!(event.id, "event_1".to_string()); - - // let query = "{ events(keys: [\"key_1\", \"key_2\"]) { edges { node { id keys data \ - // createdAt } } } }"; - // let value = run_graphql_query(&pool, query).await; - // let events = value.get("events").ok_or("no event found").unwrap(); - // let edges = events.get("edges").ok_or("no event found").unwrap(); - // let edges: Vec = serde_json::from_value(edges.clone()).unwrap(); - // assert_eq!(edges.len(), 2); - - // let query = "{ events(keys: [\"key_3\"]) { edges { node { id keys data \ - // createdAt } } } }"; - // let value = run_graphql_query(&pool, query).await; - // let events = value.get("events").ok_or("no event found").unwrap(); - // let edges = events.get("edges").ok_or("no event found").unwrap(); - // let edges: Vec = serde_json::from_value(edges.clone()).unwrap(); - // let node = edges[0].get("node").ok_or("no event found").unwrap(); - // let event: Event = serde_json::from_value(node.clone()).unwrap(); - // assert_eq!(event.id, "event_3".to_string()); - // } -} diff --git a/crates/torii/graphql/src/tests/fixtures/events.sql b/crates/torii/graphql/src/tests/fixtures/events.sql deleted file mode 100644 index 9fb5ab89d1..0000000000 --- a/crates/torii/graphql/src/tests/fixtures/events.sql +++ /dev/null @@ -1,6 +0,0 @@ -INSERT INTO events (id, system_call_id, keys, data) -VALUES ('event_1', 1, 'key_1,key_2,key_3', '0x1,0x2,0x3'); -INSERT INTO events (id, system_call_id, keys, data) -VALUES ('event_2', 2, 'key_1,key_2', '0x1,0x2,0x3'); -INSERT INTO events (id, system_call_id, keys, data) -VALUES ('event_3', 3, 'key_3', '0x1,0x2,0x3'); \ No newline at end of file diff --git a/crates/torii/graphql/src/tests/fixtures/system_calls.sql b/crates/torii/graphql/src/tests/fixtures/system_calls.sql deleted file mode 100644 index b475d36073..0000000000 --- a/crates/torii/graphql/src/tests/fixtures/system_calls.sql +++ /dev/null @@ -1,6 +0,0 @@ -INSERT INTO system_calls (id, system_id, transaction_hash, data) -VALUES (1, 'system_1', '0x0', "0x1,0x2,0x3"); -INSERT INTO system_calls (id, system_id, transaction_hash, data) -VALUES (2, 'system_2', '0x0', "0x1,0x2,0x3"); -INSERT INTO system_calls (id, system_id, transaction_hash, data) -VALUES (3, 'system_3', '0x0', "0x1,0x2,0x3"); \ No newline at end of file diff --git a/crates/torii/graphql/src/tests/fixtures/systems.sql b/crates/torii/graphql/src/tests/fixtures/systems.sql deleted file mode 100644 index 1fc0ae4f9d..0000000000 --- a/crates/torii/graphql/src/tests/fixtures/systems.sql +++ /dev/null @@ -1,6 +0,0 @@ -INSERT INTO systems (id, name, class_hash, transaction_hash) -VALUES ('system_1', 'System1', '0x0', '0x0'); -INSERT INTO systems (id, name, class_hash, transaction_hash) -VALUES ('system_2', 'System2', '0x0', '0x0'); -INSERT INTO systems (id, name, class_hash, transaction_hash) -VALUES ('system_3', 'System3', '0x0', '0x0'); \ No newline at end of file diff --git a/crates/torii/graphql/src/tests/mod.rs b/crates/torii/graphql/src/tests/mod.rs index 04b79d880c..93d63a4072 100644 --- a/crates/torii/graphql/src/tests/mod.rs +++ b/crates/torii/graphql/src/tests/mod.rs @@ -1,5 +1,4 @@ mod common; mod components_test; mod entities_test; -mod events_test; mod subscription_test; diff --git a/crates/torii/graphql/src/types.rs b/crates/torii/graphql/src/types.rs index d355ce1b1f..fe7b22e3ff 100644 --- a/crates/torii/graphql/src/types.rs +++ b/crates/torii/graphql/src/types.rs @@ -19,6 +19,7 @@ pub enum ScalarType { ClassHash, DateTime, Felt252, + Enum, } impl fmt::Display for ScalarType { @@ -37,6 +38,7 @@ impl fmt::Display for ScalarType { ScalarType::ClassHash => write!(f, "ClassHash"), ScalarType::DateTime => write!(f, "DateTime"), ScalarType::Felt252 => write!(f, "felt252"), + ScalarType::Enum => write!(f, "Enum"), } } } @@ -57,6 +59,7 @@ impl ScalarType { ScalarType::ClassHash, ScalarType::DateTime, ScalarType::Felt252, + ScalarType::Enum, ] .into_iter() .collect() @@ -70,6 +73,7 @@ impl ScalarType { ScalarType::U64, ScalarType::USize, ScalarType::Bool, + ScalarType::Enum, ] .into_iter() .collect() @@ -114,6 +118,7 @@ impl FromStr for ScalarType { "ClassHash" => Ok(ScalarType::ClassHash), "DateTime" => Ok(ScalarType::DateTime), "felt252" => Ok(ScalarType::Felt252), + "Enum" => Ok(ScalarType::Enum), _ => Err(anyhow::anyhow!("Unknown type {}", s.to_string())), } } diff --git a/examples/ecs/src/components.cairo b/examples/ecs/src/components.cairo index 26c6b1b15e..398db8b7c1 100644 --- a/examples/ecs/src/components.cairo +++ b/examples/ecs/src/components.cairo @@ -1,11 +1,58 @@ use array::ArrayTrait; +use core::debug::PrintTrait; use starknet::ContractAddress; +#[derive(Serde, Copy, Drop)] +enum Direction { + None: (), + Left: (), + Right: (), + Up: (), + Down: (), +} + +impl DirectionStorageSizeImpl of dojo::StorageSize { + #[inline(always)] + fn unpacked_size() -> usize { + 1 + } + + #[inline(always)] + fn packed_size() -> usize { + 2 + } +} + +impl DirectionPrintImpl of PrintTrait { + fn print(self: Direction) { + match self { + Direction::None(()) => 0.print(), + Direction::Left(()) => 1.print(), + Direction::Right(()) => 2.print(), + Direction::Up(()) => 3.print(), + Direction::Down(()) => 4.print(), + } + } +} + +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(Component, Copy, Drop, Serde)] struct Moves { #[key] player: ContractAddress, remaining: u8, + last_direction: Direction } #[derive(Component, Copy, Drop, Serde)] @@ -54,4 +101,4 @@ mod tests { position.print(); assert(PositionTrait::is_equal(position, Position { player, x: 420, y: 0 }), 'not equal'); } -} \ No newline at end of file +} diff --git a/examples/ecs/src/systems.cairo b/examples/ecs/src/systems.cairo index 4a849f2138..46fda78a45 100644 --- a/examples/ecs/src/systems.cairo +++ b/examples/ecs/src/systems.cairo @@ -7,6 +7,7 @@ mod spawn { use dojo_examples::components::Position; use dojo_examples::components::Moves; + use dojo_examples::components::Direction; fn execute(ctx: Context) { let position = get !(ctx.world, ctx.origin, (Position)); @@ -14,7 +15,7 @@ mod spawn { ctx.world, ( Moves { - player: ctx.origin, remaining: 10 + player: ctx.origin, remaining: 10, last_direction: Direction::None(()) }, Position { player: ctx.origin, x: position.x + 10, y: position.y + 10 }, @@ -34,11 +35,12 @@ mod move { use dojo_examples::components::Position; use dojo_examples::components::Moves; + use dojo_examples::components::Direction; #[event] #[derive(Drop, starknet::Event)] enum Event { - Moved: Moved, + Moved: Moved, } #[derive(Drop, starknet::Event)] @@ -47,28 +49,10 @@ mod move { direction: Direction } - #[derive(Serde, Copy, Drop)] - enum Direction { - Left: (), - Right: (), - Up: (), - Down: (), - } - - impl DirectionIntoFelt252 of Into { - fn into(self: Direction) -> felt252 { - match self { - Direction::Left(()) => 0, - Direction::Right(()) => 1, - Direction::Up(()) => 2, - Direction::Down(()) => 3, - } - } - } - fn execute(ctx: Context, direction: Direction) { let (mut position, mut moves) = get !(ctx.world, ctx.origin, (Position, Moves)); moves.remaining -= 1; + moves.last_direction = direction; let next = next_position(position, direction); set !(ctx.world, (moves, next)); emit !(ctx.world, Moved { address: ctx.origin, direction }); @@ -77,6 +61,9 @@ mod move { fn next_position(mut position: Position, direction: Direction) -> Position { match direction { + Direction::None(()) => { + return position; + }, Direction::Left(()) => { position.x -= 1; }, @@ -140,6 +127,7 @@ mod tests { let moves = world.entity('Moves', keys.span(), 0, dojo::StorageSize::::unpacked_size()); assert(*moves[0] == 9, 'moves is wrong'); + assert(*moves[1] == move::Direction::Right(()).into(), 'last direction is wrong'); let new_position = world .entity('Position', keys.span(), 0, dojo::StorageSize::::unpacked_size()); assert(*new_position[0] == 11, 'position x is wrong'); From 933646f8c412d1d4c5197045318fc92a9f912fb5 Mon Sep 17 00:00:00 2001 From: Yun Date: Fri, 8 Sep 2023 05:12:08 -0700 Subject: [PATCH 36/77] feat(torii): index system calls (#875) * feat(torii): index system calls * fix name * remove obsolete fixtures --- crates/torii/core/src/lib.rs | 6 ++ crates/torii/core/src/processors/mod.rs | 2 +- .../core/src/processors/store_system_call.rs | 56 +++++++++++++++++++ crates/torii/core/src/sql.rs | 17 ++++++ crates/torii/core/src/sql_test.rs | 12 ++++ .../torii/migrations/20230316154230_setup.sql | 3 +- crates/torii/server/src/cli.rs | 2 + crates/torii/server/src/engine.rs | 21 ++++--- 8 files changed, 106 insertions(+), 13 deletions(-) create mode 100644 crates/torii/core/src/processors/store_system_call.rs diff --git a/crates/torii/core/src/lib.rs b/crates/torii/core/src/lib.rs index 4f67cc5056..254ced1da8 100644 --- a/crates/torii/core/src/lib.rs +++ b/crates/torii/core/src/lib.rs @@ -43,4 +43,10 @@ pub trait State { async fn delete_entity(&self, component: String, key: FieldElement) -> Result<()>; async fn entity(&self, component: String, key: FieldElement) -> Result>; async fn entities(&self, component: String) -> Result>>; + async fn store_system_call( + &self, + system: String, + tx_hash: FieldElement, + calldata: &[FieldElement], + ) -> Result<()>; } diff --git a/crates/torii/core/src/processors/mod.rs b/crates/torii/core/src/processors/mod.rs index 993c670d22..da1bd15abb 100644 --- a/crates/torii/core/src/processors/mod.rs +++ b/crates/torii/core/src/processors/mod.rs @@ -8,6 +8,7 @@ use crate::State; pub mod register_component; pub mod register_system; pub mod store_set_record; +pub mod store_system_call; #[async_trait] pub trait EventProcessor { @@ -35,7 +36,6 @@ pub trait BlockProcessor { #[async_trait] pub trait TransactionProcessor { - fn get_transaction_hash(&self) -> String; async fn process( &self, storage: &S, diff --git a/crates/torii/core/src/processors/store_system_call.rs b/crates/torii/core/src/processors/store_system_call.rs new file mode 100644 index 0000000000..658124fcfb --- /dev/null +++ b/crates/torii/core/src/processors/store_system_call.rs @@ -0,0 +1,56 @@ +use std::str::FromStr; + +use anyhow::{Error, Ok, Result}; +use async_trait::async_trait; +use starknet::core::types::{ + BlockWithTxs, FieldElement, InvokeTransaction, Transaction, TransactionReceipt, +}; +use starknet::core::utils::parse_cairo_short_string; +use starknet::providers::jsonrpc::{JsonRpcClient, JsonRpcTransport}; + +use super::TransactionProcessor; +use crate::State; + +#[derive(Default)] +pub struct StoreSystemCallProcessor; + +const SYSTEM_NAME_OFFSET: usize = 6; +const ENTRYPOINT_OFFSET: usize = 2; +const EXECUTE_ENTRYPOINT: &str = + "0x240060cdb34fcc260f41eac7474ee1d7c80b7e3607daff9ac67c7ea2ebb1c44"; + +#[async_trait] +impl TransactionProcessor for StoreSystemCallProcessor { + async fn process( + &self, + storage: &S, + _provider: &JsonRpcClient, + block: &BlockWithTxs, + transaction_receipt: &TransactionReceipt, + ) -> Result<(), Error> { + if let TransactionReceipt::Invoke(_) = transaction_receipt { + for tx in &block.transactions { + if let Some((tx_hash, system_name, calldata)) = parse_transaction(tx) { + let system_name = parse_cairo_short_string(&system_name)?; + + storage.store_system_call(system_name, tx_hash, calldata).await?; + } + } + } + + Ok(()) + } +} + +fn parse_transaction( + transaction: &Transaction, +) -> Option<(FieldElement, FieldElement, &Vec)> { + if let Transaction::Invoke(InvokeTransaction::V1(tx)) = transaction { + let entrypoint_felt = FieldElement::from_str(EXECUTE_ENTRYPOINT).unwrap(); + if tx.calldata[ENTRYPOINT_OFFSET] == entrypoint_felt { + return Some((tx.transaction_hash, tx.calldata[SYSTEM_NAME_OFFSET], &tx.calldata)); + } + } + + None +} diff --git a/crates/torii/core/src/sql.rs b/crates/torii/core/src/sql.rs index c35052415b..1f944b5f22 100644 --- a/crates/torii/core/src/sql.rs +++ b/crates/torii/core/src/sql.rs @@ -342,6 +342,23 @@ impl State for Sql { sqlx::query_as::<_, (i32, String, String)>(&query).fetch_all(&mut conn).await?; Ok(rows.drain(..).map(|row| serde_json::from_str(&row.2).unwrap()).collect()) } + + async fn store_system_call( + &self, + system: String, + transaction_hash: FieldElement, + calldata: &[FieldElement], + ) -> Result<()> { + let query = format!( + "INSERT OR IGNORE INTO system_calls (data, transaction_hash, system_id) VALUES ('{}', \ + '{:#x}', '{}')", + calldata.iter().map(|c| format!("{:#x}", c)).collect::>().join(","), + transaction_hash, + system.to_lowercase() + ); + self.queue(vec![query]).await; + Ok(()) + } } fn component_names(entity_result: Option, new_component: &str) -> Result { diff --git a/crates/torii/core/src/sql_test.rs b/crates/torii/core/src/sql_test.rs index d4b2f53cd1..86101ca71e 100644 --- a/crates/torii/core/src/sql_test.rs +++ b/crates/torii/core/src/sql_test.rs @@ -1,3 +1,5 @@ +use std::str::FromStr; + use camino::Utf8PathBuf; use dojo_types::component::Member; use dojo_world::manifest::{Component, System}; @@ -111,5 +113,15 @@ async fn test_load_from_manifest(pool: SqlitePool) { ) .await .unwrap(); + + state + .store_system_call( + "Test".into(), + FieldElement::from_str("0x4").unwrap(), + &[FieldElement::ONE, FieldElement::TWO, FieldElement::THREE], + ) + .await + .unwrap(); + state.execute().await.unwrap(); } diff --git a/crates/torii/migrations/20230316154230_setup.sql b/crates/torii/migrations/20230316154230_setup.sql index 5590bf6532..1e20084e7a 100644 --- a/crates/torii/migrations/20230316154230_setup.sql +++ b/crates/torii/migrations/20230316154230_setup.sql @@ -42,7 +42,8 @@ CREATE TABLE system_calls ( transaction_hash TEXT NOT NULL, system_id TEXT NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (system_id) REFERENCES systems(id) + FOREIGN KEY (system_id) REFERENCES systems(id), + UNIQUE (transaction_hash) ); CREATE INDEX idx_system_calls_created_at ON system_calls (created_at); diff --git a/crates/torii/server/src/cli.rs b/crates/torii/server/src/cli.rs index d29f1e8ea3..bb3737caff 100644 --- a/crates/torii/server/src/cli.rs +++ b/crates/torii/server/src/cli.rs @@ -15,6 +15,7 @@ use tokio_util::sync::CancellationToken; use torii_core::processors::register_component::RegisterComponentProcessor; use torii_core::processors::register_system::RegisterSystemProcessor; use torii_core::processors::store_set_record::StoreSetRecordProcessor; +use torii_core::processors::store_system_call::StoreSystemCallProcessor; use torii_core::sql::Sql; use torii_core::State; use tracing::error; @@ -89,6 +90,7 @@ async fn main() -> anyhow::Result<()> { Box::new(RegisterSystemProcessor), Box::new(StoreSetRecordProcessor), ], + transaction: vec![Box::new(StoreSystemCallProcessor)], ..Processors::default() }; diff --git a/crates/torii/server/src/engine.rs b/crates/torii/server/src/engine.rs index bb62c1d885..8bf5d06a1e 100644 --- a/crates/torii/server/src/engine.rs +++ b/crates/torii/server/src/engine.rs @@ -149,19 +149,9 @@ impl<'a, S: State + Executable, T: JsonRpcTransport + Sync + Send> Engine<'a, S, _ => continue, }; - process_transaction( - self.storage, - self.provider, - &self.processors.transaction, - &block, - &receipt.clone(), - ) - .await?; - if let TransactionReceipt::Invoke(invoke_receipt) = receipt.clone() { for event in &invoke_receipt.events { if event.from_address != self.config.world_address { - info!("event not from world address, skipping"); continue; } @@ -176,6 +166,15 @@ impl<'a, S: State + Executable, T: JsonRpcTransport + Sync + Send> Engine<'a, S, .await?; } } + + process_transaction( + self.storage, + self.provider, + &self.processors.transaction, + &block, + &receipt.clone(), + ) + .await?; } info!("processed block: {}", block.block_number); @@ -204,7 +203,7 @@ async fn process_transaction Result<(), Box> { for processor in processors { - processor.process(storage, provider, block, receipt).await?; + processor.process(storage, provider, block, receipt).await? } Ok(()) From b99ba3d5bc32079177c21e6f553929aa9b1edda2 Mon Sep 17 00:00:00 2001 From: lambda-0x <0xlambda@protonmail.com> Date: Fri, 8 Sep 2023 17:56:13 +0530 Subject: [PATCH 37/77] feat(sozo): Print events in human readable form (#838) * parse events from manifest file * try to parse events gotten from RPC and convert it one of the parsed event from manifest file * parse felt252 as cairo short string when possible * fix clippy lints * fix: add a new line in component name, fix equality check, fix formatting --- crates/sozo/src/commands/events.rs | 61 ++++++++++++- crates/sozo/src/ops/events.rs | 135 ++++++++++++++++++++++++++++- 2 files changed, 190 insertions(+), 6 deletions(-) diff --git a/crates/sozo/src/commands/events.rs b/crates/sozo/src/commands/events.rs index d97435d772..254553dccc 100644 --- a/crates/sozo/src/commands/events.rs +++ b/crates/sozo/src/commands/events.rs @@ -1,7 +1,12 @@ -use anyhow::Result; +use std::collections::HashMap; + +use anyhow::{anyhow, Result}; +use cairo_lang_starknet::abi::{self, Event, Item}; use clap::Parser; +use dojo_world::manifest::Manifest; use dojo_world::metadata::dojo_metadata_from_workspace; use scarb::core::Config; +use starknet::core::utils::starknet_keccak; use super::options::starknet::StarknetOptions; use super::options::world::WorldOptions; @@ -29,6 +34,10 @@ pub struct EventsArgs { #[arg(help = "Continuation string to be passed for rpc request")] pub continuation_token: Option, + #[arg(long)] + #[arg(help = "Print values as raw json")] + pub json: bool, + #[command(flatten)] pub world: WorldOptions, @@ -38,6 +47,15 @@ pub struct EventsArgs { impl EventsArgs { pub fn run(self, config: &Config) -> Result<()> { + let target_dir = config.target_dir().path_existent().unwrap(); + let manifest_path = target_dir.join(config.profile().as_str()).join("manifest.json"); + + if !manifest_path.exists() { + return Err(anyhow!("Run scarb migrate before running this command")); + } + + let manifest = Manifest::load_from_path(manifest_path)?; + let events = extract_events(&manifest); let env_metadata = if config.manifest_path().exists() { let ws = scarb::ops::read_workspace(config.manifest_path(), config)?; @@ -46,10 +64,49 @@ impl EventsArgs { } else { None }; - config.tokio_handle().block_on(events::execute(self, env_metadata)) + config.tokio_handle().block_on(events::execute(self, env_metadata, events)) } } +fn extract_events(manifest: &Manifest) -> HashMap> { + fn inner_helper(events: &mut HashMap>, contract: &Option) { + if let Some(contract) = contract { + for item in &contract.items { + if let Item::Event(e) = item { + match e.kind { + abi::EventKind::Struct { .. } => { + let event_name = + starknet_keccak(e.name.split("::").last().unwrap().as_bytes()); + let vec = events.entry(event_name.to_string()).or_insert(Vec::new()); + vec.push(e.clone()); + } + abi::EventKind::Enum { .. } => (), + } + } + } + } + } + + let mut events_map = HashMap::new(); + + inner_helper(&mut events_map, &manifest.world.abi); + inner_helper(&mut events_map, &manifest.executor.abi); + + for system in &manifest.systems { + inner_helper(&mut events_map, &system.abi); + } + + for contract in &manifest.contracts { + inner_helper(&mut events_map, &contract.abi); + } + + for component in &manifest.components { + inner_helper(&mut events_map, &component.abi); + } + + events_map +} + #[cfg(test)] mod test { use super::*; diff --git a/crates/sozo/src/ops/events.rs b/crates/sozo/src/ops/events.rs index 2f560696ac..184d552dfd 100644 --- a/crates/sozo/src/ops/events.rs +++ b/crates/sozo/src/ops/events.rs @@ -1,12 +1,20 @@ +use std::collections::{HashMap, VecDeque}; + use anyhow::Result; +use cairo_lang_starknet::abi::{Event, EventKind}; +use cairo_lang_starknet::plugin::events::EventFieldKind; use dojo_world::metadata::Environment; use starknet::core::types::{BlockId, EventFilter}; -use starknet::core::utils::starknet_keccak; +use starknet::core::utils::{parse_cairo_short_string, starknet_keccak}; use starknet::providers::Provider; use crate::commands::events::EventsArgs; -pub async fn execute(args: EventsArgs, env_metadata: Option) -> Result<()> { +pub async fn execute( + args: EventsArgs, + env_metadata: Option, + events_map: HashMap>, +) -> Result<()> { let EventsArgs { chunk_size, starknet, @@ -15,6 +23,7 @@ pub async fn execute(args: EventsArgs, env_metadata: Option) -> Res to_block, events, continuation_token, + json, } = args; let from_block = from_block.map(BlockId::Number); @@ -29,7 +38,125 @@ pub async fn execute(args: EventsArgs, env_metadata: Option) -> Res let res = provider.get_events(event_filter, continuation_token, chunk_size).await?; - let value = serde_json::to_value(res)?; - println!("{}", serde_json::to_string_pretty(&value)?); + if json { + let value = serde_json::to_value(res)?; + println!("{}", serde_json::to_string_pretty(&value)?); + } else { + parse_and_print_events(res, events_map); + } Ok(()) } + +fn parse_and_print_events( + res: starknet::core::types::EventsPage, + events_map: HashMap>, +) { + println!("Continuation token: {:?}", res.continuation_token); + println!("----------------------------------------------"); + for event in res.events { + if let Some(e) = parse_event(event.clone(), &events_map) { + println!("{}\n", e); + } else { + // Couldn't parse event + println!("{}\n", serde_json::to_string_pretty(&event).unwrap()); + } + } +} + +fn parse_event( + event: starknet::core::types::EmittedEvent, + events_map: &HashMap>, +) -> Option { + let keys = event.keys; + let event_hash = keys[0].to_string(); + let Some(events) = events_map.get(&event_hash) else { return None }; + + 'outer: for e in events { + let mut ret = format!("Event name: {}\n", e.name); + let mut data = VecDeque::from(event.data.clone()); + + // Length is two only when its custom event + if keys.len() == 2 { + let name = parse_cairo_short_string(&keys[1]).ok()?; + ret.push_str(&format!("Component name: {}\n", name)); + } + + match &e.kind { + EventKind::Struct { members } => { + for field in members { + if field.kind != EventFieldKind::DataSerde { + continue; + } + match field.ty.as_str() { + "core::starknet::contract_address::ContractAddress" + | "core::starknet::class_hash::ClassHash" => { + let value = match data.pop_front() { + Some(addr) => addr, + None => continue 'outer, + }; + ret.push_str(&format!("{}: {:#x}\n", field.name, value)); + } + "core::felt252" => { + let value = match data.pop_front() { + Some(addr) => addr, + None => continue 'outer, + }; + let value = match parse_cairo_short_string(&value) { + Ok(v) => v, + Err(_) => format!("{:#x}", value), + }; + ret.push_str(&format!("{}: {}\n", field.name, value)); + } + "core::integer::u8" => { + let value = match data.pop_front() { + Some(addr) => addr, + None => continue 'outer, + }; + let num = match value.to_string().parse::() { + Ok(num) => num, + Err(_) => continue 'outer, + }; + + ret.push_str(&format!("{}: {}\n", field.name, num)); + } + "dojo_examples::systems::move::Direction" => { + let value = match data.pop_front() { + Some(addr) => addr, + None => continue 'outer, + }; + ret.push_str(&format!("{}: {}\n", field.name, value)); + } + "core::array::Span::" => { + let length = match data.pop_front() { + Some(addr) => addr, + None => continue 'outer, + }; + let length = match length.to_string().parse::() { + Ok(len) => len, + Err(_) => continue 'outer, + }; + ret.push_str(&format!("{}: ", field.name)); + if data.len() >= length { + ret.push_str(&format!( + "{:?}\n", + data.drain(..length) + .map(|e| format!("{:#x}", e)) + .collect::>() + )); + } else { + continue 'outer; + } + } + _ => { + return None; + } + } + } + return Some(ret); + } + EventKind::Enum { .. } => unreachable!("shouldn't reach here"), + } + } + + None +} From 1d3f47dfcade922449f2499cb40c3fc6033134ae Mon Sep 17 00:00:00 2001 From: Yun Date: Fri, 8 Sep 2023 13:42:34 -0700 Subject: [PATCH 38/77] fix(torii): graphql tests failing (#884) --- crates/torii/graphql/src/tests/common/mod.rs | 17 +++++++++-------- .../torii/graphql/src/tests/components_test.rs | 1 + crates/torii/graphql/src/tests/entities_test.rs | 12 ++++++------ .../graphql/src/tests/subscription_test.rs | 4 ++-- 4 files changed, 18 insertions(+), 16 deletions(-) diff --git a/crates/torii/graphql/src/tests/common/mod.rs b/crates/torii/graphql/src/tests/common/mod.rs index c0d0bb599f..fd10ea3a77 100644 --- a/crates/torii/graphql/src/tests/common/mod.rs +++ b/crates/torii/graphql/src/tests/common/mod.rs @@ -35,6 +35,7 @@ pub struct Entity { pub struct Moves { pub __typename: String, pub remaining: u32, + pub last_direction: u8, pub entity: Option, } @@ -46,7 +47,7 @@ pub struct Position { pub entity: Option, } -pub enum Direction { +pub enum Paginate { Forward, Backward, } @@ -74,9 +75,9 @@ pub async fn entity_fixtures(pool: &SqlitePool) { let state = init(pool).await; // Set entity with one moves component - // remaining: 10 + // remaining: 10, last_direction: 0 let key = vec![FieldElement::ONE]; - let moves_values = vec![FieldElement::from_hex_be("0xa").unwrap()]; + let moves_values = vec![FieldElement::from_hex_be("0xa").unwrap(), FieldElement::ZERO]; state.set_entity("Moves".to_string(), key, moves_values.clone()).await.unwrap(); // Set entity with one position component @@ -90,11 +91,11 @@ pub async fn entity_fixtures(pool: &SqlitePool) { state.set_entity("Position".to_string(), key, position_values.clone()).await.unwrap(); // Set an entity with both moves and position components - // remaining: 1 + // remaining: 1, last_direction: 0 // x: 69 // y: 42 let key = vec![FieldElement::THREE]; - let moves_values = vec![FieldElement::from_hex_be("0x1").unwrap()]; + let moves_values = vec![FieldElement::from_hex_be("0x1").unwrap(), FieldElement::ZERO]; let position_values = vec![ FieldElement::from_hex_be("0x45").unwrap(), FieldElement::from_hex_be("0x2a").unwrap(), @@ -120,12 +121,12 @@ pub async fn init(pool: &SqlitePool) -> Sql { pub async fn paginate( pool: &SqlitePool, cursor: Option, - direction: Direction, + direction: Paginate, page_size: usize, ) -> Connection { let (first_last, before_after) = match direction { - Direction::Forward => ("first", "after"), - Direction::Backward => ("last", "before"), + Paginate::Forward => ("first", "after"), + Paginate::Backward => ("last", "before"), }; let cursor = cursor.map_or(String::new(), |c| format!(", {before_after}: \"{c}\"")); diff --git a/crates/torii/graphql/src/tests/components_test.rs b/crates/torii/graphql/src/tests/components_test.rs index 333829b486..7924f31df6 100644 --- a/crates/torii/graphql/src/tests/components_test.rs +++ b/crates/torii/graphql/src/tests/components_test.rs @@ -26,6 +26,7 @@ mod tests { node { __typename remaining + last_direction } cursor } diff --git a/crates/torii/graphql/src/tests/entities_test.rs b/crates/torii/graphql/src/tests/entities_test.rs index d19110875d..d187b886e0 100644 --- a/crates/torii/graphql/src/tests/entities_test.rs +++ b/crates/torii/graphql/src/tests/entities_test.rs @@ -4,7 +4,7 @@ mod tests { use starknet_crypto::{poseidon_hash_many, FieldElement}; use crate::tests::common::{ - entity_fixtures, paginate, run_graphql_query, Direction, Entity, Moves, Position, + entity_fixtures, paginate, run_graphql_query, Entity, Moves, Paginate, Position, }; #[sqlx::test(migrations = "../migrations")] @@ -41,6 +41,7 @@ mod tests { __typename ... on Moves {{ remaining + last_direction }} ... on Position {{ x @@ -73,27 +74,26 @@ mod tests { let page_size = 2; // Forward pagination - let entities_connection = paginate(&pool, None, Direction::Forward, page_size).await; + let entities_connection = paginate(&pool, None, Paginate::Forward, page_size).await; assert_eq!(entities_connection.total_count, 3); assert_eq!(entities_connection.edges.len(), page_size); let cursor: String = entities_connection.edges[0].cursor.clone(); let next_cursor: String = entities_connection.edges[1].cursor.clone(); - let entities_connection = - paginate(&pool, Some(cursor), Direction::Forward, page_size).await; + let entities_connection = paginate(&pool, Some(cursor), Paginate::Forward, page_size).await; assert_eq!(entities_connection.total_count, 3); assert_eq!(entities_connection.edges.len(), page_size); assert_eq!(entities_connection.edges[0].cursor, next_cursor); // Backward pagination - let entities_connection = paginate(&pool, None, Direction::Backward, page_size).await; + let entities_connection = paginate(&pool, None, Paginate::Backward, page_size).await; assert_eq!(entities_connection.total_count, 3); assert_eq!(entities_connection.edges.len(), page_size); let cursor: String = entities_connection.edges[0].cursor.clone(); let next_cursor: String = entities_connection.edges[1].cursor.clone(); let entities_connection = - paginate(&pool, Some(cursor), Direction::Backward, page_size).await; + paginate(&pool, Some(cursor), Paginate::Backward, page_size).await; assert_eq!(entities_connection.total_count, 3); assert_eq!(entities_connection.edges.len(), page_size); assert_eq!(entities_connection.edges[0].cursor, next_cursor); diff --git a/crates/torii/graphql/src/tests/subscription_test.rs b/crates/torii/graphql/src/tests/subscription_test.rs index ca28e127f1..1a2ea0b552 100644 --- a/crates/torii/graphql/src/tests/subscription_test.rs +++ b/crates/torii/graphql/src/tests/subscription_test.rs @@ -32,8 +32,8 @@ mod tests { tokio::time::sleep(Duration::from_secs(1)).await; // Set entity with one moves component - // remaining: 10 - let moves_values = vec![FieldElement::from_hex_be("0xa").unwrap()]; + // remaining: 10, last_direction: 0 + let moves_values = vec![FieldElement::from_hex_be("0xa").unwrap(), FieldElement::ZERO]; state.set_entity("Moves".to_string(), key, moves_values).await.unwrap(); // 3. fn publish() is called from state.set_entity() From c6eae3d25ae7e6736d1a983ac850b5e774b66692 Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Tue, 12 Sep 2023 09:50:07 +0900 Subject: [PATCH 39/77] Update `katana` for JSON-RPC v0.4.0 spec (#867) * bump `starknet-rs` and `starknet-crypto` * Update conversion method for broadcasted invoke tx * Update transaction status * Update tx waiter * wip * update tx waiter * update * update * update * fix docs * update doc --- Cargo.lock | 45 ++-- Cargo.toml | 5 +- crates/dojo-test-utils/Cargo.toml | 1 + crates/dojo-test-utils/src/lib.rs | 2 + crates/dojo-test-utils/src/sequencer.rs | 3 +- crates/dojo-world/src/migration/mod.rs | 32 +-- crates/dojo-world/src/utils.rs | 192 +++++++++++++----- crates/katana/Cargo.toml | 4 +- crates/katana/core/src/backend/mod.rs | 29 +-- .../core/src/backend/storage/transaction.rs | 127 ++++++------ crates/katana/core/src/execution.rs | 10 +- crates/katana/core/src/sequencer.rs | 24 +-- crates/katana/core/src/service.rs | 98 ++++----- crates/katana/core/src/utils/transaction.rs | 70 ++----- crates/katana/rpc/src/api/starknet.rs | 46 ++++- crates/katana/rpc/src/lib.rs | 2 +- crates/katana/rpc/src/starknet.rs | 110 +++++----- crates/katana/rpc/tests/starknet.rs | 10 +- crates/katana/src/args.rs | 6 +- crates/katana/src/main.rs | 22 +- crates/sozo/Cargo.toml | 1 - crates/sozo/src/commands/options/account.rs | 14 +- .../sozo/src/ops/migration/migration_test.rs | 66 ++---- crates/torii/client/wasm/Cargo.lock | 70 ++----- crates/torii/client/wasm/Cargo.toml | 2 +- 25 files changed, 520 insertions(+), 471 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bc40bceed5..fb0a7c08b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2144,6 +2144,7 @@ dependencies = [ "cairo-lang-starknet", "camino", "dojo-lang", + "dojo-world", "jsonrpsee", "katana-core", "katana-rpc", @@ -2193,7 +2194,7 @@ dependencies = [ "serde_with", "smol_str", "starknet", - "starknet-crypto 0.5.1", + "starknet-crypto 0.6.0", "thiserror", "tokio", "toml", @@ -3968,12 +3969,12 @@ dependencies = [ "clap", "clap_complete", "console", - "env_logger", "katana-core", "katana-rpc", - "log", "starknet_api", "tokio", + "tracing", + "tracing-subscriber", "url", ] @@ -5971,7 +5972,6 @@ dependencies = [ "dojo-lang", "dojo-test-utils", "dojo-world", - "log", "scarb", "scarb-ui", "semver", @@ -6119,9 +6119,9 @@ dependencies = [ [[package]] name = "starknet" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fcb61961b91757a9bc2d11549067445b2f921bd957f53710db35449767a1ba3" +checksum = "6f0623b045f3dc10aef030c9ddd4781cff9cbe1188b71063cc510b75d1f96be6" dependencies = [ "starknet-accounts", "starknet-contract", @@ -6135,11 +6135,12 @@ dependencies = [ [[package]] name = "starknet-accounts" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "111ed887e4db14f0df1f909905e7737e4730770c8ed70997b58a71d5d940daac" +checksum = "68e97edc480348dca300e5a8234e6c4e6f2f1ac028f2b16fcce294ebe93d07f4" dependencies = [ "async-trait", + "auto_impl", "starknet-core", "starknet-providers", "starknet-signers", @@ -6148,9 +6149,9 @@ dependencies = [ [[package]] name = "starknet-contract" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0d6f81a647694b2cb669ab60e77954b57bf5fbc757f5fcaf0a791c3bd341f04" +checksum = "69b86e3f6b3ca9a5c45271ab10871c99f7dc82fee3199d9f8c7baa2a1829947d" dependencies = [ "serde", "serde_json", @@ -6163,9 +6164,9 @@ dependencies = [ [[package]] name = "starknet-core" -version = "0.5.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91f89c79b641618de8aa9668d74c6b6634659ceca311c6318a35c025f9d4d969" +checksum = "b796a32a7400f7d85e95d3900b5cee7a392b2adbf7ad16093ed45ec6f8d85de6" dependencies = [ "base64 0.21.3", "flate2", @@ -6265,9 +6266,9 @@ dependencies = [ [[package]] name = "starknet-macros" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28a5865ee0ed22ade86bdf45e7c09c5641f1c59ccae12c21ecde535b2b6bf64a" +checksum = "ef846b6bb48fc8c3e9a2aa9b5b037414f04a908d9db56493a3ae69a857eb2506" dependencies = [ "starknet-core", "syn 2.0.29", @@ -6275,9 +6276,9 @@ dependencies = [ [[package]] name = "starknet-providers" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbbfccb46a8969fb3ac803718d9d8270cff4eed5b7f6b9ba234875ad2cc997c5" +checksum = "c3b136c26b72ff1756f0844e0aa80bab680ceb99d63921826facbb8e7340ff82" dependencies = [ "async-trait", "auto_impl", @@ -6295,9 +6296,9 @@ dependencies = [ [[package]] name = "starknet-signers" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5313524cc79344015ef2a8618947332ab17012b5c50600c7f84c60989bdec980" +checksum = "d9386015d2e6dc3df285bfb33a3afd8ad7596c70ed38ab57019de4d2dfc7826f" dependencies = [ "async-trait", "auto_impl", @@ -6773,7 +6774,7 @@ dependencies = [ "parking_lot 0.12.1", "serde", "starknet", - "starknet-crypto 0.5.1", + "starknet-crypto 0.6.0", "thiserror", "tokio", "wasm-bindgen", @@ -6801,7 +6802,7 @@ dependencies = [ "slab", "sqlx", "starknet", - "starknet-crypto 0.5.1", + "starknet-crypto 0.6.0", "tokio", "tokio-stream", "tokio-util", @@ -6827,7 +6828,7 @@ dependencies = [ "serde_json", "sqlx", "starknet", - "starknet-crypto 0.5.1", + "starknet-crypto 0.6.0", "tokio", "tokio-stream", "tokio-util", @@ -6868,7 +6869,7 @@ dependencies = [ "serde_json", "sqlx", "starknet", - "starknet-crypto 0.5.1", + "starknet-crypto 0.6.0", "tokio", "tokio-stream", "tokio-util", diff --git a/Cargo.toml b/Cargo.toml index 4885f55705..3670acabcd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,7 +62,6 @@ flate2 = "1.0.24" futures = "0.3.28" indoc = "1.0.7" itertools = "0.10.3" -log = "0.4.17" num-bigint = "0.4" parking_lot = "0.12.1" once_cell = "1.0" @@ -76,8 +75,8 @@ serde = { version = "1.0.156", features = [ "derive" ] } serde_json = "1.0" serde_with = "2.3.1" smol_str = { version = "0.2.0", features = [ "serde" ] } -starknet = "0.5.0" -starknet-crypto = "0.5.1" +starknet = "0.6.0" +starknet-crypto = "0.6.0" starknet_api = { git = "https://github.com/starkware-libs/starknet-api", rev = "ecc9b6946ef13003da202838e4124a9ad2efabb0" } test-log = "0.2.11" thiserror = "1.0.32" diff --git a/crates/dojo-test-utils/Cargo.toml b/crates/dojo-test-utils/Cargo.toml index ea2ac48380..8e2cd1ff51 100644 --- a/crates/dojo-test-utils/Cargo.toml +++ b/crates/dojo-test-utils/Cargo.toml @@ -15,6 +15,7 @@ cairo-lang-project.workspace = true cairo-lang-starknet.workspace = true camino.workspace = true dojo-lang = { path = "../dojo-lang" } +dojo-world = { path = "../dojo-world" } jsonrpsee = { version = "0.16.2", features = [ "server" ] } katana-core = { path = "../katana/core" } katana-rpc = { path = "../katana/rpc" } diff --git a/crates/dojo-test-utils/src/lib.rs b/crates/dojo-test-utils/src/lib.rs index 36a1b63872..e2f7b4af16 100644 --- a/crates/dojo-test-utils/src/lib.rs +++ b/crates/dojo-test-utils/src/lib.rs @@ -1,3 +1,5 @@ pub mod compiler; pub mod rpc; pub mod sequencer; + +pub use dojo_world::utils::{TransactionWaiter, TransactionWaitingError}; diff --git a/crates/dojo-test-utils/src/sequencer.rs b/crates/dojo-test-utils/src/sequencer.rs index aedce61725..abd214478c 100644 --- a/crates/dojo-test-utils/src/sequencer.rs +++ b/crates/dojo-test-utils/src/sequencer.rs @@ -6,7 +6,7 @@ use katana_core::sequencer::KatanaSequencer; pub use katana_core::sequencer::SequencerConfig; use katana_rpc::config::ServerConfig; use katana_rpc::{spawn, KatanaApi, NodeHandle, StarknetApi}; -use starknet::accounts::SingleOwnerAccount; +use starknet::accounts::{ExecutionEncoding, SingleOwnerAccount}; use starknet::core::chain_id; use starknet::core::types::FieldElement; use starknet::providers::jsonrpc::HttpTransport; @@ -54,6 +54,7 @@ impl TestSequencer { LocalWallet::from_signing_key(SigningKey::from_secret_scalar(self.account.private_key)), self.account.account_address, chain_id::TESTNET, + ExecutionEncoding::Legacy, ) } diff --git a/crates/dojo-world/src/migration/mod.rs b/crates/dojo-world/src/migration/mod.rs index a3f81c5407..051e8dbbc9 100644 --- a/crates/dojo-world/src/migration/mod.rs +++ b/crates/dojo-world/src/migration/mod.rs @@ -10,11 +10,11 @@ use starknet::accounts::{Account, AccountError, Call, ConnectedAccount, SingleOw use starknet::core::types::contract::{CompiledClass, SierraClass}; use starknet::core::types::{ BlockId, BlockTag, DeclareTransactionResult, FieldElement, FlattenedSierraClass, - InvokeTransactionResult, StarknetError, -}; -use starknet::core::utils::{ - get_contract_address, get_selector_from_name, CairoShortStringToFeltError, + InvokeTransactionResult, MaybePendingTransactionReceipt, StarknetError, + TransactionFinalityStatus, }; +use starknet::core::utils::{get_contract_address, CairoShortStringToFeltError}; +use starknet::macros::{felt, selector}; use starknet::providers::{ MaybeUnknownErrorCode, Provider, ProviderError, StarknetErrorWithMessage, }; @@ -194,11 +194,8 @@ pub trait Deployable: Declarable + Sync { let mut txn = account.execute(vec![Call { calldata, // devnet UDC address - to: FieldElement::from_hex_be( - "0x41a78e741e5af2fec34b695679bc6891742439f7afb8484ecd7766661ad02bf", - ) - .unwrap(), - selector: get_selector_from_name("deployContract").unwrap(), + selector: selector!("deployContract"), + to: felt!("0x41a78e741e5af2fec34b695679bc6891742439f7afb8484ecd7766661ad02bf"), }]); if let TxConfig { fee_estimate_multiplier: Some(multiplier) } = txn_config { @@ -208,16 +205,19 @@ pub trait Deployable: Declarable + Sync { let InvokeTransactionResult { transaction_hash } = txn.send().await.map_err(MigrationError::Migrator)?; - let txn = TransactionWaiter::new(transaction_hash, account.provider()) + // TODO: remove finality check once we can remove displaying the block number in the + // migration logs + let receipt = TransactionWaiter::new(transaction_hash, account.provider()) + .with_finality(TransactionFinalityStatus::AcceptedOnL2) .await .map_err(MigrationError::WaitingError)?; - Ok(DeployOutput { - transaction_hash, - contract_address, - declare, - block_number: block_number_from_receipt(&txn), - }) + let block_number = match receipt { + MaybePendingTransactionReceipt::Receipt(receipt) => block_number_from_receipt(&receipt), + _ => panic!("Transaction was not accepted on L2"), + }; + + Ok(DeployOutput { transaction_hash, contract_address, declare, block_number }) } fn salt(&self) -> FieldElement; diff --git a/crates/dojo-world/src/utils.rs b/crates/dojo-world/src/utils.rs index babfaf7143..790ad93bab 100644 --- a/crates/dojo-world/src/utils.rs +++ b/crates/dojo-world/src/utils.rs @@ -5,8 +5,8 @@ use std::time::Duration; use futures::FutureExt; use starknet::core::types::{ - FieldElement, MaybePendingTransactionReceipt, StarknetError, TransactionReceipt, - TransactionStatus, + ExecutionResult, FieldElement, MaybePendingTransactionReceipt, PendingTransactionReceipt, + StarknetError, TransactionFinalityStatus, TransactionReceipt, }; use starknet::providers::{ MaybeUnknownErrorCode, Provider, ProviderError, StarknetErrorWithMessage, @@ -20,35 +20,62 @@ type GetReceiptFuture<'a, E> = Pin> pub enum TransactionWaitingError { #[error("request timed out")] Timeout, - #[error("transaction was rejected")] - TransactionRejected, + #[error("transaction reverted due to failed execution: {0}")] + TransactionReverted(String), #[error(transparent)] Provider(ProviderError), } -/// A type that waits for a transaction to achieve `status` status. The transaction will be polled -/// for every `interval` miliseconds. If the transaction does not achieved `status` status within -/// `timeout` miliseconds, an error will be returned. An error is also returned if the transaction -/// is rejected ( i.e., the transaction returns a `REJECTED` status ). -pub struct TransactionWaiter<'a, P> -where - P: Provider, -{ +/// A type that waits for a transaction to achieve the desired status. The waiter will poll for the +/// transaction receipt every `interval` miliseconds until it achieves the desired status or until +/// `timeout` is reached. +/// +/// The waiter can be configured to wait for a specific finality status (e.g, `ACCEPTED_ON_L2`), by +/// default, it only waits until the transaction is included in the _pending_ block. It can also be +/// set to check if the transaction is executed successfully or not (reverted). +/// +/// # Examples +/// +/// ```ignore +/// ues url::Url; +/// use starknet::providers::jsonrpc::HttpTransport; +/// use starknet::providers::JsonRpcClient; +/// use starknet::core::types::TransactionFinalityStatus; +/// +/// let provider = JsonRpcClient::new(HttpTransport::new(Url::parse("http://localhost:5000").unwrap())); +/// +/// let tx_hash = FieldElement::from(0xbadbeefu64); +/// let receipt = TransactionWaiter::new(tx_hash, &provider).with_finality(TransactionFinalityStatus::ACCEPTED_ON_L2).await.unwrap(); +/// ``` +#[must_use = "TransactionWaiter does nothing unless polled"] +pub struct TransactionWaiter<'a, P: Provider> { /// The hash of the transaction to wait for. tx_hash: FieldElement, - /// The status to wait for. Defaults to `TransactionStatus::AcceptedOnL2`. - status: TransactionStatus, + /// The finality status to wait for. + /// + /// If set, the waiter will wait for the transaction to achieve this finality status. + /// Otherwise, the waiter will only wait for the transaction until it is included in the + /// _pending_ block. + finality_status: Option, + /// A flag to indicate that the waited transaction must either be successfully executed or not. + /// + /// If it's set to `true`, then the transaction execution status must be `SUCCEEDED` otherwise + /// an error will be returned. However, if set to `false`, then the execution status will not + /// be considered when waiting for the transaction, meaning `REVERTED` transaction will not + /// return an error. + must_succeed: bool, /// Poll the transaction every `interval` miliseconds. Miliseconds are used so that /// we can be more precise with the polling interval. Defaults to 250ms. interval: Interval, - /// The maximum amount of time to wait for the transaction to achieve `status` status. - /// Defaults to 60 seconds. + /// The maximum amount of time to wait for the transaction to achieve the desired status. An + /// error will be returned if it is unable to finish within the `timeout` duration. Defaults to + /// 60 seconds. timeout: Duration, /// The provider to use for polling the transaction. provider: &'a P, - /// The future that get the transaction receipt. - future: Option::Error>>, - /// The time when the transaction waiter was polled. + /// The future that gets the transaction receipt. + receipt_request_fut: Option::Error>>, + /// The time when the transaction waiter was first polled. started_at: Option, } @@ -58,15 +85,15 @@ where { const DEFAULT_TIMEOUT: Duration = Duration::from_secs(60); const DEFAULT_INTERVAL: Duration = Duration::from_millis(250); - const DEFAULT_STATUS: TransactionStatus = TransactionStatus::AcceptedOnL2; pub fn new(tx: FieldElement, provider: &'a P) -> Self { Self { provider, tx_hash: tx, - future: None, started_at: None, - status: Self::DEFAULT_STATUS, + must_succeed: true, + finality_status: None, + receipt_request_fut: None, timeout: Self::DEFAULT_TIMEOUT, interval: tokio::time::interval_at( Instant::now() + Self::DEFAULT_INTERVAL, @@ -75,20 +102,17 @@ where } } - pub fn with_interval(mut self, milisecond: u64) -> Self { + pub fn with_interval(self, milisecond: u64) -> Self { let interval = Duration::from_millis(milisecond); - self.interval = tokio::time::interval_at(Instant::now() + interval, interval); - self + Self { interval: tokio::time::interval_at(Instant::now() + interval, interval), ..self } } - pub fn with_status(mut self, status: TransactionStatus) -> Self { - self.status = status; - self + pub fn with_finality(self, status: TransactionFinalityStatus) -> Self { + Self { finality_status: Some(status), ..self } } - pub fn with_timeout(mut self, timeout: Duration) -> Self { - self.timeout = timeout; - self + pub fn with_timeout(self, timeout: Duration) -> Self { + Self { timeout, ..self } } } @@ -96,7 +120,7 @@ impl<'a, P> Future for TransactionWaiter<'a, P> where P: Provider + Send, { - type Output = Result>; + type Output = Result>; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let this = self.get_mut(); @@ -112,42 +136,76 @@ where } } - if let Some(mut flush) = this.future.take() { + if let Some(mut flush) = this.receipt_request_fut.take() { match flush.poll_unpin(cx) { Poll::Ready(res) => match res { - Ok(MaybePendingTransactionReceipt::Receipt(receipt)) => { - match transaction_status_from_receipt(&receipt) { - TransactionStatus::Rejected => { - return Poll::Ready(Err( - TransactionWaitingError::TransactionRejected, - )); + Ok(receipt) => match &receipt { + MaybePendingTransactionReceipt::PendingReceipt(r) => { + if this.finality_status.is_none() { + if this.must_succeed { + let res = match execution_status_from_pending_receipt(r) { + ExecutionResult::Succeeded => Ok(receipt), + ExecutionResult::Reverted { reason } => { + Err(TransactionWaitingError::TransactionReverted( + reason.clone(), + )) + } + }; + return Poll::Ready(res); + } + + return Poll::Ready(Ok(receipt)); } + } + + MaybePendingTransactionReceipt::Receipt(r) => { + if let Some(finality_status) = this.finality_status { + match finality_status_from_receipt(r) { + status if status == finality_status => { + if this.must_succeed { + let res = match execution_status_from_receipt(r) { + ExecutionResult::Succeeded => Ok(receipt), + ExecutionResult::Reverted { reason } => { + Err(TransactionWaitingError::TransactionReverted( + reason.clone(), + )) + } + }; + return Poll::Ready(res); + } - status if status == this.status => return Poll::Ready(Ok(receipt)), + return Poll::Ready(Ok(receipt)); + } - _ => {} + _ => {} + } + } else { + return Poll::Ready(Ok(receipt)); + } } - } + }, - Ok(MaybePendingTransactionReceipt::PendingReceipt(_)) - | Err(ProviderError::StarknetError(StarknetErrorWithMessage { + Err(ProviderError::StarknetError(StarknetErrorWithMessage { code: MaybeUnknownErrorCode::Known(StarknetError::TransactionHashNotFound), .. })) => {} - Err(e) => return Poll::Ready(Err(TransactionWaitingError::Provider(e))), + Err(e) => { + return Poll::Ready(Err(TransactionWaitingError::Provider(e))); + } }, Poll::Pending => { - this.future = Some(flush); + this.receipt_request_fut = Some(flush); return Poll::Pending; } } } if this.interval.poll_tick(cx).is_ready() { - this.future = Some(Box::pin(this.provider.get_transaction_receipt(this.tx_hash))); + this.receipt_request_fut = + Some(Box::pin(this.provider.get_transaction_receipt(this.tx_hash))); } else { break; } @@ -157,18 +215,42 @@ where } } -pub fn transaction_status_from_receipt(receipt: &TransactionReceipt) -> TransactionStatus { +#[inline] +fn execution_status_from_receipt(receipt: &TransactionReceipt) -> &ExecutionResult { match receipt { - TransactionReceipt::Invoke(receipt) => receipt.status, - TransactionReceipt::Deploy(receipt) => receipt.status, - TransactionReceipt::Declare(receipt) => receipt.status, - TransactionReceipt::L1Handler(receipt) => receipt.status, - TransactionReceipt::DeployAccount(receipt) => receipt.status, + TransactionReceipt::Invoke(receipt) => &receipt.execution_result, + TransactionReceipt::Deploy(receipt) => &receipt.execution_result, + TransactionReceipt::Declare(receipt) => &receipt.execution_result, + TransactionReceipt::L1Handler(receipt) => &receipt.execution_result, + TransactionReceipt::DeployAccount(receipt) => &receipt.execution_result, } } -pub fn block_number_from_receipt(tx: &TransactionReceipt) -> u64 { - match tx { +#[inline] +fn execution_status_from_pending_receipt(receipt: &PendingTransactionReceipt) -> &ExecutionResult { + match receipt { + PendingTransactionReceipt::Invoke(receipt) => &receipt.execution_result, + PendingTransactionReceipt::Deploy(receipt) => &receipt.execution_result, + PendingTransactionReceipt::Declare(receipt) => &receipt.execution_result, + PendingTransactionReceipt::L1Handler(receipt) => &receipt.execution_result, + PendingTransactionReceipt::DeployAccount(receipt) => &receipt.execution_result, + } +} + +#[inline] +fn finality_status_from_receipt(receipt: &TransactionReceipt) -> TransactionFinalityStatus { + match receipt { + TransactionReceipt::Invoke(receipt) => receipt.finality_status, + TransactionReceipt::Deploy(receipt) => receipt.finality_status, + TransactionReceipt::Declare(receipt) => receipt.finality_status, + TransactionReceipt::L1Handler(receipt) => receipt.finality_status, + TransactionReceipt::DeployAccount(receipt) => receipt.finality_status, + } +} + +#[inline] +pub fn block_number_from_receipt(receipt: &TransactionReceipt) -> u64 { + match receipt { TransactionReceipt::Invoke(tx) => tx.block_number, TransactionReceipt::L1Handler(tx) => tx.block_number, TransactionReceipt::Declare(tx) => tx.block_number, diff --git a/crates/katana/Cargo.toml b/crates/katana/Cargo.toml index 284b3ee6e8..a54c1f8672 100644 --- a/crates/katana/Cargo.toml +++ b/crates/katana/Cargo.toml @@ -10,12 +10,12 @@ version.workspace = true clap.workspace = true clap_complete.workspace = true console.workspace = true -env_logger.workspace = true katana-core = { path = "core" } katana-rpc = { path = "rpc" } -log.workspace = true starknet_api.workspace = true tokio.workspace = true +tracing-subscriber.workspace = true +tracing.workspace = true url.workspace = true [dev-dependencies] diff --git a/crates/katana/core/src/backend/mod.rs b/crates/katana/core/src/backend/mod.rs index 8c3c542b4d..05cfc4a636 100644 --- a/crates/katana/core/src/backend/mod.rs +++ b/crates/katana/core/src/backend/mod.rs @@ -14,7 +14,9 @@ use blockifier::transaction::objects::AccountTransactionContext; use flate2::write::GzEncoder; use flate2::Compression; use parking_lot::RwLock; -use starknet::core::types::{BlockId, BlockTag, FeeEstimate, MaybePendingBlockWithTxHashes}; +use starknet::core::types::{ + BlockId, BlockTag, FeeEstimate, MaybePendingBlockWithTxHashes, TransactionFinalityStatus, +}; use starknet::core::utils::parse_cairo_short_string; use starknet::providers::jsonrpc::HttpTransport; use starknet::providers::{JsonRpcClient, Provider}; @@ -33,7 +35,7 @@ pub mod storage; use self::config::StarknetConfig; use self::storage::block::{Block, PartialHeader}; -use self::storage::transaction::{IncludedTransaction, Transaction, TransactionStatus}; +use self::storage::transaction::{IncludedTransaction, Transaction}; use self::storage::{Blockchain, InMemoryBlockStates, Storage}; use crate::accounts::{Account, DevAccountGenerator}; use crate::backend::in_memory_db::MemDb; @@ -107,6 +109,7 @@ impl Backend { let mut state = ForkedDb::new(Arc::clone(&provider), forked_block_id); trace!( + target: "backend", "forking chain `{}` at block {} from {}", parse_cairo_short_string(&forked_chain_id).unwrap(), block.block_number, @@ -145,7 +148,7 @@ impl Backend { .await .load_state(init_state.clone()) .expect("failed to load initial state"); - info!("Successfully loaded initial state"); + info!(target: "backend", "Successfully loaded initial state"); } let blockchain = Blockchain::new(storage); @@ -222,7 +225,7 @@ impl Backend { /// Mines a new block based on the provided execution outcome. /// This method should only be called by the - /// [PendingBlockProducer](crate::service::PendingBlockProducer) when the node is running in + /// [IntervalBlockProducer](crate::service::IntervalBlockProducer) when the node is running in /// `interval` mining mode. pub async fn mine_pending_block( &self, @@ -240,6 +243,9 @@ impl Backend { } pub async fn do_mine_block(&self, execution_outcome: ExecutionOutcome) -> MinedBlockOutcome { + // lock the state for the entire block mining process + let mut state = self.state.write().await; + let partial_header = PartialHeader { gas_price: self.env.read().block.gas_price, number: self.env.read().block.block_number.0, @@ -271,7 +277,7 @@ impl Backend { block_number, block_hash, transaction: tx.clone(), - status: TransactionStatus::AcceptedOnL2, + finality_status: TransactionFinalityStatus::AcceptedOnL2, }), ), @@ -283,21 +289,16 @@ impl Backend { self.blockchain.storage.write().transactions.insert(hash, tx); }); - // get state diffs - let state_diff = convert_state_diff_to_rpc_state_diff(execution_outcome.state_diff.clone()); - // store block and the state diff + let state_diff = convert_state_diff_to_rpc_state_diff(execution_outcome.state_diff.clone()); self.blockchain.append_block(block_hash, block.clone(), state_diff); - - info!(target: "backend", "⛏️ Block {block_number} mined with {tx_count} transactions"); - // apply the pending state to the current state - let mut state = self.state.write().await; execution_outcome.apply_to(&mut *state); - // store the current state self.states.write().await.insert(block_hash, state.as_ref_db()); + info!(target: "backend", "⛏️ Block {block_number} mined with {tx_count} transactions"); + MinedBlockOutcome { block_number, transactions: execution_outcome.transactions } } @@ -346,7 +347,7 @@ impl Backend { ); if let Err(err) = &res { - warn!("Call error: {err:?}"); + warn!(target: "backend", "Call error: {err:?}"); } res diff --git a/crates/katana/core/src/backend/storage/transaction.rs b/crates/katana/core/src/backend/storage/transaction.rs index 9d5323e548..dbdf27a88c 100644 --- a/crates/katana/core/src/backend/storage/transaction.rs +++ b/crates/katana/core/src/backend/storage/transaction.rs @@ -6,41 +6,29 @@ use blockifier::transaction::transaction_execution::Transaction as ExecutionTran use blockifier::transaction::transactions::{ DeclareTransaction as ExecutionDeclareTransaction, DeployAccountTransaction as ExecutionDeployAccountTransaction, + L1HandlerTransaction as ExecutionL1HandlerTransaction, }; use starknet::core::types::{ DeclareTransactionReceipt, DeployAccountTransactionReceipt, Event, FieldElement, - FlattenedSierraClass, InvokeTransactionReceipt, MsgToL1, PendingDeclareTransactionReceipt, - PendingDeployAccountTransactionReceipt, PendingInvokeTransactionReceipt, + FlattenedSierraClass, InvokeTransactionReceipt, L1HandlerTransactionReceipt, MsgToL1, + PendingDeclareTransactionReceipt, PendingDeployAccountTransactionReceipt, + PendingInvokeTransactionReceipt, PendingL1HandlerTransactionReceipt, PendingTransactionReceipt as RpcPendingTransactionReceipt, Transaction as RpcTransaction, - TransactionReceipt as RpcTransactionReceipt, TransactionStatus as RpcTransactionStatus, + TransactionFinalityStatus, TransactionReceipt as RpcTransactionReceipt, }; use starknet_api::core::{ContractAddress, PatriciaKey}; use starknet_api::hash::StarkHash; use starknet_api::patricia_key; use starknet_api::transaction::{ DeclareTransaction as ApiDeclareTransaction, - DeployAccountTransaction as ApiDeployAccountTransaction, - InvokeTransaction as ApiInvokeTransaction, Transaction as ApiTransaction, + DeployAccountTransaction as ApiDeployAccountTransaction, Fee, + InvokeTransaction as ApiInvokeTransaction, L1HandlerTransaction as ApiL1HandlerTransaction, + Transaction as ApiTransaction, }; use crate::execution::ExecutedTransaction; use crate::utils::transaction::api_to_rpc_transaction; -/// The status of the transactions known to the sequencer. -#[derive(Debug, Clone, Copy)] -pub enum TransactionStatus { - /// Transaction executed unsuccessfully and thus was skipped. - Rejected, - /// When the transaction pass validation but encountered error during execution. - Reverted, - /// Transactions that have been included in the L2 block which have - /// passed both validation and execution. - AcceptedOnL2, - /// When the block of which the transaction is included in have been committed to the - /// L1 settlement layer. - AcceptedOnL1, -} - /// Represents all transactions that are known to the sequencer. #[derive(Debug, Clone)] pub enum KnownTransaction { @@ -73,7 +61,7 @@ pub struct IncludedTransaction { pub block_number: u64, pub block_hash: FieldElement, pub transaction: Arc, - pub status: TransactionStatus, + pub finality_status: TransactionFinalityStatus, } /// A transaction that is known to be rejected by the sequencer i.e., @@ -96,6 +84,7 @@ pub enum Transaction { Invoke(InvokeTransaction), Declare(DeclareTransaction), DeployAccount(DeployAccountTransaction), + L1Handler(L1HandlerTransaction), } impl Transaction { @@ -103,6 +92,7 @@ impl Transaction { match self { Transaction::Invoke(tx) => tx.0.transaction_hash().0.into(), Transaction::Declare(tx) => tx.inner.transaction_hash().0.into(), + Transaction::L1Handler(tx) => tx.inner.transaction_hash.0.into(), Transaction::DeployAccount(tx) => tx.inner.transaction_hash.0.into(), } } @@ -124,37 +114,59 @@ pub struct DeployAccountTransaction { pub contract_address: FieldElement, } +#[derive(Debug, Clone)] +pub struct L1HandlerTransaction { + pub inner: ApiL1HandlerTransaction, + pub paid_l1_fee: u128, +} + impl IncludedTransaction { pub fn receipt(&self) -> RpcTransactionReceipt { match &self.transaction.inner { Transaction::Invoke(_) => RpcTransactionReceipt::Invoke(InvokeTransactionReceipt { - status: self.status.into(), block_hash: self.block_hash, block_number: self.block_number, + finality_status: self.finality_status, events: self.transaction.output.events.clone(), + execution_result: self.transaction.execution_result(), messages_sent: self.transaction.output.messages_sent.clone(), transaction_hash: self.transaction.inner.hash(), actual_fee: self.transaction.execution_info.actual_fee.0.into(), }), Transaction::Declare(_) => RpcTransactionReceipt::Declare(DeclareTransactionReceipt { - status: self.status.into(), block_hash: self.block_hash, block_number: self.block_number, + finality_status: self.finality_status, events: self.transaction.output.events.clone(), transaction_hash: self.transaction.inner.hash(), + execution_result: self.transaction.execution_result(), messages_sent: self.transaction.output.messages_sent.clone(), actual_fee: self.transaction.execution_info.actual_fee.0.into(), }), Transaction::DeployAccount(tx) => { RpcTransactionReceipt::DeployAccount(DeployAccountTransactionReceipt { - status: self.status.into(), block_hash: self.block_hash, block_number: self.block_number, contract_address: tx.contract_address, + finality_status: self.finality_status, + events: self.transaction.output.events.clone(), + transaction_hash: self.transaction.inner.hash(), + execution_result: self.transaction.execution_result(), + messages_sent: self.transaction.output.messages_sent.clone(), + actual_fee: self.transaction.execution_info.actual_fee.0.into(), + }) + } + + Transaction::L1Handler(_) => { + RpcTransactionReceipt::L1Handler(L1HandlerTransactionReceipt { + block_hash: self.block_hash, + block_number: self.block_number, + finality_status: self.finality_status, events: self.transaction.output.events.clone(), transaction_hash: self.transaction.inner.hash(), + execution_result: self.transaction.execution_result(), messages_sent: self.transaction.output.messages_sent.clone(), actual_fee: self.transaction.execution_info.actual_fee.0.into(), }) @@ -170,6 +182,7 @@ impl PendingTransaction { RpcPendingTransactionReceipt::Invoke(PendingInvokeTransactionReceipt { events: self.0.output.events.clone(), transaction_hash: self.0.inner.hash(), + execution_result: self.0.execution_result(), messages_sent: self.0.output.messages_sent.clone(), actual_fee: self.0.execution_info.actual_fee.0.into(), }) @@ -179,6 +192,7 @@ impl PendingTransaction { RpcPendingTransactionReceipt::Declare(PendingDeclareTransactionReceipt { events: self.0.output.events.clone(), transaction_hash: self.0.inner.hash(), + execution_result: self.0.execution_result(), messages_sent: self.0.output.messages_sent.clone(), actual_fee: self.0.execution_info.actual_fee.0.into(), }) @@ -188,32 +202,21 @@ impl PendingTransaction { PendingDeployAccountTransactionReceipt { events: self.0.output.events.clone(), transaction_hash: self.0.inner.hash(), + execution_result: self.0.execution_result(), messages_sent: self.0.output.messages_sent.clone(), actual_fee: self.0.execution_info.actual_fee.0.into(), }, ), - } - } -} - -impl KnownTransaction { - pub fn status(&self) -> TransactionStatus { - match self { - KnownTransaction::Pending(_) => TransactionStatus::AcceptedOnL2, - KnownTransaction::Rejected(_) => TransactionStatus::Rejected, - KnownTransaction::Included(tx) => tx.status, - } - } -} -impl From for RpcTransactionStatus { - fn from(status: TransactionStatus) -> Self { - match status { - TransactionStatus::AcceptedOnL2 => RpcTransactionStatus::AcceptedOnL2, - TransactionStatus::AcceptedOnL1 => RpcTransactionStatus::AcceptedOnL1, - TransactionStatus::Rejected => RpcTransactionStatus::Rejected, - // TODO: change this to `REVERTED` once the status is implemented in `starknet-rs` - TransactionStatus::Reverted => RpcTransactionStatus::AcceptedOnL2, + Transaction::L1Handler(_) => { + RpcPendingTransactionReceipt::L1Handler(PendingL1HandlerTransactionReceipt { + events: self.0.output.events.clone(), + transaction_hash: self.0.inner.hash(), + execution_result: self.0.execution_result(), + messages_sent: self.0.output.messages_sent.clone(), + actual_fee: self.0.execution_info.actual_fee.0.into(), + }) + } } } } @@ -253,31 +256,39 @@ impl From for ApiTransaction { match value { Transaction::Invoke(tx) => ApiTransaction::Invoke(tx.0), Transaction::Declare(tx) => ApiTransaction::Declare(tx.inner), + Transaction::L1Handler(tx) => ApiTransaction::L1Handler(tx.inner), Transaction::DeployAccount(tx) => ApiTransaction::DeployAccount(tx.inner), } } } -impl From for AccountTransaction { +impl From for ExecutionTransaction { fn from(value: Transaction) -> Self { match value { - Transaction::Invoke(tx) => AccountTransaction::Invoke(tx.0), - Transaction::Declare(tx) => AccountTransaction::Declare( - ExecutionDeclareTransaction::new(tx.inner, tx.compiled_class) - .expect("declare tx must have valid compiled class"), - ), - Transaction::DeployAccount(tx) => { + Transaction::Invoke(tx) => { + ExecutionTransaction::AccountTransaction(AccountTransaction::Invoke(tx.0)) + } + + Transaction::Declare(tx) => { + ExecutionTransaction::AccountTransaction(AccountTransaction::Declare( + ExecutionDeclareTransaction::new(tx.inner, tx.compiled_class) + .expect("declare tx must have valid compiled class"), + )) + } + + Transaction::DeployAccount(tx) => ExecutionTransaction::AccountTransaction( AccountTransaction::DeployAccount(ExecutionDeployAccountTransaction { tx: tx.inner, contract_address: ContractAddress(patricia_key!(tx.contract_address)), + }), + ), + + Transaction::L1Handler(tx) => { + ExecutionTransaction::L1HandlerTransaction(ExecutionL1HandlerTransaction { + tx: tx.inner, + paid_fee_on_l1: Fee(tx.paid_l1_fee), }) } } } } - -impl From for ExecutionTransaction { - fn from(value: Transaction) -> Self { - ExecutionTransaction::AccountTransaction(value.into()) - } -} diff --git a/crates/katana/core/src/execution.rs b/crates/katana/core/src/execution.rs index 855d7de374..fddff2d714 100644 --- a/crates/katana/core/src/execution.rs +++ b/crates/katana/core/src/execution.rs @@ -12,7 +12,7 @@ use blockifier::transaction::transaction_execution::Transaction as ExecutionTran use blockifier::transaction::transactions::ExecutableTransaction; use convert_case::{Case, Casing}; use parking_lot::RwLock; -use starknet::core::types::{Event, FieldElement, FlattenedSierraClass, MsgToL1}; +use starknet::core::types::{Event, ExecutionResult, FieldElement, FlattenedSierraClass, MsgToL1}; use starknet_api::core::ClassHash; use tracing::{trace, warn}; @@ -239,6 +239,14 @@ impl ExecutedTransaction { output: TransactionOutput { actual_fee, events, messages_sent }, } } + + pub fn execution_result(&self) -> ExecutionResult { + if let Some(ref revert_err) = self.execution_info.revert_error { + ExecutionResult::Reverted { reason: revert_err.clone() } + } else { + ExecutionResult::Succeeded + } + } } pub fn events_from_exec_info(execution_info: &TransactionExecutionInfo) -> Vec { diff --git a/crates/katana/core/src/sequencer.rs b/crates/katana/core/src/sequencer.rs index 5694b4cda6..8011022257 100644 --- a/crates/katana/core/src/sequencer.rs +++ b/crates/katana/core/src/sequencer.rs @@ -21,7 +21,7 @@ use crate::backend::contract::StarknetContract; use crate::backend::storage::block::{ExecutedBlock, PartialBlock, PartialHeader}; use crate::backend::storage::transaction::{ DeclareTransaction, DeployAccountTransaction, InvokeTransaction, KnownTransaction, - PendingTransaction, Transaction, TransactionStatus, + PendingTransaction, Transaction, }; use crate::backend::{Backend, ExternalFunctionCall}; use crate::db::{AsStateRefDb, StateExtRef, StateRefDb}; @@ -55,8 +55,6 @@ pub trait Sequencer { hash: &FieldElement, ) -> Option; - async fn transaction_status(&self, hash: &FieldElement) -> Option; - async fn nonce_at( &self, block_id: BlockId, @@ -390,26 +388,6 @@ impl Sequencer for KatanaSequencer { .map(|execution_info| execution_info.execution.retdata.0) } - async fn transaction_status(&self, hash: &FieldElement) -> Option { - let tx = self.backend.blockchain.storage.read().transactions.get(hash).cloned(); - match tx { - Some(tx) => Some(tx.status()), - // If the requested transaction is not available in the storage then - // check if it is available in the pending block. - None => self.pending_state().as_ref().and_then(|state| { - state.executed_transactions.read().iter().find_map(|tx| match tx { - MaybeInvalidExecutedTransaction::Valid(tx) if tx.inner.hash() == *hash => { - Some(TransactionStatus::AcceptedOnL2) - } - MaybeInvalidExecutedTransaction::Invalid(tx) if tx.inner.hash() == *hash => { - Some(TransactionStatus::Rejected) - } - _ => None, - }) - }), - } - } - async fn transaction_receipt( &self, hash: &FieldElement, diff --git a/crates/katana/core/src/service.rs b/crates/katana/core/src/service.rs index ebe228ea18..94407c6573 100644 --- a/crates/katana/core/src/service.rs +++ b/crates/katana/core/src/service.rs @@ -80,21 +80,9 @@ impl Future for NodeService { type ServiceFuture = Pin + Send + Sync>>; type InstantBlockMiningFuture = ServiceFuture; -type PendingBlockMiningFuture = ServiceFuture<(MinedBlockOutcome, StateRefDb)>; +type IntervalBlockMiningFuture = ServiceFuture; /// The type which responsible for block production. -/// -/// On _interval_ mining, a new block is opened for a fixed amount of interval. Within this -/// interval, it executes all the queued transactions and keep hold of the pending state after -/// executing all the transactions. Once the interval is over, the block producer will close/mine -/// the block with all the transactions that have been executed within the interval and applies the -/// resulting state to the latest state. Then, a new block is opened for the next interval. As such, -/// the block context is updated only when a new block is opened. -/// -/// On _instant_ mining, a new block is mined as soon as there are transactions in the tx pool. The -/// block producer will execute all the transactions in the mempool and mine a new block with the -/// resulting state. The block context is only updated every time a new block is mined as opposed to -/// updating it when the block is opened (in _interval_ mode). #[must_use = "BlockProducer does nothing unless polled"] #[derive(Clone)] pub struct BlockProducer { @@ -106,7 +94,7 @@ impl BlockProducer { /// Creates a block producer that mines a new block every `interval` milliseconds. pub fn interval(backend: Arc, initial_state: StateRefDb, interval: u64) -> Self { Self { - inner: Arc::new(RwLock::new(BlockProducerMode::Interval(PendingBlockProducer::new( + inner: Arc::new(RwLock::new(BlockProducerMode::Interval(IntervalBlockProducer::new( backend, initial_state, interval, @@ -119,7 +107,7 @@ impl BlockProducer { pub fn on_demand(backend: Arc, initial_state: StateRefDb) -> Self { Self { inner: Arc::new(RwLock::new(BlockProducerMode::Interval( - PendingBlockProducer::new_no_mining(backend, initial_state), + IntervalBlockProducer::new_no_mining(backend, initial_state), ))), } } @@ -178,17 +166,30 @@ impl Stream for BlockProducer { } } +/// The inner type of [BlockProducer]. +/// +/// On _interval_ mining, a new block is opened for a fixed amount of interval. Within this +/// interval, it executes all the queued transactions and keep hold of the pending state after +/// executing all the transactions. Once the interval is over, the block producer will close/mine +/// the block with all the transactions that have been executed within the interval and applies the +/// resulting state to the latest state. Then, a new block is opened for the next interval. As such, +/// the block context is updated only when a new block is opened. +/// +/// On _instant_ mining, a new block is mined as soon as there are transactions in the tx pool. The +/// block producer will execute all the transactions in the mempool and mine a new block with the +/// resulting state. The block context is only updated every time a new block is mined as opposed to +/// updating it when the block is opened (in _interval_ mode). pub enum BlockProducerMode { - Interval(PendingBlockProducer), + Interval(IntervalBlockProducer), Instant(InstantBlockProducer), } -pub struct PendingBlockProducer { +pub struct IntervalBlockProducer { /// The interval at which new blocks are mined. interval: Option, backend: Arc, /// Single active future that mines a new block - block_mining: Option, + block_mining: Option, /// Backlog of sets of transactions ready to be mined queued: VecDeque>, /// The state of the pending block after executing all the transactions within the interval. @@ -198,9 +199,15 @@ pub struct PendingBlockProducer { is_initialized: bool, } -impl PendingBlockProducer { +impl IntervalBlockProducer { pub fn new(backend: Arc, db: StateRefDb, interval: u64) -> Self { - let interval = Duration::from_millis(interval); + let interval = { + let duration = Duration::from_millis(interval); + let mut interval = interval_at(Instant::now() + duration, duration); + interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay); + interval + }; + let state = Arc::new(PendingState { state: RwLock::new(CachedStateWrapper::new(db)), executed_transactions: Default::default(), @@ -211,12 +218,12 @@ impl PendingBlockProducer { backend, block_mining: None, is_initialized: false, + interval: Some(interval), queued: VecDeque::default(), - interval: Some(interval_at(Instant::now() + interval, interval)), } } - /// Creates a new [PendingBlockProducer] with no `interval`. This mode will not produce blocks + /// Creates a new [IntervalBlockProducer] with no `interval`. This mode will not produce blocks /// for every fixed interval, although it will still execute all queued transactions and /// keep hold of the pending state. pub fn new_no_mining(backend: Arc, db: StateRefDb) -> Self { @@ -228,10 +235,10 @@ impl PendingBlockProducer { Self { state, backend, + interval: None, block_mining: None, is_initialized: false, queued: VecDeque::default(), - interval: None, } } @@ -243,8 +250,7 @@ impl PendingBlockProducer { pub async fn force_mine(&self) { if self.block_mining.is_none() { let outcome = self.outcome(); - let (_, new_state) = Self::do_mine(outcome, self.backend.clone()).await; - self.reset(new_state); + let _ = Self::do_mine(outcome, self.backend.clone(), self.state.clone()).await; } else { trace!(target: "miner", "unable to force mine while a mining process is running") } @@ -253,30 +259,27 @@ impl PendingBlockProducer { async fn do_mine( execution_outcome: ExecutionOutcome, backend: Arc, - ) -> (MinedBlockOutcome, StateRefDb) { + pending_state: Arc, + ) -> MinedBlockOutcome { trace!(target: "miner", "creating new block"); let (outcome, new_state) = backend.mine_pending_block(execution_outcome).await; trace!(target: "miner", "created new block: {}", outcome.block_number); + backend.update_block_context(); - (outcome, new_state) - } + // reset the state for the next block + pending_state.executed_transactions.write().clear(); + *pending_state.state.write() = CachedStateWrapper::new(new_state); - fn reset(&self, state: StateRefDb) { - self.state.executed_transactions.write().clear(); - *self.state.state.write() = CachedStateWrapper::new(state); + outcome } - fn execute_transactions( - transactions: Vec, - state: Arc, - backend: Arc, - ) { + fn execute_transactions(&self, transactions: Vec) { let transactions = { - let mut state = state.state.write(); + let mut state = self.state.state.write(); TransactionExecutor::new( &mut state, - &backend.env.read().block, - !backend.config.read().disable_fee, + &self.backend.env.read().block, + !self.backend.config.read().disable_fee, ) .with_error_log() .with_events_log() @@ -298,7 +301,7 @@ impl PendingBlockProducer { .collect::>() }; - state.executed_transactions.write().extend(transactions) + self.state.executed_transactions.write().extend(transactions); } fn outcome(&self) -> ExecutionOutcome { @@ -326,7 +329,7 @@ impl PendingBlockProducer { } } -impl Stream for PendingBlockProducer { +impl Stream for IntervalBlockProducer { // mined block outcome and the new state type Item = MinedBlockOutcome; @@ -340,23 +343,24 @@ impl Stream for PendingBlockProducer { if let Some(interval) = &mut pin.interval { if interval.poll_tick(cx).is_ready() && pin.block_mining.is_none() { - pin.block_mining = - Some(Box::pin(Self::do_mine(pin.outcome(), pin.backend.clone()))); + pin.block_mining = Some(Box::pin(Self::do_mine( + pin.outcome(), + pin.backend.clone(), + pin.state.clone(), + ))); } } // only execute transactions if there is no mining in progress if !pin.queued.is_empty() && pin.block_mining.is_none() { let transactions = pin.queued.pop_front().expect("not empty; qed"); - Self::execute_transactions(transactions, pin.state.clone(), pin.backend.clone()); + pin.execute_transactions(transactions); } // poll the mining future if let Some(mut mining) = pin.block_mining.take() { // reset the executor for the next block - if let Poll::Ready((outcome, new_state)) = mining.poll_unpin(cx) { - // update the block context for the next pending block - pin.reset(new_state); + if let Poll::Ready(outcome) = mining.poll_unpin(cx) { return Poll::Ready(Some(outcome)); } else { pin.block_mining = Some(mining) diff --git a/crates/katana/core/src/utils/transaction.rs b/crates/katana/core/src/utils/transaction.rs index 77dd4afcf6..e45c4803db 100644 --- a/crates/katana/core/src/utils/transaction.rs +++ b/crates/katana/core/src/utils/transaction.rs @@ -11,9 +11,7 @@ use starknet::core::types::{ InvokeTransactionV0, InvokeTransactionV1, L1HandlerTransaction, Transaction as RpcTransaction, }; use starknet::core::utils::get_contract_address; -use starknet_api::core::{ - ClassHash, CompiledClassHash, ContractAddress, EntryPointSelector, Nonce, PatriciaKey, -}; +use starknet_api::core::{ClassHash, CompiledClassHash, ContractAddress, Nonce, PatriciaKey}; use starknet_api::hash::{StarkFelt, StarkHash}; use starknet_api::transaction::{ Calldata, ContractAddressSalt, DeclareTransaction as DeclareApiTransaction, @@ -21,9 +19,8 @@ use starknet_api::transaction::{ DeclareTransactionV2 as DeclareApiTransactionV2, DeployAccountTransaction as DeployAccountApiTransaction, DeployTransaction as DeployApiTransaction, Fee, InvokeTransaction as InvokeApiTransaction, - InvokeTransactionV0 as InvokeApiTransactionV0, InvokeTransactionV1 as InvokeApiTransactionV1, - L1HandlerTransaction as L1HandlerApiTransaction, Transaction as ApiTransaction, - TransactionHash, TransactionSignature, TransactionVersion, + InvokeTransactionV1 as InvokeApiTransactionV1, L1HandlerTransaction as L1HandlerApiTransaction, + Transaction as ApiTransaction, TransactionHash, TransactionSignature, TransactionVersion, }; use starknet_api::{patricia_key, stark_felt}; @@ -237,7 +234,6 @@ fn api_deploy_account_to_rpc_transaction( fn api_invoke_to_rpc_transaction(transaction: InvokeApiTransaction) -> InvokeTransaction { match transaction { InvokeApiTransaction::V0(tx) => InvokeTransaction::V0(InvokeTransactionV0 { - nonce: FieldElement::ZERO, max_fee: tx.max_fee.0.into(), transaction_hash: tx.transaction_hash.0.into(), contract_address: (*tx.contract_address.0.key()).into(), @@ -298,55 +294,23 @@ pub fn broadcasted_invoke_rpc_to_api_transaction( transaction: BroadcastedInvokeTransaction, chain_id: FieldElement, ) -> InvokeApiTransaction { - match transaction { - BroadcastedInvokeTransaction::V0(tx) => { - let transaction_hash = compute_invoke_v0_transaction_hash( - tx.contract_address, - tx.entry_point_selector, - &tx.calldata, - tx.max_fee, - chain_id, - ); - - let transaction = InvokeApiTransactionV0 { - transaction_hash: TransactionHash(transaction_hash.into()), - contract_address: ContractAddress(patricia_key!(tx.contract_address)), - entry_point_selector: EntryPointSelector(tx.entry_point_selector.into()), - calldata: Calldata(Arc::new(tx.calldata.into_iter().map(|c| c.into()).collect())), - max_fee: Fee(starkfelt_to_u128(tx.max_fee.into()) - .expect("convert max fee StarkFelt to u128")), - signature: TransactionSignature( - tx.signature.into_iter().map(|e| e.into()).collect(), - ), - }; - - InvokeApiTransaction::V0(transaction) - } + let BroadcastedInvokeTransaction { + calldata, max_fee, nonce, sender_address, signature, .. + } = transaction; - BroadcastedInvokeTransaction::V1(tx) => { - let transaction_hash = compute_invoke_v1_transaction_hash( - tx.sender_address, - &tx.calldata, - tx.max_fee, - chain_id, - tx.nonce, - ); + let hash = + compute_invoke_v1_transaction_hash(sender_address, &calldata, max_fee, chain_id, nonce); - let transaction = InvokeApiTransactionV1 { - transaction_hash: TransactionHash(transaction_hash.into()), - sender_address: ContractAddress(patricia_key!(tx.sender_address)), - nonce: Nonce(StarkFelt::from(tx.nonce)), - calldata: Calldata(Arc::new(tx.calldata.into_iter().map(|c| c.into()).collect())), - max_fee: Fee(starkfelt_to_u128(tx.max_fee.into()) - .expect("convert max fee StarkFelt to u128")), - signature: TransactionSignature( - tx.signature.into_iter().map(|e| e.into()).collect(), - ), - }; + let transaction = InvokeApiTransactionV1 { + nonce: Nonce(nonce.into()), + transaction_hash: TransactionHash(hash.into()), + sender_address: ContractAddress(patricia_key!(sender_address)), + signature: TransactionSignature(signature.into_iter().map(|e| e.into()).collect()), + calldata: Calldata(Arc::new(calldata.into_iter().map(|c| c.into()).collect())), + max_fee: Fee(starkfelt_to_u128(max_fee.into()).expect("convert max fee StarkFelt to u128")), + }; - InvokeApiTransaction::V1(transaction) - } - } + InvokeApiTransaction::V1(transaction) } /// Convert broadcasted Declare transaction type from `starknet-rs` to `starknet_api`'s diff --git a/crates/katana/rpc/src/api/starknet.rs b/crates/katana/rpc/src/api/starknet.rs index 5103b16323..4ad5467987 100644 --- a/crates/katana/rpc/src/api/starknet.rs +++ b/crates/katana/rpc/src/api/starknet.rs @@ -10,13 +10,14 @@ use starknet::core::types::{ ContractClass, DeclareTransactionResult, DeployAccountTransactionResult, EventFilterWithPage, EventsPage, FeeEstimate, FieldElement, FunctionCall, InvokeTransactionResult, MaybePendingBlockWithTxHashes, MaybePendingBlockWithTxs, MaybePendingTransactionReceipt, - StateUpdate, Transaction, + MsgFromL1, StateUpdate, Transaction, }; #[serde_as] #[derive(Serialize, Deserialize)] pub struct Felt(#[serde_as(as = "UfeHex")] pub FieldElement); +// TODO: implement From for StarknetApiError #[derive(thiserror::Error, Clone, Copy, Debug)] pub enum StarknetApiError { #[error("Failed to write transaction")] @@ -45,14 +46,36 @@ pub enum StarknetApiError { ContractError = 40, #[error("Invalid contract class")] InvalidContractClass = 50, + #[error("Class already declared")] + ClassAlreadyDeclared = 51, + #[error("Invalid transaction nonce")] + InvalidTransactionNonce = 52, + #[error("Max fee is smaller than the minimal transaction cost (validation plus fee transfer)")] + InsufficientMaxFee = 53, + #[error("Account balance is smaller than the transaction's max_fee")] + InsufficientAccountBalance = 54, + #[error("Account validation failed")] + ValidationFailure = 55, + #[error("Compilation failed")] + CompilationFailed = 56, + #[error("Contract class size is too large")] + ContractClassSizeIsTooLarge = 57, + #[error("Sender address in not an account contract")] + NonAccount = 58, + #[error("A transaction with the same hash already exists in the mempool")] + DuplicateTransaction = 59, + #[error("The compiled class hash did not match the one supplied in the transaction")] + CompiledClassHashMismatch = 60, + #[error("The transaction version is not supported")] + UnsupportedTransactionVersion = 61, + #[error("The contract class version is not supported")] + UnsupportedContractClassVersion = 62, + #[error("An unexpected error occured")] + UnexpectedError = 63, #[error("Too many storage keys requested")] ProofLimitExceeded = 10000, #[error("Too many keys provided in a filter")] TooManyKeysInFilter = 34, - #[error("Internal server error")] - InternalServerError = 500, - #[error("Unsupported transaction version")] - UnsupportedTransactionVersion = 53, #[error("Failed to fetch pending transactions")] FailedToFetchPendingTransactions = 38, } @@ -65,6 +88,8 @@ impl From for Error { #[rpc(server, namespace = "starknet")] pub trait StarknetApi { + // Read API + #[method(name = "chainId")] async fn chain_id(&self) -> Result; @@ -142,10 +167,17 @@ pub trait StarknetApi { #[method(name = "estimateFee")] async fn estimate_fee( &self, - request: Vec, + transactions: Vec, block_id: BlockId, ) -> Result, Error>; + #[method(name = "estimateMessageFee")] + async fn estimate_message_fee( + &self, + message: MsgFromL1, + block_id: BlockId, + ) -> Result; + #[method(name = "call")] async fn call(&self, request: FunctionCall, block_id: BlockId) -> Result, Error>; @@ -157,6 +189,8 @@ pub trait StarknetApi { block_id: BlockId, ) -> Result; + // Write API + #[method(name = "addDeployAccountTransaction")] async fn add_deploy_account_transaction( &self, diff --git a/crates/katana/rpc/src/lib.rs b/crates/katana/rpc/src/lib.rs index f20ffcce5f..466e3d5f03 100644 --- a/crates/katana/rpc/src/lib.rs +++ b/crates/katana/rpc/src/lib.rs @@ -93,7 +93,7 @@ impl Logger for RpcLogger { _kind: MethodKind, _transport: TransportProtocol, ) { - debug!(method = ?method_name); + debug!(target: "server", method = ?method_name); } fn on_result( diff --git a/crates/katana/rpc/src/starknet.rs b/crates/katana/rpc/src/starknet.rs index 00f33becb4..22219e3e64 100644 --- a/crates/katana/rpc/src/starknet.rs +++ b/crates/katana/rpc/src/starknet.rs @@ -5,7 +5,7 @@ use jsonrpsee::core::{async_trait, Error}; use katana_core::backend::contract::StarknetContract; use katana_core::backend::storage::transaction::{ DeclareTransaction, DeployAccountTransaction, InvokeTransaction, KnownTransaction, - PendingTransaction, Transaction, + L1HandlerTransaction, PendingTransaction, Transaction, }; use katana_core::backend::ExternalFunctionCall; use katana_core::sequencer::Sequencer; @@ -21,7 +21,7 @@ use starknet::core::types::{ ContractClass, DeclareTransactionResult, DeployAccountTransactionResult, EventFilterWithPage, EventsPage, FeeEstimate, FieldElement, FunctionCall, InvokeTransactionResult, MaybePendingBlockWithTxHashes, MaybePendingBlockWithTxs, MaybePendingTransactionReceipt, - StateUpdate, Transaction as RpcTransaction, + MsgFromL1, StateUpdate, Transaction as RpcTransaction, }; use starknet_api::core::{ClassHash, ContractAddress, EntryPointSelector, PatriciaKey}; use starknet_api::hash::{StarkFelt, StarkHash}; @@ -62,11 +62,9 @@ where .nonce_at(block_id, ContractAddress(patricia_key!(contract_address))) .await .map_err(|e| match e { - SequencerError::StateNotFound(_) => Error::from(StarknetApiError::BlockNotFound), - SequencerError::ContractNotFound(_) => { - Error::from(StarknetApiError::ContractNotFound) - } - _ => Error::from(StarknetApiError::InternalServerError), + SequencerError::StateNotFound(_) => StarknetApiError::BlockNotFound, + SequencerError::ContractNotFound(_) => StarknetApiError::ContractNotFound, + _ => StarknetApiError::UnexpectedError, })?; Ok(Felt(nonce.0.into())) @@ -117,12 +115,7 @@ where &self, block_id: BlockId, ) -> Result { - let block = self - .sequencer - .block(block_id) - .await - .ok_or(Error::from(StarknetApiError::BlockNotFound))?; - + let block = self.sequencer.block(block_id).await.ok_or(StarknetApiError::BlockNotFound)?; Ok(block.into()) } @@ -131,28 +124,19 @@ where block_id: BlockId, index: usize, ) -> Result { - let block = self - .sequencer - .block(block_id) - .await - .ok_or(Error::from(StarknetApiError::BlockNotFound))?; + let block = self.sequencer.block(block_id).await.ok_or(StarknetApiError::BlockNotFound)?; let hash: FieldElement = block .transactions() .get(index) .map(|t| t.inner.hash()) - .ok_or(Error::from(StarknetApiError::InvalidTxnIndex))?; + .ok_or(StarknetApiError::InvalidTxnIndex)?; self.transaction_by_hash(hash).await } async fn block_with_txs(&self, block_id: BlockId) -> Result { - let block = self - .sequencer - .block(block_id) - .await - .ok_or(Error::from(StarknetApiError::BlockNotFound))?; - + let block = self.sequencer.block(block_id).await.ok_or(StarknetApiError::BlockNotFound)?; Ok(block.into()) } @@ -160,7 +144,7 @@ where self.sequencer .state_update(block_id) .await - .map_err(|_| Error::from(StarknetApiError::BlockNotFound)) + .map_err(|_| StarknetApiError::BlockNotFound.into()) } async fn transaction_receipt( @@ -170,7 +154,7 @@ where self.sequencer .transaction_receipt(&transaction_hash) .await - .ok_or(Error::from(StarknetApiError::TxnHashNotFound)) + .ok_or(StarknetApiError::TxnHashNotFound.into()) } async fn class_hash_at( @@ -185,7 +169,7 @@ where .map_err(|e| match e { SequencerError::BlockNotFound(_) => StarknetApiError::BlockNotFound, SequencerError::ContractNotFound(_) => StarknetApiError::ContractNotFound, - _ => StarknetApiError::InternalServerError, + _ => StarknetApiError::UnexpectedError, })?; Ok(Felt(class_hash.0.into())) @@ -202,14 +186,14 @@ where SequencerError::State(StateError::UndeclaredClassHash(_)) => { StarknetApiError::ClassHashNotFound } - _ => StarknetApiError::InternalServerError, + _ => StarknetApiError::UnexpectedError, }, )?; match contract { StarknetContract::Legacy(c) => { - let contract = legacy_inner_to_rpc_class(c) - .map_err(|_| StarknetApiError::InternalServerError)?; + let contract = + legacy_inner_to_rpc_class(c).map_err(|_| StarknetApiError::UnexpectedError)?; Ok(contract) } StarknetContract::Sierra(c) => Ok(ContractClass::Sierra(c)), @@ -236,7 +220,7 @@ where .await .map_err(|e| match e { SequencerError::BlockNotFound(_) => StarknetApiError::BlockNotFound, - _ => StarknetApiError::InternalServerError, + _ => StarknetApiError::UnexpectedError, })?; Ok(events) @@ -265,10 +249,10 @@ where }; let res = self.sequencer.call(block_id, call).await.map_err(|e| match e { - SequencerError::BlockNotFound(_) => Error::from(StarknetApiError::BlockNotFound), - SequencerError::ContractNotFound(_) => Error::from(StarknetApiError::ContractNotFound), - SequencerError::EntryPointExecution(_) => Error::from(StarknetApiError::ContractError), - _ => Error::from(StarknetApiError::InternalServerError), + SequencerError::BlockNotFound(_) => StarknetApiError::BlockNotFound, + SequencerError::ContractNotFound(_) => StarknetApiError::ContractNotFound, + SequencerError::EntryPointExecution(_) => StarknetApiError::ContractError, + _ => StarknetApiError::UnexpectedError, })?; let mut values = vec![]; @@ -295,9 +279,9 @@ where ) .await .map_err(|e| match e { - SequencerError::StateNotFound(_) => Error::from(StarknetApiError::BlockNotFound), - SequencerError::State(_) => Error::from(StarknetApiError::ContractNotFound), - _ => Error::from(StarknetApiError::InternalServerError), + SequencerError::StateNotFound(_) => StarknetApiError::BlockNotFound, + SequencerError::State(_) => StarknetApiError::ContractNotFound, + _ => StarknetApiError::UnexpectedError, })?; Ok(Felt(value.into())) @@ -308,7 +292,7 @@ where deploy_account_transaction: BroadcastedDeployAccountTransaction, ) -> Result { let chain_id = FieldElement::from_hex_be(&self.sequencer.chain_id().await.as_hex()) - .map_err(|_| Error::from(StarknetApiError::InternalServerError))?; + .map_err(|_| StarknetApiError::UnexpectedError)?; let (transaction, contract_address) = broadcasted_deploy_account_rpc_to_api_transaction(deploy_account_transaction, chain_id); @@ -330,7 +314,7 @@ where block_id: BlockId, ) -> Result, Error> { let chain_id = FieldElement::from_hex_be(&self.sequencer.chain_id().await.as_hex()) - .map_err(|_| Error::from(StarknetApiError::InternalServerError))?; + .map_err(|_| StarknetApiError::UnexpectedError)?; let transactions = request .into_iter() @@ -372,22 +356,52 @@ where let res = self.sequencer.estimate_fee(transactions, block_id).await.map_err(|e| match e { - SequencerError::BlockNotFound(_) => Error::from(StarknetApiError::BlockNotFound), - SequencerError::TransactionExecution(_) => { - Error::from(StarknetApiError::ContractError) - } - _ => Error::from(StarknetApiError::InternalServerError), + SequencerError::BlockNotFound(_) => StarknetApiError::BlockNotFound, + SequencerError::TransactionExecution(_) => StarknetApiError::ContractError, + _ => StarknetApiError::UnexpectedError, })?; Ok(res) } + async fn estimate_message_fee( + &self, + message: MsgFromL1, + block_id: BlockId, + ) -> Result { + let l1handler_tx = L1HandlerTransaction { + inner: starknet_api::transaction::L1HandlerTransaction { + contract_address: ContractAddress(patricia_key!(message.to_address)), + calldata: Calldata(Arc::new( + message.payload.into_iter().map(|f| f.into()).collect(), + )), + entry_point_selector: EntryPointSelector(message.entry_point_selector.into()), + ..Default::default() + }, + paid_l1_fee: 1, + }; + + let res = self + .sequencer + .estimate_fee(vec![Transaction::L1Handler(l1handler_tx)], block_id) + .await + .map_err(|e| match e { + SequencerError::BlockNotFound(_) => StarknetApiError::BlockNotFound, + SequencerError::TransactionExecution(_) => StarknetApiError::ContractError, + _ => StarknetApiError::UnexpectedError, + })? + .pop() + .expect("should have estimate result"); + + Ok(res) + } + async fn add_declare_transaction( &self, declare_transaction: BroadcastedDeclareTransaction, ) -> Result { let chain_id = FieldElement::from_hex_be(&self.sequencer.chain_id().await.as_hex()) - .map_err(|_| Error::from(StarknetApiError::InternalServerError))?; + .map_err(|_| StarknetApiError::UnexpectedError)?; let sierra_class = match declare_transaction { BroadcastedDeclareTransaction::V2(ref tx) => Some(tx.contract_class.as_ref().clone()), @@ -414,7 +428,7 @@ where invoke_transaction: BroadcastedInvokeTransaction, ) -> Result { let chain_id = FieldElement::from_hex_be(&self.sequencer.chain_id().await.as_hex()) - .map_err(|_| Error::from(StarknetApiError::InternalServerError))?; + .map_err(|_| StarknetApiError::UnexpectedError)?; let transaction = broadcasted_invoke_rpc_to_api_transaction(invoke_transaction, chain_id); let transaction_hash = transaction.transaction_hash().0.into(); diff --git a/crates/katana/rpc/tests/starknet.rs b/crates/katana/rpc/tests/starknet.rs index e6ccae46d8..a0ece05aa2 100644 --- a/crates/katana/rpc/tests/starknet.rs +++ b/crates/katana/rpc/tests/starknet.rs @@ -13,7 +13,7 @@ use starknet::core::types::contract::legacy::LegacyContractClass; use starknet::core::types::contract::{CompiledClass, SierraClass}; use starknet::core::types::{ BlockId, BlockTag, DeclareTransactionReceipt, FieldElement, FlattenedSierraClass, - MaybePendingTransactionReceipt, TransactionReceipt, TransactionStatus, + MaybePendingTransactionReceipt, TransactionFinalityStatus, TransactionReceipt, }; use starknet::core::utils::{get_contract_address, get_selector_from_name}; use starknet::providers::Provider; @@ -37,9 +37,9 @@ async fn test_send_declare_and_deploy_contract() { match receipt { MaybePendingTransactionReceipt::Receipt(TransactionReceipt::Declare( - DeclareTransactionReceipt { status, .. }, + DeclareTransactionReceipt { finality_status, .. }, )) => { - assert_eq!(status, TransactionStatus::AcceptedOnL2); + assert_eq!(finality_status, TransactionFinalityStatus::AcceptedOnL2); } _ => panic!("invalid tx receipt"), } @@ -116,9 +116,9 @@ async fn test_send_declare_and_deploy_legacy_contract() { match receipt { MaybePendingTransactionReceipt::Receipt(TransactionReceipt::Declare( - DeclareTransactionReceipt { status, .. }, + DeclareTransactionReceipt { finality_status, .. }, )) => { - assert_eq!(status, TransactionStatus::AcceptedOnL2); + assert_eq!(finality_status, TransactionFinalityStatus::AcceptedOnL2); } _ => panic!("invalid tx receipt"), } diff --git a/crates/katana/src/args.rs b/crates/katana/src/args.rs index f85b636279..88f0885a99 100644 --- a/crates/katana/src/args.rs +++ b/crates/katana/src/args.rs @@ -21,12 +21,12 @@ pub struct KatanaArgs { #[arg(long)] #[arg(conflicts_with = "block_time")] - #[arg(help = "Disable auto and interval mining, and mine on demand instead.")] + #[arg(help = "Disable auto and interval mining, and mine on demand instead via an endpoint.")] pub no_mining: bool, #[arg(short, long)] - #[arg(value_name = "SECONDS")] - #[arg(help = "Block time in seconds for interval mining.")] + #[arg(value_name = "MILLISECONDS")] + #[arg(help = "Block time in milliseconds for interval mining.")] pub block_time: Option, #[arg(long)] diff --git a/crates/katana/src/main.rs b/crates/katana/src/main.rs index 74f10277a5..208300b12b 100644 --- a/crates/katana/src/main.rs +++ b/crates/katana/src/main.rs @@ -5,11 +5,11 @@ use std::{fs, io}; use clap::{CommandFactory, Parser}; use clap_complete::{generate, Shell}; use console::Style; -use env_logger::Env; use katana_core::sequencer::{KatanaSequencer, Sequencer}; use katana_rpc::{spawn, KatanaApi, NodeHandle, StarknetApi}; -use log::{error, info}; use tokio::signal::ctrl_c; +use tracing::{error, info}; +use tracing_subscriber::fmt; mod args; @@ -18,11 +18,15 @@ use args::KatanaArgs; #[tokio::main] async fn main() { - env_logger::Builder::from_env(Env::default().default_filter_or( - "info,executor=trace,katana_rpc=debug,katana_core=trace,blockifier=off,\ - jsonrpsee_server=off,hyper=off,", - )) - .init(); + tracing::subscriber::set_global_default( + fmt::Subscriber::builder() + .with_env_filter( + "info,executor=trace,server=debug,katana_core=trace,blockifier=off,\ + jsonrpsee_server=off,hyper=off", + ) + .finish(), + ) + .expect("Failed to set the global tracing subscriber"); let config = KatanaArgs::parse(); if let Some(command) = config.command { @@ -63,15 +67,13 @@ async fn main() { ); } - // sequencer.start().await; - // Wait until Ctrl + C is pressed, then shutdown ctrl_c().await.unwrap(); shutdown_handler(sequencer.clone(), config).await; handle.stop().unwrap(); } Err(err) => { - error! {"{}", err}; + error! {"{err}"}; exit(1); } }; diff --git a/crates/sozo/Cargo.toml b/crates/sozo/Cargo.toml index 68932adf12..7aeb0e59c2 100644 --- a/crates/sozo/Cargo.toml +++ b/crates/sozo/Cargo.toml @@ -23,7 +23,6 @@ clap_complete.workspace = true console.workspace = true dojo-lang = { path = "../dojo-lang" } dojo-world = { path = "../dojo-world" } -log.workspace = true scarb-ui.workspace = true scarb.workspace = true semver.workspace = true diff --git a/crates/sozo/src/commands/options/account.rs b/crates/sozo/src/commands/options/account.rs index 97e4ceb018..9936eebe81 100644 --- a/crates/sozo/src/commands/options/account.rs +++ b/crates/sozo/src/commands/options/account.rs @@ -3,7 +3,7 @@ use std::str::FromStr; use anyhow::{anyhow, Context, Result}; use clap::Args; use dojo_world::metadata::Environment; -use starknet::accounts::SingleOwnerAccount; +use starknet::accounts::{ExecutionEncoding, SingleOwnerAccount}; use starknet::core::types::FieldElement; use starknet::providers::Provider; use starknet::signers::{LocalWallet, SigningKey}; @@ -50,7 +50,17 @@ impl AccountOptions { let chain_id = provider.chain_id().await.with_context(|| "Failed to retrieve network chain id.")?; - Ok(SingleOwnerAccount::new(provider, signer, account_address, chain_id)) + Ok(SingleOwnerAccount::new( + provider, + signer, + account_address, + chain_id, + // This is made under the assumption that the accounts used with `sozo` commands would + // be one of the `katana` dev accounts. The dev accounts deployed on `katana` are + // legacy accounts (Cairo 0). + // TODO: Make this configurable + ExecutionEncoding::Legacy, + )) } fn signer(&self, env_metadata: Option<&Environment>) -> Result { diff --git a/crates/sozo/src/ops/migration/migration_test.rs b/crates/sozo/src/ops/migration/migration_test.rs index 4f21ce18ed..790c5188a7 100644 --- a/crates/sozo/src/ops/migration/migration_test.rs +++ b/crates/sozo/src/ops/migration/migration_test.rs @@ -7,9 +7,10 @@ use dojo_world::migration::strategy::prepare_for_migration; use dojo_world::migration::world::WorldDiff; use scarb::core::Config; use scarb_ui::Verbosity; -use starknet::accounts::SingleOwnerAccount; +use starknet::accounts::{ExecutionEncoding, SingleOwnerAccount}; use starknet::core::chain_id; -use starknet::core::types::FieldElement; +use starknet::core::types::{BlockId, BlockTag}; +use starknet::macros::felt; use starknet::providers::jsonrpc::HttpTransport; use starknet::providers::JsonRpcClient; use starknet::signers::{LocalWallet, SigningKey}; @@ -24,14 +25,8 @@ async fn migrate_with_auto_mine() { let sequencer = TestSequencer::start(SequencerConfig::default(), get_default_test_starknet_config()).await; - let account = SingleOwnerAccount::new( - JsonRpcClient::new(HttpTransport::new(sequencer.url())), - LocalWallet::from_signing_key(SigningKey::from_secret_scalar( - sequencer.raw_account().private_key, - )), - sequencer.raw_account().account_address, - chain_id::TESTNET, - ); + let mut account = sequencer.account(); + account.set_block_id(BlockId::Tag(BlockTag::Pending)); let config = Config::builder(Utf8PathBuf::from_path_buf("../../examples/ecs/".into()).unwrap()) .ui_verbosity(Verbosity::Quiet) @@ -41,13 +36,7 @@ async fn migrate_with_auto_mine() { let manifest = Manifest::load_from_path(target_dir.join("manifest.json")).unwrap(); let world = WorldDiff::compute(manifest, None); - let migration = prepare_for_migration( - None, - Some(FieldElement::from_hex_be("0x12345").unwrap()), - target_dir, - world, - ) - .unwrap(); + let migration = prepare_for_migration(None, Some(felt!("0x12345")), target_dir, world).unwrap(); execute_strategy(&migration, &account, &config, None).await.unwrap(); sequencer.stop().unwrap(); @@ -58,19 +47,13 @@ async fn migrate_with_block_time() { let target_dir = Utf8PathBuf::from_path_buf("../../examples/ecs/target/dev".into()).unwrap(); let sequencer = TestSequencer::start( - SequencerConfig { block_time: Some(1), ..Default::default() }, + SequencerConfig { block_time: Some(1000), ..Default::default() }, get_default_test_starknet_config(), ) .await; - let account = SingleOwnerAccount::new( - JsonRpcClient::new(HttpTransport::new(sequencer.url())), - LocalWallet::from_signing_key(SigningKey::from_secret_scalar( - sequencer.raw_account().private_key, - )), - sequencer.raw_account().account_address, - chain_id::TESTNET, - ); + let mut account = sequencer.account(); + account.set_block_id(BlockId::Tag(BlockTag::Pending)); let config = Config::builder(Utf8PathBuf::from_path_buf("../../examples/ecs/".into()).unwrap()) .ui_verbosity(Verbosity::Quiet) @@ -80,16 +63,8 @@ async fn migrate_with_block_time() { let manifest = Manifest::load_from_path(target_dir.join("manifest.json")).unwrap(); let world = WorldDiff::compute(manifest, None); - let migration = prepare_for_migration( - None, - Some(FieldElement::from_hex_be("0x12345").unwrap()), - target_dir, - world, - ) - .unwrap(); + let migration = prepare_for_migration(None, Some(felt!("0x12345")), target_dir, world).unwrap(); execute_strategy(&migration, &account, &config, None).await.unwrap(); - - sequencer.stop().unwrap(); } #[tokio::test(flavor = "multi_thread")] @@ -97,7 +72,7 @@ async fn migrate_with_small_fee_multiplier_will_fail() { let target_dir = Utf8PathBuf::from_path_buf("../../examples/ecs/target/dev".into()).unwrap(); let sequencer = TestSequencer::start( - SequencerConfig { block_time: Some(1), ..Default::default() }, + Default::default(), StarknetConfig { disable_fee: false, ..Default::default() }, ) .await; @@ -109,6 +84,7 @@ async fn migrate_with_small_fee_multiplier_will_fail() { )), sequencer.raw_account().account_address, chain_id::TESTNET, + ExecutionEncoding::Legacy, ); let config = Config::builder(Utf8PathBuf::from_path_buf("../../examples/ecs/".into()).unwrap()) @@ -119,13 +95,7 @@ async fn migrate_with_small_fee_multiplier_will_fail() { let manifest = Manifest::load_from_path(target_dir.join("manifest.json")).unwrap(); let world = WorldDiff::compute(manifest, None); - let migration = prepare_for_migration( - None, - Some(FieldElement::from_hex_be("0x12345").unwrap()), - target_dir, - world, - ) - .unwrap(); + let migration = prepare_for_migration(None, Some(felt!("0x12345")), target_dir, world).unwrap(); assert!( execute_strategy( @@ -163,6 +133,7 @@ async fn migration_from_remote() { )), sequencer.raw_account().account_address, chain_id::TESTNET, + ExecutionEncoding::Legacy, ); let config = Config::builder(Utf8PathBuf::from_path_buf("../../examples/ecs/".into()).unwrap()) @@ -173,13 +144,8 @@ async fn migration_from_remote() { let manifest = Manifest::load_from_path(target_dir.clone()).unwrap(); let world = WorldDiff::compute(manifest, None); - let migration = prepare_for_migration( - None, - Some(FieldElement::from_hex_be("0x12345").unwrap()), - target_dir.clone(), - world, - ) - .unwrap(); + let migration = + prepare_for_migration(None, Some(felt!("0x12345")), target_dir.clone(), world).unwrap(); execute_strategy(&migration, &account, &config, None).await.unwrap(); diff --git a/crates/torii/client/wasm/Cargo.lock b/crates/torii/client/wasm/Cargo.lock index 026446c6d5..f98ac3670f 100644 --- a/crates/torii/client/wasm/Cargo.lock +++ b/crates/torii/client/wasm/Cargo.lock @@ -1571,14 +1571,14 @@ checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" [[package]] name = "starknet" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fcb61961b91757a9bc2d11549067445b2f921bd957f53710db35449767a1ba3" +checksum = "6f0623b045f3dc10aef030c9ddd4781cff9cbe1188b71063cc510b75d1f96be6" dependencies = [ "starknet-accounts", "starknet-contract", "starknet-core", - "starknet-crypto 0.6.0", + "starknet-crypto", "starknet-ff", "starknet-macros", "starknet-providers", @@ -1587,11 +1587,12 @@ dependencies = [ [[package]] name = "starknet-accounts" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "111ed887e4db14f0df1f909905e7737e4730770c8ed70997b58a71d5d940daac" +checksum = "68e97edc480348dca300e5a8234e6c4e6f2f1ac028f2b16fcce294ebe93d07f4" dependencies = [ "async-trait", + "auto_impl", "starknet-core", "starknet-providers", "starknet-signers", @@ -1600,9 +1601,9 @@ dependencies = [ [[package]] name = "starknet-contract" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0d6f81a647694b2cb669ab60e77954b57bf5fbc757f5fcaf0a791c3bd341f04" +checksum = "69b86e3f6b3ca9a5c45271ab10871c99f7dc82fee3199d9f8c7baa2a1829947d" dependencies = [ "serde", "serde_json", @@ -1615,9 +1616,9 @@ dependencies = [ [[package]] name = "starknet-core" -version = "0.5.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91f89c79b641618de8aa9668d74c6b6634659ceca311c6318a35c025f9d4d969" +checksum = "b796a32a7400f7d85e95d3900b5cee7a392b2adbf7ad16093ed45ec6f8d85de6" dependencies = [ "base64 0.21.2", "flate2", @@ -1627,30 +1628,10 @@ dependencies = [ "serde_json_pythonic", "serde_with", "sha3", - "starknet-crypto 0.6.0", + "starknet-crypto", "starknet-ff", ] -[[package]] -name = "starknet-crypto" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693e6362f150f9276e429a910481fb7f3bcb8d6aa643743f587cfece0b374874" -dependencies = [ - "crypto-bigint", - "hex", - "hmac", - "num-bigint", - "num-integer", - "num-traits", - "rfc6979", - "sha2", - "starknet-crypto-codegen", - "starknet-curve 0.3.0", - "starknet-ff", - "zeroize", -] - [[package]] name = "starknet-crypto" version = "0.6.0" @@ -1666,7 +1647,7 @@ dependencies = [ "rfc6979", "sha2", "starknet-crypto-codegen", - "starknet-curve 0.4.0", + "starknet-curve", "starknet-ff", "zeroize", ] @@ -1677,20 +1658,11 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af6527b845423542c8a16e060ea1bc43f67229848e7cd4c4d80be994a84220ce" dependencies = [ - "starknet-curve 0.4.0", + "starknet-curve", "starknet-ff", "syn 2.0.29", ] -[[package]] -name = "starknet-curve" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "252610baff59e4c4332ce3569f7469c5d3f9b415a2240d698fb238b2b4fc0942" -dependencies = [ - "starknet-ff", -] - [[package]] name = "starknet-curve" version = "0.4.0" @@ -1717,9 +1689,9 @@ dependencies = [ [[package]] name = "starknet-macros" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28a5865ee0ed22ade86bdf45e7c09c5641f1c59ccae12c21ecde535b2b6bf64a" +checksum = "ef846b6bb48fc8c3e9a2aa9b5b037414f04a908d9db56493a3ae69a857eb2506" dependencies = [ "starknet-core", "syn 2.0.29", @@ -1727,9 +1699,9 @@ dependencies = [ [[package]] name = "starknet-providers" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbbfccb46a8969fb3ac803718d9d8270cff4eed5b7f6b9ba234875ad2cc997c5" +checksum = "c3b136c26b72ff1756f0844e0aa80bab680ceb99d63921826facbb8e7340ff82" dependencies = [ "async-trait", "auto_impl", @@ -1747,9 +1719,9 @@ dependencies = [ [[package]] name = "starknet-signers" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5313524cc79344015ef2a8618947332ab17012b5c50600c7f84c60989bdec980" +checksum = "d9386015d2e6dc3df285bfb33a3afd8ad7596c70ed38ab57019de4d2dfc7826f" dependencies = [ "async-trait", "auto_impl", @@ -1757,7 +1729,7 @@ dependencies = [ "eth-keystore", "rand", "starknet-core", - "starknet-crypto 0.6.0", + "starknet-crypto", "thiserror", ] @@ -1946,7 +1918,7 @@ dependencies = [ "parking_lot", "serde", "starknet", - "starknet-crypto 0.5.1", + "starknet-crypto", "thiserror", "tokio", "wasm-bindgen", diff --git a/crates/torii/client/wasm/Cargo.toml b/crates/torii/client/wasm/Cargo.toml index 9ed334a067..d7df6350a1 100644 --- a/crates/torii/client/wasm/Cargo.toml +++ b/crates/torii/client/wasm/Cargo.toml @@ -15,7 +15,7 @@ async-std = { version = "1.12.0", default-features = false, features = [ "std" ] async-trait = "0.1.68" parking_lot = "0.12.1" serde = { version = "1.0.156", features = [ "derive" ] } -starknet = "0.5.0" +starknet = "0.6.0" thiserror = "1.0.32" torii-client = { path = ".." } url = "2.4.0" From 1850f7e3221e9a2ed15a357b058e49c3c12211ba Mon Sep 17 00:00:00 2001 From: Kariy Date: Tue, 12 Sep 2023 16:35:01 +0900 Subject: [PATCH 40/77] fix: enable `env-filter` feature for `tracing-subscriber` --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3670acabcd..9f95e06c8f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,8 +63,8 @@ futures = "0.3.28" indoc = "1.0.7" itertools = "0.10.3" num-bigint = "0.4" -parking_lot = "0.12.1" once_cell = "1.0" +parking_lot = "0.12.1" pretty_assertions = "1.2.1" rayon = "0.9.0" salsa = "0.16.1" @@ -83,7 +83,7 @@ thiserror = "1.0.32" tokio = { version = "1.32.0", features = [ "full" ] } toml = "0.7.4" tracing = "0.1.34" -tracing-subscriber = "0.3.16" +tracing-subscriber = { version = "0.3.16", features = [ "env-filter" ] } url = "2.4.0" [patch."https://github.com/starkware-libs/blockifier"] From 555cf079648ebdef38800e1b44e7d463a34dbd5f Mon Sep 17 00:00:00 2001 From: Yun Date: Tue, 12 Sep 2023 12:55:39 -0700 Subject: [PATCH 41/77] chore(torii): graphql/grpc ports argument (#889) * chore(torii): graphql/grpc ports argument * Fix clippy --------- Co-authored-by: Tarrence van As --- crates/torii/graphql/src/server.rs | 8 ++++---- crates/torii/grpc/src/server.rs | 11 +++++++---- crates/torii/server/src/cli.rs | 13 +++++++++++-- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/crates/torii/graphql/src/server.rs b/crates/torii/graphql/src/server.rs index 14b2cf36a4..91f9b397f3 100644 --- a/crates/torii/graphql/src/server.rs +++ b/crates/torii/graphql/src/server.rs @@ -13,16 +13,16 @@ async fn graphiql() -> impl IntoResponse { Html(GraphiQLSource::build().endpoint("/").subscription_endpoint("/ws").finish()) } -pub async fn start(pool: Pool) -> anyhow::Result<()> { - let schema = build_schema(&pool).await?; +pub async fn start(host: &String, port: u16, pool: &Pool) -> anyhow::Result<()> { + let schema = build_schema(pool).await?; let app = Route::new() .at("/", get(graphiql).post(GraphQL::new(schema.clone()))) .at("/ws", get(GraphQLSubscription::new(schema))) .with(Cors::new()); - println!("Open GraphiQL IDE: http://localhost:8080"); - Server::new(TcpListener::bind("0.0.0.0:8080")).run(app).await?; + println!("Open GraphiQL IDE: http://{}:{}", host, port); + Server::new(TcpListener::bind(format!("{}:{}", host, port))).run(app).await?; Ok(()) } diff --git a/crates/torii/grpc/src/server.rs b/crates/torii/grpc/src/server.rs index 28f0a10cc4..02bd68f728 100644 --- a/crates/torii/grpc/src/server.rs +++ b/crates/torii/grpc/src/server.rs @@ -55,11 +55,14 @@ impl World for DojoWorld { } } -pub async fn start(pool: Pool) -> Result<(), Box> { - let addr = "[::1]:50051".parse()?; - let world = DojoWorld::new(pool); +pub async fn start( + host: &String, + port: u16, + pool: &Pool, +) -> Result<(), Box> { + let addr = format!("{}:{}", host, port).parse()?; + let world = DojoWorld::new(pool.clone()); Server::builder().add_service(WorldServer::new(world)).serve(addr).await?; - Ok(()) } diff --git a/crates/torii/server/src/cli.rs b/crates/torii/server/src/cli.rs index bb3737caff..55e45402af 100644 --- a/crates/torii/server/src/cli.rs +++ b/crates/torii/server/src/cli.rs @@ -47,6 +47,15 @@ struct Args { /// Specify a block to start indexing from, ignored if stored head exists #[arg(short, long, default_value = "0")] start_block: u64, + /// Host address for GraphQL/gRPC endpoints + #[arg(long, default_value = "0.0.0.0")] + host: String, + /// Port number for GraphQL endpoint + #[arg(long, default_value = "8080")] + graphql_port: u16, + /// Port number for gRPC endpoint + #[arg(long, default_value = "50051")] + grpc_port: u16, } #[tokio::main] @@ -96,8 +105,8 @@ async fn main() -> anyhow::Result<()> { let indexer = Indexer::new(&state, &provider, processors, manifest, world_address, args.start_block); - let graphql = torii_graphql::server::start(pool.clone()); - let grpc = torii_grpc::server::start(pool); + let graphql = torii_graphql::server::start(&args.host, args.graphql_port, &pool); + let grpc = torii_grpc::server::start(&args.host, args.grpc_port, &pool); tokio::select! { res = indexer.start() => { From 825b9dcadcfcc72e14f7e48b6f09f599b07f58a6 Mon Sep 17 00:00:00 2001 From: Loaf <90423308+ponderingdemocritus@users.noreply.github.com> Date: Wed, 13 Sep 2023 16:43:42 +1000 Subject: [PATCH 42/77] remove packages (#888) --- packages/core/.gitignore | 2 - packages/core/.npmignore | 6 - packages/core/bin/generateComponents.cjs | 70 - packages/core/changelog.md | 0 packages/core/global.d.ts | 1 - packages/core/jest.config.js | 5 - packages/core/package.json | 26 - packages/core/readme.md | 11 - packages/core/src/constants/abi.json | 335 -- packages/core/src/constants/index.ts | 10 - packages/core/src/index.ts | 3 - packages/core/src/provider/RPCProvider.ts | 152 - packages/core/src/provider/index.ts | 1 - packages/core/src/provider/provider.ts | 52 - .../src/provider/tests/RPCProvider.test.ts | 28 - packages/core/src/types/index.ts | 61 - packages/core/src/utils/index.ts | 56 - packages/core/tsconfig.json | 29 - packages/core/yarn.lock | 2367 ------------ packages/react/.gitignore | 2 - packages/react/package.json | 37 - packages/react/src/index.ts | 3 - packages/react/src/useComponentValue.ts | 50 - packages/react/src/useEntityQuery.ts | 39 - packages/react/src/useObservableValue.ts | 19 - packages/react/src/utils/useDeepMemo.ts | 17 - packages/react/tsconfig.json | 20 - packages/react/yarn.lock | 3426 ----------------- packages/readme.md | 25 - 29 files changed, 6853 deletions(-) delete mode 100644 packages/core/.gitignore delete mode 100644 packages/core/.npmignore delete mode 100644 packages/core/bin/generateComponents.cjs delete mode 100644 packages/core/changelog.md delete mode 100644 packages/core/global.d.ts delete mode 100644 packages/core/jest.config.js delete mode 100644 packages/core/package.json delete mode 100644 packages/core/readme.md delete mode 100644 packages/core/src/constants/abi.json delete mode 100644 packages/core/src/constants/index.ts delete mode 100644 packages/core/src/index.ts delete mode 100644 packages/core/src/provider/RPCProvider.ts delete mode 100644 packages/core/src/provider/index.ts delete mode 100644 packages/core/src/provider/provider.ts delete mode 100644 packages/core/src/provider/tests/RPCProvider.test.ts delete mode 100644 packages/core/src/types/index.ts delete mode 100644 packages/core/src/utils/index.ts delete mode 100644 packages/core/tsconfig.json delete mode 100644 packages/core/yarn.lock delete mode 100644 packages/react/.gitignore delete mode 100644 packages/react/package.json delete mode 100644 packages/react/src/index.ts delete mode 100644 packages/react/src/useComponentValue.ts delete mode 100644 packages/react/src/useEntityQuery.ts delete mode 100644 packages/react/src/useObservableValue.ts delete mode 100644 packages/react/src/utils/useDeepMemo.ts delete mode 100644 packages/react/tsconfig.json delete mode 100644 packages/react/yarn.lock delete mode 100644 packages/readme.md diff --git a/packages/core/.gitignore b/packages/core/.gitignore deleted file mode 100644 index 76add878f8..0000000000 --- a/packages/core/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -node_modules -dist \ No newline at end of file diff --git a/packages/core/.npmignore b/packages/core/.npmignore deleted file mode 100644 index e30ed30a9d..0000000000 --- a/packages/core/.npmignore +++ /dev/null @@ -1,6 +0,0 @@ -* - -!dist/** -!package.json -!readme.md -!changelog.md \ No newline at end of file diff --git a/packages/core/bin/generateComponents.cjs b/packages/core/bin/generateComponents.cjs deleted file mode 100644 index e78e369606..0000000000 --- a/packages/core/bin/generateComponents.cjs +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env node - -const fs = require("fs"); -const path = require("path"); - -// Check for the required arguments -if (process.argv.length !== 4) { - console.log("Usage: - - - - - \ No newline at end of file + + + + + + + + + + + + diff --git a/crates/torii/client/wasm/index.js b/crates/torii/client/wasm/index.js index 62419c9f8b..af79a2b9ff 100644 --- a/crates/torii/client/wasm/index.js +++ b/crates/torii/client/wasm/index.js @@ -1,8 +1,3 @@ -// We only need `startup` here which is the main entry point -// In theory, we could also use all other functions/struct types from Rust which we have bound with -// `#[wasm_bindgen]` -const { setup } = wasm_bindgen; - async function run_wasm() { // Load the wasm file by awaiting the Promise returned by `wasm_bindgen` // `wasm_bindgen` was imported in `index.html` @@ -10,9 +5,9 @@ async function run_wasm() { console.log("index.js loaded"); - const syncWorker = new Worker("./worker.js"); + const clientWorker = new Worker("./worker.js"); - syncWorker.onmessage = function (e) { + clientWorker.onmessage = function (e) { const event = e.data.type; const data = e.data.data; @@ -31,30 +26,18 @@ async function run_wasm() { }; setTimeout(() => { - // Add the entity to sync - syncWorker.postMessage({ - type: "addEntityToSync", - data: { - model: "Position", - keys: [ - "0x517ececd29116499f4a1b64b094da79ba08dfd54a3edaa316134c41f8160973", - ], - }, - }); - setInterval(() => { // Get the entity values from the sync worker - syncWorker.postMessage({ + clientWorker.postMessage({ type: "getModelValue", data: { model: "Position", keys: [ "0x517ececd29116499f4a1b64b094da79ba08dfd54a3edaa316134c41f8160973", ], - length: 2, }, }); - }, 1000); + }, 2000); }, 1000); } diff --git a/crates/torii/client/wasm/rust-toolchain.toml b/crates/torii/client/wasm/rust-toolchain.toml new file mode 100644 index 0000000000..5d56faf9ae --- /dev/null +++ b/crates/torii/client/wasm/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "nightly" diff --git a/crates/torii/client/wasm/src/lib.rs b/crates/torii/client/wasm/src/lib.rs index 15a4fea46f..bbc4f251f5 100644 --- a/crates/torii/client/wasm/src/lib.rs +++ b/crates/torii/client/wasm/src/lib.rs @@ -1,92 +1,88 @@ -use std::sync::Arc; +use std::str::FromStr; -use async_std::sync::RwLock as AsyncRwLock; use starknet::core::types::FieldElement; -use starknet::core::utils::cairo_short_string_to_felt; -use starknet::providers::jsonrpc::HttpTransport; -use starknet::providers::JsonRpcClient; -use torii_client::provider::jsonrpc::JsonRpcProvider; -use torii_client::storage::EntityStorage; -use torii_client::sync::{self, Client, Entity}; -use url::Url; use wasm_bindgen::prelude::*; -mod storage; +type JsFieldElement = JsValue; +type JsEntityComponent = JsValue; -use storage::InMemoryStorage; - -/// A type wrapper to expose the client to WASM. #[wasm_bindgen] -pub struct WasmClient(Client>); +extern "C" { + #[wasm_bindgen(js_namespace = console)] + fn log(s: &str); +} #[wasm_bindgen] -impl WasmClient { - /// Create an instance of the client. This will create an instance of the client - /// without any entity models to sync. - /// - /// # Arguments - /// * `url` - The url of the Starknet JSON-RPC provider. - /// * `world_address` - The address of the World contract to sync with. - #[wasm_bindgen(constructor)] - pub fn new(url: &str, world_address: &str) -> Self { - let world_address = FieldElement::from_hex_be(world_address).unwrap(); - - let storage = Arc::new(AsyncRwLock::new(InMemoryStorage::new())); - let provider = JsonRpcProvider::new( - JsonRpcClient::new(HttpTransport::new(Url::parse(url).unwrap())), - world_address, - ); - - Self(sync::Client::new(storage, provider, vec![])) - } - - /// Start the syncing loop. - pub async fn start(&self) -> Result<(), JsValue> { - console_error_panic_hook::set_once(); - self.0.start().await.map_err(|e| JsValue::from_str(&e.to_string())) - } +pub struct Client(torii_client::client::Client); - /// Returns the model values of the requested entity keys. +#[wasm_bindgen] +impl Client { + /// Returns the model values of the requested entity. #[wasm_bindgen(js_name = getModelValue)] pub async fn get_model_value( &self, model: &str, - keys: JsValue, - length: usize, - ) -> Result { + keys: Vec, + ) -> Result, JsValue> { console_error_panic_hook::set_once(); - let keys = serde_wasm_bindgen::from_value::>(keys)?; - let values = self - .0 - .storage() - .read() - .await - .get( - cairo_short_string_to_felt(model) - .map_err(|e| JsValue::from_str(&e.to_string()))?, - keys, - length, - ) - .await - .map_err(|e| JsValue::from_str(&e.to_string()))?; + let keys = keys + .into_iter() + .map(serde_wasm_bindgen::from_value::) + .collect::, _>>() + .map_err(|err| { + JsValue::from_str(format!("failed to parse entity keys: {err}").as_str()) + })?; - Ok(serde_wasm_bindgen::to_value(&values)?) + match self.0.entity(model.to_string(), keys) { + Some(values) => Ok(Some(serde_wasm_bindgen::to_value(&values)?)), + None => Ok(None), + } } /// Add a new entity to be synced by the client. #[wasm_bindgen(js_name = addEntityToSync)] - pub fn add_entity_to_sync(&self, entity: JsValue) -> Result<(), JsValue> { + pub fn add_entities_to_sync(&self, entities: Vec) -> Result<(), JsValue> { console_error_panic_hook::set_once(); - let entity = serde_wasm_bindgen::from_value::(entity)?; - self.0.sync_entities.write().insert(entity); - Ok(()) + let _entities = entities + .into_iter() + .map(serde_wasm_bindgen::from_value::) + .collect::, _>>()?; + unimplemented!("add_entity_to_sync"); } /// Returns the list of entities that are currently being synced. #[wasm_bindgen(getter, js_name = syncedEntities)] pub fn synced_entities(&self) -> Result { console_error_panic_hook::set_once(); - Ok(serde_wasm_bindgen::to_value(&self.0.sync_entities.read().iter().collect::>())?) + let entities = self.0.synced_entities(); + serde_wasm_bindgen::to_value(&entities).map_err(|e| e.into()) } } + +#[wasm_bindgen] +pub async fn spawn_client( + torii_url: &str, + rpc_url: &str, + world_address: &str, + initial_entities_to_sync: Vec, +) -> Result { + console_error_panic_hook::set_once(); + + let entities = initial_entities_to_sync + .into_iter() + .map(serde_wasm_bindgen::from_value::) + .collect::, _>>()?; + + let world_address = FieldElement::from_str(world_address).map_err(|err| { + JsValue::from_str(format!("failed to parse World address: {err}").as_str()) + })?; + + let client = torii_client::client::ClientBuilder::new() + .set_entities_to_sync(entities) + .build(torii_url.into(), rpc_url.into(), world_address) + .await + .map_err(|err| JsValue::from_str(format!("failed to build client: {err}").as_str()))?; + + Ok(Client(client)) +} diff --git a/crates/torii/client/wasm/src/storage.rs b/crates/torii/client/wasm/src/storage.rs deleted file mode 100644 index df87cb387c..0000000000 --- a/crates/torii/client/wasm/src/storage.rs +++ /dev/null @@ -1,58 +0,0 @@ -use std::collections::HashMap; - -use async_trait::async_trait; -use serde::{Deserialize, Serialize}; -use starknet::core::types::FieldElement; -use torii_client::storage::{model_storage_base_address, EntityStorage}; - -/// Simple in memory implementation of [EntityStorage] -#[derive(Serialize, Deserialize)] -pub struct InMemoryStorage { - /// storage key -> Model value - pub inner: HashMap, -} - -impl InMemoryStorage { - pub fn new() -> Self { - Self { inner: HashMap::new() } - } -} - -#[derive(Debug, thiserror::Error)] -pub enum InMemoryStorageError {} - -// Example implementation of [EntityStorage] -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -impl EntityStorage for InMemoryStorage { - type Error = InMemoryStorageError; - - async fn set( - &mut self, - model: FieldElement, - keys: Vec, - values: Vec, - ) -> Result<(), Self::Error> { - let base_address = model_storage_base_address(model, &keys); - for (offset, value) in values.into_iter().enumerate() { - self.inner.insert(base_address + offset.into(), value); - } - Ok(()) - } - - async fn get( - &self, - model: FieldElement, - keys: Vec, - length: usize, - ) -> Result, Self::Error> { - let base_address = model_storage_base_address(model, &keys); - let mut values = Vec::with_capacity(length); - for i in 0..length { - let address = base_address + i.into(); - let value = self.inner.get(&address).cloned(); - values.push(value.unwrap_or(FieldElement::ZERO)); - } - Ok(values) - } -} diff --git a/crates/torii/client/wasm/worker.js b/crates/torii/client/wasm/worker.js index 5beb1b6af0..737cd8f34f 100644 --- a/crates/torii/client/wasm/worker.js +++ b/crates/torii/client/wasm/worker.js @@ -3,42 +3,53 @@ // available which we need to initialize our Wasm code. importScripts("./pkg/torii_client_wasm.js"); -console.log("Initializing worker"); +console.log("Initializing client worker..."); // In the worker, we have a different struct that we want to use as in // `index.js`. -const { WasmClient } = wasm_bindgen; +const { spawn_client } = wasm_bindgen; async function setup() { // Load the wasm file by awaiting the Promise returned by `wasm_bindgen`. await wasm_bindgen("./pkg/torii_client_wasm_bg.wasm"); - const client = new WasmClient( - "http://localhost:5050", - "0x398c6b4f479e2a6181ae895ad34333b44e419e48098d2a9622f976216d044dd" - ); + try { + const client = await spawn_client( + "http://localhost:8080/grpc", + "http://localhost:5050", + "0x2430f23de0cd9a957e1beb7aa8ef2db2af872cc7bb3058b9be833111d5518f5", + [ + { + model: "Position", + keys: [ + "0x517ececd29116499f4a1b64b094da79ba08dfd54a3edaa316134c41f8160973", + ], + }, + ] + ); - client.start(); + // setup the message handler for the worker + self.onmessage = function (e) { + const event = e.data.type; + const data = e.data.data; - // setup the message handler for the worker - self.onmessage = function (e) { - const event = e.data.type; - const data = e.data.data; - - if (event === "getModelValue") { - getModelValueHandler(client, data); - } else if (event === "addEntityToSync") { - addEntityToSyncHandler(client, data); - } else { - console.log("Sync Worker: Unknown event type", event); - } - }; + if (event === "getModelValue") { + getModelValueHandler(client, data); + } else if (event === "addEntityToSync") { + addEntityToSyncHandler(client, data); + } else { + console.log("Sync Worker: Unknown event type", event); + } + }; + } catch (e) { + console.error("error spawning client: ", e); + } } -function addEntityToSyncHandler(client, data) { - console.log("Sync Worker | Adding new entity to sync | data: ", data); - client.addEntityToSync(data); -} +// function addEntityToSyncHandler(client, data) { +// console.log("Sync Worker | Adding new entity to sync | data: ", data); +// client.addEntityToSync(data); +// } /// Handler for the `get_entity` event from the main thread. /// Returns back the entity data to the main thread via `postMessage`. @@ -47,9 +58,8 @@ async function getModelValueHandler(client, data) { const model = data.model; const keys = data.keys; - const length = data.length; - const values = await client.getModelValue(model, keys, length); + const values = await client.getModelValue(model, keys); self.postMessage({ type: "getModelValue", diff --git a/crates/torii/core/Cargo.toml b/crates/torii/core/Cargo.toml index 166ea4d4ea..12591f12f5 100644 --- a/crates/torii/core/Cargo.toml +++ b/crates/torii/core/Cargo.toml @@ -27,9 +27,9 @@ slab = "0.4.2" sqlx = { version = "0.6.2", features = [ "chrono", "macros", "offline", "runtime-actix-rustls", "sqlite", "uuid" ] } starknet-crypto.workspace = true starknet.workspace = true +tokio = { version = "1.32.0", features = [ "sync" ], default-features = true } tokio-stream = "0.1.11" tokio-util = "0.7.7" -tokio.workspace = true torii-client = { path = "../client" } tracing.workspace = true diff --git a/crates/torii/core/src/engine.rs b/crates/torii/core/src/engine.rs index 398b17902a..5207b8b130 100644 --- a/crates/torii/core/src/engine.rs +++ b/crates/torii/core/src/engine.rs @@ -7,6 +7,7 @@ use starknet::core::types::{ }; use starknet::core::utils::get_selector_from_name; use starknet::providers::Provider; +use tokio::sync::mpsc::Sender as BoundedSender; use tokio::time::sleep; use tokio_util::sync::CancellationToken; use torii_client::contract::world::WorldContractReader; @@ -48,6 +49,7 @@ where provider: &'a P, processors: Processors

, config: EngineConfig, + block_sender: Option>, } impl<'a, P: Provider + Sync> Engine<'a, P> @@ -60,8 +62,9 @@ where provider: &'a P, processors: Processors

, config: EngineConfig, + block_sender: Option>, ) -> Self { - Self { world, db, provider, processors, config } + Self { world, db, provider, processors, config, block_sender } } pub async fn start(&self, cts: CancellationToken) -> Result<(), Box> { @@ -113,6 +116,11 @@ where } }; + // send the current block number + if let Some(ref block_sender) = self.block_sender { + block_sender.send(from).await.expect("failed to send block number to gRPC server"); + } + self.process(block_with_txs).await?; self.db.set_head(from).await?; diff --git a/crates/torii/core/src/simple_broker.rs b/crates/torii/core/src/simple_broker.rs index 4615137fda..d254c3008b 100644 --- a/crates/torii/core/src/simple_broker.rs +++ b/crates/torii/core/src/simple_broker.rs @@ -12,7 +12,7 @@ use slab::Slab; static SUBSCRIBERS: Lazy>>> = Lazy::new(Default::default); -struct Senders(Slab>); +pub struct Senders(pub Slab>); struct BrokerStream(usize, UnboundedReceiver); @@ -63,4 +63,13 @@ impl SimpleBroker { BrokerStream(id, rx) }) } + + /// Execute the given function with the _subscribers_ of the specified subscription type. + pub fn with_subscribers(f: F) -> R + where + T: Sync + Send + Clone + 'static, + F: FnOnce(&mut Senders) -> R, + { + with_senders(f) + } } diff --git a/crates/torii/core/src/sql.rs b/crates/torii/core/src/sql.rs index 144dcdc09d..768b9f67be 100644 --- a/crates/torii/core/src/sql.rs +++ b/crates/torii/core/src/sql.rs @@ -220,7 +220,7 @@ impl Sql { // keys are part of model members, so combine keys and model values array let mut member_values: Vec = Vec::new(); - member_values.extend(keys); + member_values.extend(keys.clone()); member_values.extend(values); let insert_models: Vec<_> = primitive_members diff --git a/crates/torii/graphql/src/tests/mod.rs b/crates/torii/graphql/src/tests/mod.rs index e5ed72f2c5..37b23363a1 100644 --- a/crates/torii/graphql/src/tests/mod.rs +++ b/crates/torii/graphql/src/tests/mod.rs @@ -117,6 +117,7 @@ pub async fn bootstrap_engine<'a>( ..Processors::default() }, EngineConfig::default(), + None, ); let _ = engine.sync_to_head(0).await?; diff --git a/crates/torii/grpc/Cargo.toml b/crates/torii/grpc/Cargo.toml index 7c86677292..9b3dbc8030 100644 --- a/crates/torii/grpc/Cargo.toml +++ b/crates/torii/grpc/Cargo.toml @@ -6,10 +6,41 @@ repository.workspace = true version.workspace = true [dependencies] +anyhow.workspace = true +bytes = "1.0" +dojo-types = { path = "../../dojo-types" } +futures.workspace = true +parking_lot.workspace = true +rayon.workspace = true +starknet-crypto.workspace = true +starknet.workspace = true +thiserror.workspace = true + +# server +hyper = "0.14.27" +tonic-web.workspace = true +tower = "0.4.13" +tracing.workspace = true + +[target.'cfg(target_arch = "wasm32")'.dependencies] +tonic-web-wasm-client.workspace = true +wasm-prost.workspace = true +wasm-tonic.workspace = true + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +futures-util = "0.3.28" prost.workspace = true sqlx = { version = "0.6.2", features = [ "chrono", "macros", "offline", "runtime-actix-rustls", "sqlite", "uuid" ] } +tokio-stream = "0.1.14" +tokio.workspace = true tonic.workspace = true +url.workspace = true warp.workspace = true [build-dependencies] tonic-build.workspace = true +wasm-tonic-build.workspace = true + +[features] +client = [ ] +server = [ ] # this feature can't be build on wasm32 diff --git a/crates/torii/grpc/build.rs b/crates/torii/grpc/build.rs index 97ef8bcd78..095d64820b 100644 --- a/crates/torii/grpc/build.rs +++ b/crates/torii/grpc/build.rs @@ -1,4 +1,22 @@ fn main() -> Result<(), Box> { - tonic_build::compile_protos("proto/world.proto")?; + let target = std::env::var("TARGET").expect("failed to get TARGET environment variable"); + let feature_client = std::env::var("CARGO_FEATURE_CLIENT"); + let feature_server = std::env::var("CARGO_FEATURE_SERVER"); + + if target.contains("wasm32") { + if feature_server.is_ok() { + panic!("feature `server` is not supported on target `{}`", target); + } + + wasm_tonic_build::configure() + .build_server(false) + .build_client(feature_client.is_ok()) + .compile(&["proto/world.proto"], &["proto"])?; + } else { + tonic_build::configure() + .build_server(feature_server.is_ok()) + .build_client(feature_client.is_ok()) + .compile(&["proto/world.proto"], &["proto"])?; + } Ok(()) } diff --git a/crates/torii/grpc/proto/types.proto b/crates/torii/grpc/proto/types.proto new file mode 100644 index 0000000000..57827fd96f --- /dev/null +++ b/crates/torii/grpc/proto/types.proto @@ -0,0 +1,76 @@ +syntax = "proto3"; +package types; + +message WorldMetadata { + // The hex-encoded address of the world. + string world_address = 1; + // The hex-encoded class hash of the world. + string world_class_hash = 2; + // The hex-encoded address of the executor. + string executor_address = 3; + // The hex-encoded class hash of the executor. + string executor_class_hash = 4; + // A list of metadata for all registered components in the world. + repeated ModelMetadata models = 5; + // A list of metadata for all registered systems in the world. + repeated SystemMetadata systems = 6; +} + +message SystemMetadata { + // System name + string name = 1; + // hex-encoded class hash of the system + string class_hash = 2; +} + +message ModelMetadata { + // Model name + string name = 1; + // Model size + uint32 size = 2; + // hex-encoded class hash of the component + string class_hash = 3; +} + +/// Represents a component for a given entity. +message EntityModel { + /// Model name + string model = 1; + /// Entity keys + repeated string keys = 2; +} + +message StorageEntry { + // The key of the changed value + string key = 1; + // The new value applied to the given address + string value = 2; +} + +message StorageDiff { + // The contract address for which the storage changed + string address = 1; + // The changes in the storage of the contract + repeated StorageEntry storage_entries = 2; +} + +message EntityDiff { + // Storage diffs + repeated StorageDiff storage_diffs = 1; +} + +message EntityUpdate { + string block_hash = 1; + EntityDiff entity_diff = 2; +} + +message PendingEntityUpdate { + EntityDiff entity_diff = 1; +} + +message MaybePendingEntityUpdate { + oneof update { + EntityUpdate entity_update = 1; + PendingEntityUpdate pending_entity_update = 2; + } +} \ No newline at end of file diff --git a/crates/torii/grpc/proto/world.proto b/crates/torii/grpc/proto/world.proto index 720a66d6d8..a170ddaf59 100644 --- a/crates/torii/grpc/proto/world.proto +++ b/crates/torii/grpc/proto/world.proto @@ -1,26 +1,55 @@ syntax = "proto3"; package world; +import "types.proto"; + // The World service provides information about the world. service World { - // Retrieves metadata about the world. - rpc Meta (MetaRequest) returns (MetaReply); + // Retrieves metadata about the World including all the registered components and systems. + rpc WorldMetadata (MetadataRequest) returns (MetadataResponse); + + // rpc ComponentMetadata () returns (); + // rpc SystemMetadata () returns (); + + // Retrieve the component values of the requested entity. + rpc GetEntity (GetEntityRequest) returns (GetEntityResponse); + + /* + * Subscribes to entity updates. + * Bidirectional streaming as we want to allow user to change the list of entities to subscribe to without closing the connection. + */ + rpc SubscribeEntities (SubscribeEntitiesRequest) returns (stream SubscribeEntitiesResponse); } + // A request to retrieve metadata for a specific world ID. -message MetaRequest { - // The address of the world. - string id = 1; +message MetadataRequest { + } // The metadata response contains addresses and class hashes for the world. -message MetaReply { - // The hex-encoded address of the world. - string world_address = 1; - // The hex-encoded class hash of the world. - string world_class_hash = 2; - // The hex-encoded address of the executor. - string executor_address = 3; - // The hex-encoded class hash of the executor. - string executor_class_hash = 4; +message MetadataResponse { + types.WorldMetadata metadata = 1; +} + +// A request to retrieve a component value of an entity. +message GetEntityRequest { + types.EntityModel entity = 1; +} + +// The entity response contains the component values for the requested entities. +message GetEntityResponse { + repeated string values = 1; +} + +message SubscribeEntitiesRequest { + // The address of the World whose entities to subscribe to. + string world = 1; + // The list of entities to subscribe to. + repeated types.EntityModel entities = 2; +} + +message SubscribeEntitiesResponse { + // List of entities that have been updated. + types.MaybePendingEntityUpdate entity_update = 1; } diff --git a/crates/torii/grpc/src/client.rs b/crates/torii/grpc/src/client.rs new file mode 100644 index 0000000000..3c9b4052bf --- /dev/null +++ b/crates/torii/grpc/src/client.rs @@ -0,0 +1,112 @@ +//! Client implementation for the gRPC service. + +use std::str::FromStr; + +use protos::world::{world_client, SubscribeEntitiesRequest}; +use starknet::core::types::FromStrError; +use starknet_crypto::FieldElement; + +use crate::protos::world::{GetEntityRequest, MetadataRequest, SubscribeEntitiesResponse}; +use crate::protos::{self, types}; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error(transparent)] + Grpc(tonic::Status), + #[error("Missing expected data")] + MissingExpectedData, + #[error(transparent)] + Parsing(FromStrError), + + #[cfg(not(target_arch = "wasm32"))] + #[error(transparent)] + Transport(tonic::transport::Error), +} + +/// A lightweight wrapper around the grpc client. +pub struct WorldClient { + world_address: FieldElement, + #[cfg(not(target_arch = "wasm32"))] + inner: world_client::WorldClient, + #[cfg(target_arch = "wasm32")] + inner: world_client::WorldClient, +} + +impl WorldClient { + #[cfg(not(target_arch = "wasm32"))] + pub async fn new(dst: D, world_address: FieldElement) -> Result + where + D: TryInto, + D::Error: Into>, + { + Ok(Self { + world_address, + inner: world_client::WorldClient::connect(dst).await.map_err(Error::Transport)?, + }) + } + + // we make this function async so that we can keep the function signature similar + #[cfg(target_arch = "wasm32")] + pub async fn new(endpoint: String, world_address: FieldElement) -> Result { + Ok(Self { + world_address, + inner: world_client::WorldClient::new(tonic_web_wasm_client::Client::new(endpoint)), + }) + } + + /// Retrieve the metadata of the World. + pub async fn metadata(&mut self) -> Result { + self.inner + .world_metadata(MetadataRequest {}) + .await + .map_err(Error::Grpc) + .and_then(|res| res.into_inner().metadata.ok_or(Error::MissingExpectedData)) + .and_then(|metadata| metadata.try_into().map_err(Error::Parsing)) + } + + /// Retrieves the latest model value of the requested entity keys + pub async fn get_entity( + &mut self, + model: String, + keys: Vec, + ) -> Result, Error> { + let values = self + .inner + .get_entity(GetEntityRequest { + entity: Some(types::EntityModel { + model, + keys: keys.into_iter().map(|k| format!("{k:#x}")).collect(), + }), + }) + .await + .map(|res| res.into_inner().values) + .map_err(Error::Grpc)?; + + values + .iter() + .map(|v| FieldElement::from_str(v)) + .collect::, _>>() + .map_err(Error::Parsing) + } + + /// Subscribe to the state diff for a set of entities of a World. + pub async fn subscribe_entities( + &mut self, + entities: Vec, + ) -> Result, Error> { + self.inner + .subscribe_entities(SubscribeEntitiesRequest { + entities: entities + .into_iter() + .map(|e| protos::types::EntityModel { + model: e.model, + keys: e.keys.into_iter().map(|felt| format!("{felt:#x}")).collect(), + }) + .collect(), + world: format!("{:#x}", self.world_address), + }) + .await + .map_err(Error::Grpc) + .map(|res| res.into_inner()) + } +} diff --git a/crates/torii/grpc/src/conversion.rs b/crates/torii/grpc/src/conversion.rs new file mode 100644 index 0000000000..31d9884443 --- /dev/null +++ b/crates/torii/grpc/src/conversion.rs @@ -0,0 +1,60 @@ +use std::collections::HashMap; +use std::str::FromStr; + +use starknet::core::types::FromStrError; +use starknet_crypto::FieldElement; + +use crate::protos; + +impl TryFrom for dojo_types::model::ModelMetadata { + type Error = FromStrError; + fn try_from(value: protos::types::ModelMetadata) -> Result { + Ok(Self { + name: value.name, + size: value.size, + class_hash: FieldElement::from_str(&value.class_hash)?, + }) + } +} + +impl TryFrom for dojo_types::system::SystemMetadata { + type Error = FromStrError; + fn try_from(value: protos::types::SystemMetadata) -> Result { + Ok(Self { name: value.name, class_hash: FieldElement::from_str(&value.class_hash)? }) + } +} + +impl TryFrom for dojo_types::WorldMetadata { + type Error = FromStrError; + fn try_from(value: protos::types::WorldMetadata) -> Result { + let components = value + .models + .into_iter() + .map(|component| Ok((component.name.clone(), component.try_into()?))) + .collect::, _>>()?; + + let systems = value + .systems + .into_iter() + .map(|system| Ok((system.name.clone(), system.try_into()?))) + .collect::, _>>()?; + + Ok(dojo_types::WorldMetadata { + systems, + components, + world_address: FieldElement::from_str(&value.world_address)?, + world_class_hash: FieldElement::from_str(&value.world_class_hash)?, + executor_address: FieldElement::from_str(&value.executor_address)?, + executor_class_hash: FieldElement::from_str(&value.executor_class_hash)?, + }) + } +} + +impl From for protos::types::EntityModel { + fn from(value: dojo_types::model::EntityModel) -> Self { + Self { + model: value.model, + keys: value.keys.into_iter().map(|key| format!("{key:#}")).collect(), + } + } +} diff --git a/crates/torii/grpc/src/lib.rs b/crates/torii/grpc/src/lib.rs index 1b956ac6ee..56afad464c 100644 --- a/crates/torii/grpc/src/lib.rs +++ b/crates/torii/grpc/src/lib.rs @@ -1,55 +1,21 @@ -use sqlx::{Pool, Sqlite}; -use tonic::{Request, Response, Status}; -use world::world_server::World; -use world::{MetaReply, MetaRequest}; +#[cfg(target_arch = "wasm32")] +extern crate wasm_prost as prost; +#[cfg(target_arch = "wasm32")] +extern crate wasm_tonic as tonic; -pub mod world { - tonic::include_proto!("world"); -} - -#[derive(Clone, Debug)] -pub struct DojoWorld { - pool: Pool, -} - -impl DojoWorld { - pub fn new(pool: Pool) -> Self { - Self { pool } - } -} +pub mod conversion; -#[tonic::async_trait] -impl World for DojoWorld { - async fn meta( - &self, - request: Request, // Accept request of type MetaRequest - ) -> Result, Status> { - let id = request.into_inner().id; +#[cfg(feature = "client")] +pub mod client; - let (world_address, world_class_hash, executor_address, executor_class_hash): ( - String, - String, - String, - String, - ) = sqlx::query_as( - "SELECT world_address, world_class_hash, executor_address, executor_class_hash FROM \ - worlds WHERE id = ?", - ) - .bind(id) - .fetch_one(&self.pool) - .await - .map_err(|e| match e { - sqlx::Error::RowNotFound => Status::not_found("World not found"), - _ => Status::internal("Internal error"), - })?; +#[cfg(feature = "server")] +pub mod server; - let reply = world::MetaReply { - world_address, - world_class_hash, - executor_address, - executor_class_hash, - }; - - Ok(Response::new(reply)) // Send back our formatted greeting +pub mod protos { + pub mod world { + tonic::include_proto!("world"); + } + pub mod types { + tonic::include_proto!("types"); } } diff --git a/crates/torii/grpc/src/server/error.rs b/crates/torii/grpc/src/server/error.rs new file mode 100644 index 0000000000..747dc837c5 --- /dev/null +++ b/crates/torii/grpc/src/server/error.rs @@ -0,0 +1,25 @@ +use starknet::core::types::FromStrError; +use starknet::core::utils::CairoShortStringToFeltError; +use starknet::providers::jsonrpc::HttpTransport; +use starknet::providers::{JsonRpcClient, Provider}; + +type JsonRpcClientError = as Provider>::Error; +type ProviderError = starknet::providers::ProviderError; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("parsing error: {0}")] + Parse(#[from] ParseError), + #[error(transparent)] + Provider(#[from] ProviderError), + #[error(transparent)] + Sql(#[from] sqlx::Error), +} + +#[derive(Debug, thiserror::Error)] +pub enum ParseError { + #[error(transparent)] + FromStr(#[from] FromStrError), + #[error(transparent)] + CairoShortStringToFelt(#[from] CairoShortStringToFeltError), +} diff --git a/crates/torii/grpc/src/server/logger.rs b/crates/torii/grpc/src/server/logger.rs new file mode 100644 index 0000000000..093a5bb50d --- /dev/null +++ b/crates/torii/grpc/src/server/logger.rs @@ -0,0 +1,49 @@ +use std::task::{Context, Poll}; + +use hyper::Body; +use tonic::body::BoxBody; +use tower::{Layer, Service}; +use tracing::info; + +#[derive(Debug, Clone, Default)] +pub struct Logger { + inner: S, +} + +impl Layer for Logger { + type Service = Logger; + fn layer(&self, inner: S) -> Self::Service { + Logger { inner } + } +} + +impl Service> for Logger +where + S: Service, Response = hyper::Response> + Clone + Send + 'static, + S::Future: Send + 'static, +{ + type Response = S::Response; + type Error = S::Error; + type Future = futures::future::BoxFuture<'static, Result>; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.inner.poll_ready(cx) + } + + fn call(&mut self, req: hyper::Request) -> Self::Future { + // This is necessary because tonic internally uses `tower::buffer::Buffer`. + // See https://github.com/tower-rs/tower/issues/547#issuecomment-767629149 + // for details on why this is necessary + let clone = self.inner.clone(); + let mut inner = std::mem::replace(&mut self.inner, clone); + + Box::pin(async move { + // Do extra async work here... + let uri = req.uri().path(); + let method = req.method(); + + info!(target: "grpc", ?method, ?uri); + inner.call(req).await + }) + } +} diff --git a/crates/torii/grpc/src/server/mod.rs b/crates/torii/grpc/src/server/mod.rs new file mode 100644 index 0000000000..1ede07d180 --- /dev/null +++ b/crates/torii/grpc/src/server/mod.rs @@ -0,0 +1,255 @@ +pub mod error; +pub mod logger; +pub mod subscription; + +use std::pin::Pin; +use std::str::FromStr; +use std::sync::Arc; + +use futures::Stream; +use protos::world::{ + MetadataRequest, MetadataResponse, SubscribeEntitiesRequest, SubscribeEntitiesResponse, +}; +use sqlx::{Executor, Pool, Row, Sqlite}; +use starknet::core::types::FromStrError; +use starknet::core::utils::cairo_short_string_to_felt; +use starknet::providers::jsonrpc::HttpTransport; +use starknet::providers::JsonRpcClient; +use starknet_crypto::{poseidon_hash_many, FieldElement}; +use tokio::sync::mpsc::{Receiver, Sender}; +use tokio_stream::wrappers::ReceiverStream; +use tonic::{Request, Response, Status}; + +use self::error::Error; +use self::subscription::{EntityModelRequest, EntitySubscriptionService}; +use crate::protos::types::EntityModel; +use crate::protos::world::{GetEntityRequest, GetEntityResponse}; +use crate::protos::{self}; + +#[derive(Debug, Clone)] +pub struct DojoWorld { + world_address: FieldElement, + pool: Pool, + /// Sender<(subscription requests, oneshot sender to send back the response)> + subscription_req_sender: + Sender<(EntityModelRequest, Sender>)>, +} + +impl DojoWorld { + pub fn new( + pool: Pool, + block_rx: Receiver, + world_address: FieldElement, + provider: Arc>, + ) -> Self { + let (subscription_req_sender, rx) = tokio::sync::mpsc::channel(1); + // spawn thread for state update service + tokio::task::spawn(EntitySubscriptionService::new(provider, rx, block_rx)); + Self { pool, subscription_req_sender, world_address } + } +} + +impl DojoWorld { + pub async fn metadata(&self) -> Result { + let (world_address, world_class_hash, executor_address, executor_class_hash): ( + String, + String, + String, + String, + ) = sqlx::query_as(&format!( + "SELECT world_address, world_class_hash, executor_address, executor_class_hash FROM \ + worlds WHERE id = '{:#x}'", + self.world_address + )) + .fetch_one(&self.pool) + .await?; + + let models = sqlx::query_as( + "SELECT c.name, c.class_hash, COUNT(cm.id) FROM models c LEFT JOIN model_members cm \ + ON c.id = cm.model_id GROUP BY c.id", + ) + .fetch_all(&self.pool) + .await? + .into_iter() + .map(|(name, class_hash, size)| protos::types::ModelMetadata { name, class_hash, size }) + .collect::>(); + + let systems = sqlx::query_as("SELECT name, class_hash FROM systems") + .fetch_all(&self.pool) + .await? + .into_iter() + .map(|(name, class_hash)| protos::types::SystemMetadata { name, class_hash }) + .collect::>(); + + Ok(protos::types::WorldMetadata { + systems, + models, + world_address, + world_class_hash, + executor_address, + executor_class_hash, + }) + } + + #[allow(unused)] + pub async fn model_metadata( + &self, + component: String, + ) -> Result { + sqlx::query_as( + "SELECT c.name, c.class_hash, COUNT(cm.id) FROM models c LEFT JOIN model_members cm \ + ON c.id = cm.model_id WHERE c.id = ? GROUP BY c.id", + ) + .bind(component.to_lowercase()) + .fetch_one(&self.pool) + .await + .map(|(name, class_hash, size)| protos::types::ModelMetadata { name, size, class_hash }) + .map_err(Error::from) + } + + #[allow(unused)] + pub async fn system_metadata( + &self, + system: String, + ) -> Result { + sqlx::query_as("SELECT name, class_hash FROM systems WHERE id = ?") + .bind(system.to_lowercase()) + .fetch_one(&self.pool) + .await + .map(|(name, class_hash)| protos::types::SystemMetadata { name, class_hash }) + .map_err(Error::from) + } + + #[allow(unused)] + async fn entity( + &self, + component: String, + entity_keys: Vec, + ) -> Result, Error> { + let entity_id = format!("{:#x}", poseidon_hash_many(&entity_keys)); + // TODO: there's definitely a better way for doing this + self.pool + .fetch_one( + format!( + "SELECT * FROM external_{} WHERE entity_id = '{entity_id}'", + component.to_lowercase() + ) + .as_ref(), + ) + .await + .map_err(Error::from) + .map(|row| { + let size = row.columns().len() - 2; + let mut values = Vec::with_capacity(size); + for (i, _) in row.columns().iter().enumerate().skip(1).take(size) { + let value = match row.try_get::(i) { + Ok(value) => value, + Err(sqlx::Error::ColumnDecode { .. }) => { + row.try_get::(i).expect("decode failed").to_string() + } + Err(e) => panic!("{e}"), + }; + values.push(value); + } + values + }) + } +} + +type ServiceResult = Result, Status>; +type SubscribeEntitiesResponseStream = + Pin> + Send>>; + +#[tonic::async_trait] +impl protos::world::world_server::World for DojoWorld { + async fn world_metadata( + &self, + _request: Request, + ) -> Result, Status> { + let metadata = self.metadata().await.map_err(|e| match e { + Error::Sql(sqlx::Error::RowNotFound) => Status::not_found("World not found"), + e => Status::internal(e.to_string()), + })?; + + Ok(Response::new(MetadataResponse { metadata: Some(metadata) })) + } + + async fn get_entity( + &self, + request: Request, + ) -> Result, Status> { + let GetEntityRequest { entity } = request.into_inner(); + + let Some(EntityModel { model, keys }) = entity else { + return Err(Status::invalid_argument("Entity not specified")); + }; + + let entity_keys = keys + .iter() + .map(|k| FieldElement::from_str(k)) + .collect::, _>>() + .map_err(|e| Status::invalid_argument(format!("Invalid key: {e}")))?; + + let values = self.entity(model, entity_keys).await.map_err(|e| match e { + Error::Sql(sqlx::Error::RowNotFound) => Status::not_found("Entity not found"), + e => Status::internal(e.to_string()), + })?; + + Ok(Response::new(GetEntityResponse { values })) + } + + type SubscribeEntitiesStream = SubscribeEntitiesResponseStream; + + async fn subscribe_entities( + &self, + request: Request, + ) -> ServiceResult { + let SubscribeEntitiesRequest { entities: raw_entities, world } = request.into_inner(); + let (sender, rx) = tokio::sync::mpsc::channel(128); + + let world = FieldElement::from_str(&world) + .map_err(|e| Status::internal(format!("Invalid world address: {e}")))?; + + // in order to be able to compute all the storage address for all the requested entities, we + // need to know the size of the entity component. we can get this information from the + // sql database by querying the component metadata. + + let mut entities = Vec::with_capacity(raw_entities.len()); + for entity in raw_entities { + let keys = entity + .keys + .into_iter() + .map(|v| FieldElement::from_str(&v)) + .collect::, FromStrError>>() + .map_err(|e| Status::internal(format!("parsing error: {e}")))?; + + let model = cairo_short_string_to_felt(&entity.model) + .map_err(|e| Status::internal(format!("parsing error: {e}")))?; + + let (component_len,): (i64,) = + sqlx::query_as("SELECT COUNT(*) FROM model_members WHERE model_id = ?") + .bind(entity.model.to_lowercase()) + .fetch_one(&self.pool) + .await + .map_err(|e| match e { + sqlx::Error::RowNotFound => Status::not_found("Model not found"), + e => Status::internal(e.to_string()), + })?; + + entities.push(self::subscription::Entity { + model: self::subscription::ModelMetadata { + name: model, + len: component_len as usize, + }, + keys, + }) + } + + self.subscription_req_sender + .send((EntityModelRequest { world, entities }, sender)) + .await + .expect("should send subscriber request"); + + Ok(Response::new(Box::pin(ReceiverStream::new(rx)) as Self::SubscribeEntitiesStream)) + } +} diff --git a/crates/torii/grpc/src/server/subscription.rs b/crates/torii/grpc/src/server/subscription.rs new file mode 100644 index 0000000000..d6343d36c1 --- /dev/null +++ b/crates/torii/grpc/src/server/subscription.rs @@ -0,0 +1,286 @@ +//! TODO: move the subscription to a separate file + +use std::collections::{HashSet, VecDeque}; +use std::pin::Pin; +use std::sync::Arc; +use std::task::{Context, Poll}; + +use futures::Future; +use futures_util::FutureExt; +use protos::types::maybe_pending_entity_update::Update; +use protos::world::SubscribeEntitiesResponse; +use rayon::prelude::*; +use starknet::core::types::{BlockId, ContractStorageDiffItem, MaybePendingStateUpdate}; +use starknet::macros::short_string; +use starknet::providers::{Provider, ProviderError}; +use starknet_crypto::{poseidon_hash_many, FieldElement}; +use tokio::sync::mpsc::{Receiver, Sender}; +use tonic::Status; + +use crate::protos::{self}; + +type GetStateUpdateResult

= + Result::Error>>; +type StateUpdateFuture

= Pin> + Send>>; +type PublishStateUpdateFuture = Pin + Send>>; + +pub struct ModelMetadata { + pub name: FieldElement, + pub len: usize, +} + +pub struct Entity { + pub model: ModelMetadata, + pub keys: Vec, +} + +pub struct EntityModelRequest { + pub world: FieldElement, + pub entities: Vec, +} + +pub struct Subscriber { + /// The world address that the subscriber is interested in. + world: FieldElement, + /// The storage addresses that the subscriber is interested in. + storage_addresses: HashSet, + /// The channel to send the response back to the subscriber. + sender: Sender>, +} + +pub struct SubscriberManager { + /// (set of storage addresses they care about, sender channel to send back the response) + pub subscribers: Vec>, +} + +impl SubscriberManager { + pub fn new() -> Self { + Self { subscribers: Vec::default() } + } + + fn add_subscriber( + &mut self, + request: (EntityModelRequest, Sender>), + ) { + let (EntityModelRequest { world, entities }, sender) = request; + + // convert the list of entites into a list storage addresses + let storage_addresses = entities + .par_iter() + .map(|entity| { + let base = poseidon_hash_many(&[ + short_string!("dojo_storage"), + entity.model.name, + poseidon_hash_many(&entity.keys), + ]); + + (0..entity.model.len) + .into_par_iter() + .map(|i| base + i.into()) + .collect::>() + }) + .flatten() + .collect::>(); + + self.subscribers.push(Arc::new(Subscriber { world, storage_addresses, sender })) + } +} + +impl Default for SubscriberManager { + fn default() -> Self { + Self::new() + } +} + +/// a service which handles entity subscription requests. it is an endless future where it awaits +/// for new blocks, fetch its state update, and publish them to the subscribers. +pub struct EntitySubscriptionService { + /// A channel to communicate with the indexer engine, in order to receive the block number that + /// the indexer engine is processing at any moment. This way, we can sync with the indexer and + /// request the state update of the current block that the indexer is currently processing. + block_rx: Receiver, + /// The Starknet provider. + provider: Arc

, + /// A list of state update futures, each corresponding to a block number that was received from + /// the indexer engine. + state_update_req_futs: VecDeque<(u64, StateUpdateFuture

)>, + + publish_update_fut: Option, + + block_num_queue: Vec, + /// Receive subscribers from gRPC server. + /// This receives streams of (sender channel, list of entities to subscribe) tuple + subscriber_recv: + Receiver<(EntityModelRequest, Sender>)>, + + subscriber_manager: SubscriberManager, +} + +impl

EntitySubscriptionService

+where + P: Provider, +{ + pub fn new( + provider: P, + subscriber_recv: Receiver<( + EntityModelRequest, + Sender>, + )>, + block_rx: Receiver, + ) -> Self { + Self { + block_rx, + subscriber_recv, + provider: Arc::new(provider), + block_num_queue: Default::default(), + publish_update_fut: Default::default(), + state_update_req_futs: Default::default(), + subscriber_manager: SubscriberManager::new(), + } + } + + /// Process the fetched state update, and publish to the subscribers, the relevant values for + /// them. + async fn publish_state_updates_to_subscribers( + subscribers: Vec>, + state_update: MaybePendingStateUpdate, + ) { + let state_diff = match &state_update { + MaybePendingStateUpdate::PendingUpdate(update) => &update.state_diff, + MaybePendingStateUpdate::Update(update) => &update.state_diff, + }; + + // iterate over the list of subscribers, and construct the relevant state diffs for each + // subscriber + for sub in subscribers { + // if there is no state diff for the current world, then skip, otherwise, extract the + // state diffs of the world + let Some(ContractStorageDiffItem { storage_entries: diff_entries, .. }) = + state_diff.storage_diffs.iter().find(|d| d.address == sub.world) + else { + continue; + }; + + let relevant_storage_entries = diff_entries + .iter() + .filter(|entry| sub.storage_addresses.contains(&entry.key)) + .map(|entry| protos::types::StorageEntry { + key: format!("{:#x}", entry.key), + value: format!("{:#x}", entry.value), + }) + .collect::>(); + + // if there is no state diffs relevant to the current subscriber, then skip + if relevant_storage_entries.is_empty() { + continue; + } + + let response = SubscribeEntitiesResponse { + entity_update: Some(protos::types::MaybePendingEntityUpdate { + update: Some(match &state_update { + MaybePendingStateUpdate::PendingUpdate(_) => { + Update::PendingEntityUpdate(protos::types::PendingEntityUpdate { + entity_diff: Some(protos::types::EntityDiff { + storage_diffs: vec![protos::types::StorageDiff { + address: format!("{:#x}", sub.world), + storage_entries: relevant_storage_entries, + }], + }), + }) + } + + MaybePendingStateUpdate::Update(update) => { + Update::EntityUpdate(protos::types::EntityUpdate { + block_hash: format!("{:#x}", update.block_hash), + entity_diff: Some(protos::types::EntityDiff { + storage_diffs: vec![protos::types::StorageDiff { + address: format!("{:#x}", sub.world), + storage_entries: relevant_storage_entries, + }], + }), + }) + } + }), + }), + }; + + match sub.sender.send(Ok(response)).await { + Ok(_) => { + println!("state diff sent") + } + Err(e) => { + println!("stream closed: {e:?}"); + } + } + } + } + + async fn do_get_state_update(provider: Arc

, block_number: u64) -> GetStateUpdateResult

{ + provider.get_state_update(BlockId::Number(block_number)).await + } +} + +// an endless future which will receive the block number from the indexer engine, and will +// request its corresponding state update. +impl

Future for EntitySubscriptionService

+where + P: Provider + Send + Sync + Unpin + 'static, +{ + type Output = (); + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let pin = self.get_mut(); + + // drain the stream + while let Poll::Ready(Some(block_num)) = pin.block_rx.poll_recv(cx) { + // we still need to drain the stream, even if there are no subscribers. But dont have to + // queue for the block number + if !pin.subscriber_manager.subscribers.is_empty() { + pin.block_num_queue.push(block_num); + } + } + + // if there are any queued block numbers, then fetch the corresponding state updates + while let Some(block_num) = pin.block_num_queue.pop() { + let fut = Box::pin(Self::do_get_state_update(Arc::clone(&pin.provider), block_num)); + pin.state_update_req_futs.push_back((block_num, fut)); + } + + // handle incoming new subscribers + while let Poll::Ready(Some(request)) = pin.subscriber_recv.poll_recv(cx) { + println!("received new subscriber"); + pin.subscriber_manager.add_subscriber(request); + } + + // check if there's ongoing publish future, if yes, poll it and if its still not ready + // then return pending, + // dont request for state update, since we are still waiting for the previous state update + // to be published + if let Some(mut fut) = pin.publish_update_fut.take() { + if fut.poll_unpin(cx).is_pending() { + pin.publish_update_fut = Some(fut); + return Poll::Pending; + } + } + + // poll ongoing state update requests + if let Some((block_num, mut fut)) = pin.state_update_req_futs.pop_front() { + match fut.poll_unpin(cx) { + Poll::Ready(Ok(state_update)) => { + let subscribers = pin.subscriber_manager.subscribers.clone(); + pin.publish_update_fut = Some(Box::pin( + Self::publish_state_updates_to_subscribers(subscribers, state_update), + )); + } + + Poll::Ready(Err(e)) => { + println!("error fetching state update for block {block_num}: {:?}", e) + } + + Poll::Pending => pin.state_update_req_futs.push_back((block_num, fut)), + } + } + + Poll::Pending + } +} diff --git a/crates/torii/server/Cargo.toml b/crates/torii/server/Cargo.toml index 7ecd3f7e45..ce7267a860 100644 --- a/crates/torii/server/Cargo.toml +++ b/crates/torii/server/Cargo.toml @@ -16,7 +16,9 @@ clap.workspace = true ctrlc = "3.2.5" dojo-types = { path = "../../dojo-types" } dojo-world = { path = "../../dojo-world" } +either = "1.9.0" http = "0.2.9" +http-body = "0.4.5" hyper.workspace = true indexmap = "1.9.3" poem = "1.3.48" @@ -30,24 +32,23 @@ tokio-stream = "0.1.11" tokio-util = "0.7.7" tokio.workspace = true tonic-web.workspace = true +tonic.workspace = true torii-client = { path = "../client" } torii-core = { path = "../core" } torii-graphql = { path = "../graphql" } -torii-grpc = { path = "../grpc" } +torii-grpc = { path = "../grpc", features = [ "server" ] } tower = "0.4.13" tracing-subscriber.workspace = true tracing.workspace = true url.workspace = true warp.workspace = true -http-body = "0.4.5" -either = "1.9.0" [dev-dependencies] camino.workspace = true [features] -default = ["sqlite"] -sqlite = ["sqlx/sqlite"] +default = [ "sqlite" ] +sqlite = [ "sqlx/sqlite" ] [[bin]] name = "torii" diff --git a/crates/torii/server/src/cli.rs b/crates/torii/server/src/cli.rs index 80f73dd460..2017151a61 100644 --- a/crates/torii/server/src/cli.rs +++ b/crates/torii/server/src/cli.rs @@ -1,6 +1,7 @@ use std::env; use std::net::SocketAddr; use std::str::FromStr; +use std::sync::Arc; use anyhow::{anyhow, Context}; use camino::Utf8PathBuf; @@ -77,7 +78,7 @@ async fn main() -> anyhow::Result<()> { let pool = SqlitePoolOptions::new().max_connections(5).connect(database_url).await?; sqlx::migrate!("../migrations").run(&pool).await?; - let provider = JsonRpcClient::new(HttpTransport::new(Url::parse(&args.rpc).unwrap())); + let provider: Arc<_> = JsonRpcClient::new(HttpTransport::new(Url::parse(&args.rpc)?)).into(); let (manifest, env) = get_manifest_and_env(args.manifest.as_ref()) .with_context(|| "Failed to get manifest file".to_string())?; @@ -98,17 +99,18 @@ async fn main() -> anyhow::Result<()> { ..Processors::default() }; + let (block_sender, block_receiver) = tokio::sync::mpsc::channel(100); + let engine = Engine::new( &world, &db, &provider, processors, EngineConfig { start_block: args.start_block, ..Default::default() }, + Some(block_sender), ); - let addr = format!("{}:{}", args.host, args.port) - .parse::() - .expect("able to parse address"); + let addr: SocketAddr = format!("{}:{}", args.host, args.port).parse()?; tokio::select! { res = engine.start(cts) => { @@ -117,7 +119,7 @@ async fn main() -> anyhow::Result<()> { } } - res = server::spawn_server(&addr, &pool) => { + res = server::spawn_server(&addr, &pool, world_address, block_receiver, Arc::clone(&provider)) => { if let Err(e) = res { error!("Server failed with error: {e}"); } diff --git a/crates/torii/server/src/server.rs b/crates/torii/server/src/server.rs index bfe61bab4c..b906237d7e 100644 --- a/crates/torii/server/src/server.rs +++ b/crates/torii/server/src/server.rs @@ -2,19 +2,32 @@ use std::convert::Infallible; use std::net::SocketAddr; use std::pin::Pin; use std::str::FromStr; +use std::sync::Arc; use std::task::Poll; use either::Either; use hyper::service::{make_service_fn, Service}; use hyper::Uri; use sqlx::{Pool, Sqlite}; +use starknet::providers::jsonrpc::HttpTransport; +use starknet::providers::JsonRpcClient; +use starknet_crypto::FieldElement; +use tokio::sync::mpsc::Receiver as BoundedReceiver; +use torii_grpc::protos; use warp::Filter; type Error = Box; // TODO: check if there's a nicer way to implement this -pub async fn spawn_server(addr: &SocketAddr, pool: &Pool) -> anyhow::Result<()> { - let world_server = torii_grpc::DojoWorld::new(pool.clone()); +pub async fn spawn_server( + addr: &SocketAddr, + pool: &Pool, + world_address: FieldElement, + block_receiver: BoundedReceiver, + provider: Arc>, +) -> anyhow::Result<()> { + let world_server = + torii_grpc::server::DojoWorld::new(pool.clone(), block_receiver, world_address, provider); let base_route = warp::path::end() .and(warp::get()) @@ -22,7 +35,7 @@ pub async fn spawn_server(addr: &SocketAddr, pool: &Pool) -> anyhow::Res let routes = torii_graphql::route::filter(pool).await.or(base_route); let warp = warp::service(routes); - let tonic = tonic_web::enable(torii_grpc::world::world_server::WorldServer::new(world_server)); + let tonic = tonic_web::enable(protos::world::world_server::WorldServer::new(world_server)); hyper::Server::bind(addr) .serve(make_service_fn(move |_| { diff --git a/examples/ecs/Scarb.toml b/examples/ecs/Scarb.toml index d9ef9bfbee..0fc9f4616f 100644 --- a/examples/ecs/Scarb.toml +++ b/examples/ecs/Scarb.toml @@ -21,4 +21,4 @@ rpc_url = "http://localhost:5050/" # Default account for katana with seed = 0 account_address = "0x517ececd29116499f4a1b64b094da79ba08dfd54a3edaa316134c41f8160973" -private_key = "0x1800000000300000180000000000030000000000003006001800006600" \ No newline at end of file +private_key = "0x1800000000300000180000000000030000000000003006001800006600" diff --git a/examples/ecs/src/systems/with_decorator.cairo b/examples/ecs/src/systems/with_decorator.cairo index 4214e1026d..e7b60e3383 100644 --- a/examples/ecs/src/systems/with_decorator.cairo +++ b/examples/ecs/src/systems/with_decorator.cairo @@ -39,7 +39,9 @@ mod player_actions { world, ( Moves { player, remaining: 10, last_direction: Direction::None(()) }, - Position { player, vec: Vec2 { x: 10, y: 10 } }, + Position { + player, vec: Vec2 { x: position.vec.x + 10, y: position.vec.y + 10 } + }, ) ); } From 503a775933953f8a11905d03549f3f601bd8f387 Mon Sep 17 00:00:00 2001 From: Yun Date: Thu, 28 Sep 2023 10:31:46 -0700 Subject: [PATCH 75/77] Update core cairo type (#937) * Update core cairo type * remove derive * use strum macros --- Cargo.lock | 25 +++++++++++++++++++++++-- Cargo.toml | 2 ++ crates/dojo-types/Cargo.toml | 2 ++ crates/dojo-types/src/core.rs | 32 +++++++------------------------- 4 files changed, 34 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9104c72952..57bc48b09b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -692,8 +692,8 @@ dependencies = [ "sha3", "starknet-crypto 0.5.1", "starknet_api", - "strum", - "strum_macros", + "strum 0.24.1", + "strum_macros 0.24.3", "thiserror", ] @@ -2118,6 +2118,8 @@ dependencies = [ "hex", "serde", "starknet", + "strum 0.25.0", + "strum_macros 0.25.2", "thiserror", ] @@ -6489,6 +6491,12 @@ dependencies = [ "strum_macros", ] +[[package]] +name = "strum" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" + [[package]] name = "strum_macros" version = "0.24.3" @@ -6502,6 +6510,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "strum_macros" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8d03b598d3d0fff69bf533ee3ef19b8eeb342729596df84bcc7e1f96ec4059" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.32", +] + [[package]] name = "subtle" version = "2.5.0" diff --git a/Cargo.toml b/Cargo.toml index 4236a73398..ccaf6c783d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -78,6 +78,8 @@ smol_str = { version = "0.2.0", features = [ "serde" ] } starknet = "0.6.0" starknet-crypto = "0.6.0" starknet_api = { git = "https://github.com/starkware-libs/starknet-api", rev = "ecc9b6946ef13003da202838e4124a9ad2efabb0" } +strum = "0.25" +strum_macros = "0.25" test-log = "0.2.11" thiserror = "1.0.32" tokio = { version = "1.32.0", features = [ "full" ] } diff --git a/crates/dojo-types/Cargo.toml b/crates/dojo-types/Cargo.toml index 0606c9d2e4..09d62e8de9 100644 --- a/crates/dojo-types/Cargo.toml +++ b/crates/dojo-types/Cargo.toml @@ -7,6 +7,8 @@ version = "0.2.1" [dependencies] hex = "0.4.3" +strum.workspace = true +strum_macros.workspace = true serde.workspace = true starknet.workspace = true thiserror.workspace = true diff --git a/crates/dojo-types/src/core.rs b/crates/dojo-types/src/core.rs index d234cbc246..996e047589 100644 --- a/crates/dojo-types/src/core.rs +++ b/crates/dojo-types/src/core.rs @@ -1,7 +1,8 @@ -use std::str::FromStr; - use starknet::core::types::FieldElement; +use strum_macros::{AsRefStr, Display, EnumIter, EnumString}; +#[derive(AsRefStr, Display, EnumIter, EnumString, Debug)] +#[strum(serialize_all = "lowercase")] pub enum CairoType { U8, U16, @@ -11,9 +12,11 @@ pub enum CairoType { U256, USize, Bool, - ContractAddress, - ClassHash, Felt252, + #[strum(serialize = "ClassHash")] + ClassHash, + #[strum(serialize = "ContractAddress")] + ContractAddress, } #[derive(Debug, thiserror::Error)] @@ -26,27 +29,6 @@ pub enum CairoTypeError { UnsupportedType, } -impl FromStr for CairoType { - type Err = (); - - fn from_str(s: &str) -> Result { - match s { - "u8" => Ok(CairoType::U8), - "u16" => Ok(CairoType::U16), - "u32" => Ok(CairoType::U32), - "u64" => Ok(CairoType::U64), - "u128" => Ok(CairoType::U128), - "u256" => Ok(CairoType::U256), - "usize" => Ok(CairoType::USize), - "bool" => Ok(CairoType::Bool), - "ContractAddress" => Ok(CairoType::ContractAddress), - "ClassHash" => Ok(CairoType::ClassHash), - "felt252" => Ok(CairoType::Felt252), - _ => Err(()), - } - } -} - impl CairoType { pub fn to_sql_type(&self) -> String { match self { From 45ce4ce3c1d2a09ff9504215918d41f997cd23d4 Mon Sep 17 00:00:00 2001 From: Kariy Date: Fri, 29 Sep 2023 00:52:16 +0700 Subject: [PATCH 76/77] refactor(katana-core): refactor service module commit-id:aac1f033 --- Cargo.lock | 6 +- crates/katana/core/src/backend/mod.rs | 6 +- crates/katana/core/src/sequencer.rs | 3 +- .../{service.rs => service/block_producer.rs} | 106 +---------------- crates/katana/core/src/service/mod.rs | 107 ++++++++++++++++++ 5 files changed, 121 insertions(+), 107 deletions(-) rename crates/katana/core/src/{service.rs => service/block_producer.rs} (82%) create mode 100644 crates/katana/core/src/service/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 57bc48b09b..3725ba9e77 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4822,7 +4822,7 @@ dependencies = [ "regex-cache", "serde", "serde_derive", - "strum", + "strum 0.24.1", "thiserror", ] @@ -6488,7 +6488,7 @@ version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" dependencies = [ - "strum_macros", + "strum_macros 0.24.3", ] [[package]] @@ -6520,7 +6520,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.32", + "syn 2.0.37", ] [[package]] diff --git a/crates/katana/core/src/backend/mod.rs b/crates/katana/core/src/backend/mod.rs index 5b7ab19a17..a57690686a 100644 --- a/crates/katana/core/src/backend/mod.rs +++ b/crates/katana/core/src/backend/mod.rs @@ -48,7 +48,7 @@ use crate::env::{BlockContextGenerator, Env}; use crate::execution::{ExecutionOutcome, MaybeInvalidExecutedTransaction, TransactionExecutor}; use crate::fork::db::ForkedDb; use crate::sequencer_error::SequencerError; -use crate::service::MinedBlockOutcome; +use crate::service::block_producer::MinedBlockOutcome; use crate::utils::{convert_state_diff_to_rpc_state_diff, get_current_timestamp}; pub struct ExternalFunctionCall { @@ -218,8 +218,8 @@ impl Backend { /// Mines a new block based on the provided execution outcome. /// This method should only be called by the - /// [IntervalBlockProducer](crate::service::IntervalBlockProducer) when the node is running in - /// `interval` mining mode. + /// [IntervalBlockProducer](crate::service::block_producer::IntervalBlockProducer) when the node + /// is running in `interval` mining mode. pub async fn mine_pending_block( &self, execution_outcome: ExecutionOutcome, diff --git a/crates/katana/core/src/sequencer.rs b/crates/katana/core/src/sequencer.rs index 8011022257..4e0d87d55c 100644 --- a/crates/katana/core/src/sequencer.rs +++ b/crates/katana/core/src/sequencer.rs @@ -28,7 +28,8 @@ use crate::db::{AsStateRefDb, StateExtRef, StateRefDb}; use crate::execution::{MaybeInvalidExecutedTransaction, PendingState}; use crate::pool::TransactionPool; use crate::sequencer_error::SequencerError; -use crate::service::{BlockProducer, BlockProducerMode, NodeService, TransactionMiner}; +use crate::service::block_producer::{BlockProducer, BlockProducerMode}; +use crate::service::{NodeService, TransactionMiner}; use crate::utils::event::{ContinuationToken, ContinuationTokenError}; type SequencerResult = Result; diff --git a/crates/katana/core/src/service.rs b/crates/katana/core/src/service/block_producer.rs similarity index 82% rename from crates/katana/core/src/service.rs rename to crates/katana/core/src/service/block_producer.rs index 94407c6573..807e449866 100644 --- a/crates/katana/core/src/service.rs +++ b/crates/katana/core/src/service/block_producer.rs @@ -1,7 +1,3 @@ -// Code adapted from Foundry's Anvil - -//! background service - use std::collections::{HashMap, VecDeque}; use std::future::Future; use std::pin::Pin; @@ -10,11 +6,9 @@ use std::task::{Context, Poll}; use std::time::Duration; use blockifier::state::state_api::{State, StateReader}; -use futures::channel::mpsc::Receiver; -use futures::stream::{Fuse, Stream, StreamExt}; +use futures::stream::{Stream, StreamExt}; use futures::FutureExt; use parking_lot::RwLock; -use starknet::core::types::FieldElement; use tokio::time::{interval_at, Instant, Interval}; use tracing::trace; @@ -26,56 +20,10 @@ use crate::execution::{ create_execution_outcome, ExecutedTransaction, ExecutionOutcome, MaybeInvalidExecutedTransaction, PendingState, TransactionExecutor, }; -use crate::pool::TransactionPool; - -/// The type that drives the blockchain's state -/// -/// This service is basically an endless future that continuously polls the miner which returns -/// transactions for the next block, then those transactions are handed off to the [BlockProducer] -/// to construct a new block. -pub struct NodeService { - /// the pool that holds all transactions - pool: Arc, - /// creates new blocks - block_producer: BlockProducer, - /// the miner responsible to select transactions from the `pool´ - miner: TransactionMiner, -} - -impl NodeService { - pub fn new( - pool: Arc, - miner: TransactionMiner, - block_producer: BlockProducer, - ) -> Self { - Self { pool, block_producer, miner } - } -} - -impl Future for NodeService { - type Output = (); - - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - let pin = self.get_mut(); - // this drives block production and feeds new sets of ready transactions to the block - // producer - loop { - while let Poll::Ready(Some(outcome)) = pin.block_producer.poll_next_unpin(cx) { - trace!(target: "node", "mined block {}", outcome.block_number); - } - - if let Poll::Ready(transactions) = pin.miner.poll(&pin.pool, cx) { - // miner returned a set of transaction that we feed to the producer - pin.block_producer.queue(transactions); - } else { - // no progress made - break; - } - } - - Poll::Pending - } +pub struct MinedBlockOutcome { + pub block_number: u64, + pub transactions: Vec, } type ServiceFuture = Pin + Send + Sync>>; @@ -122,7 +70,7 @@ impl BlockProducer { } } - fn queue(&self, transactions: Vec) { + pub(super) fn queue(&self, transactions: Vec) { let mut mode = self.inner.write(); match &mut *mode { BlockProducerMode::Instant(producer) => producer.queued.push_back(transactions), @@ -439,6 +387,7 @@ impl Stream for InstantBlockProducer { // poll the mining future if let Some(mut mining) = pin.block_mining.take() { + println!("ohayo"); if let Poll::Ready(outcome) = mining.poll_unpin(cx) { return Poll::Ready(Some(outcome)); } else { @@ -449,46 +398,3 @@ impl Stream for InstantBlockProducer { Poll::Pending } } - -pub struct MinedBlockOutcome { - pub block_number: u64, - pub transactions: Vec, -} - -/// The type which takes the transaction from the pool and feeds them to the block producer. -pub struct TransactionMiner { - /// stores whether there are pending transacions (if known) - has_pending_txs: Option, - /// Receives hashes of transactions that are ready from the pool - rx: Fuse>, -} - -impl TransactionMiner { - pub fn new(rx: Receiver) -> Self { - Self { rx: rx.fuse(), has_pending_txs: None } - } - - fn poll( - &mut self, - pool: &Arc, - cx: &mut Context<'_>, - ) -> Poll> { - // drain the notification stream - while let Poll::Ready(Some(_)) = Pin::new(&mut self.rx).poll_next(cx) { - self.has_pending_txs = Some(true); - } - - if self.has_pending_txs == Some(false) { - return Poll::Pending; - } - - // take all the transactions from the pool - let transactions = pool.get_transactions(); - - if transactions.is_empty() { - return Poll::Pending; - } - - Poll::Ready(transactions) - } -} diff --git a/crates/katana/core/src/service/mod.rs b/crates/katana/core/src/service/mod.rs new file mode 100644 index 0000000000..3460640f9c --- /dev/null +++ b/crates/katana/core/src/service/mod.rs @@ -0,0 +1,107 @@ +// Code adapted from Foundry's Anvil + +//! background service + +use std::future::Future; +use std::pin::Pin; +use std::sync::Arc; +use std::task::{Context, Poll}; + +use futures::channel::mpsc::Receiver; +use futures::stream::{Fuse, Stream, StreamExt}; +use starknet::core::types::FieldElement; +use tracing::trace; + +use self::block_producer::BlockProducer; +use crate::backend::storage::transaction::Transaction; +use crate::pool::TransactionPool; + +pub mod block_producer; + +/// The type that drives the blockchain's state +/// +/// This service is basically an endless future that continuously polls the miner which returns +/// transactions for the next block, then those transactions are handed off to the [BlockProducer] +/// to construct a new block. +pub struct NodeService { + /// the pool that holds all transactions + pool: Arc, + /// creates new blocks + block_producer: BlockProducer, + /// the miner responsible to select transactions from the `pool´ + miner: TransactionMiner, +} + +impl NodeService { + pub fn new( + pool: Arc, + miner: TransactionMiner, + block_producer: BlockProducer, + ) -> Self { + Self { pool, block_producer, miner } + } +} + +impl Future for NodeService { + type Output = (); + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let pin = self.get_mut(); + + // this drives block production and feeds new sets of ready transactions to the block + // producer + loop { + while let Poll::Ready(Some(outcome)) = pin.block_producer.poll_next_unpin(cx) { + trace!(target: "node", "mined block {}", outcome.block_number); + } + + if let Poll::Ready(transactions) = pin.miner.poll(&pin.pool, cx) { + // miner returned a set of transaction that we feed to the producer + pin.block_producer.queue(transactions); + } else { + // no progress made + break; + } + } + + Poll::Pending + } +} + +/// The type which takes the transaction from the pool and feeds them to the block producer. +pub struct TransactionMiner { + /// stores whether there are pending transacions (if known) + has_pending_txs: Option, + /// Receives hashes of transactions that are ready from the pool + rx: Fuse>, +} + +impl TransactionMiner { + pub fn new(rx: Receiver) -> Self { + Self { rx: rx.fuse(), has_pending_txs: None } + } + + fn poll( + &mut self, + pool: &Arc, + cx: &mut Context<'_>, + ) -> Poll> { + // drain the notification stream + while let Poll::Ready(Some(_)) = Pin::new(&mut self.rx).poll_next(cx) { + self.has_pending_txs = Some(true); + } + + if self.has_pending_txs == Some(false) { + return Poll::Pending; + } + + // take all the transactions from the pool + let transactions = pool.get_transactions(); + + if transactions.is_empty() { + return Poll::Pending; + } + + Poll::Ready(transactions) + } +} From 23f6e64f0ec895c215628176116683dd66270e70 Mon Sep 17 00:00:00 2001 From: Kariy Date: Fri, 29 Sep 2023 00:54:15 +0700 Subject: [PATCH 77/77] refactor(katana): remove `Sequencer` trait commit-id:90847a65 --- Cargo.lock | 2 - crates/katana/core/Cargo.toml | 2 - crates/katana/core/src/sequencer.rs | 152 +++++--------------------- crates/katana/core/tests/sequencer.rs | 2 +- crates/katana/rpc/src/katana.rs | 20 ++-- crates/katana/rpc/src/lib.rs | 12 +- crates/katana/rpc/src/starknet.rs | 18 +-- crates/katana/src/main.rs | 4 +- crates/torii/client/wasm/Cargo.lock | 21 ++++ 9 files changed, 69 insertions(+), 164 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3725ba9e77..e3c3b429c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3971,8 +3971,6 @@ version = "0.2.1" dependencies = [ "anyhow", "assert_matches", - "async-trait", - "auto_impl", "blockifier", "cairo-lang-casm", "cairo-lang-starknet", diff --git a/crates/katana/core/Cargo.toml b/crates/katana/core/Cargo.toml index 4e95f0e7d4..9a298c4664 100644 --- a/crates/katana/core/Cargo.toml +++ b/crates/katana/core/Cargo.toml @@ -8,8 +8,6 @@ version.workspace = true [dependencies] anyhow.workspace = true -async-trait.workspace = true -auto_impl = "1.1.0" blockifier.workspace = true cairo-lang-casm = "2.2.0" cairo-lang-starknet = "2.2.0" diff --git a/crates/katana/core/src/sequencer.rs b/crates/katana/core/src/sequencer.rs index 4e0d87d55c..59e56f650f 100644 --- a/crates/katana/core/src/sequencer.rs +++ b/crates/katana/core/src/sequencer.rs @@ -4,8 +4,6 @@ use std::slice::Iter; use std::sync::Arc; use anyhow::Result; -use async_trait::async_trait; -use auto_impl::auto_impl; use blockifier::execution::contract_class::ContractClass; use blockifier::state::state_api::{State, StateReader}; use starknet::core::types::{ @@ -40,102 +38,6 @@ pub struct SequencerConfig { pub no_mining: bool, } -#[async_trait] -#[auto_impl(Arc)] -pub trait Sequencer { - fn block_producer(&self) -> &BlockProducer; - - fn backend(&self) -> &Backend; - - async fn state(&self, block_id: &BlockId) -> SequencerResult; - - async fn chain_id(&self) -> ChainId; - - async fn transaction_receipt( - &self, - hash: &FieldElement, - ) -> Option; - - async fn nonce_at( - &self, - block_id: BlockId, - contract_address: ContractAddress, - ) -> SequencerResult; - - async fn block_number(&self) -> u64; - - async fn block(&self, block_id: BlockId) -> Option; - - async fn transaction(&self, hash: &FieldElement) -> Option; - - async fn class_hash_at( - &self, - block_id: BlockId, - contract_address: ContractAddress, - ) -> SequencerResult; - - async fn class( - &self, - block_id: BlockId, - class_hash: ClassHash, - ) -> SequencerResult; - - async fn block_hash_and_number(&self) -> (FieldElement, u64); - - async fn call( - &self, - block_id: BlockId, - function_call: ExternalFunctionCall, - ) -> SequencerResult>; - - async fn storage_at( - &self, - contract_address: ContractAddress, - storage_key: StorageKey, - block_id: BlockId, - ) -> SequencerResult; - - async fn add_deploy_account_transaction( - &self, - transaction: DeployAccountTransaction, - ) -> (FieldElement, FieldElement); - - fn add_declare_transaction(&self, transaction: DeclareTransaction); - - fn add_invoke_transaction(&self, transaction: InvokeTransaction); - - async fn estimate_fee( - &self, - transactions: Vec, - block_id: BlockId, - ) -> SequencerResult>; - - async fn events( - &self, - from_block: BlockId, - to_block: BlockId, - address: Option, - keys: Option>>, - continuation_token: Option, - chunk_size: u64, - ) -> SequencerResult; - - async fn state_update(&self, block_id: BlockId) -> SequencerResult; - - async fn set_next_block_timestamp(&self, timestamp: u64) -> Result<(), SequencerError>; - - async fn increase_next_block_timestamp(&self, timestamp: u64) -> Result<(), SequencerError>; - - async fn has_pending_transactions(&self) -> bool; - - async fn set_storage_at( - &self, - contract_address: ContractAddress, - storage_key: StorageKey, - value: StarkFelt, - ) -> Result<(), SequencerError>; -} - pub struct KatanaSequencer { pub config: SequencerConfig, pub pool: Arc, @@ -183,19 +85,16 @@ impl KatanaSequencer { .get_class_hash_at(*contract_address) .is_ok_and(|c| c != ClassHash::default()) } -} -#[async_trait] -impl Sequencer for KatanaSequencer { - fn block_producer(&self) -> &BlockProducer { + pub fn block_producer(&self) -> &BlockProducer { &self.block_producer } - fn backend(&self) -> &Backend { + pub fn backend(&self) -> &Backend { &self.backend } - async fn state(&self, block_id: &BlockId) -> SequencerResult { + pub async fn state(&self, block_id: &BlockId) -> SequencerResult { match block_id { BlockId::Tag(BlockTag::Latest) => Ok(self.backend.state.read().await.as_ref_db()), @@ -223,7 +122,7 @@ impl Sequencer for KatanaSequencer { } } - async fn add_deploy_account_transaction( + pub async fn add_deploy_account_transaction( &self, transaction: DeployAccountTransaction, ) -> (FieldElement, FieldElement) { @@ -235,15 +134,15 @@ impl Sequencer for KatanaSequencer { (transaction_hash, contract_address) } - fn add_declare_transaction(&self, transaction: DeclareTransaction) { + pub fn add_declare_transaction(&self, transaction: DeclareTransaction) { self.pool.add_transaction(Transaction::Declare(transaction)) } - fn add_invoke_transaction(&self, transaction: InvokeTransaction) { + pub fn add_invoke_transaction(&self, transaction: InvokeTransaction) { self.pool.add_transaction(Transaction::Invoke(transaction)) } - async fn estimate_fee( + pub async fn estimate_fee( &self, transactions: Vec, block_id: BlockId, @@ -252,13 +151,13 @@ impl Sequencer for KatanaSequencer { self.backend.estimate_fee(transactions, state).map_err(SequencerError::TransactionExecution) } - async fn block_hash_and_number(&self) -> (FieldElement, u64) { + pub async fn block_hash_and_number(&self) -> (FieldElement, u64) { let hash = self.backend.blockchain.storage.read().latest_hash; let number = self.backend.blockchain.storage.read().latest_number; (hash, number) } - async fn class_hash_at( + pub async fn class_hash_at( &self, block_id: BlockId, contract_address: ContractAddress, @@ -271,7 +170,7 @@ impl Sequencer for KatanaSequencer { state.get_class_hash_at(contract_address).map_err(SequencerError::State) } - async fn class( + pub async fn class( &self, block_id: BlockId, class_hash: ClassHash, @@ -290,7 +189,7 @@ impl Sequencer for KatanaSequencer { } } - async fn storage_at( + pub async fn storage_at( &self, contract_address: ContractAddress, storage_key: StorageKey, @@ -304,15 +203,15 @@ impl Sequencer for KatanaSequencer { state.get_storage_at(contract_address, storage_key).map_err(SequencerError::State) } - async fn chain_id(&self) -> ChainId { + pub async fn chain_id(&self) -> ChainId { self.backend.env.read().block.chain_id.clone() } - async fn block_number(&self) -> u64 { + pub async fn block_number(&self) -> u64 { self.backend.blockchain.storage.read().latest_number } - async fn block(&self, block_id: BlockId) -> Option { + pub async fn block(&self, block_id: BlockId) -> Option { let block_id = match block_id { BlockId::Tag(BlockTag::Pending) if self.block_producer.is_instant_mining() => { BlockId::Tag(BlockTag::Latest) @@ -359,7 +258,7 @@ impl Sequencer for KatanaSequencer { } } - async fn nonce_at( + pub async fn nonce_at( &self, block_id: BlockId, contract_address: ContractAddress, @@ -372,7 +271,7 @@ impl Sequencer for KatanaSequencer { state.get_nonce_at(contract_address).map_err(SequencerError::State) } - async fn call( + pub async fn call( &self, block_id: BlockId, function_call: ExternalFunctionCall, @@ -389,7 +288,7 @@ impl Sequencer for KatanaSequencer { .map(|execution_info| execution_info.execution.retdata.0) } - async fn transaction_receipt( + pub async fn transaction_receipt( &self, hash: &FieldElement, ) -> Option { @@ -406,7 +305,7 @@ impl Sequencer for KatanaSequencer { } } - async fn transaction(&self, hash: &FieldElement) -> Option { + pub async fn transaction(&self, hash: &FieldElement) -> Option { let tx = self.backend.blockchain.storage.read().transactions.get(hash).cloned(); match tx { Some(tx) => Some(tx), @@ -426,7 +325,7 @@ impl Sequencer for KatanaSequencer { } } - async fn events( + pub async fn events( &self, from_block: BlockId, to_block: BlockId, @@ -557,7 +456,7 @@ impl Sequencer for KatanaSequencer { Ok(EventsPage { events: filtered_events, continuation_token: None }) } - async fn state_update(&self, block_id: BlockId) -> SequencerResult { + pub async fn state_update(&self, block_id: BlockId) -> SequencerResult { let block_number = self .backend .blockchain @@ -574,7 +473,7 @@ impl Sequencer for KatanaSequencer { .ok_or(SequencerError::StateUpdateNotFound(block_id)) } - async fn set_next_block_timestamp(&self, timestamp: u64) -> Result<(), SequencerError> { + pub async fn set_next_block_timestamp(&self, timestamp: u64) -> Result<(), SequencerError> { if self.has_pending_transactions().await { return Err(SequencerError::PendingTransactions); } @@ -582,7 +481,10 @@ impl Sequencer for KatanaSequencer { Ok(()) } - async fn increase_next_block_timestamp(&self, timestamp: u64) -> Result<(), SequencerError> { + pub async fn increase_next_block_timestamp( + &self, + timestamp: u64, + ) -> Result<(), SequencerError> { if self.has_pending_transactions().await { return Err(SequencerError::PendingTransactions); } @@ -590,7 +492,7 @@ impl Sequencer for KatanaSequencer { Ok(()) } - async fn has_pending_transactions(&self) -> bool { + pub async fn has_pending_transactions(&self) -> bool { if let Some(ref pending) = self.pending_state() { !pending.executed_transactions.read().is_empty() } else { @@ -598,7 +500,7 @@ impl Sequencer for KatanaSequencer { } } - async fn set_storage_at( + pub async fn set_storage_at( &self, contract_address: ContractAddress, storage_key: StorageKey, diff --git a/crates/katana/core/tests/sequencer.rs b/crates/katana/core/tests/sequencer.rs index 0f9183dffd..ec8726027b 100644 --- a/crates/katana/core/tests/sequencer.rs +++ b/crates/katana/core/tests/sequencer.rs @@ -2,7 +2,7 @@ use std::time::Duration; use katana_core::backend::config::{Environment, StarknetConfig}; use katana_core::backend::storage::transaction::{DeclareTransaction, KnownTransaction}; -use katana_core::sequencer::{KatanaSequencer, Sequencer, SequencerConfig}; +use katana_core::sequencer::{KatanaSequencer, SequencerConfig}; use katana_core::utils::contract::get_contract_class; use starknet::core::types::FieldElement; use starknet_api::core::{ClassHash, ContractAddress, Nonce, PatriciaKey}; diff --git a/crates/katana/rpc/src/katana.rs b/crates/katana/rpc/src/katana.rs index 95160d3546..966d9f7384 100644 --- a/crates/katana/rpc/src/katana.rs +++ b/crates/katana/rpc/src/katana.rs @@ -1,6 +1,8 @@ +use std::sync::Arc; + use jsonrpsee::core::{async_trait, Error}; use katana_core::accounts::Account; -use katana_core::sequencer::Sequencer; +use katana_core::sequencer::KatanaSequencer; use starknet::core::types::FieldElement; use starknet_api::core::{ContractAddress, PatriciaKey}; use starknet_api::hash::{StarkFelt, StarkHash}; @@ -9,24 +11,18 @@ use starknet_api::{patricia_key, stark_felt}; use crate::api::katana::{KatanaApiError, KatanaApiServer}; -pub struct KatanaApi { - sequencer: S, +pub struct KatanaApi { + sequencer: Arc, } -impl KatanaApi -where - S: Sequencer + Send + 'static, -{ - pub fn new(sequencer: S) -> Self { +impl KatanaApi { + pub fn new(sequencer: Arc) -> Self { Self { sequencer } } } #[async_trait] -impl KatanaApiServer for KatanaApi -where - S: Sequencer + Send + Sync + 'static, -{ +impl KatanaApiServer for KatanaApi { async fn generate_block(&self) -> Result<(), Error> { self.sequencer.block_producer().force_mine(); Ok(()) diff --git a/crates/katana/rpc/src/lib.rs b/crates/katana/rpc/src/lib.rs index 466e3d5f03..1ab9673782 100644 --- a/crates/katana/rpc/src/lib.rs +++ b/crates/katana/rpc/src/lib.rs @@ -15,7 +15,6 @@ use jsonrpsee::server::{AllowHosts, ServerBuilder, ServerHandle}; use jsonrpsee::tracing::debug; use jsonrpsee::types::Params; use jsonrpsee::RpcModule; -use katana_core::sequencer::Sequencer; use tower_http::cors::{Any, CorsLayer}; use crate::api::katana::KatanaApiServer; @@ -23,14 +22,11 @@ use crate::api::starknet::StarknetApiServer; pub use crate::katana::KatanaApi; pub use crate::starknet::StarknetApi; -pub async fn spawn( - katana_api: KatanaApi, - starknet_api: StarknetApi, +pub async fn spawn( + katana_api: KatanaApi, + starknet_api: StarknetApi, config: ServerConfig, -) -> Result -where - S: Sequencer + Send + Sync + 'static, -{ +) -> Result { let mut methods = RpcModule::new(()); methods.merge(starknet_api.into_rpc())?; methods.merge(katana_api.into_rpc())?; diff --git a/crates/katana/rpc/src/starknet.rs b/crates/katana/rpc/src/starknet.rs index 4949b3464f..c0edfb61a8 100644 --- a/crates/katana/rpc/src/starknet.rs +++ b/crates/katana/rpc/src/starknet.rs @@ -8,7 +8,7 @@ use katana_core::backend::storage::transaction::{ L1HandlerTransaction, PendingTransaction, Transaction, }; use katana_core::backend::ExternalFunctionCall; -use katana_core::sequencer::Sequencer; +use katana_core::sequencer::KatanaSequencer; use katana_core::sequencer_error::SequencerError; use katana_core::utils::contract::legacy_inner_to_rpc_class; use katana_core::utils::transaction::{ @@ -31,23 +31,17 @@ use starknet_api::transaction::Calldata; use crate::api::starknet::{Felt, StarknetApiError, StarknetApiServer}; -pub struct StarknetApi { - sequencer: S, +pub struct StarknetApi { + sequencer: Arc, } -impl StarknetApi -where - S: Sequencer + Send + Sync + 'static, -{ - pub fn new(sequencer: S) -> Self { +impl StarknetApi { + pub fn new(sequencer: Arc) -> Self { Self { sequencer } } } #[async_trait] -impl StarknetApiServer for StarknetApi -where - S: Sequencer + Send + Sync + 'static, -{ +impl StarknetApiServer for StarknetApi { async fn chain_id(&self) -> Result { Ok(self.sequencer.chain_id().await.as_hex()) } diff --git a/crates/katana/src/main.rs b/crates/katana/src/main.rs index 208300b12b..ec6c9ce81e 100644 --- a/crates/katana/src/main.rs +++ b/crates/katana/src/main.rs @@ -5,7 +5,7 @@ use std::{fs, io}; use clap::{CommandFactory, Parser}; use clap_complete::{generate, Shell}; use console::Style; -use katana_core::sequencer::{KatanaSequencer, Sequencer}; +use katana_core::sequencer::KatanaSequencer; use katana_rpc::{spawn, KatanaApi, NodeHandle, StarknetApi}; use tokio::signal::ctrl_c; use tracing::{error, info}; @@ -122,7 +122,7 @@ ACCOUNTS SEED println!("\n{address}\n\n"); } -pub async fn shutdown_handler(sequencer: Arc, config: KatanaArgs) { +pub async fn shutdown_handler(sequencer: Arc, config: KatanaArgs) { if let Some(path) = config.dump_state { info!("Dumping state on shutdown"); let state = (*sequencer).backend().dump_state().await; diff --git a/crates/torii/client/wasm/Cargo.lock b/crates/torii/client/wasm/Cargo.lock index da16f604d9..8e455f5432 100644 --- a/crates/torii/client/wasm/Cargo.lock +++ b/crates/torii/client/wasm/Cargo.lock @@ -646,6 +646,8 @@ dependencies = [ "hex", "serde", "starknet", + "strum", + "strum_macros", "thiserror", ] @@ -2646,6 +2648,25 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "strum" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" + +[[package]] +name = "strum_macros" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8d03b598d3d0fff69bf533ee3ef19b8eeb342729596df84bcc7e1f96ec4059" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.37", +] + [[package]] name = "subtle" version = "2.5.0"