diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 4f071a0f..5512023f 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -10,6 +10,7 @@ jobs: test: name: test runs-on: ubuntu-latest + environment: testing steps: - uses: actions/checkout@v3 diff --git a/Cargo.lock b/Cargo.lock index c068bb87..a784487d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2834,6 +2834,18 @@ dependencies = [ "once_cell", ] +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "fastrand" version = "2.3.0" @@ -3250,6 +3262,15 @@ dependencies = [ "fxhash", ] +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.3", +] + [[package]] name = "heck" version = "0.3.3" @@ -4022,6 +4043,16 @@ dependencies = [ "libc", ] +[[package]] +name = "libsqlite3-sys" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f" +dependencies = [ + "pkg-config", + "vcpkg", +] + [[package]] name = "linked-hash-map" version = "0.5.6" @@ -5073,7 +5104,7 @@ dependencies = [ [[package]] name = "python-bindings" -version = "0.1.21" +version = "0.1.24" dependencies = [ "futures", "num-bigint", @@ -5164,6 +5195,28 @@ version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +[[package]] +name = "r2d2" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93" +dependencies = [ + "log", + "parking_lot 0.12.4", + "scheduled-thread-pool", +] + +[[package]] +name = "r2d2_sqlite" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63417e83dc891797eea3ad379f52a5986da4bca0d6ef28baf4d14034dd111b0c" +dependencies = [ + "r2d2", + "rusqlite", + "uuid 1.17.0", +] + [[package]] name = "radium" version = "0.7.0" @@ -5530,6 +5583,20 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "rusqlite" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f" +dependencies = [ + "bitflags 2.9.1", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + [[package]] name = "rust-analyzer-salsa" version = "0.17.0-pre.6" @@ -5783,6 +5850,15 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "scheduled-thread-pool" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19" +dependencies = [ + "parking_lot 0.12.4", +] + [[package]] name = "schemars" version = "0.8.22" @@ -6228,11 +6304,14 @@ dependencies = [ "assert_matches", "async-trait", "auto_impl", + "blockifier", "cainome", "cargo_metadata", "chrono", "crossbeam-channel", "futures", + "r2d2", + "r2d2_sqlite", "serde", "serde_json", "starkbiter-bindings", @@ -6248,6 +6327,7 @@ dependencies = [ "tracing", "tracing-subscriber", "url", + "zstd 0.13.3", ] [[package]] @@ -6431,7 +6511,7 @@ dependencies = [ [[package]] name = "starknet-devnet-core" version = "0.4.1" -source = "git+https://github.com/baitcode/starknet-devnet.git?branch=experiment#991e344a7aae5ffe58c4fd32b3125ca935b3b11b" +source = "git+https://github.com/baitcode/starknet-devnet.git?branch=experiment#9dc1c6e3128705f59ccb97b46da6b8f4469c4d4e" dependencies = [ "blockifier", "cairo-lang-starknet-classes", @@ -6461,7 +6541,7 @@ dependencies = [ [[package]] name = "starknet-devnet-types" version = "0.4.1" -source = "git+https://github.com/baitcode/starknet-devnet.git?branch=experiment#991e344a7aae5ffe58c4fd32b3125ca935b3b11b" +source = "git+https://github.com/baitcode/starknet-devnet.git?branch=experiment#9dc1c6e3128705f59ccb97b46da6b8f4469c4d4e" dependencies = [ "base64 0.22.1", "bigdecimal", @@ -7525,6 +7605,7 @@ checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" dependencies = [ "getrandom 0.3.3", "js-sys", + "rand 0.9.1", "wasm-bindgen", ] @@ -8216,7 +8297,7 @@ dependencies = [ "pbkdf2 0.11.0", "sha1", "time", - "zstd", + "zstd 0.11.2+zstd.1.5.2", ] [[package]] @@ -8225,7 +8306,16 @@ version = "0.11.2+zstd.1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" dependencies = [ - "zstd-safe", + "zstd-safe 5.0.2+zstd.1.5.2", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe 7.2.4", ] [[package]] @@ -8238,6 +8328,15 @@ dependencies = [ "zstd-sys", ] +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + [[package]] name = "zstd-sys" version = "2.0.15+zstd.1.5.7" diff --git a/Cargo.toml b/Cargo.toml index 5392cf77..3e72efcc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,7 @@ starkbiter-macros = { path = "macros" } uuid = { version = "=1.17.0", features = ["v4"] } + starknet-devnet-core = { git = "https://github.com/baitcode/starknet-devnet.git", branch = "experiment" } starknet-devnet-types = { git = "https://github.com/baitcode/starknet-devnet.git", branch = "experiment" } starknet_api = "=0.14.0-rc.3" @@ -56,6 +57,14 @@ starknet-core = "=0.14.0" starknet-accounts = "=0.14.0" starknet-signers = "=0.12.0" +blockifier = { version = "0.14.0-rc.3" } + +r2d2 = { version = "0.8.10" } +r2d2_sqlite = { version = "0.31.0" } +rusqlite = { version = "0.37.0" } + +zstd = { version = "0.13.3" } + # Arbiter crates.io for release, these need to be used to do crate releases! # arbiter-bindings = "0.1.7" # arbiter-core = "0.11.0" @@ -78,6 +87,7 @@ async-trait = { version = "0.1.80" } crossbeam-channel = { version = "0.5.12" } syn = { version = "2.0.60", features = ["full"] } +proc-macro = { version = "*" } proc-macro2 = { version = "1.0.79" } tracing = "0.1.40" diff --git a/README.md b/README.md index d951883b..ec9fea98 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ ![Github Actions](https://github.com/astraly-labs/starkbiter/workflows/test/badge.svg) ## Overview -> **Starkbiter** is a framework for orchestrating event based agentic simulations on top of Starknet Devnet sandbox. +> **Starkbiter** is a framework for orchestrating event based agentic simulations on top of Starknet Devnet sandbox. Allows for using with local sqlite snapshot (`Pathfinder` structure) which operates blazingly fast. Starkbiter provides a Full JSON-RPC Starknet Node capabilities with additional methods to control block production, contract and account deployments and declaration. It integrates deeply with starknet-devnet and starknet-rs types crates allowing for seamless integration for all the tooling that depends on those. Thus, it also provides additional layer of well known contract bindings generated by [cainome](https://github.com/cartridge-gg/cainome). @@ -47,7 +47,12 @@ It will also be helpful to get the `cargo-generate` package, which you can insta cargo install cargo-generate ``` +## Using from python code + +For an example usage of python library and reference wrapper implementation check out [this](https://github.com/baitcode/starkbiter-python-example) repo + ### Examples + We have an example that will run what we have set up in a template. To run this, you can clone the repository and update the submodules: ```bash diff --git a/core/Cargo.toml b/core/Cargo.toml index 10737bbb..7b35f1b9 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -21,6 +21,8 @@ starknet-devnet-core.workspace = true starknet-devnet-types.workspace = true starknet_api.workspace = true +blockifier.workspace = true + # Serialization serde.workspace = true serde_json.workspace = true @@ -32,6 +34,14 @@ crossbeam-channel.workspace = true futures.workspace = true auto_impl = "1.3.0" +url.workspace = true + +# Database +r2d2.workspace = true +r2d2_sqlite.workspace = true + +# Utils +zstd.workspace = true # Errors anyhow.workspace = true @@ -44,7 +54,6 @@ tracing.workspace = true [dev-dependencies] cainome.workspace = true starkbiter-bindings.workspace = true -url.workspace = true tracing-subscriber = "0.3.18" # For bench diff --git a/core/mainnet-trimmed.sqlite b/core/mainnet-trimmed.sqlite new file mode 100644 index 00000000..7cd6f94d Binary files /dev/null and b/core/mainnet-trimmed.sqlite differ diff --git a/core/src/environment/instruction.rs b/core/src/environment/instruction.rs index 74f42d1c..2cb39790 100644 --- a/core/src/environment/instruction.rs +++ b/core/src/environment/instruction.rs @@ -446,6 +446,8 @@ pub enum CheatInstruction { /// Fetches block with transactions by its identifier /// and adds them on top of pending block in DevNet. ReplayBlockWithTxs { + /// The URL of the node API to fetch block from. + url: Url, /// The identifier of the block to retrieve. block_id: core_types::BlockId, /// Checks if transaction has events matching filters and applies it if diff --git a/core/src/environment/mod.rs b/core/src/environment/mod.rs index 3c09ecf5..c6897c3a 100644 --- a/core/src/environment/mod.rs +++ b/core/src/environment/mod.rs @@ -65,8 +65,14 @@ use tracing::debug; use super::*; use crate::tokens::get_token_data; - pub mod instruction; + +/// This module provides a reader for the Starknet state stored in a SQLite +/// database. It is compatible with starknet-devnet defaulter implementation, +/// and allows to replace node api http requests for state storage data with +/// SQLite queries for Pathfinder formatted sqlite database. +pub mod sqlite_state_reader; + use instruction::{Instruction, NodeInstruction, NodeOutcome, Outcome}; mod utils; @@ -1679,6 +1685,7 @@ async fn process_instructions( } } instruction::CheatInstruction::ReplayBlockWithTxs { + url, block_id, has_events: filters, override_nonce, @@ -1689,16 +1696,6 @@ async fn process_instructions( "Environment. Received ReplayBlockWithTxs instruction: tx_hash: {:?}", block_id, ); - let maybe_url = starknet.config.fork_config.clone().url; - if maybe_url.is_none() { - if let Err(e) = sender.send(Err(StarkbiterCoreError::NoForkConfig)) { - error!("Failed to send Cheating GetBlockWithTxs error: {:?}", e); - stop = true; - } - continue; - } - - let url = maybe_url.unwrap(); let provider = JsonRpcClient::new(HttpTransport::new(url.clone())); @@ -1708,7 +1705,7 @@ async fn process_instructions( if let Err(e) = sender.send(Err(StarkbiterCoreError::InternalError(e.to_string()))) { - error!("Failed to send Cheating GetBlockWithTxs error: {:?}", e); + error!("Failed to send Cheating ReplayBlockWithTxs error: {:?}", e); stop = true; } continue; diff --git a/core/src/environment/sqlite_state_reader.rs b/core/src/environment/sqlite_state_reader.rs new file mode 100644 index 00000000..90df5590 --- /dev/null +++ b/core/src/environment/sqlite_state_reader.rs @@ -0,0 +1,390 @@ +use std::sync::Arc; + +/// SQLiteStateReader +/// This module provides a reader for the Starknet state stored in a SQLite +/// database. It is compatible with starknet-devnet defaulter implementation, +/// and allows to replace node api http requests for state storage data with +/// SQLite queries for Pathfinder formatted sqlite database. +use blockifier::{ + execution::contract_class::RunnableCompiledClass, + state::{errors::StateError, state_api::StateResult}, +}; +use r2d2_sqlite::{ + rusqlite::{params, OptionalExtension}, + SqliteConnectionManager, +}; +use starknet_devnet_core::starknet::defaulter::{OriginReader, StarknetDefaulter}; +use url::Url; + +#[derive(Debug, Clone)] +/// Represents a reader for the Starknet state stored in a SQLite database. +/// This reader allows querying the state at a specific block number. +/// It implements the `OriginReader` trait, which provides methods to read +/// storage, nonce, compiled classes, and class hashes from the state. +pub struct SQLiteStateReader { + block_number: u64, + pool: r2d2::Pool, +} + +impl SQLiteStateReader { + /// Factory function to create a new SQLiteStateReader. Complying with the + /// OriginReaderFactory type from starknet-devnet fork. + pub fn new_sqlite_state_reader(url: Url, block_number: u64) -> StarknetDefaulter { + let manager = SqliteConnectionManager::file(url.path()); + let pool = r2d2::Pool::new(manager).unwrap(); + StarknetDefaulter::new_with_reader(Arc::new(SQLiteStateReader { block_number, pool })) + } +} + +impl OriginReader for SQLiteStateReader { + fn get_storage_at( + &self, + contract_address: starknet_api::core::ContractAddress, + key: starknet_api::state::StorageKey, + ) -> StateResult { + let connection = self.pool.get().unwrap(); + let result = connection.query_one::, _, _>( + r" + SELECT storage_value + FROM storage_updates + JOIN contract_addresses ON contract_addresses.id = storage_updates.contract_address_id + JOIN storage_addresses ON storage_addresses.id = storage_updates.storage_address_id + WHERE contract_address = ? + AND storage_address = ? + AND block_number <= ? + ORDER BY block_number DESC LIMIT 1 + ", + params![contract_address.to_bytes_be(), key.to_bytes_be(), self.block_number], + |row| row.get(0) + ) + .optional() + .map_err(|err| { + StateError::StateReadError(format!("Failed to read storage. {}", err)) + })?; + + Ok(starknet_core::types::Felt::from_bytes_be_slice( + result.unwrap_or_default().as_slice(), + )) + } + + fn get_nonce_at( + &self, + contract_address: starknet_api::core::ContractAddress, + ) -> StateResult { + let connection = self.pool.get().unwrap(); + let result = connection + .query_one::, _, _>( + r" + SELECT nonce + FROM nonce_updates + JOIN contract_addresses ON contract_addresses.id = nonce_updates.contract_address_id + WHERE contract_address = ? + AND block_number <= ? + ORDER BY block_number DESC LIMIT 1 + ", + params![contract_address.to_bytes_be(), self.block_number], + |row| row.get(0), + ) + .optional() + .map_err(|err| StateError::StateReadError(format!("Failed to read nonce. {}", err)))?; + let felt = + starknet_core::types::Felt::from_bytes_be_slice(result.unwrap_or_default().as_slice()); + Ok(starknet_api::core::Nonce(felt)) // Placeholder implementation + } + + fn get_compiled_class( + &self, + class_hash: starknet_api::core::ClassHash, + ) -> StateResult { + let connection = self.pool.get().unwrap(); + + let casm_definition = connection + .query_row::, _, _>( + r" + SELECT + casm_definitions.definition, + class_definitions.block_number + FROM + casm_definitions + INNER JOIN class_definitions ON ( + class_definitions.hash = casm_definitions.hash + ) + WHERE + casm_definitions.hash = ? + AND class_definitions.block_number <= ?", + params![class_hash.to_bytes_be(), self.block_number], + |row| row.get(0), + ) + .optional() + .map_err(|err| { + StateError::StateReadError(format!("Failed to read storage. {}", err)) + })?; + + let class_definition = connection + .query_row::, _, _>( + r" + SELECT + definition, + block_number + FROM + class_definitions + WHERE 1=1 + AND hash = :hash + AND block_number <= :block_number + ORDER BY block_number DESC + LIMIT 1 + ", + params![class_hash.to_bytes_be(), self.block_number], + |row| { + let rf = row.get_ref(0)?; + let blob = rf.as_blob()?; + Ok(blob.to_vec()) + }, + ) + .map_err(|err| StateError::StateReadError(format!("Failed to create stmt. {}", err)))?; + + // If we have a CASM class_definition, we can return it + let class_definition = zstd::decode_all(class_definition.as_slice()).map_err(|err| { + StateError::StateReadError(format!("Decompressing compiled class definition. {}", err)) + })?; + + let class_definition_str = String::from_utf8(class_definition).map_err(|error| { + StateError::StateReadError(format!("Class definition is not valid UTF-8: {error}")) + })?; + + if let Some(casm_definition) = casm_definition { + let json_repr: serde_json::Value = serde_json::from_str(&class_definition_str) + .map_err(|err| { + StateError::StateReadError(format!( + "Decompressing compiled class definition. {}", + err + )) + })?; + + let repr = json_repr.as_object().unwrap(); + let program = repr.get("sierra_program").unwrap().as_array().unwrap(); + let (version_components_raw, _) = program.split_first_chunk::<3>().unwrap(); + + let version_components = version_components_raw.clone().map(|s| { + u64::from_str_radix(s.as_str().unwrap().trim_start_matches("0x"), 16).unwrap() + }); + + let version = starknet_api::contract_class::SierraVersion::new( + version_components[0], + version_components[1], + version_components[2], + ); + + // If we have a class definition, we can return it + let casm_definition = zstd::decode_all(casm_definition.as_slice()).map_err(|err| { + StateError::StateReadError(format!( + "Decompressing compiled class definition. {}", + err + )) + })?; + + let casm_definition = String::from_utf8(casm_definition).map_err(|error| { + StateError::StateReadError(format!("CASM definition is not valid UTF-8: {error}")) + })?; + + let casm_class = + blockifier::execution::contract_class::CompiledClassV1::try_from_json_string( + &casm_definition, + version, + ) + .map_err(StateError::ProgramError)?; + + Ok(RunnableCompiledClass::V1(casm_class)) + } else { + let class = + blockifier::execution::contract_class::CompiledClassV0::try_from_json_string( + &class_definition_str, + ) + .map_err(StateError::ProgramError)?; + + Ok(RunnableCompiledClass::V0(class)) + } + } + + fn get_class_hash_at( + &self, + contract_address: starknet_api::core::ContractAddress, + ) -> StateResult { + let connection = self.pool.get().unwrap(); + let result = connection + .query_one::, _, _>( + r" + SELECT + class_hash + FROM contract_updates + WHERE contract_address = ? + AND block_number <= ? + ORDER BY block_number DESC + LIMIT 1", + params![contract_address.to_bytes_be(), self.block_number], + |row| row.get(0), + ) + .optional() + .map_err(|err| { + StateError::StateReadError(format!("Failed to read storage. {}", err)) + })?; + let stark_hash = + starknet_core::types::Felt::from_bytes_be_slice(result.unwrap_or_default().as_slice()); + Ok(starknet_api::core::ClassHash(stark_hash)) + } +} + +#[cfg(test)] +mod tests { + + use starknet_api::abi::abi_utils::get_storage_var_address; + use starknet_core::types::Felt; + + use super::*; + + #[test] + fn test_sqlite_get_compiled_class_v1() { + let dir = std::env::current_dir().unwrap(); + let db_path = format!("sqlite://{}/mainnet-trimmed.sqlite", dir.to_str().unwrap()); + + let reader = + SQLiteStateReader::new_sqlite_state_reader(Url::parse(&db_path).unwrap(), 1642947); + + let class_hash = + starknet_api::core::ClassHash(starknet_core::types::Felt::from_hex_unchecked( + "070D4F063FE6CA22667E3B451D9AAF298337153982D45DD91CFFD2AE7939C074", + )); + let res = reader.get_compiled_class(class_hash); + + match res { + Ok(RunnableCompiledClass::V1(_)) => {} + _ => { + assert!(false, "Expected a V1 compiled class. Got: {:?}", res); + } + } + } + + #[test] + fn test_sqlite_get_compiled_class_v0() { + let dir = std::env::current_dir().unwrap(); + let db_path = format!("sqlite://{}/mainnet-trimmed.sqlite", dir.to_str().unwrap()); + + let reader = + SQLiteStateReader::new_sqlite_state_reader(Url::parse(&db_path).unwrap(), 1642947); + + let class_hash = + starknet_api::core::ClassHash(starknet_core::types::Felt::from_hex_unchecked( + "03a140031fa515aab4e798a9d9265db749f9ea49d2ecb839a2efcc9e0f57b10e", + )); + let res = reader.get_compiled_class(class_hash); + + match res { + Ok(RunnableCompiledClass::V0(_)) => {} + _ => { + assert!(false, "Expected a V1 compiled class. Got: {:?}", res); + } + } + } + + #[test] + fn test_sqlite_get_storage_at() { + let dir = std::env::current_dir().unwrap(); + let db_path = format!("sqlite://{}/mainnet-trimmed.sqlite", dir.to_str().unwrap()); + + let reader = + SQLiteStateReader::new_sqlite_state_reader(Url::parse(&db_path).unwrap(), 1642947); + + let address = get_storage_var_address( + "ERC20_balances", + [ + starknet_core::types::Felt::from_hex_unchecked( + "0x049D36570D4e46f48e99674bd3fcc84644DdD6b96F7C741B1562B82f9e004dC7", + ), // Stark Gate contract address + ] + .as_slice(), + ); + + // Ether: Stark Gate + let contract_address = starknet_api::core::ContractAddress::try_from( + starknet_core::types::Felt::from_hex_unchecked( + "0x049D36570D4e46f48e99674bd3fcc84644DdD6b96F7C741B1562B82f9e004dC7", + ), + ) + .unwrap(); + + println!("Address: {:?}", address); + + let res = reader.get_storage_at(contract_address, address); + + assert!( + res.is_ok(), + "Expected to read storage successfully. Got: {:?}", + res + ); + + assert!( + Felt::from_hex_unchecked("0xd501e7d0039aec6b") == res.unwrap(), + "Expected storage value to match" + ); + } + + #[test] + fn test_sqlite_get_nonce_at() { + let dir = std::env::current_dir().unwrap(); + let db_path = format!("sqlite://{}/mainnet-trimmed.sqlite", dir.to_str().unwrap()); + + let reader = + SQLiteStateReader::new_sqlite_state_reader(Url::parse(&db_path).unwrap(), 2002947); + + let contract_address = starknet_api::core::ContractAddress::try_from( + starknet_core::types::Felt::from_hex_unchecked( + "0x01a8b86c9bb05047b0136a96146c3a5bb5c806afa90687756be45341a86f8e37", + ), + ) + .unwrap(); + let res = reader.get_nonce_at(contract_address); + + assert!( + res.is_ok(), + "Expected to read nonce successfully. Got: {:?}", + res + ); + + println!("Nonce: {:?}", res); + + assert!( + res.unwrap().0 == starknet_core::types::Felt::from_hex_unchecked("0x2ad22"), + "Expected nonce to match" + ); + } + + #[test] + fn test_sqlite_get_class_hash_at() { + let dir = std::env::current_dir().unwrap(); + let db_path = format!("sqlite://{}/mainnet-trimmed.sqlite", dir.to_str().unwrap()); + + let reader = + SQLiteStateReader::new_sqlite_state_reader(Url::parse(&db_path).unwrap(), 1642947); + + let contract_address = starknet_api::core::ContractAddress::try_from( + starknet_core::types::Felt::from_hex_unchecked( + "0x049D36570D4e46f48e99674bd3fcc84644DdD6b96F7C741B1562B82f9e004dC7", + ), + ) + .unwrap(); + let res = reader.get_class_hash_at(contract_address); + + assert!( + res.is_ok(), + "Expected to read class hash successfully. Got: {:?}", + res + ); + assert!( + res.unwrap() + == starknet_api::core::ClassHash(starknet_core::types::Felt::from_hex_unchecked( + "0x07f3777c99f3700505ea966676aac4a0d692c2a9f5e667f4c606b51ca1dd3420" + )), + "Expected class hash to match" + ); + } +} diff --git a/core/src/middleware/cheating_provider.rs b/core/src/middleware/cheating_provider.rs index 7acf5005..e7424f68 100644 --- a/core/src/middleware/cheating_provider.rs +++ b/core/src/middleware/cheating_provider.rs @@ -8,6 +8,7 @@ use starknet_devnet_types::{ num_bigint::BigUint, rpc::gas_modification::{GasModification, GasModificationRequest}, }; +use url::Url; use crate::{environment::instruction::EventFilter, tokens::TokenId}; @@ -115,6 +116,7 @@ pub trait CheatingProvider { /// against pending block of the local version. async fn replay_block_with_txs( &self, + url: Url, block_id: B, filters: F, override_nonce: bool, diff --git a/core/src/middleware/connection.rs b/core/src/middleware/connection.rs index b1254e6e..cb63fef1 100644 --- a/core/src/middleware/connection.rs +++ b/core/src/middleware/connection.rs @@ -7,6 +7,7 @@ use starknet::providers::{Provider, ProviderError}; use starknet_core::types::{self as core_types}; use starknet_devnet_types::num_bigint::BigUint; use tokio::sync::broadcast; +use url::Url; use super::*; use crate::{ @@ -354,6 +355,7 @@ impl CheatingProvider for Connection { async fn replay_block_with_txs( &self, + url: Url, block_id: B, filters: F, override_nonce: bool, @@ -363,6 +365,7 @@ impl CheatingProvider for Connection { F: Into>> + Send + Sync, { let to_send = Instruction::Cheat(CheatInstruction::ReplayBlockWithTxs { + url, block_id: *block_id.as_ref(), has_events: filters.into(), override_nonce, diff --git a/core/src/middleware/mod.rs b/core/src/middleware/mod.rs index f6a557de..4222debd 100644 --- a/core/src/middleware/mod.rs +++ b/core/src/middleware/mod.rs @@ -25,6 +25,7 @@ use starknet_devnet_types::{ num_bigint::BigUint, rpc::gas_modification::{GasModification, GasModificationRequest}, }; +use url::Url; use super::*; use crate::{ @@ -661,6 +662,7 @@ impl Middleware for StarkbiterMiddleware { async fn replay_block_with_txs( &self, + url: Url, block_id: B, filters: F, override_nonce: bool, @@ -670,7 +672,7 @@ impl Middleware for StarkbiterMiddleware { F: Into>> + Send + Sync, { self.connection() - .replay_block_with_txs(block_id, filters, override_nonce) + .replay_block_with_txs(url, block_id, filters, override_nonce) .await } diff --git a/core/src/middleware/traits.rs b/core/src/middleware/traits.rs index 831764e4..b0860a9a 100644 --- a/core/src/middleware/traits.rs +++ b/core/src/middleware/traits.rs @@ -595,6 +595,7 @@ pub trait Middleware { /// against pending block of the local version. async fn replay_block_with_txs( &self, + url: Url, block_id: B, filters: F, override_nonce: bool, @@ -604,7 +605,7 @@ pub trait Middleware { F: Into>> + Send + Sync, { self.inner() - .replay_block_with_txs(block_id, filters, override_nonce) + .replay_block_with_txs(url, block_id, filters, override_nonce) .await } } diff --git a/python-bindings/Cargo.toml b/python-bindings/Cargo.toml index 22da7187..d6716184 100644 --- a/python-bindings/Cargo.toml +++ b/python-bindings/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "python-bindings" -version = "0.1.21" +version = "0.1.24" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/python-bindings/poetry.lock b/python-bindings/poetry.lock index 57e10ca0..9dbda86d 100644 --- a/python-bindings/poetry.lock +++ b/python-bindings/poetry.lock @@ -1,76 +1,7 @@ # This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. - -[[package]] -name = "pycryptodome" -version = "3.23.0" -description = "Cryptographic library for Python" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -groups = ["main"] -files = [ - {file = "pycryptodome-3.23.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a176b79c49af27d7f6c12e4b178b0824626f40a7b9fed08f712291b6d54bf566"}, - {file = "pycryptodome-3.23.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:573a0b3017e06f2cffd27d92ef22e46aa3be87a2d317a5abf7cc0e84e321bd75"}, - {file = "pycryptodome-3.23.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:63dad881b99ca653302b2c7191998dd677226222a3f2ea79999aa51ce695f720"}, - {file = "pycryptodome-3.23.0-cp27-cp27m-win32.whl", hash = "sha256:b34e8e11d97889df57166eda1e1ddd7676da5fcd4d71a0062a760e75060514b4"}, - {file = "pycryptodome-3.23.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:7ac1080a8da569bde76c0a104589c4f414b8ba296c0b3738cf39a466a9fb1818"}, - {file = "pycryptodome-3.23.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:6fe8258e2039eceb74dfec66b3672552b6b7d2c235b2dfecc05d16b8921649a8"}, - {file = "pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:0011f7f00cdb74879142011f95133274741778abba114ceca229adbf8e62c3e4"}, - {file = "pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:90460fc9e088ce095f9ee8356722d4f10f86e5be06e2354230a9880b9c549aae"}, - {file = "pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4764e64b269fc83b00f682c47443c2e6e85b18273712b98aa43bcb77f8570477"}, - {file = "pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb8f24adb74984aa0e5d07a2368ad95276cf38051fe2dc6605cbcf482e04f2a7"}, - {file = "pycryptodome-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d97618c9c6684a97ef7637ba43bdf6663a2e2e77efe0f863cce97a76af396446"}, - {file = "pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a53a4fe5cb075075d515797d6ce2f56772ea7e6a1e5e4b96cf78a14bac3d265"}, - {file = "pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:763d1d74f56f031788e5d307029caef067febf890cd1f8bf61183ae142f1a77b"}, - {file = "pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:954af0e2bd7cea83ce72243b14e4fb518b18f0c1649b576d114973e2073b273d"}, - {file = "pycryptodome-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:257bb3572c63ad8ba40b89f6fc9d63a2a628e9f9708d31ee26560925ebe0210a"}, - {file = "pycryptodome-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6501790c5b62a29fcb227bd6b62012181d886a767ce9ed03b303d1f22eb5c625"}, - {file = "pycryptodome-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9a77627a330ab23ca43b48b130e202582e91cc69619947840ea4d2d1be21eb39"}, - {file = "pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27"}, - {file = "pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843"}, - {file = "pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490"}, - {file = "pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575"}, - {file = "pycryptodome-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b"}, - {file = "pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a"}, - {file = "pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f"}, - {file = "pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa"}, - {file = "pycryptodome-3.23.0-cp37-abi3-win32.whl", hash = "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886"}, - {file = "pycryptodome-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2"}, - {file = "pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c"}, - {file = "pycryptodome-3.23.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:350ebc1eba1da729b35ab7627a833a1a355ee4e852d8ba0447fafe7b14504d56"}, - {file = "pycryptodome-3.23.0-pp27-pypy_73-win32.whl", hash = "sha256:93837e379a3e5fd2bb00302a47aee9fdf7940d83595be3915752c74033d17ca7"}, - {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ddb95b49df036ddd264a0ad246d1be5b672000f12d6961ea2c267083a5e19379"}, - {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e95564beb8782abfd9e431c974e14563a794a4944c29d6d3b7b5ea042110b4"}, - {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14e15c081e912c4b0d75632acd8382dfce45b258667aa3c67caf7a4d4c13f630"}, - {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7fc76bf273353dc7e5207d172b83f569540fc9a28d63171061c42e361d22353"}, - {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:45c69ad715ca1a94f778215a11e66b7ff989d792a4d63b68dc586a1da1392ff5"}, - {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:865d83c906b0fc6a59b510deceee656b6bc1c4fa0d82176e2b77e97a420a996a"}, - {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89d4d56153efc4d81defe8b65fd0821ef8b2d5ddf8ed19df31ba2f00872b8002"}, - {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3f2d0aaf8080bda0587d58fc9fe4766e012441e2eed4269a77de6aea981c8be"}, - {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:64093fc334c1eccfd3933c134c4457c34eaca235eeae49d69449dc4728079339"}, - {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ce64e84a962b63a47a592690bdc16a7eaf709d2c2697ababf24a0def566899a6"}, - {file = "pycryptodome-3.23.0.tar.gz", hash = "sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef"}, -] - -[[package]] -name = "starknet-abi" -version = "1.0.0" -description = "Performant Starkent ABI Decoding Library designed for Data Indexing" -optional = false -python-versions = ">=3.10,<4.0" -groups = ["main"] -files = [] -develop = false - -[package.dependencies] -pycryptodome = ">=3.4.6" - -[package.source] -type = "git" -url = "https://github.com/NethermindEth/starknet-abi.git" -reference = "HEAD" -resolved_reference = "7af5967ebd9f8647025f3648102dfc51c104c3e1" +package = [] [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "f2ff70fe261709741d0de01e0eb9cd842c8b807497d4c832795490402fe7b2c8" +content-hash = "1771a49dfde8732f30c1b0759f77fb125004b2ee7e201dcef85af0b88defb518" diff --git a/python-bindings/pyproject.toml b/python-bindings/pyproject.toml index 40c48260..6c18233e 100644 --- a/python-bindings/pyproject.toml +++ b/python-bindings/pyproject.toml @@ -11,6 +11,7 @@ classifiers = [ "Programming Language :: Python :: Implementation :: PyPy", ] dynamic = ["version"] +# version = "0.1.22" [tool.maturin] features = ["pyo3/extension-module"] diff --git a/python-bindings/src/environment.rs b/python-bindings/src/environment.rs index e6766fe6..6f7a6b3c 100644 --- a/python-bindings/src/environment.rs +++ b/python-bindings/src/environment.rs @@ -3,7 +3,7 @@ use std::{collections::HashMap, sync::OnceLock}; use pyo3::prelude::*; use starkbiter_core::{ - environment, + environment::{self, sqlite_state_reader::SQLiteStateReader}, tokens::{self, TokenId}, }; use starknet::providers::Url; @@ -135,6 +135,12 @@ pub fn create_environment<'p>( pyo3_asyncio::tokio::future_into_py::<_, _>(py, async move { let chain_id = Felt::from_hex(&chain_id_local).unwrap(); + starknet_devnet_core::starknet::defaulter::StarknetDefaulter::register_defaulter( + "sqlite", + SQLiteStateReader::new_sqlite_state_reader, + ) + .expect("Can't register SQLite state reader"); + // Spin up a new environment with the specified chain ID let mut builder = environment::Environment::builder() .with_chain_id(chain_id.into()) diff --git a/python-bindings/src/middleware.rs b/python-bindings/src/middleware.rs index 38fe2bf8..13169af4 100644 --- a/python-bindings/src/middleware.rs +++ b/python-bindings/src/middleware.rs @@ -13,6 +13,7 @@ use starkbiter_core::{ middleware::{connection::Connection, traits::Middleware, StarkbiterMiddleware}, tokens, }; +use starknet::providers::Url; use starknet_accounts::{Account, SingleOwnerAccount}; use starknet_core::types::{self}; use starknet_devnet_types::rpc::gas_modification::GasModificationRequest; @@ -988,11 +989,13 @@ impl From<&EventFilter> for types::EventFilter { pub fn replay_block_with_txs<'p>( py: Python<'p>, middleware_id: &str, + url: &str, block_id: BlockId, has_events: Option>, override_nonce: Option, ) -> PyResult<&'p PyAny> { let middleware_id_local = middleware_id.to_string(); + let url_local = url.to_string(); pyo3_asyncio::tokio::future_into_py::<_, _>(py, async move { let middlewares_lock = middlewares().lock().await; @@ -1005,6 +1008,10 @@ pub fn replay_block_with_txs<'p>( ))); } + let url = Url::parse(&url_local).map_err(|e| { + PyErr::new::(format!("Invalid url: {:?}", e)) + })?; + let middleware = maybe_middleware.unwrap(); let block_id = types::BlockId::try_from(block_id).map_err(|e| { @@ -1016,7 +1023,7 @@ pub fn replay_block_with_txs<'p>( has_events.map(|filters| filters.iter().map(|f| f.into()).collect::>()); middleware - .replay_block_with_txs(block_id, has_events, override_nonce.unwrap_or(false)) + .replay_block_with_txs(url, block_id, has_events, override_nonce.unwrap_or(false)) .await .map_err(|e| { PyErr::new::(format!("Failed to replay block: {}", e)) diff --git a/tests/basic_middleware.rs b/tests/basic_middleware.rs index 871fecb4..d5598e9e 100644 --- a/tests/basic_middleware.rs +++ b/tests/basic_middleware.rs @@ -360,17 +360,17 @@ async fn test_replay_block_transactions_containing_ekubo_swaps() { let chain_id = ChainId::Mainnet; let alchemy_key = std::env::var("ALCHEMY_KEY").expect("ALCHEMY_KEY must be set"); - let url = Url::parse(&format!( + let node_url = format!( "https://starknet-mainnet.g.alchemy.com/starknet/version/rpc/v0_8/{}/", alchemy_key - )) - .unwrap(); + ); + let url = Url::parse(&node_url).unwrap(); // Spin up a new environment with the specified chain ID let env = Environment::builder() .with_chain_id(chain_id.into()) .with_fork( - url, + url.clone(), 1586288, Felt::from_hex_unchecked( "0x634060800585f64b2f5c51cfd14f2770057b7a28ab39767558159e8037acd38", @@ -392,7 +392,7 @@ async fn test_replay_block_transactions_containing_ekubo_swaps() { }; let (added, ignored, failed) = client - .replay_block_with_txs(BlockId::Number(1586289), Some(vec![filter]), false) + .replay_block_with_txs(url, BlockId::Number(1586289), Some(vec![filter]), false) .await .expect("Block was not found should've been");