diff --git a/.gitignore b/.gitignore index 6498958892..4632fee075 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,8 @@ crates/benches/bench_results.txt **/generated .vscode bindings +./bin/solis/.env + +messaging.local.json +addresses.json +existing-katana-db \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index b8b0ff571c..ace78f9245 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1705,10 +1705,34 @@ source = "git+https://github.com/cartridge-gg/cainome?tag=v0.2.5#54df2a4114c0c61 dependencies = [ "anyhow", "async-trait", - "cainome-cairo-serde", - "cainome-parser", - "cainome-rs", - "cainome-rs-macro", + "cainome-cairo-serde 0.1.0 (git+https://github.com/cartridge-gg/cainome?tag=v0.2.5)", + "cainome-parser 0.1.0 (git+https://github.com/cartridge-gg/cainome?tag=v0.2.5)", + "cainome-rs 0.1.0 (git+https://github.com/cartridge-gg/cainome?tag=v0.2.5)", + "cainome-rs-macro 0.1.0 (git+https://github.com/cartridge-gg/cainome?tag=v0.2.5)", + "camino", + "clap", + "clap_complete", + "convert_case 0.6.0", + "serde", + "serde_json", + "starknet 0.9.0", + "thiserror", + "tracing", + "tracing-subscriber", + "url", +] + +[[package]] +name = "cainome" +version = "0.2.3" +source = "git+https://github.com/cartridge-gg/cainome?tag=v0.2.6#07ba9c91baa2f6aa2bc850b6699ffc2266be9a13" +dependencies = [ + "anyhow", + "async-trait", + "cainome-cairo-serde 0.1.0 (git+https://github.com/cartridge-gg/cainome?tag=v0.2.6)", + "cainome-parser 0.1.0 (git+https://github.com/cartridge-gg/cainome?tag=v0.2.6)", + "cainome-rs 0.1.0 (git+https://github.com/cartridge-gg/cainome?tag=v0.2.6)", + "cainome-rs-macro 0.1.0 (git+https://github.com/cartridge-gg/cainome?tag=v0.2.6)", "camino", "clap", "clap_complete", @@ -1731,6 +1755,15 @@ dependencies = [ "thiserror", ] +[[package]] +name = "cainome-cairo-serde" +version = "0.1.0" +source = "git+https://github.com/cartridge-gg/cainome?tag=v0.2.6#07ba9c91baa2f6aa2bc850b6699ffc2266be9a13" +dependencies = [ + "starknet 0.9.0", + "thiserror", +] + [[package]] name = "cainome-parser" version = "0.1.0" @@ -1744,14 +1777,43 @@ dependencies = [ "thiserror", ] +[[package]] +name = "cainome-parser" +version = "0.1.0" +source = "git+https://github.com/cartridge-gg/cainome?tag=v0.2.6#07ba9c91baa2f6aa2bc850b6699ffc2266be9a13" +dependencies = [ + "convert_case 0.6.0", + "quote", + "serde_json", + "starknet 0.9.0", + "syn 2.0.55", + "thiserror", +] + [[package]] name = "cainome-rs" version = "0.1.0" source = "git+https://github.com/cartridge-gg/cainome?tag=v0.2.5#54df2a4114c0c61359c2f1a70bc7e5fb57d9eaf2" dependencies = [ "anyhow", - "cainome-cairo-serde", - "cainome-parser", + "cainome-cairo-serde 0.1.0 (git+https://github.com/cartridge-gg/cainome?tag=v0.2.5)", + "cainome-parser 0.1.0 (git+https://github.com/cartridge-gg/cainome?tag=v0.2.5)", + "proc-macro2", + "quote", + "serde_json", + "starknet 0.9.0", + "syn 2.0.55", + "thiserror", +] + +[[package]] +name = "cainome-rs" +version = "0.1.0" +source = "git+https://github.com/cartridge-gg/cainome?tag=v0.2.6#07ba9c91baa2f6aa2bc850b6699ffc2266be9a13" +dependencies = [ + "anyhow", + "cainome-cairo-serde 0.1.0 (git+https://github.com/cartridge-gg/cainome?tag=v0.2.6)", + "cainome-parser 0.1.0 (git+https://github.com/cartridge-gg/cainome?tag=v0.2.6)", "proc-macro2", "quote", "serde_json", @@ -1766,9 +1828,26 @@ version = "0.1.0" source = "git+https://github.com/cartridge-gg/cainome?tag=v0.2.5#54df2a4114c0c61359c2f1a70bc7e5fb57d9eaf2" dependencies = [ "anyhow", - "cainome-cairo-serde", - "cainome-parser", - "cainome-rs", + "cainome-cairo-serde 0.1.0 (git+https://github.com/cartridge-gg/cainome?tag=v0.2.5)", + "cainome-parser 0.1.0 (git+https://github.com/cartridge-gg/cainome?tag=v0.2.5)", + "cainome-rs 0.1.0 (git+https://github.com/cartridge-gg/cainome?tag=v0.2.5)", + "proc-macro2", + "quote", + "serde_json", + "starknet 0.9.0", + "syn 2.0.55", + "thiserror", +] + +[[package]] +name = "cainome-rs-macro" +version = "0.1.0" +source = "git+https://github.com/cartridge-gg/cainome?tag=v0.2.6#07ba9c91baa2f6aa2bc850b6699ffc2266be9a13" +dependencies = [ + "anyhow", + "cainome-cairo-serde 0.1.0 (git+https://github.com/cartridge-gg/cainome?tag=v0.2.6)", + "cainome-parser 0.1.0 (git+https://github.com/cartridge-gg/cainome?tag=v0.2.6)", + "cainome-rs 0.1.0 (git+https://github.com/cartridge-gg/cainome?tag=v0.2.6)", "proc-macro2", "quote", "serde_json", @@ -3574,7 +3653,7 @@ name = "dojo-bindgen" version = "0.7.0-alpha.1" dependencies = [ "async-trait", - "cainome", + "cainome 0.2.3 (git+https://github.com/cartridge-gg/cainome?tag=v0.2.5)", "camino", "chrono", "convert_case 0.6.0", @@ -3741,7 +3820,7 @@ dependencies = [ "assert_fs", "assert_matches", "async-trait", - "cainome", + "cainome 0.2.3 (git+https://github.com/cartridge-gg/cainome?tag=v0.2.5)", "cairo-lang-filesystem", "cairo-lang-project", "cairo-lang-starknet", @@ -3784,6 +3863,12 @@ dependencies = [ "serde_json", ] +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + [[package]] name = "dotenvy" version = "0.15.7" @@ -6928,6 +7013,7 @@ version = "0.7.0-alpha.1" dependencies = [ "anyhow", "assert_matches", + "base64 0.13.1", "cairo-lang-starknet", "cairo-lang-starknet-classes", "dojo-metrics", @@ -11308,6 +11394,37 @@ dependencies = [ "sha-1", ] +[[package]] +name = "solis" +version = "0.7.0-alpha.1" +dependencies = [ + "alloy-primitives", + "anyhow", + "assert_matches", + "async-trait", + "cainome 0.2.3 (git+https://github.com/cartridge-gg/cainome?tag=v0.2.6)", + "cfg-if", + "clap", + "clap_complete", + "common", + "console", + "dojo-metrics", + "dotenv", + "katana-core", + "katana-executor", + "katana-primitives", + "katana-rpc", + "katana-rpc-api", + "serde_json", + "shellexpand", + "starknet 0.9.0", + "starknet_api", + "tokio", + "tracing", + "tracing-subscriber", + "url", +] + [[package]] name = "sozo" version = "0.7.0-alpha.1" @@ -11316,7 +11433,7 @@ dependencies = [ "assert_fs", "async-trait", "bigdecimal 0.4.3", - "cainome", + "cainome 0.2.3 (git+https://github.com/cartridge-gg/cainome?tag=v0.2.5)", "cairo-lang-compiler", "cairo-lang-defs", "cairo-lang-filesystem", @@ -11371,7 +11488,7 @@ dependencies = [ "anyhow", "assert_fs", "async-trait", - "cainome", + "cainome 0.2.3 (git+https://github.com/cartridge-gg/cainome?tag=v0.2.5)", "cairo-lang-compiler", "cairo-lang-defs", "cairo-lang-filesystem", diff --git a/Cargo.toml b/Cargo.toml index 7a285254d1..78030f2f55 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ resolver = "2" members = [ "bin/dojo-language-server", "bin/katana", + "bin/solis", "bin/saya", "bin/sozo", "bin/torii", diff --git a/artifacts/contract.abi.json b/artifacts/contract.abi.json new file mode 100644 index 0000000000..5ac0f4f06d --- /dev/null +++ b/artifacts/contract.abi.json @@ -0,0 +1,63 @@ +[ + { + "type": "impl", + "name": "ITestImpl", + "interface_name": "package_name::ITest" + }, + { + "type": "struct", + "name": "core::array::Span::", + "members": [ + { + "name": "snapshot", + "type": "@core::array::Array::" + } + ] + }, + { + "type": "struct", + "name": "package_name::TxCall", + "members": [ + { + "name": "to", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "selector", + "type": "core::felt252" + }, + { + "name": "calldata", + "type": "core::array::Span::" + } + ] + }, + { + "type": "interface", + "name": "package_name::ITest", + "items": [ + { + "type": "function", + "name": "ev", + "inputs": [], + "outputs": [ + { + "type": "package_name::TxCall" + } + ], + "state_mutability": "view" + } + ] + }, + { + "type": "constructor", + "name": "constructor", + "inputs": [] + }, + { + "type": "event", + "name": "package_name::c1::Event", + "kind": "enum", + "variants": [] + } +] diff --git a/artifacts/orderbook.abi.json b/artifacts/orderbook.abi.json new file mode 100644 index 0000000000..667d00b5df --- /dev/null +++ b/artifacts/orderbook.abi.json @@ -0,0 +1,671 @@ +[ + { + "type": "impl", + "name": "ImplOrderbook", + "interface_name": "ark_orderbook::orderbook::Orderbook" + }, + { + "type": "enum", + "name": "ark_common::protocol::order_types::RouteType", + "variants": [ + { + "name": "Erc20ToErc721", + "type": "()" + }, + { + "name": "Erc721ToErc20", + "type": "()" + } + ] + }, + { + "type": "struct", + "name": "core::integer::u256", + "members": [ + { + "name": "low", + "type": "core::integer::u128" + }, + { + "name": "high", + "type": "core::integer::u128" + } + ] + }, + { + "type": "enum", + "name": "core::option::Option::", + "variants": [ + { + "name": "Some", + "type": "core::integer::u256" + }, + { + "name": "None", + "type": "()" + } + ] + }, + { + "type": "struct", + "name": "core::array::Span::", + "members": [ + { + "name": "snapshot", + "type": "@core::array::Array::" + } + ] + }, + { + "type": "struct", + "name": "ark_orderbook::order::order_v1::OrderV1", + "members": [ + { + "name": "route", + "type": "ark_common::protocol::order_types::RouteType" + }, + { + "name": "currency_address", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "currency_chain_id", + "type": "core::felt252" + }, + { + "name": "salt", + "type": "core::felt252" + }, + { + "name": "offerer", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "token_chain_id", + "type": "core::felt252" + }, + { + "name": "token_address", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "token_id", + "type": "core::option::Option::" + }, + { + "name": "quantity", + "type": "core::integer::u256" + }, + { + "name": "start_amount", + "type": "core::integer::u256" + }, + { + "name": "end_amount", + "type": "core::integer::u256" + }, + { + "name": "start_date", + "type": "core::integer::u64" + }, + { + "name": "end_date", + "type": "core::integer::u64" + }, + { + "name": "broker_id", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "additional_data", + "type": "core::array::Span::" + } + ] + }, + { + "type": "struct", + "name": "ark_common::crypto::signer::SignInfo", + "members": [ + { + "name": "user_pubkey", + "type": "core::felt252" + }, + { + "name": "user_sig_r", + "type": "core::felt252" + }, + { + "name": "user_sig_s", + "type": "core::felt252" + } + ] + }, + { + "type": "enum", + "name": "ark_common::crypto::signer::Signer", + "variants": [ + { + "name": "WEIERSTRESS_STARKNET", + "type": "ark_common::crypto::signer::SignInfo" + } + ] + }, + { + "type": "struct", + "name": "ark_common::protocol::order_types::CancelInfo", + "members": [ + { + "name": "order_hash", + "type": "core::felt252" + }, + { + "name": "canceller", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "token_chain_id", + "type": "core::felt252" + }, + { + "name": "token_address", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "token_id", + "type": "core::option::Option::" + } + ] + }, + { + "type": "enum", + "name": "core::option::Option::", + "variants": [ + { + "name": "Some", + "type": "core::felt252" + }, + { + "name": "None", + "type": "()" + } + ] + }, + { + "type": "struct", + "name": "ark_common::protocol::order_types::FulfillInfo", + "members": [ + { + "name": "order_hash", + "type": "core::felt252" + }, + { + "name": "related_order_hash", + "type": "core::option::Option::" + }, + { + "name": "fulfiller", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "token_chain_id", + "type": "core::felt252" + }, + { + "name": "token_address", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "token_id", + "type": "core::option::Option::" + }, + { + "name": "fulfill_broker_address", + "type": "core::starknet::contract_address::ContractAddress" + } + ] + }, + { + "type": "enum", + "name": "ark_common::protocol::order_types::OrderType", + "variants": [ + { + "name": "Listing", + "type": "()" + }, + { + "name": "Auction", + "type": "()" + }, + { + "name": "Offer", + "type": "()" + }, + { + "name": "CollectionOffer", + "type": "()" + } + ] + }, + { + "type": "interface", + "name": "ark_orderbook::orderbook::Orderbook", + "items": [ + { + "type": "function", + "name": "whitelist_broker", + "inputs": [ + { + "name": "broker_id", + "type": "core::starknet::contract_address::ContractAddress" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "create_order", + "inputs": [ + { + "name": "order", + "type": "ark_orderbook::order::order_v1::OrderV1" + }, + { + "name": "signer", + "type": "ark_common::crypto::signer::Signer" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "cancel_order", + "inputs": [ + { + "name": "cancel_info", + "type": "ark_common::protocol::order_types::CancelInfo" + }, + { + "name": "signer", + "type": "ark_common::crypto::signer::Signer" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "fulfill_order", + "inputs": [ + { + "name": "fulfill_info", + "type": "ark_common::protocol::order_types::FulfillInfo" + }, + { + "name": "signer", + "type": "ark_common::crypto::signer::Signer" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "get_order_type", + "inputs": [ + { + "name": "order_hash", + "type": "core::felt252" + } + ], + "outputs": [ + { + "type": "ark_common::protocol::order_types::OrderType" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "get_order_status", + "inputs": [ + { + "name": "order_hash", + "type": "core::felt252" + } + ], + "outputs": [ + { + "type": "core::felt252" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "get_auction_expiration", + "inputs": [ + { + "name": "order_hash", + "type": "core::felt252" + } + ], + "outputs": [ + { + "type": "core::integer::u64" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "get_order", + "inputs": [ + { + "name": "order_hash", + "type": "core::felt252" + } + ], + "outputs": [ + { + "type": "ark_orderbook::order::order_v1::OrderV1" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "get_order_signer", + "inputs": [ + { + "name": "order_hash", + "type": "core::felt252" + } + ], + "outputs": [ + { + "type": "core::felt252" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "get_order_hash", + "inputs": [ + { + "name": "token_hash", + "type": "core::felt252" + } + ], + "outputs": [ + { + "type": "core::felt252" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "upgrade", + "inputs": [ + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "function", + "name": "update_starknet_executor_address", + "inputs": [ + { + "name": "value", + "type": "core::starknet::contract_address::ContractAddress" + } + ], + "outputs": [], + "state_mutability": "external" + } + ] + }, + { + "type": "constructor", + "name": "constructor", + "inputs": [ + { + "name": "admin", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "chain_id", + "type": "core::felt252" + } + ] + }, + { + "type": "struct", + "name": "ark_common::protocol::order_types::ExecutionValidationInfo", + "members": [ + { + "name": "order_hash", + "type": "core::felt252" + }, + { + "name": "transaction_hash", + "type": "core::felt252" + }, + { + "name": "starknet_block_timestamp", + "type": "core::integer::u64" + } + ] + }, + { + "type": "l1_handler", + "name": "validate_order_execution", + "inputs": [ + { + "name": "_from_address", + "type": "core::felt252" + }, + { + "name": "info", + "type": "ark_common::protocol::order_types::ExecutionValidationInfo" + } + ], + "outputs": [], + "state_mutability": "external" + }, + { + "type": "event", + "name": "ark_orderbook::orderbook::orderbook::OrderPlaced", + "kind": "struct", + "members": [ + { + "name": "order_hash", + "type": "core::felt252", + "kind": "key" + }, + { + "name": "order_version", + "type": "core::felt252", + "kind": "key" + }, + { + "name": "order_type", + "type": "ark_common::protocol::order_types::OrderType", + "kind": "key" + }, + { + "name": "cancelled_order_hash", + "type": "core::option::Option::", + "kind": "data" + }, + { + "name": "order", + "type": "ark_orderbook::order::order_v1::OrderV1", + "kind": "data" + } + ] + }, + { + "type": "enum", + "name": "ark_common::protocol::order_types::OrderStatus", + "variants": [ + { + "name": "Open", + "type": "()" + }, + { + "name": "Fulfilled", + "type": "()" + }, + { + "name": "Executed", + "type": "()" + }, + { + "name": "CancelledUser", + "type": "()" + }, + { + "name": "CancelledByNewOrder", + "type": "()" + }, + { + "name": "CancelledAssetFault", + "type": "()" + }, + { + "name": "CancelledOwnership", + "type": "()" + } + ] + }, + { + "type": "event", + "name": "ark_orderbook::orderbook::orderbook::OrderExecuted", + "kind": "struct", + "members": [ + { + "name": "order_hash", + "type": "core::felt252", + "kind": "key" + }, + { + "name": "order_status", + "type": "ark_common::protocol::order_types::OrderStatus", + "kind": "key" + } + ] + }, + { + "type": "event", + "name": "ark_orderbook::orderbook::orderbook::OrderCancelled", + "kind": "struct", + "members": [ + { + "name": "order_hash", + "type": "core::felt252", + "kind": "key" + }, + { + "name": "reason", + "type": "core::felt252", + "kind": "key" + } + ] + }, + { + "type": "event", + "name": "ark_orderbook::orderbook::orderbook::OrderFulfilled", + "kind": "struct", + "members": [ + { + "name": "order_hash", + "type": "core::felt252", + "kind": "key" + }, + { + "name": "fulfiller", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "key" + }, + { + "name": "related_order_hash", + "type": "core::option::Option::", + "kind": "key" + } + ] + }, + { + "type": "event", + "name": "ark_orderbook::orderbook::orderbook::Upgraded", + "kind": "struct", + "members": [ + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash", + "kind": "data" + } + ] + }, + { + "type": "event", + "name": "ark_orderbook::orderbook::orderbook::RollbackStatus", + "kind": "struct", + "members": [ + { + "name": "order_hash", + "type": "core::felt252", + "kind": "key" + }, + { + "name": "reason", + "type": "core::felt252", + "kind": "key" + } + ] + }, + { + "type": "event", + "name": "ark_orderbook::orderbook::orderbook::Event", + "kind": "enum", + "variants": [ + { + "name": "OrderPlaced", + "type": "ark_orderbook::orderbook::orderbook::OrderPlaced", + "kind": "nested" + }, + { + "name": "OrderExecuted", + "type": "ark_orderbook::orderbook::orderbook::OrderExecuted", + "kind": "nested" + }, + { + "name": "OrderCancelled", + "type": "ark_orderbook::orderbook::orderbook::OrderCancelled", + "kind": "nested" + }, + { + "name": "OrderFulfilled", + "type": "ark_orderbook::orderbook::orderbook::OrderFulfilled", + "kind": "nested" + }, + { + "name": "Upgraded", + "type": "ark_orderbook::orderbook::orderbook::Upgraded", + "kind": "nested" + }, + { + "name": "RollbackStatus", + "type": "ark_orderbook::orderbook::orderbook::RollbackStatus", + "kind": "nested" + } + ] + } +] diff --git a/artifacts/starknet_utils.json b/artifacts/starknet_utils.json new file mode 100644 index 0000000000..2abf79b609 --- /dev/null +++ b/artifacts/starknet_utils.json @@ -0,0 +1,224 @@ +[ + { + "type": "struct", + "name": "package_name::ExecutionInfo", + "members": [ + { + "name": "order_hash", + "type": "core::felt252" + }, + { + "name": "nft_address", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "nft_from", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "nft_to", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "nft_token_id", + "type": "core::integer::u256" + }, + { + "name": "payment_from", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "payment_to", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "payment_amount", + "type": "core::integer::u256" + }, + { + "name": "payment_currency_address", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "listing_broker_address", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "fulfill_broker_address", + "type": "core::starknet::contract_address::ContractAddress" + } + ] + }, + { + "type": "struct", + "name": "core::integer::u256", + "members": [ + { + "name": "low", + "type": "core::integer::u128" + }, + { + "name": "high", + "type": "core::integer::u128" + } + ] + }, + { + "type": "function", + "name": "owner_of", + "inputs": [ + { + "name": "token_id", + "type": "core::integer::u256" + } + ], + "outputs": [ + { + "type": "core::starknet::contract_address::ContractAddress" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "ownerOf", + "inputs": [ + { + "name": "token_id", + "type": "core::integer::u256" + } + ], + "outputs": [ + { + "type": "core::starknet::contract_address::ContractAddress" + } + ], + "state_mutability": "view" + }, + { + "type": "enum", + "name": "core::bool", + "variants": [ + { + "name": "False", + "type": "()" + }, + { + "name": "True", + "type": "()" + } + ] + }, + { + "type": "function", + "name": "is_approved_for_all", + "inputs": [ + { + "name": "owner", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "operator", + "type": "core::starknet::contract_address::ContractAddress" + } + ], + "outputs": [ + { + "type": "core::bool" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "isApprovedForAll", + "inputs": [ + { + "name": "owner", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "operator", + "type": "core::starknet::contract_address::ContractAddress" + } + ], + "outputs": [ + { + "type": "core::bool" + } + ], + "state_mutability": "view" + }, + { + "type": "struct", + "name": "core::array::Span::", + "members": [ + { + "name": "snapshot", + "type": "@core::array::Array::" + } + ] + }, + { + "type": "function", + "name": "is_valid_signature", + "inputs": [ + { + "name": "hash", + "type": "core::felt252" + }, + { + "name": "signature", + "type": "core::array::Span::" + } + ], + "outputs": [ + { + "type": "core::bool" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "balanceOf", + "inputs": [ + { + "name": "account", + "type": "core::starknet::contract_address::ContractAddress" + } + ], + "outputs": [ + { + "type": "core::integer::u256" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "allowance", + "inputs": [ + { + "name": "owner", + "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "spender", + "type": "core::starknet::contract_address::ContractAddress" + } + ], + "outputs": [ + { + "type": "core::integer::u256" + } + ], + "state_mutability": "view" + }, + { + "type": "event", + "name": "solis::starknet_utils::starknet_utils::Event", + "kind": "enum", + "variants": [] + } +] diff --git a/bin/katana/src/args.rs b/bin/katana/src/args.rs index 3c3c99499c..8eb4a84524 100644 --- a/bin/katana/src/args.rs +++ b/bin/katana/src/args.rs @@ -242,6 +242,8 @@ impl KatanaArgs { host: self.server.host.clone().unwrap_or("0.0.0.0".into()), max_connections: self.server.max_connections, allowed_origins: self.server.allowed_origins.clone(), + rpc_user: "user".into(), + rpc_password: "password".into(), } } diff --git a/bin/katana/src/main.rs b/bin/katana/src/main.rs index ffd96c4dec..f0d8668f92 100644 --- a/bin/katana/src/main.rs +++ b/bin/katana/src/main.rs @@ -93,8 +93,9 @@ async fn main() -> Result<(), Box> { .await?; } - let sequencer = - Arc::new(KatanaSequencer::new(executor_factory, sequencer_config, starknet_config).await?); + let sequencer = Arc::new( + KatanaSequencer::new(executor_factory, sequencer_config, starknet_config, None).await?, + ); let NodeHandle { addr, handle, .. } = spawn(Arc::clone(&sequencer), server_config).await?; if !args.silent { diff --git a/bin/solis/.env.example b/bin/solis/.env.example new file mode 100644 index 0000000000..eba6147747 --- /dev/null +++ b/bin/solis/.env.example @@ -0,0 +1,2 @@ +RPC_USER=user +RPC_PASSWORD=password diff --git a/bin/solis/Cargo.toml b/bin/solis/Cargo.toml new file mode 100644 index 0000000000..0a0c5a07d9 --- /dev/null +++ b/bin/solis/Cargo.toml @@ -0,0 +1,49 @@ +[package] +description = "Modified katana to match ArkProject requirement." +edition.workspace = true +license-file.workspace = true +name = "solis" +repository.workspace = true +version.workspace = true + +[dependencies] +alloy-primitives.workspace = true +anyhow.workspace = true +async-trait = "0.1.57" +cainome = { git = "https://github.com/cartridge-gg/cainome", tag = "v0.2.6", features = [ + "abigen-rs", +] } +cfg-if = "1.0.0" +clap.workspace = true +clap_complete.workspace = true +common.workspace = true +console.workspace = true +dojo-metrics.workspace = true +dotenv = "0.15.0" +katana-core.workspace = true +katana-executor.workspace = true +katana-primitives.workspace = true +katana-rpc-api.workspace = true +katana-rpc.workspace = true +serde_json.workspace = true +shellexpand = "3.1.0" +starknet = "0.9.0" +starknet_api = "0.10.0" +tokio.workspace = true +tracing-subscriber.workspace = true +tracing.workspace = true +url.workspace = true + +[dev-dependencies] +assert_matches = "1.5.0" + +[features] +default = [ "blockifier", "jemalloc", "messaging" ] + +blockifier = [ "katana-executor/blockifier" ] +# Disable until SIR support Cairo 2.6.3 +#sir = [ "katana-executor/sir" ] + +jemalloc = [ "dojo-metrics/jemalloc" ] +messaging = [ "katana-core/messaging" ] +starknet-messaging = [ "katana-core/starknet-messaging", "messaging" ] diff --git a/bin/solis/src/args.rs b/bin/solis/src/args.rs new file mode 100644 index 0000000000..9fe912a31d --- /dev/null +++ b/bin/solis/src/args.rs @@ -0,0 +1,347 @@ +//! Katana binary executable. +//! +//! ## Feature Flags +//! +//! - `jemalloc`: Uses [jemallocator](https://github.com/tikv/jemallocator) as the global allocator. +//! This is **not recommended on Windows**. See [here](https://rust-lang.github.io/rfcs/1974-global-allocators.html#jemalloc) +//! for more info. +//! - `jemalloc-prof`: Enables [jemallocator's](https://github.com/tikv/jemallocator) heap profiling +//! and leak detection functionality. See [jemalloc's opt.prof](https://jemalloc.net/jemalloc.3.html#opt.prof) +//! documentation for usage details. This is **not recommended on Windows**. See [here](https://rust-lang.github.io/rfcs/1974-global-allocators.html#jemalloc) +//! for more info. +use std::env; +use std::net::SocketAddr; +use std::path::PathBuf; + +use alloy_primitives::U256; +use clap::{Args, Parser, Subcommand}; +use clap_complete::Shell; +use common::parse::parse_socket_address; +use katana_core::backend::config::{Environment, StarknetConfig}; +use katana_core::constants::{ + DEFAULT_ETH_L1_GAS_PRICE, DEFAULT_INVOKE_MAX_STEPS, DEFAULT_SEQUENCER_ADDRESS, + DEFAULT_STRK_L1_GAS_PRICE, DEFAULT_VALIDATE_MAX_STEPS, +}; +use katana_core::sequencer::SequencerConfig; +use katana_primitives::block::GasPrices; +use katana_primitives::chain::ChainId; +use katana_primitives::genesis::allocation::DevAllocationsGenerator; +use katana_primitives::genesis::constant::DEFAULT_PREFUNDED_ACCOUNT_BALANCE; +use katana_primitives::genesis::Genesis; +use katana_rpc::config::ServerConfig; +use katana_rpc_api::ApiKind; +use tracing::Subscriber; +use tracing_subscriber::{fmt, EnvFilter}; +use url::Url; + +use crate::utils::{parse_genesis, parse_seed}; + +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +#[command(propagate_version = true)] +pub struct KatanaArgs { + #[arg(long)] + #[arg(help = "Don't print anything on startup.")] + pub silent: bool, + + #[arg(long)] + #[arg(conflicts_with = "block_time")] + #[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 = "MILLISECONDS")] + #[arg(help = "Block time in milliseconds for interval mining.")] + pub block_time: Option, + + #[arg(long)] + #[arg(value_name = "PATH")] + #[arg(help = "Directory path of the database to initialize from.")] + #[arg(long_help = "Directory path of the database to initialize from. The path must either \ + be an empty directory or a directory which already contains a previously \ + initialized Katana database.")] + pub db_dir: Option, + + #[arg(long)] + #[arg(value_name = "URL")] + #[arg(help = "The Starknet RPC provider to fork the network from.")] + pub rpc_url: Option, + + #[arg(long)] + pub dev: bool, + + #[arg(long)] + #[arg(help = "Output logs in JSON format.")] + pub json_log: bool, + + /// Enable Prometheus metrics. + /// + /// The metrics will be served at the given interface and port. + #[arg(long, value_name = "SOCKET", value_parser = parse_socket_address, help_heading = "Metrics")] + pub metrics: 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, + + #[cfg(feature = "messaging")] + #[arg(long)] + #[arg(value_name = "PATH")] + #[arg(value_parser = katana_core::service::messaging::MessagingConfig::parse)] + #[arg(help = "Configure the messaging with an other chain.")] + #[arg(long_help = "Configure the messaging to allow Katana listening/sending messages on a \ + settlement chain that can be Ethereum or an other Starknet sequencer. \ + The configuration file details and examples can be found here: https://book.dojoengine.org/toolchain/katana/reference#messaging")] + pub messaging: Option, + + #[command(flatten)] + #[command(next_help_heading = "Server options")] + pub server: ServerOptions, + + #[command(flatten)] + #[command(next_help_heading = "Starknet options")] + pub starknet: StarknetOptions, + + #[command(subcommand)] + pub command: Option, +} + +#[derive(Debug, Subcommand)] +pub enum Commands { + #[command(about = "Generate shell completion file for specified shell")] + Completions { shell: Shell }, +} + +#[derive(Debug, Args, Clone)] +pub struct ServerOptions { + #[arg(short, long)] + #[arg(default_value = "7777")] + #[arg(help = "Port number to listen on.")] + pub port: u16, + + #[arg(long)] + #[arg(help = "The IP address the server will listen on.")] + pub host: Option, + + #[arg(long)] + #[arg(default_value = "100")] + #[arg(help = "Maximum number of concurrent connections allowed.")] + pub max_connections: u32, + + #[arg(long)] + #[arg(value_delimiter = ',')] + #[arg(help = "Enables the CORS layer and sets the allowed origins, separated by commas.")] + pub allowed_origins: Option>, +} + +#[derive(Debug, Args, Clone)] +pub struct StarknetOptions { + #[arg(long)] + #[arg(default_value = "0")] + #[arg(help = "Specify the seed for randomness of accounts to be predeployed.")] + pub seed: String, + + #[arg(long = "accounts")] + #[arg(value_name = "NUM")] + #[arg(default_value = "10")] + #[arg(help = "Number of pre-funded accounts to generate.")] + pub total_accounts: u16, + + #[arg(long)] + #[arg(help = "Disable charging fee when executing transactions.")] + pub disable_fee: bool, + + #[arg(long)] + #[arg(help = "Disable validation when executing transactions.")] + pub disable_validate: bool, + + #[command(flatten)] + #[command(next_help_heading = "Environment options")] + pub environment: EnvironmentOptions, + + #[arg(long)] + #[arg(value_parser = parse_genesis)] + #[arg(conflicts_with_all(["rpc_url", "seed", "total_accounts"]))] + pub genesis: Option, +} + +#[derive(Debug, Args, Clone)] +pub struct EnvironmentOptions { + #[arg(long)] + #[arg(help = "The chain ID.")] + #[arg(long_help = "The chain ID. If a raw hex string (`0x` prefix) is provided, then it'd \ + used as the actual chain ID. Otherwise, it's represented as the raw \ + ASCII values. It must be a valid Cairo short string.")] + #[arg(default_value = "SOLIS")] + #[arg(value_parser = ChainId::parse)] + pub chain_id: ChainId, + + #[arg(long)] + #[arg(help = "The maximum number of steps available for the account validation logic.")] + #[arg(default_value_t = DEFAULT_VALIDATE_MAX_STEPS)] + pub validate_max_steps: u32, + + #[arg(long)] + #[arg(help = "The maximum number of steps available for the account execution logic.")] + #[arg(default_value_t = DEFAULT_INVOKE_MAX_STEPS)] + pub invoke_max_steps: u32, + + #[arg(long = "eth-gas-price")] + #[arg(conflicts_with = "genesis")] + #[arg(help = "The L1 ETH gas price. (denominated in wei)")] + #[arg(default_value_t = DEFAULT_ETH_L1_GAS_PRICE)] + pub l1_eth_gas_price: u128, + + #[arg(long = "strk-gas-price")] + #[arg(conflicts_with = "genesis")] + #[arg(help = "The L1 STRK gas price. (denominated in fri)")] + #[arg(default_value_t = DEFAULT_STRK_L1_GAS_PRICE)] + pub l1_strk_gas_price: u128, +} + +impl KatanaArgs { + pub fn init_logging(&self) -> Result<(), Box> { + const DEFAULT_LOG_FILTER: &str = "info,executor=trace,forked_backend=trace,server=debug,\ + katana_core=trace,blockifier=off,jsonrpsee_server=off,\ + hyper=off,messaging=debug,node=error"; + + let builder = fmt::Subscriber::builder().with_env_filter( + EnvFilter::try_from_default_env().or(EnvFilter::try_new(DEFAULT_LOG_FILTER))?, + ); + + let subscriber: Box = if self.json_log { + Box::new(builder.json().finish()) + } else { + Box::new(builder.finish()) + }; + + Ok(tracing::subscriber::set_global_default(subscriber)?) + } + + pub fn sequencer_config(&self) -> SequencerConfig { + SequencerConfig { + block_time: self.block_time, + no_mining: self.no_mining, + #[cfg(feature = "messaging")] + messaging: self.messaging.clone(), + } + } + + pub fn server_config(&self) -> ServerConfig { + let mut apis = vec![ApiKind::Starknet, ApiKind::Katana, ApiKind::Torii, ApiKind::Saya, ApiKind::Solis]; + // only enable `katana` API in dev mode + if self.dev { + apis.push(ApiKind::Dev); + } + + let rpc_user = env::var("RPC_USER").unwrap_or_else(|_| "userDefault".to_string()); + let rpc_password = + env::var("RPC_PASSWORD").unwrap_or_else(|_| "passwordDefault".to_string()); + + ServerConfig { + apis, + port: self.server.port, + host: self.server.host.clone().unwrap_or("0.0.0.0".into()), + max_connections: self.server.max_connections, + allowed_origins: self.server.allowed_origins.clone(), + rpc_user, + rpc_password, + } + } + + pub fn starknet_config(&self) -> StarknetConfig { + let genesis = match self.starknet.genesis.clone() { + Some(genesis) => genesis, + None => { + let gas_prices = GasPrices { + eth: self.starknet.environment.l1_eth_gas_price, + strk: self.starknet.environment.l1_strk_gas_price, + }; + + let accounts = DevAllocationsGenerator::new(self.starknet.total_accounts) + .with_seed(parse_seed(&self.starknet.seed)) + .with_balance(U256::from(DEFAULT_PREFUNDED_ACCOUNT_BALANCE)) + .generate(); + + let mut genesis = Genesis { + gas_prices, + sequencer_address: *DEFAULT_SEQUENCER_ADDRESS, + ..Default::default() + }; + + genesis.extend_allocations(accounts.into_iter().map(|(k, v)| (k, v.into()))); + genesis + } + }; + + StarknetConfig { + disable_fee: self.starknet.disable_fee, + disable_validate: self.starknet.disable_validate, + fork_rpc_url: self.rpc_url.clone(), + fork_block_number: self.fork_block_number, + env: Environment { + chain_id: self.starknet.environment.chain_id, + invoke_max_steps: self.starknet.environment.invoke_max_steps, + validate_max_steps: self.starknet.environment.validate_max_steps, + }, + db_dir: self.db_dir.clone(), + genesis, + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_starknet_config_default() { + let args = KatanaArgs::parse_from(["katana"]); + let config = args.starknet_config(); + + assert!(!config.disable_fee); + assert!(!config.disable_validate); + assert_eq!(config.fork_rpc_url, None); + assert_eq!(config.fork_block_number, None); + assert_eq!(config.env.chain_id, ChainId::parse("KATANA").unwrap()); + assert_eq!(config.env.invoke_max_steps, DEFAULT_INVOKE_MAX_STEPS); + assert_eq!(config.env.validate_max_steps, DEFAULT_VALIDATE_MAX_STEPS); + assert_eq!(config.db_dir, None); + assert_eq!(config.genesis.gas_prices.eth, DEFAULT_ETH_L1_GAS_PRICE); + assert_eq!(config.genesis.gas_prices.strk, DEFAULT_STRK_L1_GAS_PRICE); + assert_eq!(config.genesis.sequencer_address, *DEFAULT_SEQUENCER_ADDRESS); + } + + #[test] + fn test_starknet_config_custom() { + let args = KatanaArgs::parse_from([ + "katana", + "--disable-fee", + "--disable-validate", + "--chain-id", + "SN_GOERLI", + "--invoke-max-steps", + "200", + "--validate-max-steps", + "100", + "--db-dir", + "/path/to/db", + "--eth-gas-price", + "10", + "--strk-gas-price", + "20", + ]); + let config = args.starknet_config(); + + assert!(config.disable_fee); + assert!(config.disable_validate); + assert_eq!(config.env.chain_id, ChainId::GOERLI); + assert_eq!(config.env.invoke_max_steps, 200); + assert_eq!(config.env.validate_max_steps, 100); + assert_eq!(config.db_dir, Some(PathBuf::from("/path/to/db"))); + assert_eq!(config.genesis.gas_prices.eth, 10); + assert_eq!(config.genesis.gas_prices.strk, 20); + } +} diff --git a/bin/solis/src/contracts/account.rs b/bin/solis/src/contracts/account.rs new file mode 100644 index 0000000000..f19a7d9c6c --- /dev/null +++ b/bin/solis/src/contracts/account.rs @@ -0,0 +1,42 @@ +use starknet::{ + accounts::{ExecutionEncoding, SingleOwnerAccount}, + core::types::FieldElement, + providers::{jsonrpc::HttpTransport, AnyProvider, JsonRpcClient, Provider}, + signers::{LocalWallet, SigningKey}, +}; + +use url::Url; + +/// Initializes a new account to interact with Starknet. +/// +/// # Arguments +/// +/// * `provider_url` - Starknet provider's url. +/// * `account_address` - Starknet account's address. +/// * `private_key` - Private key associated to the Starknet account. +#[allow(dead_code)] +pub async fn new_account( + provider_url: &str, + account_address: FieldElement, + private_key: FieldElement, +) -> SingleOwnerAccount { + let rpc_url = Url::parse(provider_url).expect("Expecting valid Starknet RPC URL"); + let provider = + AnyProvider::JsonRpcHttp(JsonRpcClient::new(HttpTransport::new(rpc_url.clone()))); + + // TODO: need error instead of expect. + let chain_id = provider + .chain_id() + .await + .expect("couldn't get chain_id from provider"); + + let signer = LocalWallet::from(SigningKey::from_secret_scalar(private_key)); + + SingleOwnerAccount::new( + provider, + signer, + account_address, + chain_id, + ExecutionEncoding::Legacy, + ) +} diff --git a/bin/solis/src/contracts/executor.rs b/bin/solis/src/contracts/executor.rs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/bin/solis/src/contracts/mod.rs b/bin/solis/src/contracts/mod.rs new file mode 100644 index 0000000000..df2f73e1e5 --- /dev/null +++ b/bin/solis/src/contracts/mod.rs @@ -0,0 +1,3 @@ +pub mod account; +pub mod orderbook; +pub mod starknet_utils; diff --git a/bin/solis/src/contracts/orderbook.rs b/bin/solis/src/contracts/orderbook.rs new file mode 100644 index 0000000000..9bc801643b --- /dev/null +++ b/bin/solis/src/contracts/orderbook.rs @@ -0,0 +1,12 @@ +use cainome::rs::abigen; +use starknet::{accounts::ConnectedAccount, core::types::FieldElement}; + +abigen!(OrderbookContract, "./artifacts/orderbook.abi.json"); + +#[allow(dead_code)] +pub fn new_orderbook( + contract_address: FieldElement, + account: A, +) -> OrderbookContract { + OrderbookContract::new(contract_address, account) +} diff --git a/bin/solis/src/contracts/starknet_utils.rs b/bin/solis/src/contracts/starknet_utils.rs new file mode 100644 index 0000000000..280c9f2e6d --- /dev/null +++ b/bin/solis/src/contracts/starknet_utils.rs @@ -0,0 +1,19 @@ +use cainome::rs::abigen; +use starknet::{ + core::types::FieldElement, + providers::{jsonrpc::HttpTransport, AnyProvider, JsonRpcClient}, +}; + +use url::Url; + +abigen!(StarknetUtils, "./artifacts/starknet_utils.json"); +pub fn new_starknet_utils_reader( + contract_address: FieldElement, + provider_url: &str, +) -> StarknetUtilsReader { + let rpc_url = Url::parse(provider_url).expect("Expecting valid Starknet RPC URL"); + let provider = + AnyProvider::JsonRpcHttp(JsonRpcClient::new(HttpTransport::new(rpc_url.clone()))); + + StarknetUtilsReader::new(contract_address, provider) +} diff --git a/bin/solis/src/hooker.rs b/bin/solis/src/hooker.rs new file mode 100644 index 0000000000..a967b65139 --- /dev/null +++ b/bin/solis/src/hooker.rs @@ -0,0 +1,557 @@ +//! Solis hooker on Katana transaction lifecycle. +//! +use crate::contracts::starknet_utils::{ExecutionInfo, U256}; +use async_trait::async_trait; +use cainome::cairo_serde::CairoSerde; +use cainome::rs::abigen; +use katana_core::hooker::{HookerAddresses, KatanaHooker}; +use katana_core::sequencer::KatanaSequencer; +use katana_executor::ExecutorFactory; + +use katana_primitives::chain::ChainId; +use katana_primitives::contract::ContractAddress; +use katana_primitives::transaction::{ExecutableTx, ExecutableTxWithHash, L1HandlerTx}; +use katana_primitives::utils::transaction::compute_l1_message_hash; +use serde_json::json; +use serde_json::Value; +use starknet::accounts::Call; +use starknet::core::types::BroadcastedInvokeTransaction; +use starknet::core::types::FieldElement; +use starknet::macros::selector; +use starknet::providers::Provider; +use std::fs::File; +use std::fs::OpenOptions; +use std::io::Read; +use std::io::Write; +use std::path::Path; +use std::sync::Arc; + +const FILE_PATH_ADDRESSES: &str = "addresses.json"; + +use crate::contracts::orderbook::{OrderV1, RouteType}; +use crate::contracts::starknet_utils::StarknetUtilsReader; +use crate::CHAIN_ID_SOLIS; +use tracing::info; + +#[allow(dead_code)] +pub enum CancelStatus { + CancelledUser, + CancelledByNewOrder, + CancelledAssetFault, + CancelledOwnership, +} + +impl CancelStatus { + fn to_u32(&self) -> u32 { + match self { + CancelStatus::CancelledUser => 1, + CancelStatus::CancelledByNewOrder => 2, + CancelStatus::CancelledAssetFault => 3, + CancelStatus::CancelledOwnership => 4, + } + } +} + +struct OwnershipVerifier { + token_address: ContractAddress, + token_id: U256, + current_owner: cainome::cairo_serde::ContractAddress, +} + +struct BalanceVerifier { + currency_address: ContractAddress, + offerer: cainome::cairo_serde::ContractAddress, + start_amount: U256, +} + +abigen!(CallContract, "./artifacts/contract.abi.json"); + +/// Hooker struct, with already instanciated contracts/readers +/// to avoid allocating them at each transaction that is being +/// verified. +pub struct SolisHooker { + // Solis interacts with the orderbook only via `L1HandlerTransaction`. Only the + // address is required. + pub orderbook_address: FieldElement, + // TODO: replace this by the Executor Contract object! + pub sn_executor_address: FieldElement, + pub sn_utils_reader: StarknetUtilsReader

, + sequencer: Option>>, +} + +impl + SolisHooker +{ + /// Verify the ownership of a token + async fn verify_ownership(&self, ownership_verifier: &OwnershipVerifier) -> bool { + let sn_utils_reader_nft_address = StarknetUtilsReader::new( + ownership_verifier.token_address.into(), + self.sn_utils_reader.provider(), + ); + + // check the current owner of the token. + let owner = sn_utils_reader_nft_address.ownerOf(&ownership_verifier.token_id).call().await; + + if let Ok(owner_address) = owner { + if owner_address != ownership_verifier.current_owner { + tracing::trace!( + "\nOwner {:?} differs from offerer {:?} ", + owner, + ownership_verifier.current_owner + ); + + println!( + "\nOwner {:?} differs from offerer {:?} ", + owner, ownership_verifier.current_owner + ); + + return false; + } + } + + true + } + + async fn verify_balance(&self, balance_verifier: &BalanceVerifier) -> bool { + info!("HOOKER: Verify Balance"); + + let sn_utils_reader_erc20_address = StarknetUtilsReader::new( + balance_verifier.currency_address.into(), + self.sn_utils_reader.provider(), + ); + let allowance = sn_utils_reader_erc20_address + .allowance(&balance_verifier.offerer, &self.sn_executor_address.into()) + .call() + .await; + + info!( + "HOOKER: Verify Balance allowance {:?}, amount {:?}", + allowance, balance_verifier.start_amount + ); + + if let Ok(allowance) = allowance { + if allowance < balance_verifier.start_amount { + println!( + "\nAllowance {:?} is not enough {:?} for offerer {:?}", + allowance, balance_verifier.start_amount, balance_verifier.offerer + ); + return false; + } + } + + // check the balance + let balance = + sn_utils_reader_erc20_address.balanceOf(&balance_verifier.offerer).call().await; + if let Ok(balance) = balance { + if balance < balance_verifier.start_amount { + tracing::trace!( + "\nBalance {:?} is not enough {:?} ", + balance, + balance_verifier.start_amount + ); + println!( + "\nBalance {:?} is not enough {:?} ", + balance, balance_verifier.start_amount + ); + return false; + } + } + + true + } + + async fn verify_call(&self, call: &TxCall) -> bool { + let order = match OrderV1::cairo_deserialize(&call.calldata, 0) { + Ok(order) => order, + Err(e) => { + tracing::error!("Fail deserializing OrderV1: {:?}", e); + return false; + } + }; + + // ERC721 to ERC20 + if order.route == RouteType::Erc721ToErc20 { + let token_id = order.token_id.clone().unwrap(); + let n_token_id = U256 { low: token_id.low, high: token_id.high }; + + let verifier = OwnershipVerifier { + token_address: ContractAddress(order.token_address.into()), + token_id: n_token_id, + current_owner: cainome::cairo_serde::ContractAddress(order.offerer.into()), + }; + + let owner_ship_verification = self.verify_ownership(&verifier).await; + if !owner_ship_verification { + return false; + } + } + + // ERC20 to ERC721 : we check the allowance and the offerer balance. + if order.route == RouteType::Erc20ToErc721 { + if !self + .verify_balance(&BalanceVerifier { + currency_address: ContractAddress(order.currency_address.into()), + offerer: cainome::cairo_serde::ContractAddress(order.offerer.into()), + start_amount: U256 { + low: order.start_amount.low, + high: order.start_amount.high, + }, + }) + .await + { + println!("verify balance for starknet before failed"); + return false; + } + } + return true; + } +} + +impl + SolisHooker +{ + fn get_addresses_from_file() -> Result<(FieldElement, FieldElement), Box> + { + let mut file = match File::open(FILE_PATH_ADDRESSES) { + Ok(file) => file, + Err(_) => return Err("File not found".into()), + }; + + let mut contents = String::new(); + file.read_to_string(&mut contents)?; + + let v: Value = match serde_json::from_str(&contents) { + Ok(value) => value, + Err(_) => return Err("Error parsing JSON".into()), + }; + + let orderbook_address = match v["orderbook_address"].as_str() { + Some(address) => address, + None => return Err("orderbook_address key not found in JSON".into()), + }; + + let sn_executor_address = match v["sn_executor_address"].as_str() { + Some(address) => address, + None => return Err("sn_executor_address key not found in JSON".into()), + }; + + let orderbook_address = match FieldElement::from_hex_be(&orderbook_address[2..]) { + Ok(val) => FieldElement::from_dec_str(&val.to_string()), + Err(_) => return Err("Failed to parse orderbook_address".into()), + }; + + let sn_executor_address = match FieldElement::from_hex_be(&sn_executor_address[2..]) { + Ok(val) => FieldElement::from_dec_str(&val.to_string()), + Err(_) => return Err("Failed to parse sn_executor_address".into()), + }; + + println!("Addresses loaded from file: {:?}, {:?}", orderbook_address, sn_executor_address); + Ok((orderbook_address?, sn_executor_address?)) + } + + /// Initializes a new instance. + pub fn new( + sn_utils_reader: StarknetUtilsReader

, + orderbook_address: FieldElement, + sn_executor_address: FieldElement, + ) -> Self { + let (orderbook_address, sn_executor_address) = if orderbook_address == FieldElement::ZERO + && sn_executor_address == FieldElement::ZERO + { + match Self::get_addresses_from_file() { + Ok((orderbook, executor)) => (orderbook, executor), + Err(e) => { + eprintln!("Error reading addresses from file: {}", e); + (orderbook_address, sn_executor_address) + } + } + } else { + (orderbook_address, sn_executor_address) + }; + + Self { orderbook_address, sn_utils_reader, sn_executor_address, sequencer: None } + } + + /// Retrieves a reference to the sequencer. + #[allow(dead_code)] + pub fn sequencer_ref(&self) -> &Arc> { + // The expect is used here as it must always be set by Katana core. + // If not set, the merge on Katana may be revised. + self.sequencer.as_ref().expect("Sequencer must be set to get a reference to it") + } + + /// Adds a `L1HandlerTransaction` to the transaction pool that is directed to the + /// orderbook only. + /// `L1HandlerTransaction` is a special type of transaction that can only be + /// sent by the sequencer itself. This transaction is not validated by any account. + /// + /// In the case of Solis, `L1HandlerTransaction` are sent by Solis for two purposes: + /// 1. A message was collected from the L2, and it must be executed. + /// 2. A transaction has been rejected by Solis (asset faults), and the order + /// must then be updated. + /// + /// This function is used for the scenario 2. For this reason, the `from_address` + /// field is automatically filled up by the sequencer to use the executor address + /// deployed on L2 to avoid any attack by other contracts. + /// + /// # Arguments + /// + /// * `selector` - The selector of the recipient contract to execute. + /// * `payload` - The payload of the message. + #[allow(dead_code)] + pub fn add_l1_handler_transaction_for_orderbook( + &self, + selector: FieldElement, + payload: &[FieldElement], + ) { + let to_address = self.orderbook_address; + let from_address = self.sn_executor_address; + let chain_id = ChainId::Id(CHAIN_ID_SOLIS); + + // The nonce is normally used by the messaging contract on Starknet. But in the + // case of those transaction, as they are only sent by Solis itself, we use 0. + // TODO: this value of 0 must be checked by the `l1_handler` function. + let nonce = FieldElement::ZERO; + + // The calldata always starts with the from_address. + let mut calldata: Vec = vec![from_address]; + for p in payload.into_iter() { + calldata.push(*p); + } + + let message_hash = compute_l1_message_hash(from_address, to_address, payload); + + let tx = L1HandlerTx { + nonce, + chain_id, + paid_fee_on_l1: 30000_u128, + version: FieldElement::ZERO, + message_hash, + calldata, + contract_address: ContractAddress(to_address), + entry_point_selector: selector, + }; + + if let Some(seq) = &self.sequencer { + let exe = ExecutableTxWithHash::new_query(ExecutableTx::L1Handler(tx), false); + seq.add_transaction_to_pool(exe); + } + } +} + +/// Solis hooker relies on verifiers to inspect and verify +/// the transaction and starknet state before acceptance. +#[async_trait] +impl KatanaHooker + for SolisHooker +{ + fn set_sequencer(&mut self, sequencer: Arc>) { + self.sequencer = Some(sequencer); + } + + fn set_addresses(&mut self, addresses: HookerAddresses) { + info!("HOOKER: Addresses set for hooker: {:?}", addresses); + self.orderbook_address = addresses.orderbook_arkchain; + self.sn_executor_address = addresses.executor_starknet; + + let path = Path::new(FILE_PATH_ADDRESSES); + let file = OpenOptions::new().write(true).create(true).open(&path); + + match file { + Ok(mut file) => { + let data = json!({ + "orderbook_address": format!("{:#x}", self.orderbook_address), + "sn_executor_address": format!("{:#x}", self.sn_executor_address) + }); + + if let Err(e) = writeln!(file, "{}", data.to_string()) { + eprintln!("Error writing file : {}", e); + } + } + Err(e) => { + eprintln!("Error opening file : {}", e); + } + } + } + + /// Verifies if the message is directed to the orderbook and comes from + /// the executor contract on L2. + /// + /// Currently, only the `from` and `to` are checked. + /// More checks may be added on the selector, and the data. + async fn verify_message_to_appchain( + &self, + from: FieldElement, + to: FieldElement, + _selector: FieldElement, + ) -> bool { + info!( + "HOOKER: verify_message_to_appchain called with from: {:?}, to: {:?}, {:?}, {:?}", + from, to, self.sn_executor_address, self.orderbook_address + ); + // For now, only the from/to are checked. + from == self.sn_executor_address && to == self.orderbook_address + } + + /// Verifies an invoke transaction that is: + /// 1. Directed to the orderbook only. + /// 2. With the selector `create_order` only as the fulfill + /// is verified by `verify_message_to_starknet_before_tx`. + async fn verify_invoke_tx_before_pool( + &self, + transaction: BroadcastedInvokeTransaction, + ) -> bool { + info!("HOOKER: verify_invoke_tx_before_pool called with transaction: {:?}", transaction); + + let calldata = match transaction { + BroadcastedInvokeTransaction::V1(v1_transaction) => v1_transaction.calldata, + BroadcastedInvokeTransaction::V3(v3_transaction) => v3_transaction.calldata, + }; + info!("HOOKER: cairo_deserialize called with transaction: {:?}", calldata); + + let calls = match Vec::::cairo_deserialize(&calldata, 0) { + Ok(calls) => calls, + Err(e) => { + tracing::error!("Fail deserializing OrderV1: {:?}", e); + return false; + } + }; + + for call in calls { + if call.selector != selector!("create_order") + && call.selector != selector!("create_order_from_l2") + && call.selector != selector!("fulfill_order_from_l2") + { + continue; + } + + if !self.verify_call(&call).await { + return false; + } + + // TODO: check assets on starknet. + // TODO: if not valid, in some cases we want to send L1HandlerTransaction + // to change the status of the order. (entrypoint to be written). + } + true + } + + async fn verify_tx_for_starknet(&self, call: Call) -> bool { + println!("verify message to starknet before tx: {:?}", call); + if call.selector != selector!("fulfill_order") { + return true; + } + + let execution_info = match ExecutionInfo::cairo_deserialize(&call.calldata, 0) { + Ok(execution_info) => execution_info, + Err(e) => { + tracing::error!("Fail deserializing ExecutionInfo: {:?}", e); + return false; + } + }; + + let verifier = OwnershipVerifier { + token_address: ContractAddress(execution_info.nft_address.into()), + token_id: execution_info.nft_token_id, + current_owner: cainome::cairo_serde::ContractAddress(execution_info.nft_from.into()), + }; + + let owner_ship_verification = self.verify_ownership(&verifier).await; + if !owner_ship_verification { + // rollback the status + let status = CancelStatus::CancelledOwnership; + + self.add_l1_handler_transaction_for_orderbook( + selector!("rollback_status_order"), + &[execution_info.order_hash, status.to_u32().into()], + ); + return false; + } + + if !self + .verify_balance(&BalanceVerifier { + currency_address: ContractAddress(execution_info.payment_currency_address.into()), + offerer: cainome::cairo_serde::ContractAddress(execution_info.nft_to.into()), + start_amount: U256 { + low: execution_info.payment_amount.low, + high: execution_info.payment_amount.high, + }, + }) + .await + { + // rollback the status + let status = CancelStatus::CancelledAssetFault; + + self.add_l1_handler_transaction_for_orderbook( + selector!("rollback_status_order"), + &[execution_info.order_hash, status.to_u32().into()], + ); + return false; + } + + true + } + + async fn on_starknet_tx_failed(&self, call: Call) { + println!("Starknet tx failed: {:?}", call); + + let execution_info = match ExecutionInfo::cairo_deserialize(&call.calldata, 0) { + Ok(execution_info) => execution_info, + Err(e) => { + tracing::error!("Fail deserializing ExecutionInfo: {:?}", e); + return; + } + }; + + println!("orderhash: {:?}:", execution_info.order_hash); + // rollback the status + let status = CancelStatus::CancelledUser; + self.add_l1_handler_transaction_for_orderbook( + selector!("rollback_status_order"), + &[execution_info.order_hash, status.to_u32().into()], + ); + } +} + +#[cfg(test)] +mod test { + use super::*; + use starknet::macros::{felt, selector}; + + #[test] + fn test_calldata_calls_parsing_new_encoding() { + // Calldata for a transaction to starkgate: + // Tx hash Goerli: 0x78140a4777bdf508feec62485c2d49b90b8346875c19470790935bcfbb9594 + let data = vec![ + FieldElement::ONE, + // to (starkgate). + felt!("0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7"), + // selector (transfert). + felt!("0x83afd3f4caedc6eebf44246fe54e38c95e3179a5ec9ea81740eca5b482d12e"), + // data offset. + felt!("0x0"), + // data len. + felt!("0x0"), + // Calldata len. + FieldElement::THREE, + felt!("0x06cdcce7333a7143ad0aebbaffe54a809cc53b65c0936ecfbebaecc0de099e8e"), + felt!("0x071afd498d0000"), + felt!("0x00"), + ]; + + let calls = match Vec::::cairo_deserialize(&data, 0) { + Ok(calls) => calls, + Err(e) => { + tracing::error!("Fail deserializing OrderV1: {:?}", e); + Vec::new() + } + }; + + assert_eq!(calls.len(), 1); + assert_eq!( + calls[0].to, + felt!("0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7").into() + ); + assert_eq!(calls[0].selector, selector!("transfer")); + } +} diff --git a/bin/solis/src/main.rs b/bin/solis/src/main.rs new file mode 100644 index 0000000000..14fca64404 --- /dev/null +++ b/bin/solis/src/main.rs @@ -0,0 +1,278 @@ +use std::io; +use std::net::SocketAddr; +use std::sync::Arc; + +use crate::hooker::SolisHooker; +use clap::{CommandFactory, Parser}; +use clap_complete::{generate, Shell}; +use console::Style; +use dojo_metrics::{metrics_process, prometheus_exporter}; +use katana_core::constants::MAX_RECURSION_DEPTH; +use katana_core::env::get_default_vm_resource_fee_cost; +use katana_core::hooker::KatanaHooker; +use katana_core::sequencer::KatanaSequencer; +use katana_executor::SimulationFlag; +use katana_primitives::class::ClassHash; +use katana_primitives::contract::ContractAddress; +use katana_primitives::env::{CfgEnv, FeeTokenAddressses}; +use katana_primitives::genesis::allocation::GenesisAccountAlloc; +use katana_primitives::genesis::Genesis; +use katana_rpc::{spawn, NodeHandle}; +use starknet::core::types::FieldElement; +use tokio::signal::ctrl_c; +use tokio::sync::RwLock as AsyncRwLock; +use tracing::info; + +mod args; +mod utils; +mod hooker; +mod contracts; + +// Chain ID: 'SOLIS' cairo short string. +pub const CHAIN_ID_SOLIS: FieldElement = FieldElement::from_mont([ + 18446732623703627169, + 18446744073709551615, + 18446744073709551615, + 576266102202707888, +]); + +use args::Commands::Completions; +use args::KatanaArgs; + +pub(crate) const LOG_TARGET: &str = "katana::cli"; + +#[tokio::main] +async fn main() -> Result<(), Box> { + dotenv::dotenv().ok(); + + let args = KatanaArgs::parse(); + args.init_logging()?; + + if let Some(command) = args.command { + match command { + Completions { shell } => { + print_completion(shell); + return Ok(()); + } + } + } + + let server_config = args.server_config(); + let sequencer_config = args.sequencer_config(); + let starknet_config = args.starknet_config(); + + let cfg_env = CfgEnv { + chain_id: starknet_config.env.chain_id, + vm_resource_fee_cost: get_default_vm_resource_fee_cost(), + invoke_tx_max_n_steps: starknet_config.env.invoke_max_steps, + validate_max_n_steps: starknet_config.env.validate_max_steps, + max_recursion_depth: MAX_RECURSION_DEPTH, + fee_token_addresses: FeeTokenAddressses { + eth: starknet_config.genesis.fee_token.address, + strk: Default::default(), + }, + }; + + let simulation_flags = SimulationFlag { + skip_validate: starknet_config.disable_validate, + skip_fee_transfer: starknet_config.disable_fee, + ..Default::default() + }; + + // TODO: Uncomment this once we enable the 'sir' feature again because it's not compatible with + // our current Cairo version (2.6.3). cfg_if::cfg_if! { + // if #[cfg(all(feature = "blockifier", feature = "sir"))] { + // compile_error!("Cannot enable both `blockifier` and `sir` features at the same + // time"); } else if #[cfg(feature = "blockifier")] { + // use katana_executor::implementation::blockifier::BlockifierFactory; + // let executor_factory = BlockifierFactory::new(cfg_env, simulation_flags); + // } else if #[cfg(feature = "sir")] { + // use katana_executor::implementation::sir::NativeExecutorFactory; + // let executor_factory = NativeExecutorFactory::new(cfg_env, simulation_flags); + // } else { + // compile_error!("At least one of the following features must be enabled: blockifier, + // sir"); } + // } + + use katana_executor::implementation::blockifier::BlockifierFactory; + let executor_factory = BlockifierFactory::new(cfg_env, simulation_flags); + + if let Some(listen_addr) = args.metrics { + let prometheus_handle = prometheus_exporter::install_recorder("katana")?; + + info!(target: LOG_TARGET, addr = %listen_addr, "Starting metrics endpoint."); + prometheus_exporter::serve( + listen_addr, + prometheus_handle, + metrics_process::Collector::default(), + ) + .await?; + } + + // ** SOLIS + let sn_utils_reader = contracts::starknet_utils::new_starknet_utils_reader( + FieldElement::ZERO, + &sequencer_config.messaging.clone().unwrap().rpc_url, + ); + + let executor_address = FieldElement::ZERO; + let orderbook_address = FieldElement::ZERO; + + let hooker:Arc + Send + Sync>> = + Arc::new(AsyncRwLock::new(SolisHooker::new( + sn_utils_reader, + orderbook_address, + executor_address, + ))); + // ** + + let sequencer = Arc::new( + KatanaSequencer::new(executor_factory, sequencer_config, starknet_config, Some(hooker.clone())) + .await?, + ); + let NodeHandle { addr, handle, .. } = spawn(Arc::clone(&sequencer), server_config).await?; + + // ** SOLIS + // Important to set the sequencer reference in the hooker, to allow the hooker + // to send `L1HandlerTransaction` to the orderbook. + hooker.write().await.set_sequencer(sequencer.clone()); + // ** + + if !args.silent { + let genesis = &sequencer.backend().config.genesis; + print_intro(&args, genesis, addr); + } + + // Wait until Ctrl + C is pressed, then shutdown + ctrl_c().await?; + handle.stop()?; + + Ok(()) +} + +fn print_completion(shell: Shell) { + let mut command = KatanaArgs::command(); + let name = command.get_name().to_string(); + generate(shell, &mut command, name, &mut io::stdout()); +} + +fn print_intro(args: &KatanaArgs, genesis: &Genesis, address: SocketAddr) { + let mut accounts = genesis.accounts().peekable(); + let account_class_hash = accounts.peek().map(|e| e.1.class_hash()); + let seed = &args.starknet.seed; + + if args.json_log { + info!( + target: LOG_TARGET, + "{}", + serde_json::json!({ + "accounts": accounts.map(|a| serde_json::json!(a)).collect::>(), + "seed": format!("{}", seed), + "address": format!("{address}"), + }) + ) + } else { + println!( + "{}", + Style::new().blue().apply_to( + r" + +░█████╗░██████╗░██╗░░██╗██████╗░██████╗░░█████╗░░░░░░██╗███████╗░█████╗░████████╗ +██╔══██╗██╔══██╗██║░██╔╝██╔══██╗██╔══██╗██╔══██╗░░░░░██║██╔════╝██╔══██╗╚══██╔══╝ +███████║██████╔╝█████═╝░██████╔╝██████╔╝██║░░██║░░░░░██║█████╗░░██║░░╚═╝░░░██║░░░ +██╔══██║██╔══██╗██╔═██╗░██╔═══╝░██╔══██╗██║░░██║██╗░░██║██╔══╝░░██║░░██╗░░░██║░░░ +██║░░██║██║░░██║██║░╚██╗██║░░░░░██║░░██║╚█████╔╝╚█████╔╝███████╗╚█████╔╝░░░██║░░░ +╚═╝░░╚═╝╚═╝░░╚═╝╚═╝░░╚═╝╚═╝░░░░░╚═╝░░╚═╝░╚════╝░░╚════╝░╚══════╝░╚════╝░░░░╚═╝░░░ + +░██████╗░█████╗░██╗░░░░░██╗░██████╗ +██╔════╝██╔══██╗██║░░░░░██║██╔════╝ +╚█████╗░██║░░██║██║░░░░░██║╚█████╗░ +░╚═══██╗██║░░██║██║░░░░░██║░╚═══██╗ +██████╔╝╚█████╔╝███████╗██║██████╔╝ +╚═════╝░░╚════╝░╚══════╝╚═╝╚═════╝░ +" + ) + ); + + print_genesis_contracts(genesis, account_class_hash); + print_genesis_accounts(accounts); + + println!( + r" + +ACCOUNTS SEED +============= +{seed} + " + ); + + let addr = format!( + "🚀 JSON-RPC server started: {}", + Style::new().red().apply_to(format!("http://{address}")) + ); + + println!("\n{addr}\n\n",); + } +} + +fn print_genesis_contracts(genesis: &Genesis, account_class_hash: Option) { + println!( + r" +PREDEPLOYED CONTRACTS +================== + +| Contract | Fee Token +| Address | {} +| Class Hash | {:#064x}", + genesis.fee_token.address, genesis.fee_token.class_hash, + ); + + if let Some(ref udc) = genesis.universal_deployer { + println!( + r" +| Contract | Universal Deployer +| Address | {} +| Class Hash | {:#064x}", + udc.address, udc.class_hash + ) + } + + if let Some(hash) = account_class_hash { + println!( + r" +| Contract | Account Contract +| Class Hash | {hash:#064x}" + ) + } +} + +fn print_genesis_accounts<'a, Accounts>(accounts: Accounts) +where + Accounts: Iterator, +{ + println!( + r" + +PREFUNDED ACCOUNTS +==================" + ); + + for (addr, account) in accounts { + if let Some(pk) = account.private_key() { + println!( + r" +| Account address | {addr} +| Private key | {pk:#x} +| Public key | {:#x}", + account.public_key() + ) + } else { + println!( + r" +| Account address | {addr} +| Public key | {:#x}", + account.public_key() + ) + } + } +} diff --git a/bin/solis/src/utils.rs b/bin/solis/src/utils.rs new file mode 100644 index 0000000000..f05f5a7634 --- /dev/null +++ b/bin/solis/src/utils.rs @@ -0,0 +1,34 @@ +use std::path::PathBuf; + +use katana_primitives::genesis::json::GenesisJson; +use katana_primitives::genesis::Genesis; + +pub fn parse_seed(seed: &str) -> [u8; 32] { + let seed = seed.as_bytes(); + + if seed.len() >= 32 { + unsafe { *(seed[..32].as_ptr() as *const [u8; 32]) } + } else { + let mut actual_seed = [0u8; 32]; + seed.iter().enumerate().for_each(|(i, b)| actual_seed[i] = *b); + actual_seed + } +} + +/// Used as clap value parser for [Genesis]. +pub fn parse_genesis(value: &str) -> Result { + let path = PathBuf::from(shellexpand::full(value)?.into_owned()); + let genesis = Genesis::try_from(GenesisJson::load(path)?)?; + Ok(genesis) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_genesis_file() { + let path = "./tests/test-data/genesis.json"; + parse_genesis(path).unwrap(); + } +} diff --git a/bin/solis/tests/test-data/genesis.json b/bin/solis/tests/test-data/genesis.json new file mode 100644 index 0000000000..51436f4832 --- /dev/null +++ b/bin/solis/tests/test-data/genesis.json @@ -0,0 +1,42 @@ +{ + "number": 0, + "parentHash": "0x999", + "timestamp": 5123512314, + "stateRoot": "0x99", + "sequencerAddress": "0x100", + "gasPrices": { + "ETH": 1111, + "STRK": 2222 + }, + "feeToken": { + "address": "0x55", + "name": "ETHER", + "symbol": "ETH", + "decimals": 18, + "storage": { + "0x111": "0x1", + "0x222": "0x2" + } + }, + "universalDeployer": { + "address": "0x041a78e741e5af2fec34b695679bc6891742439f7afb8484ecd7766661ad02bf", + "storage": { + "0x10": "0x100" + } + }, + "accounts": { + "0x66efb28ac62686966ae85095ff3a772e014e7fbf56d4c5f6fac5606d4dde23a": { + "publicKey": "0x1", + "balance": "0xD3C21BCECCEDA1000000", + "nonce": "0x1", + "storage": { + "0x1": "0x1", + "0x2": "0x2" + } + } + }, + "contracts": { + }, + "classes": [ + ] +} diff --git a/crates/dojo-test-utils/src/sequencer.rs b/crates/dojo-test-utils/src/sequencer.rs index 086d4dc26b..6435cc8918 100644 --- a/crates/dojo-test-utils/src/sequencer.rs +++ b/crates/dojo-test-utils/src/sequencer.rs @@ -57,7 +57,7 @@ impl TestSequencer { let executor_factory = BlockifierFactory::new(cfg_env, simulation_flags); let sequencer = Arc::new( - KatanaSequencer::new(executor_factory, config, starknet_config) + KatanaSequencer::new(executor_factory, config, starknet_config, None) .await .expect("Failed to create sequencer"), ); @@ -69,12 +69,15 @@ impl TestSequencer { host: "127.0.0.1".into(), max_connections: 100, allowed_origins: None, + rpc_password: "password".into(), + rpc_user: "user".into(), apis: vec![ ApiKind::Starknet, ApiKind::Katana, ApiKind::Dev, ApiKind::Saya, ApiKind::Torii, + ApiKind::Solis, ], }, ) diff --git a/crates/katana/core/src/hooker.rs b/crates/katana/core/src/hooker.rs new file mode 100644 index 0000000000..a7724a96bb --- /dev/null +++ b/crates/katana/core/src/hooker.rs @@ -0,0 +1,131 @@ +//! This module contains a hooker trait, that is added to katana in order to +//! allow external code to react at some precise moment of katana processing. + +use crate::sequencer::KatanaSequencer; +use async_trait::async_trait; +use katana_executor::ExecutorFactory; +use starknet::accounts::Call; +use starknet::core::types::{BroadcastedInvokeTransaction, FieldElement}; +use std::sync::Arc; +use tracing::{error, info}; + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, Copy, PartialEq, Eq)] +pub struct HookerAddresses { + pub orderbook_arkchain: FieldElement, + pub executor_starknet: FieldElement, +} + +#[async_trait] +pub trait KatanaHooker { + /// Sets a reference to the underlying sequencer. + fn set_sequencer(&mut self, sequencer: Arc>); + + /// Runs code right before a message from the L1 is converted + /// into a `L1HandlerTransaction`. This hook is useful to + /// apply conditions on the message being captured. + /// + /// # Arguments + /// + /// * `from` - The contract on L2 sending the message. + /// * `to` - The recipient contract on the appchain. + /// * `selector` - The l1_handler of the appchain contract to execute. + async fn verify_message_to_appchain( + &self, + from: FieldElement, + to: FieldElement, + selector: FieldElement, + ) -> bool; + + /// Runs code right before an invoke transaction + /// is being added to the pool. + /// Returns true if the transaction should be included + /// in the pool, false otherwise. + /// + /// # Arguments + /// + /// * `transaction` - The invoke transaction to be verified. + async fn verify_invoke_tx_before_pool(&self, transaction: BroadcastedInvokeTransaction) + -> bool; + + /// Runs code right before a message to starknet + /// is being sent via a direct transaction. + /// As the message is sent to starknet in a transaction + /// the `Call` of the transaction is being verified. + /// + /// # Arguments + /// + /// * `call` - The `Call` to inspect, built from the + /// message. + async fn verify_tx_for_starknet(&self, call: Call) -> bool; + + /// Runs when Solis attempts to execute an order on Starknet, + /// but it fails. + /// + /// # Arguments + /// + /// * `call` - The `Call` of the transaction that has failed. Usually the same as + /// `verify_message_to_starknet_before_tx`. + async fn on_starknet_tx_failed(&self, call: Call); + + /// Sets important addresses. + /// + /// # Arguments + /// + /// * `addresses` - Important addresses related to solis. + fn set_addresses(&mut self, addresses: HookerAddresses); +} + +pub struct DefaultKatanaHooker { + sequencer: Option>>, + addresses: Option, +} + +impl DefaultKatanaHooker { + pub fn new() -> Self { + DefaultKatanaHooker { sequencer: None, addresses: None } + } +} + +#[async_trait] +impl KatanaHooker for DefaultKatanaHooker { + fn set_sequencer(&mut self, sequencer: Arc>) { + self.sequencer = Some(sequencer); + info!("HOOKER: Sequencer set for hooker"); + } + + async fn verify_message_to_appchain( + &self, + from: FieldElement, + to: FieldElement, + selector: FieldElement, + ) -> bool { + info!( + "HOOKER: verify_message_to_appchain called with from: {:?}, to: {:?}, selector: {:?}", + from, to, selector + ); + true + } + + async fn verify_invoke_tx_before_pool( + &self, + transaction: BroadcastedInvokeTransaction, + ) -> bool { + info!("HOOKER: verify_invoke_tx_before_pool called with transaction: {:?}", transaction); + true + } + + async fn verify_tx_for_starknet(&self, call: Call) -> bool { + info!("HOOKER: verify_tx_for_starknet called with call: {:?}", call); + true + } + + async fn on_starknet_tx_failed(&self, call: Call) { + // Log the failure or handle it according to your needs. No-op by default. + error!("HOOKER: Starknet transaction failed: {:?}", call); + } + + fn set_addresses(&mut self, addresses: HookerAddresses) { + self.addresses = Some(addresses); + info!("HOOKER: Addresses set for hooker: {:?}", addresses); + } +} diff --git a/crates/katana/core/src/lib.rs b/crates/katana/core/src/lib.rs index 44d3fa5ab0..099e04b025 100644 --- a/crates/katana/core/src/lib.rs +++ b/crates/katana/core/src/lib.rs @@ -5,5 +5,6 @@ pub mod pool; pub mod sequencer; pub mod service; pub mod utils; +pub mod hooker; pub mod sequencer_error; diff --git a/crates/katana/core/src/sequencer.rs b/crates/katana/core/src/sequencer.rs index 0a0db8678d..cf1815eaed 100644 --- a/crates/katana/core/src/sequencer.rs +++ b/crates/katana/core/src/sequencer.rs @@ -1,3 +1,6 @@ +use crate::hooker::KatanaHooker; +use tokio::sync::RwLock as AsyncRwLock; + use std::cmp::Ordering; use std::iter::Skip; use std::slice::Iter; @@ -24,6 +27,7 @@ use katana_provider::traits::transaction::{ ReceiptProvider, TransactionProvider, TransactionsProviderExt, }; use starknet::core::types::{BlockTag, EmittedEvent, EventsPage}; +use tracing::error; use crate::backend::config::StarknetConfig; use crate::backend::contract::StarknetContract; @@ -52,6 +56,7 @@ pub struct KatanaSequencer { pub pool: Arc, pub backend: Arc>, pub block_producer: Arc>, + pub hooker: Option + Send + Sync>>>, } impl KatanaSequencer { @@ -59,6 +64,7 @@ impl KatanaSequencer { executor_factory: EF, config: SequencerConfig, starknet_config: StarknetConfig, + hooker: Option + Send + Sync>>>, ) -> anyhow::Result { let executor_factory = Arc::new(executor_factory); let backend = Arc::new(Backend::new(executor_factory.clone(), starknet_config).await); @@ -78,7 +84,20 @@ impl KatanaSequencer { #[cfg(feature = "messaging")] let messaging = if let Some(config) = config.messaging.clone() { - MessagingService::new(config, Arc::clone(&pool), Arc::clone(&backend)).await.ok() + match &hooker { + Some(hooker_ref) => MessagingService::new( + config, + Arc::clone(&pool), + Arc::clone(&backend), + hooker_ref.clone(), + ) + .await + .ok(), + None => { + error!("Messaging service is enabled but no hooker is provided. Messaging service will not be started."); + None + } + } } else { None }; @@ -93,7 +112,7 @@ impl KatanaSequencer { messaging, )); - Ok(Self { pool, config, backend, block_producer }) + Ok(Self { pool, config, backend, block_producer, hooker }) } /// Returns the pending state if the sequencer is running in _interval_ mode. Otherwise `None`. @@ -286,10 +305,13 @@ impl KatanaSequencer { let tx @ Some(_) = tx else { return Ok(self.pending_executor().as_ref().and_then(|exec| { - exec.read() - .transactions() - .iter() - .find_map(|tx| if tx.0.hash == *hash { Some(tx.0.clone()) } else { None }) + exec.read().transactions().iter().find_map(|tx| { + if tx.0.hash == *hash { + Some(tx.0.clone()) + } else { + None + } + }) })); }; @@ -497,12 +519,9 @@ fn filter_events_by_params( #[cfg(test)] mod tests { + use super::*; use katana_executor::implementation::noop::NoopExecutorFactory; use katana_provider::traits::block::BlockNumberProvider; - - use super::{KatanaSequencer, SequencerConfig}; - use crate::backend::config::StarknetConfig; - #[tokio::test] async fn init_interval_block_producer_with_correct_block_env() { let executor_factory = NoopExecutorFactory::default(); @@ -511,12 +530,12 @@ mod tests { executor_factory, SequencerConfig { no_mining: true, ..Default::default() }, StarknetConfig::default(), + None, // Pass the hooker instance here ) .await .unwrap(); let provider = sequencer.backend.blockchain.provider(); - let latest_num = provider.latest_number().unwrap(); let producer_block_env = sequencer.pending_executor().unwrap().read().block_env(); diff --git a/crates/katana/core/src/service/messaging/mod.rs b/crates/katana/core/src/service/messaging/mod.rs index f07b353e8c..b7b7b8a193 100644 --- a/crates/katana/core/src/service/messaging/mod.rs +++ b/crates/katana/core/src/service/messaging/mod.rs @@ -32,12 +32,17 @@ //! configuration file following the `MessagingConfig` format. An example of this file can be found //! in the messaging contracts. +use crate::hooker::KatanaHooker; +use std::sync::Arc; +use tokio::sync::RwLock as AsyncRwLock; + mod ethereum; mod service; -#[cfg(feature = "starknet-messaging")] mod starknet; use std::path::Path; +use std::fs::File; +use std::io::Write; use ::starknet::providers::ProviderError as StarknetProviderError; use alloy_transport::TransportError; @@ -46,16 +51,14 @@ use async_trait::async_trait; use ethereum::EthereumMessaging; use katana_primitives::chain::ChainId; use katana_primitives::receipt::MessageToL1; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use tracing::{error, info}; pub use self::service::{MessagingOutcome, MessagingService}; -#[cfg(feature = "starknet-messaging")] use self::starknet::StarknetMessaging; pub(crate) const LOG_TARGET: &str = "messaging"; pub(crate) const CONFIG_CHAIN_ETHEREUM: &str = "ethereum"; -#[cfg(feature = "starknet-messaging")] pub(crate) const CONFIG_CHAIN_STARKNET: &str = "starknet"; type MessengerResult = Result; @@ -89,7 +92,7 @@ impl From for Error { } /// The config used to initialize the messaging service. -#[derive(Debug, Default, Deserialize, Clone)] +#[derive(Debug, Default, Deserialize, Clone, Serialize)] pub struct MessagingConfig { /// The settlement chain. pub chain: String, @@ -106,7 +109,11 @@ pub struct MessagingConfig { /// from/to the settlement chain. pub interval: u64, /// The block on settlement chain from where Katana will start fetching messages. - pub from_block: u64, + pub gather_from_block: u64, + /// The block from where sequencer wil start sending messages. + pub send_from_block: u64, + /// Path to the config file. + pub config_file: String, } impl MessagingConfig { @@ -118,7 +125,24 @@ impl MessagingConfig { /// This is used as the clap `value_parser` implementation pub fn parse(path: &str) -> Result { - Self::load(path).map_err(|e| e.to_string()) + let mut config = Self::load(path).map_err(|e| e.to_string())?; + config.config_file = path.to_string(); + config.save().map_err(|e| e.to_string())?; + Ok(config) + } + + /// Save the config to a JSON file. + pub fn save(&self) -> Result<(), std::io::Error> { + if self.config_file.is_empty() { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "Config file path is empty", + )); + } + let json = serde_json::to_string_pretty(self)?; + let mut file = File::create(&self.config_file)?; + file.write_all(json.as_bytes())?; + Ok(()) } } @@ -162,14 +186,16 @@ pub trait Messenger { ) -> MessengerResult>; } -pub enum MessengerMode { +pub enum MessengerMode { Ethereum(EthereumMessaging), - #[cfg(feature = "starknet-messaging")] - Starknet(StarknetMessaging), + Starknet(StarknetMessaging), } -impl MessengerMode { - pub async fn from_config(config: MessagingConfig) -> MessengerResult { +impl MessengerMode { + pub async fn from_config( + config: MessagingConfig, + hooker: Arc + Send + Sync>>, + ) -> MessengerResult { match config.chain.as_str() { CONFIG_CHAIN_ETHEREUM => match EthereumMessaging::new(config).await { Ok(m_eth) => { @@ -182,8 +208,7 @@ impl MessengerMode { } }, - #[cfg(feature = "starknet-messaging")] - CONFIG_CHAIN_STARKNET => match StarknetMessaging::new(config).await { + CONFIG_CHAIN_STARKNET => match StarknetMessaging::new(config, hooker).await { Ok(m_sn) => { info!(target: LOG_TARGET, "Messaging enabled [Starknet]."); Ok(MessengerMode::Starknet(m_sn)) diff --git a/crates/katana/core/src/service/messaging/service.rs b/crates/katana/core/src/service/messaging/service.rs index a4a6810c29..b61f8001ab 100644 --- a/crates/katana/core/src/service/messaging/service.rs +++ b/crates/katana/core/src/service/messaging/service.rs @@ -1,7 +1,5 @@ -use std::pin::Pin; -use std::sync::Arc; -use std::task::{Context, Poll}; -use std::time::Duration; +use crate::hooker::KatanaHooker; +use tokio::sync::RwLock as AsyncRwLock; use futures::{Future, FutureExt, Stream}; use katana_executor::ExecutorFactory; @@ -10,6 +8,10 @@ use katana_primitives::receipt::MessageToL1; use katana_primitives::transaction::{ExecutableTxWithHash, L1HandlerTx, TxHash}; use katana_provider::traits::block::BlockNumberProvider; use katana_provider::traits::transaction::ReceiptProvider; +use std::pin::Pin; +use std::sync::{Arc, RwLock}; +use std::task::{Context, Poll}; +use std::time::Duration; use tokio::time::{interval_at, Instant, Interval}; use tracing::{error, info}; @@ -27,7 +29,7 @@ pub struct MessagingService { backend: Arc>, pool: Arc, /// The messenger mode the service is running in. - messenger: Arc, + messenger: Arc>, /// The block number of the settlement chain from which messages will be gathered. gather_from_block: u64, /// The message gathering future. @@ -36,6 +38,8 @@ pub struct MessagingService { send_from_block: u64, /// The message sending future. msg_send_fut: Option, + /// The messaging configuration. + messaging_config: Arc>, } impl MessagingService { @@ -45,15 +49,17 @@ impl MessagingService { config: MessagingConfig, pool: Arc, backend: Arc>, + hooker: Arc + Send + Sync>>, ) -> anyhow::Result { - let gather_from_block = config.from_block; + let gather_from_block = config.gather_from_block; + let send_from_block = config.send_from_block; let interval = interval_from_seconds(config.interval); - let messenger = match MessengerMode::from_config(config).await { + let messenger = match MessengerMode::from_config(config.clone(), hooker).await { Ok(m) => Arc::new(m), Err(_) => { panic!( "Messaging could not be initialized.\nVerify that the messaging target node \ - (anvil or other katana) is running.\n", + (anvil or other katana) is running.\n" ) } }; @@ -64,22 +70,28 @@ impl MessagingService { interval, messenger, gather_from_block, - send_from_block: 0, + send_from_block, msg_gather_fut: None, msg_send_fut: None, + messaging_config: Arc::new(RwLock::new(config)), }) } async fn gather_messages( - messenger: Arc, + messenger: Arc>, pool: Arc, backend: Arc>, from_block: u64, ) -> MessengerResult<(u64, usize)> { - // 200 avoids any possible rejection from RPC with possibly lot's of messages. + // 200 avoids any possible rejection from RPC with possibly lots of messages. // TODO: May this be configurable? let max_block = 200; - + info!( + target: LOG_TARGET, + "Starting gather_messages from block {} with max_block {}", + from_block, + max_block + ); match messenger.as_ref() { MessengerMode::Ethereum(inner) => { let (block_num, txs) = @@ -95,18 +107,23 @@ impl MessagingService { Ok((block_num, txs_count)) } - #[cfg(feature = "starknet-messaging")] MessengerMode::Starknet(inner) => { let (block_num, txs) = inner.gather_messages(from_block, max_block, backend.chain_id).await?; let txs_count = txs.len(); - + info!(target: LOG_TARGET, "Gathered {} transactions for Starknet mode.", txs_count); txs.into_iter().for_each(|tx| { let hash = tx.calculate_hash(); + info!(target: LOG_TARGET, "Processing transaction with hash: {:#x}", hash); trace_l1_handler_tx_exec(hash, &tx); pool.add_transaction(ExecutableTxWithHash { hash, transaction: tx.into() }) }); - + info!( + target: LOG_TARGET, + "gather_messages finished. Last block: {}, tx count: {}", + block_num, + txs_count + ); Ok((block_num, txs_count)) } } @@ -115,8 +132,9 @@ impl MessagingService { async fn send_messages( block_num: u64, backend: Arc>, - messenger: Arc, + messenger: Arc>, ) -> MessengerResult> { + // Retrieve messages to be sent from the local blockchain for the given block number let Some(messages) = ReceiptProvider::receipts_by_block( backend.blockchain.provider(), BlockHashOrNumber::Num(block_num), @@ -127,24 +145,43 @@ impl MessagingService { }; if messages.is_empty() { - Ok(Some((block_num, 0))) - } else { - match messenger.as_ref() { - MessengerMode::Ethereum(inner) => { - let hashes = inner.send_messages(&messages).await.map(|hashes| { - hashes.iter().map(|h| format!("{h:#x}")).collect::>() - })?; - trace_msg_to_l1_sent(&messages, &hashes); - Ok(Some((block_num, hashes.len()))) - } + info!(target: LOG_TARGET, "No messages to send from block {}", block_num); + return Ok(Some((block_num, 0))); + } + + info!(target: LOG_TARGET, "Retrieved {} messages from block {}", messages.len(), block_num); - #[cfg(feature = "starknet-messaging")] - MessengerMode::Starknet(inner) => { - let hashes = inner.send_messages(&messages).await.map(|hashes| { - hashes.iter().map(|h| format!("{h:#x}")).collect::>() - })?; - trace_msg_to_l1_sent(&messages, &hashes); - Ok(Some((block_num, hashes.len()))) + match messenger.as_ref() { + MessengerMode::Ethereum(inner) => { + match inner.send_messages(&messages).await { + Ok(hashes) => { + let hash_strings: Vec = + hashes.iter().map(|h| format!("{:#x}", h)).collect(); + trace_msg_to_l1_sent(&messages, &hash_strings); + info!(target: LOG_TARGET, "Successfully sent {} messages from block {}", hash_strings.len(), block_num); + Ok(Some((block_num, hash_strings.len()))) + } + Err(e) => { + error!(target: LOG_TARGET, error = %e, "Error sending messages from block {}", block_num); + // Even if there's an error, we should move to the next block to avoid infinite retries + Ok(Some((block_num, 0))) // Marking as processed to avoid retries + } + } + } + MessengerMode::Starknet(inner) => { + match inner.send_messages(&messages).await { + Ok(hashes) => { + let hash_strings: Vec = + hashes.iter().map(|h| format!("{:#x}", h)).collect(); + trace_msg_to_l1_sent(&messages, &hash_strings); + info!(target: LOG_TARGET, "Successfully sent {} messages from block {}", hash_strings.len(), block_num); + Ok(Some((block_num, hash_strings.len()))) + } + Err(e) => { + error!(target: LOG_TARGET, error = %e, "Error sending messages from block {}", block_num); + // Even if there's an error, we should move to the next block to avoid infinite retries + Ok(Some((block_num, 0))) // Marking as processed to avoid retries + } } } } @@ -190,50 +227,48 @@ impl Stream for MessagingService { pin.send_from_block, pin.backend.clone(), pin.messenger.clone(), - ))) + ))); } } } - // Poll the gathering future. + let mut messaging_config = pin.messaging_config.write().unwrap(); + // Poll the gathering future if let Some(mut gather_fut) = pin.msg_gather_fut.take() { match gather_fut.poll_unpin(cx) { Poll::Ready(Ok((last_block, msg_count))) => { + info!(target: LOG_TARGET, "Gathered {} transactions up to block {}", msg_count, last_block); pin.gather_from_block = last_block + 1; + messaging_config.gather_from_block = pin.gather_from_block; + let _ = messaging_config.save(); return Poll::Ready(Some(MessagingOutcome::Gather { lastest_block: last_block, msg_count, })); } Poll::Ready(Err(e)) => { - error!( - target: LOG_TARGET, - block = %pin.gather_from_block, - error = %e, - "Gathering messages for block." - ); + error!(target: LOG_TARGET, block = %pin.gather_from_block, error = %e, "Error gathering messages for block."); return Poll::Pending; } Poll::Pending => pin.msg_gather_fut = Some(gather_fut), } } - // Poll the message sending future. + // Poll the message sending future if let Some(mut send_fut) = pin.msg_send_fut.take() { match send_fut.poll_unpin(cx) { Poll::Ready(Ok(Some((block_num, msg_count)))) => { - // +1 to move to the next local block to check messages to be - // sent on the settlement chain. - pin.send_from_block += 1; + info!(target: LOG_TARGET, "Sent {} messages from block {}", msg_count, block_num); + pin.send_from_block = block_num + 1; + // update the config with the latest block number sent. + messaging_config.send_from_block = pin.send_from_block; + let _ = messaging_config.save(); return Poll::Ready(Some(MessagingOutcome::Send { block_num, msg_count })); } Poll::Ready(Err(e)) => { - error!( - target: LOG_TARGET, - block = %pin.send_from_block, - error = %e, - "Settling messages for block." - ); + error!(target: LOG_TARGET, block = %pin.send_from_block, error = %e, "Error sending messages for block."); + // Even if there's an error, we should move to the next block to avoid infinite retries + pin.send_from_block += 1; return Poll::Pending; } Poll::Ready(_) => return Poll::Pending, @@ -256,7 +291,6 @@ fn interval_from_seconds(secs: u64) -> Interval { fn trace_msg_to_l1_sent(messages: &[MessageToL1], hashes: &[String]) { assert_eq!(messages.len(), hashes.len()); - #[cfg(feature = "starknet-messaging")] let hash_exec_str = format!("{:#064x}", super::starknet::HASH_EXEC); for (i, m) in messages.iter().enumerate() { @@ -264,7 +298,6 @@ fn trace_msg_to_l1_sent(messages: &[MessageToL1], hashes: &[String]) { let hash = &hashes[i]; - #[cfg(feature = "starknet-messaging")] if hash == &hash_exec_str { let to_address = &payload_str[0]; let selector = &payload_str[1]; diff --git a/crates/katana/core/src/service/messaging/starknet.rs b/crates/katana/core/src/service/messaging/starknet.rs index 0c1b242721..6ba310dcdf 100644 --- a/crates/katana/core/src/service/messaging/starknet.rs +++ b/crates/katana/core/src/service/messaging/starknet.rs @@ -1,6 +1,5 @@ use std::collections::HashMap; -use std::sync::Arc; - +use crate::hooker::KatanaHooker; use anyhow::Result; use async_trait::async_trait; use katana_primitives::chain::ChainId; @@ -14,31 +13,36 @@ use starknet::macros::{felt, selector}; use starknet::providers::jsonrpc::HttpTransport; use starknet::providers::{AnyProvider, JsonRpcClient, Provider}; use starknet::signers::{LocalWallet, SigningKey}; -use tracing::{debug, error, trace, warn}; +use std::collections::HashSet; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::Arc; +use tokio::sync::RwLock as AsyncRwLock; +use tracing::{debug, error, info, trace, warn}; use url::Url; use super::{Error, MessagingConfig, Messenger, MessengerResult, LOG_TARGET}; -/// As messaging in starknet is only possible with EthAddress in the `to_address` -/// field, we have to set magic value to understand what the user want to do. -/// In the case of execution -> the felt 'EXE' will be passed. -/// And for normal messages, the felt 'MSG' is used. -/// Those values are very not likely a valid account address on starknet. const MSG_MAGIC: FieldElement = felt!("0x4d5347"); const EXE_MAGIC: FieldElement = felt!("0x455845"); pub const HASH_EXEC: FieldElement = felt!("0xee"); -pub struct StarknetMessaging { +pub struct StarknetMessaging { chain_id: FieldElement, provider: AnyProvider, wallet: LocalWallet, sender_account_address: FieldElement, messaging_contract_address: FieldElement, + hooker: Arc + Send + Sync>>, + event_cache: Arc>>, + latest_block: Arc, } -impl StarknetMessaging { - pub async fn new(config: MessagingConfig) -> Result { +impl StarknetMessaging { + pub async fn new( + config: MessagingConfig, + hooker: Arc + Send + Sync>>, + ) -> Result> { let provider = AnyProvider::JsonRpcHttp(JsonRpcClient::new(HttpTransport::new( Url::parse(&config.rpc_url)?, ))); @@ -46,17 +50,23 @@ impl StarknetMessaging { let private_key = FieldElement::from_hex_be(&config.private_key)?; let key = SigningKey::from_secret_scalar(private_key); let wallet = LocalWallet::from_signing_key(key); + let latest_block = Arc::new(AtomicU64::new(0)); let chain_id = provider.chain_id().await?; let sender_account_address = FieldElement::from_hex_be(&config.sender_address)?; let messaging_contract_address = FieldElement::from_hex_be(&config.contract_address)?; + info!(target: LOG_TARGET, "StarknetMessaging instance created."); + Ok(StarknetMessaging { wallet, provider, chain_id, sender_account_address, messaging_contract_address, + hooker, + event_cache: Arc::new(AsyncRwLock::new(HashSet::new())), + latest_block, }) } @@ -65,10 +75,10 @@ impl StarknetMessaging { &self, from_block: BlockId, to_block: BlockId, - ) -> Result>> { + ) -> Result> { trace!(target: LOG_TARGET, from_block = ?from_block, to_block = ?to_block, "Fetching logs."); - let mut block_to_events: HashMap> = HashMap::new(); + let mut events = vec![]; let filter = EventFilter { from_block: Some(from_block), @@ -88,11 +98,10 @@ impl StarknetMessaging { event_page.events.into_iter().for_each(|event| { // We ignore events without the block number - if let Some(block_number) = event.block_number { - block_to_events - .entry(block_number) - .and_modify(|v| v.push(event.clone())) - .or_insert(vec![event]); + if event.block_number.is_some() { + // Blocks are processed in order as retrieved by `get_events`. + // This way we keep the order and ensure the messages are executed in order. + events.push(event); } }); @@ -103,13 +112,11 @@ impl StarknetMessaging { } } - Ok(block_to_events) + Ok(events) } - /// Sends an invoke TX on starknet. async fn send_invoke_tx(&self, calls: Vec) -> Result { let signer = Arc::new(&self.wallet); - let mut account = SingleOwnerAccount::new( &self.provider, signer, @@ -118,36 +125,61 @@ impl StarknetMessaging { ExecutionEncoding::New, ); + info!(target: LOG_TARGET, "Setting block ID to Pending."); account.set_block_id(BlockId::Tag(BlockTag::Pending)); - // TODO: we need to have maximum fee configurable. let execution = account.execute(calls).fee_estimate_multiplier(10f64); - let estimated_fee = (execution.estimate_fee().await?.overall_fee) * 10u64.into(); - let tx = execution.max_fee(estimated_fee).send().await?; + let estimated_fee = match execution.estimate_fee().await { + Ok(fee) => { + info!(target: LOG_TARGET, "Estimated fee: {:?}", fee.overall_fee); + (fee.overall_fee) * 10u64.into() + } + Err(e) => { + error!(target: LOG_TARGET, "Error estimating fee: {:?}", e); + return Err(e.into()); + } + }; + + let execution_with_fee = execution.max_fee(estimated_fee); + info!(target: LOG_TARGET, "Sending invoke transaction with max fee: {:?}", estimated_fee); - Ok(tx.transaction_hash) + match execution_with_fee.send().await { + Ok(tx) => { + info!(target: LOG_TARGET, "Transaction successful: {:?}", tx); + Ok(tx.transaction_hash) + } + Err(e) => { + error!(target: LOG_TARGET, "Error sending transaction: {:?}", e); + Err(e.into()) + } + } } - /// Sends messages hashes to settlement layer by sending a transaction. async fn send_hashes(&self, mut hashes: Vec) -> MessengerResult { hashes.retain(|&x| x != HASH_EXEC); if hashes.is_empty() { + info!(target: LOG_TARGET, "No hashes to send."); return Ok(FieldElement::ZERO); } - let mut calldata = hashes; + info!(target: LOG_TARGET, "Preparing to send {} hashes.", hashes.len()); + + let mut calldata = hashes.clone(); calldata.insert(0, calldata.len().into()); let call = Call { selector: selector!("add_messages_hashes_from_appchain"), to: self.messaging_contract_address, - calldata, + calldata: calldata.clone(), }; + info!(target: LOG_TARGET, "Sending hashes to Starknet: {:?}", calldata); + match self.send_invoke_tx(vec![call]).await { Ok(tx_hash) => { trace!(target: LOG_TARGET, tx_hash = %format!("{:#064x}", tx_hash), "Hashes sending transaction."); + info!(target: LOG_TARGET, "Successfully sent hashes with transaction hash: {:#064x}", tx_hash); Ok(tx_hash) } Err(e) => { @@ -159,7 +191,7 @@ impl StarknetMessaging { } #[async_trait] -impl Messenger for StarknetMessaging { +impl Messenger for StarknetMessaging { type MessageHash = FieldElement; type MessageTransaction = L1HandlerTx; @@ -172,49 +204,38 @@ impl Messenger for StarknetMessaging { let chain_latest_block: u64 = match self.provider.block_number().await { Ok(n) => n, Err(_) => { - warn!( - target: LOG_TARGET, - "Couldn't fetch settlement chain last block number. \nSkipped, retry at the \ - next tick." - ); + warn!(target: LOG_TARGET, "Couldn't fetch settlement chain last block number"); return Err(Error::SendError); } }; - + if from_block > chain_latest_block { - // Nothing to fetch, we can skip waiting the next tick. return Ok((chain_latest_block, vec![])); } - - // +1 as the from_block counts as 1 block fetched. - let to_block = if from_block + max_blocks + 1 < chain_latest_block { - from_block + max_blocks - } else { - chain_latest_block - }; - + + // Instead of skipping blocks, process them sequentially + let to_block = std::cmp::min(from_block + max_blocks, chain_latest_block); + let mut l1_handler_txs: Vec = vec![]; - - self.fetch_events(BlockId::Number(from_block), BlockId::Number(to_block)) - .await - .map_err(|_| Error::SendError) - .unwrap() - .iter() - .for_each(|(block_number, block_events)| { - debug!( - target: LOG_TARGET, - block_number = %block_number, - events_count = %block_events.len(), - "Converting events of block into L1HandlerTx." - ); - - block_events.iter().for_each(|e| { - if let Ok(tx) = l1_handler_tx_from_event(e, chain_id) { - l1_handler_txs.push(tx) - } - }) - }); - + + // Process each block individually to ensure none are missed + for block_num in from_block..=to_block { + match self.fetch_events(BlockId::Number(block_num), BlockId::Number(block_num)).await { + Ok(events) => { + events.iter().for_each(|e| { + if let Ok(tx) = l1_handler_tx_from_event(e, chain_id) { + l1_handler_txs.push(tx) + } + }); + } + Err(e) => { + warn!(target: LOG_TARGET, "Error fetching block {}: {}", block_num, e); + // Return the last successfully processed block + return Ok((block_num - 1, l1_handler_txs)); + } + } + } + Ok((to_block, l1_handler_txs)) } @@ -223,41 +244,46 @@ impl Messenger for StarknetMessaging { messages: &[MessageToL1], ) -> MessengerResult> { if messages.is_empty() { + info!(target: LOG_TARGET, "No messages to send."); return Ok(vec![]); } let (hashes, calls) = parse_messages(messages)?; + for call in &calls { + if !self.hooker.read().await.verify_tx_for_starknet(call.clone()).await { + warn!(target: LOG_TARGET, "Call verification failed for call: {:?}", call); + continue; + } + } if !calls.is_empty() { - match self.send_invoke_tx(calls).await { - Ok(tx_hash) => { - trace!(target: LOG_TARGET, tx_hash = %format!("{:#064x}", tx_hash), "Invoke transaction hash."); - } - Err(e) => { - error!(target: LOG_TARGET, error = %e, "Sending invoke tx on Starknet."); - return Err(Error::SendError); + info!(target: LOG_TARGET, "Sending {} calls.", calls.len()); + if let Err(e) = self.send_invoke_tx(calls.clone()).await { + error!(target: LOG_TARGET, error = %e, "Error sending invoke transaction."); + for call in calls { + self.hooker.read().await.on_starknet_tx_failed(call).await; } - }; + return Err(Error::SendError); + } + info!(target: LOG_TARGET, "Successfully sent invoke transaction."); } - self.send_hashes(hashes.clone()).await?; + if let Err(e) = self.send_hashes(hashes.clone()).await { + error!(target: LOG_TARGET, error = %e, "Error sending hashes."); + return Err(Error::SendError); + } + info!(target: LOG_TARGET, "Successfully sent hashes."); + info!(target: LOG_TARGET, "Finished sending messages."); Ok(hashes) } } -/// Parses messages sent by cairo contracts to compute their hashes. -/// -/// Messages can also be labelled as EXE, which in this case generate a `Call` -/// additionally to the hash. fn parse_messages(messages: &[MessageToL1]) -> MessengerResult<(Vec, Vec)> { let mut hashes: Vec = vec![]; let mut calls: Vec = vec![]; for m in messages { - // Field `to_address` is restricted to eth addresses space. So the - // `to_address` is set to 'EXE'/'MSG' to indicate that the message - // has to be executed or sent normally. let magic = m.to_address; if magic == EXE_MAGIC { @@ -267,13 +293,13 @@ fn parse_messages(messages: &[MessageToL1]) -> MessengerResult<(Vec= 3 { calldata.extend(m.payload[2..].to_vec()); } @@ -281,15 +307,7 @@ fn parse_messages(messages: &[MessageToL1]) -> MessengerResult<(Vec = vec![]; @@ -302,7 +320,6 @@ fn parse_messages(messages: &[MessageToL1]) -> MessengerResult<(Vec Result Result Result Result<(FieldElement, FieldElement, FieldElement)> { + if event.keys[0] != selector!("MessageSentToAppchain") { + debug!( + target: LOG_TARGET, + "Event with key {:?} can't be converted into L1HandlerTx", event.keys[0], + ); + return Err(Error::GatherError.into()); + } + + if event.keys.len() != 4 || event.data.len() < 2 { + error!(target: LOG_TARGET, "Event MessageSentToAppchain is not well formatted"); + } + + let from_address = event.keys[2]; + let to_address = event.keys[3]; + let entry_point_selector = event.data[0]; + + Ok((from_address, to_address, entry_point_selector)) +} + #[cfg(test)] mod tests { - use katana_primitives::utils::transaction::compute_l1_handler_tx_hash; use starknet::macros::felt; diff --git a/crates/katana/core/tests/sequencer.rs b/crates/katana/core/tests/sequencer.rs index 4d3ffb1494..da74ce0472 100644 --- a/crates/katana/core/tests/sequencer.rs +++ b/crates/katana/core/tests/sequencer.rs @@ -25,7 +25,7 @@ fn create_test_sequencer_config() -> (SequencerConfig, StarknetConfig) { async fn create_test_sequencer() -> KatanaSequencer { let executor_factory = NoopExecutorFactory::new(); let (sequencer_config, starknet_config) = create_test_sequencer_config(); - KatanaSequencer::new(executor_factory, sequencer_config, starknet_config).await.unwrap() + KatanaSequencer::new(executor_factory, sequencer_config, starknet_config, None).await.unwrap() } #[tokio::test] diff --git a/crates/katana/rpc/rpc-api/src/lib.rs b/crates/katana/rpc/rpc-api/src/lib.rs index 198766158b..c56f5cbd27 100644 --- a/crates/katana/rpc/rpc-api/src/lib.rs +++ b/crates/katana/rpc/rpc-api/src/lib.rs @@ -1,6 +1,7 @@ pub mod dev; pub mod katana; pub mod saya; +pub mod solis; pub mod starknet; pub mod torii; @@ -12,4 +13,5 @@ pub enum ApiKind { Torii, Dev, Saya, + Solis } diff --git a/crates/katana/rpc/rpc-api/src/solis.rs b/crates/katana/rpc/rpc-api/src/solis.rs new file mode 100644 index 0000000000..ea47906f35 --- /dev/null +++ b/crates/katana/rpc/rpc-api/src/solis.rs @@ -0,0 +1,10 @@ +use jsonrpsee::core::RpcResult; +use jsonrpsee::proc_macros::rpc; +use katana_core::hooker::HookerAddresses; + +#[cfg_attr(not(feature = "client"), rpc(server, namespace = "solis"))] +#[cfg_attr(feature = "client", rpc(client, server, namespace = "solis"))] +pub trait SolisApi { + #[method(name = "setSolisAddresses")] + async fn set_addresses(&self, addresses: HookerAddresses, basic_auth: String) -> RpcResult<()>; +} diff --git a/crates/katana/rpc/rpc-types/src/error/mod.rs b/crates/katana/rpc/rpc-types/src/error/mod.rs index e935e90516..c68defcea8 100644 --- a/crates/katana/rpc/rpc-types/src/error/mod.rs +++ b/crates/katana/rpc/rpc-types/src/error/mod.rs @@ -2,3 +2,4 @@ pub mod katana; pub mod saya; pub mod starknet; pub mod torii; +pub mod solis; \ No newline at end of file diff --git a/crates/katana/rpc/rpc-types/src/error/solis.rs b/crates/katana/rpc/rpc-types/src/error/solis.rs new file mode 100644 index 0000000000..fb49ee0210 --- /dev/null +++ b/crates/katana/rpc/rpc-types/src/error/solis.rs @@ -0,0 +1,29 @@ +use jsonrpsee::core::Error as RpcError; +use jsonrpsee::types::error::CallError; +use jsonrpsee::types::ErrorObject; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum SolisApiError { + #[error("Authentication failed")] + AuthenticationFailed, + + #[error("Hooker service is unavailable")] + HookerServiceUnavailable, + + #[error("An unexpected error occurred: {0}")] + UnexpectedError(String), +} + +impl SolisApiError { + pub fn to_rpc_error(&self) -> RpcError { + let error_code = match self { + SolisApiError::AuthenticationFailed => -32000, + SolisApiError::HookerServiceUnavailable => -32001, + SolisApiError::UnexpectedError(_) => -32002, + }; + let message = self.to_string(); + let error_object = ErrorObject::owned(error_code, message, None::<()>); + RpcError::Call(CallError::Custom(error_object)) + } +} diff --git a/crates/katana/rpc/rpc-types/src/error/starknet.rs b/crates/katana/rpc/rpc-types/src/error/starknet.rs index a4aacae381..a77b1f8e95 100644 --- a/crates/katana/rpc/rpc-types/src/error/starknet.rs +++ b/crates/katana/rpc/rpc-types/src/error/starknet.rs @@ -75,6 +75,8 @@ pub enum StarknetApiError { TooManyKeysInFilter, #[error("Failed to fetch pending transactions")] FailedToFetchPendingTransactions, + #[error("Solis: Assets are invalid on L2")] + SolisAssetFault, } impl StarknetApiError { @@ -110,6 +112,7 @@ impl StarknetApiError { StarknetApiError::UnsupportedContractClassVersion => 62, StarknetApiError::UnexpectedError { .. } => 63, StarknetApiError::ProofLimitExceeded => 10000, + StarknetApiError::SolisAssetFault => 7777, } } diff --git a/crates/katana/rpc/rpc/Cargo.toml b/crates/katana/rpc/rpc/Cargo.toml index 0ffc37bd24..1ea4ee797d 100644 --- a/crates/katana/rpc/rpc/Cargo.toml +++ b/crates/katana/rpc/rpc/Cargo.toml @@ -16,6 +16,7 @@ katana-rpc-api.workspace = true katana-rpc-types-builder.workspace = true katana-rpc-types.workspace = true katana-tasks.workspace = true +base64 = "0.13.0" anyhow.workspace = true flate2.workspace = true diff --git a/crates/katana/rpc/rpc/src/config.rs b/crates/katana/rpc/rpc/src/config.rs index 5a88af1bb9..2d5b910280 100644 --- a/crates/katana/rpc/rpc/src/config.rs +++ b/crates/katana/rpc/rpc/src/config.rs @@ -7,6 +7,8 @@ pub struct ServerConfig { pub max_connections: u32, pub allowed_origins: Option>, pub apis: Vec, + pub rpc_user: String, + pub rpc_password: String, } impl ServerConfig { diff --git a/crates/katana/rpc/rpc/src/lib.rs b/crates/katana/rpc/rpc/src/lib.rs index edc855f0a3..5ec0d9167a 100644 --- a/crates/katana/rpc/rpc/src/lib.rs +++ b/crates/katana/rpc/rpc/src/lib.rs @@ -5,6 +5,7 @@ pub mod dev; pub mod katana; pub mod metrics; pub mod saya; +pub mod solis; pub mod starknet; pub mod torii; @@ -23,6 +24,7 @@ use katana_executor::ExecutorFactory; use katana_rpc_api::dev::DevApiServer; use katana_rpc_api::katana::KatanaApiServer; use katana_rpc_api::saya::SayaApiServer; +use katana_rpc_api::solis::SolisApiServer; use katana_rpc_api::starknet::StarknetApiServer; use katana_rpc_api::torii::ToriiApiServer; use katana_rpc_api::ApiKind; @@ -32,6 +34,7 @@ use tower_http::cors::{AllowOrigin, CorsLayer}; use crate::dev::DevApi; use crate::katana::KatanaApi; use crate::saya::SayaApi; +use crate::solis::SolisApi; use crate::starknet::StarknetApi; use crate::torii::ToriiApi; @@ -59,6 +62,9 @@ pub async fn spawn( ApiKind::Saya => { methods.merge(SayaApi::new(sequencer.clone()).into_rpc())?; } + ApiKind::Solis => { + methods.merge(SolisApi::new(sequencer.clone(), &config).into_rpc())?; + } } } diff --git a/crates/katana/rpc/rpc/src/solis.rs b/crates/katana/rpc/rpc/src/solis.rs new file mode 100644 index 0000000000..16d4690674 --- /dev/null +++ b/crates/katana/rpc/rpc/src/solis.rs @@ -0,0 +1,59 @@ +use base64::decode; +use std::sync::Arc; + +use crate::config::ServerConfig; +use jsonrpsee::core::{async_trait, Error as RpcError}; +use katana_rpc_types::error::solis::SolisApiError; +use katana_core::hooker::HookerAddresses; +use katana_core::sequencer::KatanaSequencer; +use katana_executor::ExecutorFactory; +use katana_rpc_api::solis::SolisApiServer; +pub struct SolisApi { + sequencer: Arc>, + pub rpc_user: String, + pub rpc_password: String, +} + +impl SolisApi { + pub fn new(sequencer: Arc>, config: &ServerConfig) -> Self { + Self { + sequencer, + rpc_user: config.rpc_user.clone(), + rpc_password: config.rpc_password.clone(), + } + } + + fn verify_basic_auth(&self, encoded_credentials: &str) -> bool { + if let Ok(credentials) = decode(encoded_credentials) { + if let Ok(credentials_str) = String::from_utf8(credentials) { + let parts: Vec<&str> = credentials_str.split(':').collect(); + if parts.len() == 2 { + let (username, password) = (parts[0], parts[1]); + return username == self.rpc_user && password == self.rpc_password; + } + } + } + false + } +} + +#[async_trait] +impl SolisApiServer for SolisApi { + async fn set_addresses( + &self, + addresses: HookerAddresses, + basic_auth: String, + ) -> Result<(), RpcError> { + if !self.verify_basic_auth(&basic_auth) { + return Err(SolisApiError::AuthenticationFailed.to_rpc_error()); + } + + if let Some(hooker_lock) = self.sequencer.hooker.as_ref() { + let mut hooker = hooker_lock.write().await; + hooker.set_addresses(addresses); + Ok(()) + } else { + return Err(SolisApiError::HookerServiceUnavailable.to_rpc_error()); + } + } +} diff --git a/crates/katana/rpc/rpc/src/starknet.rs b/crates/katana/rpc/rpc/src/starknet.rs index a02ed30c48..03695ffbbf 100644 --- a/crates/katana/rpc/rpc/src/starknet.rs +++ b/crates/katana/rpc/rpc/src/starknet.rs @@ -675,6 +675,13 @@ impl StarknetApiServer for StarknetApi { &self, invoke_transaction: BroadcastedInvokeTx, ) -> RpcResult { + + if let Some(hooker) = &self.inner.sequencer.hooker { + let tx = invoke_transaction.0.clone(); + if !hooker.read().await.verify_invoke_tx_before_pool(tx).await { + return Err(StarknetApiError::SolisAssetFault.into()); + } + } self.on_io_blocking_task(move |this| { if invoke_transaction.is_query() { return Err(StarknetApiError::UnsupportedTransactionVersion.into());