diff --git a/Cargo.lock b/Cargo.lock index c0c87a5ac..2c81d8941 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -760,6 +760,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "bollard-stubs" +version = "1.42.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed59b5c00048f48d7af971b71f800fdf23e858844a6f9e4d32ca72e9399e7864" +dependencies = [ + "serde", + "serde_with 1.14.0", +] + [[package]] name = "brotli" version = "3.4.0" @@ -2367,12 +2377,6 @@ version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" -[[package]] -name = "downcast" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" - [[package]] name = "dunce" version = "1.0.4" @@ -3034,12 +3038,6 @@ dependencies = [ "walkdir", ] -[[package]] -name = "fragile" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" - [[package]] name = "fs2" version = "0.4.3" @@ -4921,7 +4919,6 @@ dependencies = [ "async-trait", "auto_impl", "bytes", - "ctor", "dojo-test-utils", "dotenv", "env_logger", @@ -4935,7 +4932,6 @@ dependencies = [ "katana-core", "lazy_static", "log", - "mockall", "mongodb", "reqwest", "reth-primitives", @@ -4951,6 +4947,7 @@ dependencies = [ "starknet-abigen-parser", "starknet-crypto 0.6.1", "starknet_api", + "testcontainers", "thiserror", "tokio", "toml 0.7.8", @@ -5296,33 +5293,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "mockall" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43766c2b5203b10de348ffe19f7e54564b64f3d6018ff7648d1e2d6d3a0f0a48" -dependencies = [ - "cfg-if", - "downcast", - "fragile", - "lazy_static", - "mockall_derive", - "predicates", - "predicates-tree", -] - -[[package]] -name = "mockall_derive" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af7cbce79ec385a1d4f54baa90a76401eb15d9cab93685f62e7e9f942aa00ae2" -dependencies = [ - "cfg-if", - "proc-macro2", - "quote", - "syn 2.0.39", -] - [[package]] name = "modular-bitfield" version = "0.11.2" @@ -8040,6 +8010,23 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" +[[package]] +name = "testcontainers" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d2931d7f521af5bae989f716c3fa43a6af9af7ec7a5e21b59ae40878cec00" +dependencies = [ + "bollard-stubs", + "futures", + "hex", + "hmac", + "log", + "rand", + "serde", + "serde_json", + "sha2", +] + [[package]] name = "thiserror" version = "1.0.50" diff --git a/Cargo.toml b/Cargo.toml index 474ea0b2a..c9dccd5c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,7 +44,6 @@ anyhow = { version = "1.0.68", default-features = false } async-trait = { version = "0.1.58", default-features = false } auto_impl = { version = "1.1.0", default-features = false } bytes = { version = "1", default-features = false } -ctor = { version = "0.2.4", default-features = false } dotenv = { version = "0.15.0", default-features = false } env_logger = { version = "0.10.0", default-features = false } eyre = { version = "0.6.8", default-features = false } @@ -54,7 +53,6 @@ futures = { version = "0.3.26", default-features = false } hex = { version = "0.4", default-features = false } lazy_static = { version = "1.4.0", default-features = false } log = { version = "0.4.17", default-features = false } -mockall = { version = "0.12.1", default-features = false } mongodb = { version = "2.8.0", default-features = false, features = [ "tokio-runtime", ] } @@ -62,6 +60,7 @@ reqwest = { version = "0.11.13", default-features = false } ruint = { version = "1.9.0", default-features = false } rstest = { version = "0.18.1", default-features = false } +testcontainers = { version = "0.15.0", default-features = false, optional = true } thiserror = { version = "1.0.38", default-features = false } tokio = { version = "1.21.2", features = ["macros"] } tower = { version = "0.4.12", default-features = false } @@ -90,3 +89,6 @@ cairo-vm = { git = "https://github.com/dojoengine/cairo-rs.git", rev = "262b7eb4 dojo-test-utils = { git = 'https://github.com/dojoengine/dojo', rev = "be16762" } rstest = { version = "0.18.1", default-features = false } toml = { version = "0.7.5", default-features = false } + +[features] +testing = ["testcontainers"] diff --git a/Makefile b/Makefile index 52410134d..7c23c83cc 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,3 @@ -HURL_FILES = $(shell find ./rpc-call-examples/ -name '*.hurl') - -include .env export @@ -7,6 +5,9 @@ ifndef STARKNET_NETWORK override STARKNET_NETWORK = katana endif +DECLARATIONS=./lib/kakarot/deployments/$(STARKNET_NETWORK)/declarations.json +DEPLOYMENTS=./lib/kakarot/deployments/$(STARKNET_NETWORK)/deployments.json + setup: .gitmodules chmod +x ./scripts/extract_abi.sh git submodule update --init --recursive @@ -16,8 +17,14 @@ setup: .gitmodules deploy-kakarot: cd lib/kakarot && STARKNET_NETWORK=$(STARKNET_NETWORK) poetry run python ./scripts/deploy_kakarot.py && cd .. -run-dev: - PROXY_ACCOUNT_CLASS_HASH=$(shell jq -r '.proxy' ./lib/kakarot/deployments/$(STARKNET_NETWORK)/declarations.json) CONTRACT_ACCOUNT_CLASS_HASH=$(shell jq -r '.contract_account' ./lib/kakarot/deployments/$(STARKNET_NETWORK)/declarations.json) EXTERNALLY_OWNED_ACCOUNT_CLASS_HASH=$(shell jq -r '.externally_owned_account' ./lib/kakarot/deployments/$(STARKNET_NETWORK)/declarations.json) KAKAROT_ADDRESS=$(shell jq -r '.kakarot.address' ./lib/kakarot/deployments/$(STARKNET_NETWORK)/deployments.json) RUST_LOG=trace cargo run --bin kakarot-rpc +load-env: + $(eval PROXY_ACCOUNT_CLASS_HASH=$(shell jq -r '.proxy' $(DECLARATIONS))) + $(eval CONTRACT_ACCOUNT_CLASS_HASH=$(shell jq -r '.contract_account' $(DECLARATIONS))) + $(eval EXTERNALLY_OWNED_ACCOUNT_CLASS_HASH=$(shell jq -r '.externally_owned_account' $(DECLARATIONS))) + $(eval KAKAROT_ADDRESS=$(shell jq -r '.kakarot.address' $(DEPLOYMENTS))) + +run-dev: load-env + RUST_LOG=trace cargo run --bin kakarot-rpc # Run Katana, Deploy Kakarot, Run Kakarot RPC katana-rpc-up: @@ -42,16 +49,19 @@ kill-katana: dump-katana: run-katana deploy-kakarot kill-katana -test: dump-katana - cargo test --all +test: dump-katana load-env + cargo test --all --features testing -test-coverage: +test-coverage: load-env cargo llvm-cov nextest --all-features --workspace --lcov --output-path lcov.info # Make sure to have a Kakarot RPC running and the correct port set in your .env and an underlying Starknet client running. benchmark-madara: cd benchmarks && bun i && bun run benchmark-madara +test-target: load-env + cargo test --tests --features testing $(TARGET) -- --nocapture + benchmark-katana: cd benchmarks && bun i && bun run benchmark-katana diff --git a/src/starknet_client/config.rs b/src/config.rs similarity index 82% rename from src/starknet_client/config.rs rename to src/config.rs index ed1dc24ab..268d39a19 100644 --- a/src/starknet_client/config.rs +++ b/src/config.rs @@ -1,21 +1,14 @@ -use eyre::Result; +use eyre::eyre; use starknet::core::types::FieldElement; use starknet::providers::jsonrpc::{HttpTransport, JsonRpcTransport}; use starknet::providers::{JsonRpcClient, SequencerGatewayProvider}; +use std::env::var; use url::Url; -use super::constants::{KATANA_RPC_URL, MADARA_RPC_URL}; -use super::errors::ConfigError; +fn env_var_to_field_element(var_name: &str) -> Result { + let env_var = var(var_name)?; -pub fn env_var(name: &str) -> Result { - std::env::var(name).map_err(|_| ConfigError::EnvironmentVariableMissing(name.into())) -} - -fn env_var_to_field_element(var_name: &str) -> Result { - let env_var = env_var(var_name)?; - - FieldElement::from_hex_be(&env_var) - .map_err(|err| ConfigError::EnvironmentVariableSetWrong(var_name.into(), err.to_string())) + Ok(FieldElement::from_hex_be(&env_var)?) } #[derive(Default, Clone, Debug)] @@ -31,26 +24,24 @@ pub enum Network { } impl Network { - pub fn gateway_url(&self) -> Result { + pub fn gateway_url(&self) -> Result { match self { Self::MainnetGateway => Ok(Url::parse("https://alpha-mainnet.starknet.io/feeder_gateway/")?), Self::Goerli1Gateway => Ok(Url::parse("https://alpha4.starknet.io/feeder_gateway/")?), Self::Goerli2Gateway => Ok(Url::parse("https://alpha4-2.starknet.io/feeder_gateway/")?), - _ => Err(ConfigError::InvalidNetwork(format!("Network {:?} is not supported for gateway url", self))), + _ => Err(eyre!("Network {:?} is not supported for gateway url", self)), } } - pub fn provider_url(&self) -> Result { + pub fn provider_url(&self) -> Result { match self { - Self::Katana => Ok(Url::parse(KATANA_RPC_URL)?), - Self::Madara => Ok(Url::parse(MADARA_RPC_URL)?), + Self::Katana => Ok(Url::parse("http://0.0.0.0:5050")?), + Self::Madara => Ok(Url::parse("http://127.0.0.1:9944")?), Self::Sharingan => Ok(Url::parse( - std::env::var("SHARINGAN_RPC_URL") - .map_err(|_| ConfigError::EnvironmentVariableMissing("SHARINGAN_RPC_URL".to_string()))? - .as_str(), + var("SHARINGAN_RPC_URL").map_err(|_| eyre!("Missing env var SHARINGAN_RPC_URL".to_string()))?.as_str(), )?), Self::JsonRpcProvider(url) => Ok(url.clone()), - _ => Err(ConfigError::InvalidNetwork(format!("Network {:?} is not supported for provider url", self))), + _ => Err(eyre!("Network {:?} is not supported for provider url", self)), } } } @@ -91,8 +82,8 @@ impl KakarotRpcConfig { /// When using non-standard providers (i.e. not "katana", "madara", "mainnet"), the /// `STARKNET_NETWORK` environment variable should be set the URL of a JsonRpc /// starknet provider, e.g. https://starknet-goerli.g.alchemy.com/v2/some_key. - pub fn from_env() -> Result { - let network = env_var("STARKNET_NETWORK")?; + pub fn from_env() -> Result { + let network = var("STARKNET_NETWORK")?; let network = match network.to_lowercase().as_str() { "katana" => Network::Katana, "madara" => Network::Madara, @@ -159,7 +150,7 @@ impl JsonRpcClientBuilder { /// let starknet_provider: JsonRpcClient = /// JsonRpcClientBuilder::with_http(&config).unwrap().build(); /// ``` - pub fn with_http(config: &KakarotRpcConfig) -> Result { + pub fn with_http(config: &KakarotRpcConfig) -> Result { let url = config.network.provider_url()?; let transport = HttpTransport::new(url); Ok(Self::new(transport)) diff --git a/src/contracts/erc20.rs b/src/contracts/erc20.rs deleted file mode 100644 index 0a3ea0d07..000000000 --- a/src/contracts/erc20.rs +++ /dev/null @@ -1,88 +0,0 @@ -use std::sync::Arc; - -use anyhow::anyhow; -use ethers::abi::AbiEncode; -use ethers::prelude::abigen; -use ethers::types::Address; -use reth_primitives::{BlockId, U256}; -use starknet::core::types::BlockId as StarknetBlockId; -use starknet::macros::felt; -use starknet::providers::Provider; -use starknet_abigen_parser::cairo_types::CairoArrayLegacy; -use starknet_crypto::FieldElement; - -use crate::contracts::kakarot_contract::KakarotCoreReader; -use crate::contracts::kakarot_contract::Uint256 as CairoUint256; -use crate::into_via_wrapper; -use crate::models::block::EthBlockId; -use crate::models::felt::Felt252Wrapper; -use crate::starknet_client::constants::TX_ORIGIN_ZERO; -use crate::starknet_client::errors::EthApiError; -use crate::starknet_client::helpers::{try_from_u8_iterator, DataDecodingError}; - -// abigen generates a lot of unused code, needs to be benchmarked if performances ever become a -// concern -abigen!( - IERC20, - r#"[ - function balanceOf(address account) external view returns (uint256) - function allowance(address owner, address spender) external view returns (uint256) - ]"#, -); - -/// Abstraction for a Kakarot ERC20 contract. -pub struct EthereumErc20

{ - pub address: FieldElement, - pub provider: Arc

, - pub kakarot_address: FieldElement, -} - -impl EthereumErc20

{ - pub const fn new(address: FieldElement, provider: Arc

, kakarot_address: FieldElement) -> Self { - Self { address, provider, kakarot_address } - } - - pub async fn balance_of(self, evm_address: Address, block_id: BlockId) -> Result { - // Prepare the calldata for the bytecode function call - let calldata = IERC20Calls::BalanceOf(BalanceOfCall { account: evm_address }).encode(); - let calldata: Vec<_> = calldata.into_iter().map(FieldElement::from).collect(); - - let block_id = EthBlockId::new(block_id); - let block_id: StarknetBlockId = block_id.try_into()?; - - let origin = into_via_wrapper!(*TX_ORIGIN_ZERO); - - let kakarot_reader = KakarotCoreReader::new(self.kakarot_address, &self.provider); - - let gas_limit = felt!("0x100000"); - let gas_price = felt!("0x1"); - - let (_, return_data, success, _) = kakarot_reader - .eth_call( - &origin, - &self.address, - &gas_limit, - &gas_price, - &CairoUint256 { low: FieldElement::ZERO, high: FieldElement::ZERO }, - &calldata.len().into(), - &CairoArrayLegacy(calldata), - ) - .block_id(block_id) - .call() - .await?; - - if success == FieldElement::ZERO { - let revert_reason = - return_data.0.into_iter().filter_map(|x| u8::try_from(x).ok()).map(|x| x as char).collect::(); - return Err(EthApiError::Other(anyhow!("Revert reason: {}", revert_reason))); - } - - let balance: Vec = try_from_u8_iterator(return_data.0.into_iter()); - - Ok(U256::try_from_be_slice(&balance).ok_or(DataDecodingError::InvalidReturnArrayLength { - entrypoint: "balanceOf".into(), - expected: 32, - actual: balance.len(), - })?) - } -} diff --git a/src/contracts/kakarot_contract.rs b/src/contracts/kakarot_contract.rs deleted file mode 100644 index c38033c7f..000000000 --- a/src/contracts/kakarot_contract.rs +++ /dev/null @@ -1,27 +0,0 @@ -use std::sync::Arc; - -use starknet::providers::Provider; - -use starknet_abigen_macros::abigen_legacy; -use starknet_abigen_parser; -use starknet_crypto::FieldElement; - -abigen_legacy!(KakarotCore, "./artifacts/kakarot.json"); - -pub struct KakarotContract { - pub proxy_account_class_hash: FieldElement, - pub externally_owned_account_class_hash: FieldElement, - pub contract_account_class_hash: FieldElement, - pub reader: KakarotCoreReader>, -} - -impl KakarotContract

{ - pub const fn new( - proxy_account_class_hash: FieldElement, - externally_owned_account_class_hash: FieldElement, - contract_account_class_hash: FieldElement, - reader: KakarotCoreReader>, - ) -> Self { - Self { proxy_account_class_hash, externally_owned_account_class_hash, contract_account_class_hash, reader } - } -} diff --git a/src/contracts/mod.rs b/src/contracts/mod.rs deleted file mode 100644 index a1d026e72..000000000 --- a/src/contracts/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod erc20; -pub mod kakarot_contract; diff --git a/src/eth_provider/constant.rs b/src/eth_provider/constant.rs new file mode 100644 index 000000000..401dd9ac0 --- /dev/null +++ b/src/eth_provider/constant.rs @@ -0,0 +1,11 @@ +use lazy_static::lazy_static; + +lazy_static! { + pub static ref MAX_FEE: u64 = 100_000_000_000_000_000u64; + /// Since Kakarot does not have a fee market, the base fee + /// per gas should currently be the Starknet gas price. + /// Since this field is not present in the Starknet + /// block header, we arbitrarily set it to 100 Gwei. + pub static ref BASE_FEE_PER_GAS: u64 = 100_000_000_000; + pub static ref MAX_PRIORITY_FEE_PER_GAS: u64 = 0; +} diff --git a/src/eth_provider/contracts/erc20.rs b/src/eth_provider/contracts/erc20.rs new file mode 100644 index 000000000..70840dac0 --- /dev/null +++ b/src/eth_provider/contracts/erc20.rs @@ -0,0 +1,53 @@ +use ethers::abi::AbiEncode; +use ethers::prelude::abigen; +use reth_primitives::Address; + +use reth_primitives::{BlockId, U256}; +use reth_rpc_types::{CallInput, CallRequest}; + +use crate::eth_provider::provider::EthProviderResult; +use crate::eth_provider::provider::EthereumProvider; +use crate::models::errors::ConversionError; + +// abigen generates a lot of unused code, needs to be benchmarked if performances ever become a +// concern +abigen!( + IERC20, + r#"[ + function balanceOf(address account) external view returns (uint256) + function allowance(address owner, address spender) external view returns (uint256) + ]"#, +); + +/// Abstraction for a Kakarot ERC20 contract. +pub struct EthereumErc20 { + pub address: Address, + pub provider: P, +} + +impl EthereumErc20

{ + pub const fn new(address: Address, provider: P) -> Self { + Self { address, provider } + } + + pub async fn balance_of(self, evm_address: Address, block_id: BlockId) -> EthProviderResult { + // Prepare the calldata for the bytecode function call + let calldata = IERC20Calls::BalanceOf(BalanceOfCall { account: evm_address.into() }).encode(); + + let call = CallRequest { + from: Some(Address::default()), + to: Some(self.address), + gas_price: Some(U256::from(1)), + gas: Some(U256::from(1_000_000)), + value: Some(U256::ZERO), + input: CallInput { input: Some(calldata.into()), data: None }, + ..Default::default() + }; + + let ret = self.provider.call(call, Some(block_id)).await?; + let balance = U256::try_from_be_slice(&ret) + .ok_or_else(|| ConversionError::UintConversionError("Failed to convert call return to U256".to_string()))?; + + Ok(balance) + } +} diff --git a/src/eth_provider/contracts/mod.rs b/src/eth_provider/contracts/mod.rs new file mode 100644 index 000000000..8f3777f6b --- /dev/null +++ b/src/eth_provider/contracts/mod.rs @@ -0,0 +1 @@ +pub mod erc20; diff --git a/src/eth_provider/database/mod.rs b/src/eth_provider/database/mod.rs index a34d48e5c..2c67e7461 100644 --- a/src/eth_provider/database/mod.rs +++ b/src/eth_provider/database/mod.rs @@ -17,14 +17,19 @@ impl Database { pub fn new(database: MongoDatabase) -> Self { Self(database) } +} +impl Database { /// Get a list of documents from a collection - pub async fn get( + pub async fn get( &self, collection: &str, filter: impl Into>, project: impl Into>, - ) -> EthProviderResult> { + ) -> EthProviderResult> + where + T: DeserializeOwned + Unpin + Send + Sync, + { let find_options = FindOptions::builder().projection(project).build(); let collection = self.0.collection::(collection); let result = collection.find(filter, find_options).await?.try_collect().await?; @@ -32,12 +37,15 @@ impl Database { } /// Get a single document from a collection - pub async fn get_one( + pub async fn get_one( &self, collection: &str, filter: impl Into>, sort: impl Into>, - ) -> EthProviderResult> { + ) -> EthProviderResult> + where + T: DeserializeOwned + Unpin + Send + Sync, + { let find_one_option = FindOneOptions::builder().sort(sort).build(); let collection = self.0.collection::(collection); let result = collection.find_one(filter, find_one_option).await?; diff --git a/src/eth_provider/error.rs b/src/eth_provider/error.rs index dcd59bbc4..fe82a1e98 100644 --- a/src/eth_provider/error.rs +++ b/src/eth_provider/error.rs @@ -2,10 +2,28 @@ use jsonrpsee::types::ErrorObject; use starknet::providers::ProviderError as StarknetProviderError; use thiserror::Error; -use crate::{ - models::errors::ConversionError, - starknet_client::errors::{rpc_err, EthRpcErrorCode}, -}; +use crate::models::errors::ConversionError; + +/// List of JSON-RPC error codes from ETH rpc spec. +/// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1474.md +#[derive(Debug, Copy, PartialEq, Eq, Clone)] +pub enum EthRpcErrorCode { + /// Custom geth error code, + Unknown, + ExecutionError = 3, + ParseError = -32700, + InvalidRequest = -32600, + MethodNotFound = -32601, + InvalidParams = -32602, + InternalError = -32603, + InvalidInput = -32000, + ResourceNotFound = -32001, + ResourceUnavailable = -32002, + TransactionRejected = -32003, + MethodNotSupported = -32004, + RequestLimitExceeded = -32005, + JsonRpcVersionUnsupported = -32006, +} /// Error that can occur when interacting with the database. #[derive(Debug, Error)] @@ -16,6 +34,9 @@ pub enum EthProviderError { /// Starknet Provider error. #[error(transparent)] StarknetProviderError(#[from] StarknetProviderError), + /// EVM execution error. + #[error("EVM execution error: {0}")] + EvmExecutionError(String), /// Contract call error. #[error(transparent)] ContractCallError(#[from] starknet_abigen_parser::cairo_types::Error), @@ -25,6 +46,9 @@ pub enum EthProviderError { /// Value not found in the database. #[error("Did not find value in the database.")] ValueNotFound, + /// Method not supported. + #[error("Method not supported: {0}")] + MethodNotSupported(String), } impl From for jsonrpsee::core::Error { @@ -38,3 +62,8 @@ impl From for ErrorObject<'static> { rpc_err(EthRpcErrorCode::InternalError, value.to_string()) } } + +/// Constructs a JSON-RPC error object, consisting of `code` and `message`. +pub fn rpc_err(code: EthRpcErrorCode, msg: impl Into) -> jsonrpsee::types::error::ErrorObject<'static> { + jsonrpsee::types::error::ErrorObject::owned(code as i32, msg.into(), None::<()>) +} diff --git a/src/eth_provider/mod.rs b/src/eth_provider/mod.rs index 9b9809230..559b22bb3 100644 --- a/src/eth_provider/mod.rs +++ b/src/eth_provider/mod.rs @@ -1,3 +1,5 @@ +pub mod constant; +pub mod contracts; pub mod database; pub mod error; pub mod provider; diff --git a/src/eth_provider/provider.rs b/src/eth_provider/provider.rs index b3b65f67e..9a99aca28 100644 --- a/src/eth_provider/provider.rs +++ b/src/eth_provider/provider.rs @@ -1,45 +1,54 @@ use async_trait::async_trait; use auto_impl::auto_impl; use eyre::Result; -use mockall::automock; use mongodb::bson::doc; use mongodb::bson::Document; use reth_primitives::Address; use reth_primitives::BlockId; use reth_primitives::Bytes; +use reth_primitives::TransactionSigned; use reth_primitives::{BlockNumberOrTag, H256, U256, U64}; +use reth_rlp::Decodable as _; +use reth_rpc_types::CallRequest; +use reth_rpc_types::FeeHistory; use reth_rpc_types::Filter; use reth_rpc_types::FilterChanges; use reth_rpc_types::Index; -use reth_rpc_types::Transaction; +use reth_rpc_types::Transaction as RpcTransaction; use reth_rpc_types::TransactionReceipt; use reth_rpc_types::ValueOrArray; use reth_rpc_types::{Block, BlockTransactions, RichBlock}; use reth_rpc_types::{SyncInfo, SyncStatus}; use starknet::core::types::BlockId as StarknetBlockId; use starknet::core::types::SyncStatusType; -use starknet::core::utils::get_contract_address; +use starknet::core::types::ValueOutOfRangeError; use starknet::core::utils::get_storage_var_address; use starknet::providers::Provider as StarknetProvider; +use starknet_abigen_parser::cairo_types::CairoArrayLegacy; use starknet_crypto::FieldElement; +use super::constant::MAX_FEE; use super::database::types::log::StoredLog; use super::database::types::{ header::StoredHeader, receipt::StoredTransactionReceipt, transaction::StoredTransaction, transaction::StoredTransactionHash, }; use super::database::Database; +use super::starknet::kakarot_core::core::{KakarotCoreReader, Uint256}; +use super::starknet::kakarot_core::to_starknet_transaction; use super::starknet::kakarot_core::{ - ContractAccountReader, ProxyReader, CONTRACT_ACCOUNT_CLASS_HASH, EXTERNALLY_OWNED_ACCOUNT_CLASS_HASH, - KAKAROT_ADDRESS, PROXY_ACCOUNT_CLASS_HASH, + starknet_address, ContractAccountReader, ProxyReader, CONTRACT_ACCOUNT_CLASS_HASH, + EXTERNALLY_OWNED_ACCOUNT_CLASS_HASH, KAKAROT_ADDRESS, }; use super::starknet::ERC20Reader; use super::starknet::STARKNET_NATIVE_TOKEN; +use super::utils::contract_not_found; use super::utils::iter_into; use super::utils::split_u256; use super::utils::try_from_u8_iterator; use super::{error::EthProviderError, utils::into_filter}; use crate::eth_provider::utils::format_hex; +use crate::into_via_try_wrapper; use crate::into_via_wrapper; use crate::models::block::EthBlockId; use crate::models::errors::ConversionError; @@ -49,8 +58,7 @@ pub type EthProviderResult = Result; /// Ethereum provider trait. Used to abstract away the database and the network. #[async_trait] -#[auto_impl(Arc)] -#[automock] +#[auto_impl(Arc, &)] pub trait EthereumProvider { /// Returns the latest block number. async fn block_number(&self) -> EthProviderResult; @@ -71,22 +79,22 @@ pub trait EthereumProvider { /// Returns the transaction count for a block by number. async fn block_transaction_count_by_number(&self, number_or_tag: BlockNumberOrTag) -> EthProviderResult; /// Returns the transaction by hash. - async fn transaction_by_hash(&self, hash: H256) -> EthProviderResult>; + async fn transaction_by_hash(&self, hash: H256) -> EthProviderResult>; /// Returns the transaction by block hash and index. async fn transaction_by_block_hash_and_index( &self, hash: H256, index: Index, - ) -> EthProviderResult>; + ) -> EthProviderResult>; /// Returns the transaction by block number and index. async fn transaction_by_block_number_and_index( &self, number_or_tag: BlockNumberOrTag, index: Index, - ) -> EthProviderResult>; + ) -> EthProviderResult>; /// Returns the transaction receipt by hash of the transaction. async fn transaction_receipt(&self, hash: H256) -> EthProviderResult>; - /// Returns the balance of an address. + /// Returns the balance of an address in native eth. async fn balance(&self, address: Address, block_id: Option) -> EthProviderResult; /// Returns the storage of an address at a certain index. async fn storage_at(&self, address: Address, index: U256, block_id: Option) -> EthProviderResult; @@ -94,17 +102,27 @@ pub trait EthereumProvider { async fn transaction_count(&self, address: Address, block_id: Option) -> EthProviderResult; /// Returns the code for the address at the given block. async fn get_code(&self, address: Address, block_id: Option) -> EthProviderResult; - /// Returns the logs for the given filter + /// Returns the logs for the given filter. async fn get_logs(&self, filter: Filter) -> EthProviderResult; + /// Returns the result of a call. + async fn call(&self, call: CallRequest, block_id: Option) -> EthProviderResult; + /// Returns the result of a estimate gas. + async fn estimate_gas(&self, call: CallRequest, block_id: Option) -> EthProviderResult; + /// Returns the fee history given a block count and a newest block number. + async fn fee_history( + &self, + block_count: U256, + newest_block: BlockNumberOrTag, + reward_percentiles: Option>, + ) -> EthProviderResult; + /// Send a raw transaction to the network and returns the transactions hash. + async fn send_raw_transaction(&self, transaction: Bytes) -> EthProviderResult; } /// Structure that implements the EthereumProvider trait. /// Uses an access to a database to certain data, while /// the rest is fetched from the Starknet Provider. -pub struct EthDataProvider -where - SP: StarknetProvider + Send + Sync, -{ +pub struct EthDataProvider { database: Database, starknet_provider: SP, } @@ -157,6 +175,7 @@ where } } + // TODO cache chain id async fn chain_id(&self) -> EthProviderResult> { let chain_id = self.starknet_provider.chain_id().await?; let chain_id: Option = chain_id.try_into().ok(); @@ -199,7 +218,7 @@ where Ok(count.into()) } - async fn transaction_by_hash(&self, hash: H256) -> EthProviderResult> { + async fn transaction_by_hash(&self, hash: H256) -> EthProviderResult> { let filter = into_filter("tx.hash", hash, 64); let tx: Option = self.database.get_one("transactions", filter, None).await?; Ok(tx.map(Into::into)) @@ -209,7 +228,7 @@ where &self, hash: H256, index: Index, - ) -> EthProviderResult> { + ) -> EthProviderResult> { let mut filter = into_filter("tx.blockHash", hash, 64); let index: usize = index.into(); filter.insert("tx.transactionIndex", index as i32); @@ -221,7 +240,7 @@ where &self, number_or_tag: BlockNumberOrTag, index: Index, - ) -> EthProviderResult> { + ) -> EthProviderResult> { let block_number = self.tag_into_block_number(number_or_tag).await?; let mut filter = into_filter("tx.blockNumber", block_number, 64); let index: usize = index.into(); @@ -242,7 +261,7 @@ where let eth_contract = ERC20Reader::new(*STARKNET_NATIVE_TOKEN, &self.starknet_provider); - let address = self.starknet_address(address); + let address = starknet_address(address); let balance = eth_contract.balanceOf(&address).block_id(starknet_block_id).call().await?; let low: U256 = into_via_wrapper!(balance.low); @@ -254,7 +273,7 @@ where let eth_block_id = EthBlockId::new(block_id.unwrap_or(BlockId::Number(BlockNumberOrTag::Latest))); let starknet_block_id: StarknetBlockId = eth_block_id.try_into()?; - let address = self.starknet_address(address); + let address = starknet_address(address); let contract = ContractAccountReader::new(address, &self.starknet_provider); let keys = split_u256::(index); @@ -271,13 +290,18 @@ where let eth_block_id = EthBlockId::new(block_id.unwrap_or(BlockId::Number(BlockNumberOrTag::Latest))); let starknet_block_id: StarknetBlockId = eth_block_id.try_into()?; - let address = self.starknet_address(address); + let address = starknet_address(address); let proxy = ProxyReader::new(address, &self.starknet_provider); - let address_class_hash = proxy.get_implementation().block_id(starknet_block_id).call().await?; + let maybe_class_hash = proxy.get_implementation().block_id(starknet_block_id).call().await; + + if contract_not_found(&maybe_class_hash) { + return Ok(U256::ZERO); + } + let class_hash = maybe_class_hash?; - let nonce = if address_class_hash == *EXTERNALLY_OWNED_ACCOUNT_CLASS_HASH { + let nonce = if class_hash == *EXTERNALLY_OWNED_ACCOUNT_CLASS_HASH { self.starknet_provider.get_nonce(starknet_block_id, address).await? - } else if address_class_hash == *CONTRACT_ACCOUNT_CLASS_HASH { + } else if class_hash == *CONTRACT_ACCOUNT_CLASS_HASH { let contract = ContractAccountReader::new(address, &self.starknet_provider); contract.get_nonce().block_id(starknet_block_id).call().await? } else { @@ -290,7 +314,7 @@ where let eth_block_id = EthBlockId::new(block_id.unwrap_or(BlockId::Number(BlockNumberOrTag::Latest))); let starknet_block_id: StarknetBlockId = eth_block_id.try_into()?; - let address = self.starknet_address(address); + let address = starknet_address(address); let contract = ContractAccountReader::new(address, &self.starknet_provider); let (_, bytecode) = contract.bytecode().block_id(starknet_block_id).call().await?; @@ -298,7 +322,7 @@ where } async fn get_logs(&self, filter: Filter) -> EthProviderResult { - let current_block = self.block_number().await?.low_u64(); + let current_block = self.block_number().await?.as_u64(); let from = filter.get_from_block().unwrap_or_default(); let to = filter.get_to_block().unwrap_or(current_block); @@ -347,6 +371,94 @@ where let logs: Vec = self.database.get("logs", database_filter, None).await?; Ok(FilterChanges::Logs(logs.into_iter().map(Into::into).collect())) } + + async fn call(&self, call: CallRequest, block_id: Option) -> EthProviderResult { + let (output, _) = self.call_helper(call, block_id).await?; + Ok(Bytes::from(try_from_u8_iterator::<_, Vec<_>>(output.0.into_iter()))) + } + + async fn estimate_gas(&self, call: CallRequest, block_id: Option) -> EthProviderResult { + // Set a high gas limit to make sure the transaction will not fail due to gas. + let call = CallRequest { gas: Some(U256::from(*MAX_FEE)), ..call }; + + let (_, gas_used) = self.call_helper(call, block_id).await?; + Ok(U256::from(gas_used)) + } + + async fn fee_history( + &self, + mut block_count: U256, + newest_block: BlockNumberOrTag, + _reward_percentiles: Option>, + ) -> EthProviderResult { + if block_count == U256::ZERO { + return Ok(FeeHistory::default()); + } + + let end_block = U256::from(self.tag_into_block_number(newest_block).await?.as_u64()); + let end_block_plus = end_block + U256::from(1); + + // If the block count is larger than the end block, we need to reduce it. + if end_block_plus < block_count { + block_count = end_block_plus; + } + let start_block = end_block_plus - block_count; + + let bc = usize::try_from(block_count).map_err(|e| ConversionError::ValueOutOfRange(e.to_string()))?; + // We add one to the block count and fill with 0's. + // This comes from the rpc spec: `An array of block base fees per gas. + // This includes the next block after the newest of the returned range, + // because this value can be derived from the newest block. Zeroes are returned for pre-EIP-1559 blocks.` + // Since Kakarot doesn't support EIP-1559 yet, we just fill with 0's. + let base_fee_per_gas = vec![U256::ZERO; bc + 1]; + + // TODO: check if we should use a projection since we only need the gasLimit and gasUsed. + // This means we need to introduce a new type for the StoredHeader. + let header_filter = + doc! {"header.number": {"$gte": format_hex(start_block, 64), "$lte": format_hex(end_block, 64)}}; + let blocks: Vec = self.database.get("headers", header_filter, None).await?; + let gas_used_ratio = blocks + .iter() + .map(|header| { + let gas_used = header.header.gas_used.as_limbs()[0] as f64; + let gas_limit = if header.header.gas_limit != U256::ZERO { + header.header.gas_limit.as_limbs()[0] as f64 + } else { + 1.0 + }; + gas_used / gas_limit + }) + .collect::>(); + + Ok(FeeHistory { base_fee_per_gas, gas_used_ratio, oldest_block: start_block, reward: Some(vec![]) }) + } + + async fn send_raw_transaction(&self, transaction: Bytes) -> EthProviderResult { + let mut data = transaction.0.as_ref(); + let transaction_signed = TransactionSigned::decode(&mut data) + .map_err(|err| ConversionError::ToStarknetTransactionError(err.to_string()))?; + + let chain_id = self.chain_id().await?.unwrap_or_default(); + + let signer = transaction_signed + .recover_signer() + .ok_or_else(|| ConversionError::ToStarknetTransactionError("Failed to recover signer".to_string()))?; + let transaction = to_starknet_transaction(&transaction_signed, chain_id.as_u64(), signer)?; + + #[cfg(not(feature = "testing"))] + { + let hash = transaction_signed.hash(); + self.starknet_provider.add_invoke_transaction(transaction).await?; + Ok(hash) + } + // If we are currently testing, we need to return the starknet hash in order + // to be able to wait for the transaction to be mined. + #[cfg(feature = "testing")] + { + let res = self.starknet_provider.add_invoke_transaction(transaction).await?; + Ok(H256::from_slice(&res.transaction_hash.to_bytes_be()[..])) + } + } } impl EthDataProvider @@ -357,6 +469,60 @@ where Self { database, starknet_provider } } + #[cfg(feature = "testing")] + pub fn starknet_provider(&self) -> &SP { + &self.starknet_provider + } + + async fn call_helper( + &self, + call: CallRequest, + block_id: Option, + ) -> EthProviderResult<(CairoArrayLegacy, u128)> { + let eth_block_id = EthBlockId::new(block_id.unwrap_or(BlockId::Number(BlockNumberOrTag::Latest))); + let starknet_block_id: StarknetBlockId = eth_block_id.try_into()?; + + // unwrap option + let to = call.to.unwrap_or_default(); + let to = into_via_wrapper!(to); + + // Here we check if CallRequest.origin is None, if so, we insert origin = address(0) + let from = into_via_wrapper!(call.from.unwrap_or_default()); + + let data = call.input.unique_input().unwrap_or_default().cloned().unwrap_or_default(); + let calldata: Vec<_> = data.to_vec().into_iter().map(FieldElement::from).collect(); + + let gas_limit = into_via_try_wrapper!(call.gas.unwrap_or_default()); + let gas_price = into_via_try_wrapper!(call.gas_price.unwrap_or_default()); + + let value = into_via_try_wrapper!(call.value.unwrap_or_default()); + + let kakarot_contract = KakarotCoreReader::new(*KAKAROT_ADDRESS, &self.starknet_provider); + let (_, return_data, success, gas_used) = kakarot_contract + .eth_call( + &from, + &to, + &gas_limit, + &gas_price, + &Uint256 { low: value, high: FieldElement::ZERO }, + &calldata.len().into(), + &CairoArrayLegacy(calldata), + ) + .block_id(starknet_block_id) + .call() + .await?; + + if success == FieldElement::ZERO { + let revert_reason = + return_data.0.into_iter().filter_map(|x| u8::try_from(x).ok()).map(|x| x as char).collect::(); + return Err(EthProviderError::EvmExecutionError(revert_reason)); + } + let gas_used = gas_used + .try_into() + .map_err(|err: ValueOutOfRangeError| ConversionError::ValueOutOfRange(err.to_string()))?; + Ok((return_data, gas_used)) + } + /// Get a block from the database based on the header and transaction filters /// If full is true, the block will contain the full transactions, otherwise just the hashes async fn block( @@ -407,9 +573,4 @@ where BlockNumberOrTag::Pending => todo!("pending block number not implemented"), } } - - /// Compute the starknet address given a eth address - fn starknet_address(&self, address: Address) -> FieldElement { - get_contract_address(into_via_wrapper!(address), *PROXY_ACCOUNT_CLASS_HASH, &[], *KAKAROT_ADDRESS) - } } diff --git a/src/eth_provider/starknet/kakarot_core.rs b/src/eth_provider/starknet/kakarot_core.rs index eb0dba186..cea0da378 100644 --- a/src/eth_provider/starknet/kakarot_core.rs +++ b/src/eth_provider/starknet/kakarot_core.rs @@ -1,9 +1,29 @@ +use crate::models::felt::Felt252Wrapper; use dotenv::dotenv; use lazy_static::lazy_static; +use reth_primitives::{Address, Transaction, TransactionSigned}; +use starknet::{ + core::{types::BroadcastedInvokeTransaction, utils::get_contract_address}, + macros::selector, +}; use starknet_abigen_macros::abigen_legacy; use starknet_abigen_parser; use starknet_crypto::FieldElement; +use crate::{ + eth_provider::{constant::MAX_FEE, provider::EthProviderResult, utils::split_u256}, + into_via_wrapper, +}; + +// Contract ABIs +abigen_legacy!(Proxy, "./artifacts/proxy.json"); +abigen_legacy!(ContractAccount, "./artifacts/contract_account.json"); + +pub mod core { + use super::*; + abigen_legacy!(KakarotCore, "./artifacts/kakarot.json"); +} + fn env_var_to_field_element(var_name: &str) -> FieldElement { dotenv().ok(); let env_var = std::env::var(var_name).unwrap_or_else(|_| panic!("Missing environment variable {var_name}")); @@ -20,7 +40,70 @@ lazy_static! { pub static ref EXTERNALLY_OWNED_ACCOUNT_CLASS_HASH: FieldElement = env_var_to_field_element("EXTERNALLY_OWNED_ACCOUNT_CLASS_HASH"); pub static ref CONTRACT_ACCOUNT_CLASS_HASH: FieldElement = env_var_to_field_element("CONTRACT_ACCOUNT_CLASS_HASH"); + + // Contract selectors + pub static ref ETH_SEND_TRANSACTION: FieldElement = selector!("eth_send_transaction"); } -abigen_legacy!(Proxy, "./artifacts/proxy.json"); -abigen_legacy!(ContractAccount, "./artifacts/contract_account.json"); +// Kakarot utils +/// Compute the starknet address given a eth address +pub fn starknet_address(address: Address) -> FieldElement { + get_contract_address(into_via_wrapper!(address), *PROXY_ACCOUNT_CLASS_HASH, &[], *KAKAROT_ADDRESS) +} + +/// Convert a Ethereum transaction into a Starknet transaction +pub(crate) fn to_starknet_transaction( + transaction: &TransactionSigned, + chain_id: u64, + signer: Address, +) -> EthProviderResult { + let starknet_address = starknet_address(signer); + + let nonce = FieldElement::from(transaction.nonce()); + + let max_fee = (*MAX_FEE).into(); + + // Step: Signature + // Extract the signature from the Ethereum Transaction + // and place it in the Starknet signature InvokeTransaction vector + let mut signature: Vec = { + let r = split_u256(transaction.signature().r); + let s = split_u256(transaction.signature().s); + let signature = vec![r[0], r[1], s[0], s[1]]; + signature + }; + // Push the last element of the signature + // In case of a Legacy Transaction, it is v := {0, 1} + chain_id * 2 + 35 + // Else, it is odd_y_parity + if let Transaction::Legacy(_) = transaction.transaction { + signature.push(transaction.signature().v(Some(chain_id)).into()); + } else { + signature.push((transaction.signature().odd_y_parity as u64).into()); + } + + // Step: Calldata + // RLP encode the transaction without the signature + // Example: For Legacy Transactions: rlp([nonce, gas_price, gas_limit, to, value, data, chain_id, 0, 0]) + let mut signed_data = Vec::new(); + transaction.transaction.encode_without_signature(&mut signed_data); + + // Prepare the calldata for the Starknet invoke transaction + let mut execute_calldata = vec![ + FieldElement::ONE, // call array length + *KAKAROT_ADDRESS, // contract address + *ETH_SEND_TRANSACTION, // selector + FieldElement::ZERO, // data offset + FieldElement::from(signed_data.len()), // data length + FieldElement::from(signed_data.len()), // calldata length + ]; + execute_calldata.append(&mut signed_data.into_iter().map(FieldElement::from).collect()); + + Ok(BroadcastedInvokeTransaction { + max_fee, + signature, + nonce, + sender_address: starknet_address, + calldata: execute_calldata, + is_query: false, + }) +} diff --git a/src/eth_provider/utils.rs b/src/eth_provider/utils.rs index 37f734c51..b87f1e56a 100644 --- a/src/eth_provider/utils.rs +++ b/src/eth_provider/utils.rs @@ -14,7 +14,14 @@ pub(crate) fn try_from_u8_iterator, T: FromIterator>(it: impl } pub(crate) fn format_hex(value: impl LowerHex, width: usize) -> String { - format!("0x{:0width$x}", value, width = width) + // Add 2 to the width to account for the 0x prefix. + let s = format!("{:#0width$x}", value, width = width + 2); + // `s.len() < width` can happen because of the LowerHex implementation + // for Uint, which just formats 0 into 0x0, ignoring the width. + if s.len() < width { + return format!("0x{:0>width$}", &s[2..], width = width); + } + s } /// Converts a key and value into a MongoDB filter. @@ -33,3 +40,10 @@ pub fn split_u256>(value: U256) -> [T; 2] { let high: u128 = high.try_into().unwrap(); // safe to unwrap [T::from(low), T::from(high)] } + +pub(crate) fn contract_not_found(err: &Result) -> bool { + match err { + Ok(_) => false, + Err(err) => err.to_string().contains("Contract not found"), + } +} diff --git a/src/eth_rpc/api/net_api.rs b/src/eth_rpc/api/net_api.rs index 648a204af..c884edc24 100644 --- a/src/eth_rpc/api/net_api.rs +++ b/src/eth_rpc/api/net_api.rs @@ -9,7 +9,7 @@ use reth_rpc_types::PeerCount; pub trait NetApi { /// Returns the protocol version encoded as a string. #[method(name = "version")] - fn version(&self) -> Result; + async fn version(&self) -> Result; /// Returns number of peers connected to node. #[method(name = "peerCount")] diff --git a/src/eth_rpc/rpc.rs b/src/eth_rpc/rpc.rs index eb223dc5a..01dc8d0ce 100644 --- a/src/eth_rpc/rpc.rs +++ b/src/eth_rpc/rpc.rs @@ -2,10 +2,8 @@ use std::collections::HashMap; use std::marker::PhantomData; use std::sync::Arc; -use crate::starknet_client::KakarotClient; use jsonrpsee::core::Error; use jsonrpsee::{Methods, RpcModule}; -use starknet::providers::Provider; use crate::eth_provider::provider::EthereumProvider; use crate::eth_rpc::api::alchemy_api::AlchemyApiServer; @@ -26,25 +24,24 @@ pub enum KakarotRpcModule { Net, } -pub struct KakarotRpcModuleBuilder +pub struct KakarotRpcModuleBuilder

where - P: Provider + Send + Sync, - DB: EthereumProvider + Send + Sync, + P: EthereumProvider + Send + Sync, { modules: HashMap, - _phantom: PhantomData<(P, DB)>, + _phantom: PhantomData

, } -impl KakarotRpcModuleBuilder +impl

KakarotRpcModuleBuilder

where - P: Provider + Send + Sync + 'static, - DB: EthereumProvider + Send + Sync + 'static, + P: EthereumProvider + Send + Sync + 'static, { - pub fn new(kakarot_client: Arc>, eth_provider: DB) -> Self { - let eth_rpc_module = KakarotEthRpc::new(eth_provider, kakarot_client.clone()).into_rpc(); - let alchemy_rpc_module = AlchemyRpc::new(kakarot_client.clone()).into_rpc(); + pub fn new(eth_provider: P) -> Self { + let eth_provider = Arc::new(eth_provider); + let eth_rpc_module = KakarotEthRpc::new(eth_provider.clone()).into_rpc(); + let alchemy_rpc_module = AlchemyRpc::new(eth_provider.clone()).into_rpc(); let web3_rpc_module = Web3Rpc::default().into_rpc(); - let net_rpc_module = NetRpc::new(kakarot_client.clone()).into_rpc(); + let net_rpc_module = NetRpc::new(eth_provider).into_rpc(); let mut modules: HashMap = HashMap::new(); diff --git a/src/eth_rpc/servers/alchemy_rpc.rs b/src/eth_rpc/servers/alchemy_rpc.rs index 8996866b4..1c94b1245 100644 --- a/src/eth_rpc/servers/alchemy_rpc.rs +++ b/src/eth_rpc/servers/alchemy_rpc.rs @@ -1,29 +1,38 @@ -use std::sync::Arc; - -use crate::models::balance::TokenBalances; -use crate::starknet_client::KakarotClient; +use futures::future::join_all; use jsonrpsee::core::{async_trait, RpcResult as Result}; -use reth_primitives::Address; -use starknet::providers::Provider; +use reth_primitives::{Address, BlockId, BlockNumberOrTag}; +use crate::eth_provider::contracts::erc20::EthereumErc20; use crate::eth_rpc::api::alchemy_api::AlchemyApiServer; +use crate::models::balance::FutureTokenBalance; +use crate::{eth_provider::provider::EthereumProvider, models::balance::TokenBalances}; /// The RPC module for the Ethereum protocol required by Kakarot. -pub struct AlchemyRpc { - pub kakarot_client: Arc>, +pub struct AlchemyRpc { + eth_provider: P, } -impl AlchemyRpc

{ - pub fn new(kakarot_client: Arc>) -> Self { - Self { kakarot_client } +impl AlchemyRpc

{ + pub fn new(eth_provider: P) -> Self { + Self { eth_provider } } } #[async_trait] -impl AlchemyApiServer for AlchemyRpc

{ - #[tracing::instrument(skip_all, ret, fields(address = %address, contract_addresses = ?contract_addresses))] - async fn token_balances(&self, address: Address, contract_addresses: Vec

) -> Result { - let token_balances = self.kakarot_client.token_balances(address, contract_addresses).await?; - Ok(token_balances) +impl AlchemyApiServer for AlchemyRpc

{ + #[tracing::instrument(skip_all, ret, fields(address = %address, token_addresses = ?token_addresses))] + async fn token_balances(&self, address: Address, token_addresses: Vec

) -> Result { + let block_id = BlockId::Number(BlockNumberOrTag::Latest); + + let handles = token_addresses.into_iter().map(|token_addr| { + let token = EthereumErc20::new(token_addr, &self.eth_provider); + let balance = token.balance_of(address, block_id); + + FutureTokenBalance::new(Box::pin(balance), token_addr) + }); + + let token_balances = join_all(handles).await; + + Ok(TokenBalances { address, token_balances }) } } diff --git a/src/eth_rpc/servers/eth_rpc.rs b/src/eth_rpc/servers/eth_rpc.rs index 96ddd87fc..4dad121d7 100644 --- a/src/eth_rpc/servers/eth_rpc.rs +++ b/src/eth_rpc/servers/eth_rpc.rs @@ -1,7 +1,6 @@ -use std::sync::Arc; - -use crate::starknet_client::errors::EthApiError; -use crate::{eth_provider::provider::EthereumProvider, starknet_client::KakarotClient}; +use crate::eth_provider::constant::{BASE_FEE_PER_GAS, MAX_PRIORITY_FEE_PER_GAS}; +use crate::eth_provider::error::EthProviderError; +use crate::eth_provider::provider::EthereumProvider; use jsonrpsee::core::{async_trait, RpcResult as Result}; use reth_primitives::{AccessListWithGasUsed, Address, BlockId, BlockNumberOrTag, Bytes, H256, H64, U128, U256, U64}; use reth_rpc_types::{ @@ -9,36 +8,30 @@ use reth_rpc_types::{ Transaction as EtherTransaction, TransactionReceipt, TransactionRequest, Work, }; use serde_json::Value; -use starknet::providers::Provider; use crate::eth_rpc::api::eth_api::EthApiServer; /// The RPC module for the Ethereum protocol required by Kakarot. -pub struct KakarotEthRpc +pub struct KakarotEthRpc

where P: EthereumProvider, - SP: Provider + Send + Sync, { - pub eth_provider: P, - // TODO remove kakaort_client from here - pub kakarot_client: Arc>, + eth_provider: P, } -impl KakarotEthRpc +impl

KakarotEthRpc

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

EthApiServer for KakarotEthRpc

where P: EthereumProvider + Send + Sync + 'static, - SP: Provider + Send + Sync + 'static, { #[tracing::instrument(skip_all, ret, err)] async fn block_number(&self) -> Result { @@ -51,7 +44,7 @@ where } async fn coinbase(&self) -> Result

{ - Err(EthApiError::MethodNotSupported("eth_coinbase".to_string()).into()) + Err(EthProviderError::MethodNotSupported("eth_coinbase".to_string()).into()) } #[tracing::instrument(skip_all, ret, err)] @@ -163,10 +156,7 @@ where #[tracing::instrument(skip_all, ret, err, fields(request = ?request, block_id = ?block_id))] async fn call(&self, request: CallRequest, block_id: Option) -> Result { - let block_id = block_id.unwrap_or(BlockId::Number(BlockNumberOrTag::Latest)); - let result = self.kakarot_client.call(request, block_id).await?; - - Ok(result) + Ok(self.eth_provider.call(request, block_id).await?) } async fn create_access_list( @@ -174,20 +164,17 @@ where _request: CallRequest, _block_id: Option, ) -> Result { - Err(EthApiError::MethodNotSupported("eth_createAccessList".to_string()).into()) + Err(EthProviderError::MethodNotSupported("eth_createAccessList".to_string()).into()) } #[tracing::instrument(skip_all, ret, fields(request = ?request, block_id = ?block_id))] async fn estimate_gas(&self, request: CallRequest, block_id: Option) -> Result { - let block_id = block_id.unwrap_or(BlockId::Number(BlockNumberOrTag::Latest)); - - Ok(self.kakarot_client.estimate_gas(request, block_id).await?) + Ok(self.eth_provider.estimate_gas(request, block_id).await?) } #[tracing::instrument(skip_all, ret, err)] async fn gas_price(&self) -> Result { - let gas_price = self.kakarot_client.base_fee_per_gas(); - Ok(gas_price) + Ok(U256::from(*BASE_FEE_PER_GAS)) } #[tracing::instrument(skip_all, ret, err, fields(block_count = %block_count, newest_block = %newest_block, reward_percentiles = ?reward_percentiles))] @@ -197,57 +184,56 @@ where newest_block: BlockNumberOrTag, reward_percentiles: Option>, ) -> Result { - let fee_history = self.kakarot_client.fee_history(block_count, newest_block, reward_percentiles).await?; - - Ok(fee_history) + Ok(self.eth_provider.fee_history(block_count, newest_block, reward_percentiles).await?) } #[tracing::instrument(skip_all, ret, err)] async fn max_priority_fee_per_gas(&self) -> Result { - let max_priority_fee = self.kakarot_client.max_priority_fee_per_gas(); - Ok(max_priority_fee) + Ok(U128::from(*MAX_PRIORITY_FEE_PER_GAS)) } async fn mining(&self) -> Result { - Err(EthApiError::MethodNotSupported("eth_mining".to_string()).into()) + tracing::warn!("Kakarot chain does not use mining"); + Ok(false) } async fn hashrate(&self) -> Result { - Err(EthApiError::MethodNotSupported("eth_hashrate".to_string()).into()) + tracing::warn!("Kakarot chain does not produce hash rate"); + Ok(U256::ZERO) } async fn get_work(&self) -> Result { - Err(EthApiError::MethodNotSupported("eth_getWork".to_string()).into()) + tracing::warn!("Kakarot chain does not produce work"); + Ok(Work::default()) } async fn submit_hashrate(&self, _hashrate: U256, _id: H256) -> Result { - Err(EthApiError::MethodNotSupported("eth_submitHashrate".to_string()).into()) + Err(EthProviderError::MethodNotSupported("eth_submitHashrate".to_string()).into()) } async fn submit_work(&self, _nonce: H64, _pow_hash: H256, _mix_digest: H256) -> Result { - Err(EthApiError::MethodNotSupported("eth_submitWork".to_string()).into()) + Err(EthProviderError::MethodNotSupported("eth_submitWork".to_string()).into()) } async fn send_transaction(&self, _request: TransactionRequest) -> Result { - Err(EthApiError::MethodNotSupported("eth_sendTransaction".to_string()).into()) + Err(EthProviderError::MethodNotSupported("eth_sendTransaction".to_string()).into()) } #[tracing::instrument(skip_all, ret, err, fields(bytes = %bytes))] async fn send_raw_transaction(&self, bytes: Bytes) -> Result { - let transaction_hash = self.kakarot_client.send_transaction(bytes).await?; - Ok(transaction_hash) + Ok(self.eth_provider.send_raw_transaction(bytes).await?) } async fn sign(&self, _address: Address, _message: Bytes) -> Result { - Err(EthApiError::MethodNotSupported("eth_sign".to_string()).into()) + Err(EthProviderError::MethodNotSupported("eth_sign".to_string()).into()) } async fn sign_transaction(&self, _transaction: CallRequest) -> Result { - Err(EthApiError::MethodNotSupported("eth_signTransaction".to_string()).into()) + Err(EthProviderError::MethodNotSupported("eth_signTransaction".to_string()).into()) } async fn sign_typed_data(&self, _address: Address, _data: Value) -> Result { - Err(EthApiError::MethodNotSupported("eth_signTypedData".to_string()).into()) + Err(EthProviderError::MethodNotSupported("eth_signTypedData".to_string()).into()) } async fn get_proof( @@ -256,30 +242,30 @@ where _keys: Vec, _block_id: Option, ) -> Result { - Err(EthApiError::MethodNotSupported("eth_getProof".to_string()).into()) + Err(EthProviderError::MethodNotSupported("eth_getProof".to_string()).into()) } async fn new_filter(&self, _filter: Filter) -> Result { - Err(EthApiError::MethodNotSupported("eth_newFilter".to_string()).into()) + Err(EthProviderError::MethodNotSupported("eth_newFilter".to_string()).into()) } async fn new_block_filter(&self) -> Result { - Err(EthApiError::MethodNotSupported("eth_newBlockFilter".to_string()).into()) + Err(EthProviderError::MethodNotSupported("eth_newBlockFilter".to_string()).into()) } async fn new_pending_transaction_filter(&self) -> Result { - Err(EthApiError::MethodNotSupported("eth_newPendingTransactionFilter".to_string()).into()) + Err(EthProviderError::MethodNotSupported("eth_newPendingTransactionFilter".to_string()).into()) } async fn uninstall_filter(&self, _id: U64) -> Result { - Err(EthApiError::MethodNotSupported("eth_uninstallFilter".to_string()).into()) + Err(EthProviderError::MethodNotSupported("eth_uninstallFilter".to_string()).into()) } async fn get_filter_changes(&self, _id: U64) -> Result { - Err(EthApiError::MethodNotSupported("eth_getFilterChanges".to_string()).into()) + Err(EthProviderError::MethodNotSupported("eth_getFilterChanges".to_string()).into()) } async fn get_filter_logs(&self, _id: U64) -> Result { - Err(EthApiError::MethodNotSupported("eth_getFilterLogs".to_string()).into()) + Err(EthProviderError::MethodNotSupported("eth_getFilterLogs".to_string()).into()) } } diff --git a/src/eth_rpc/servers/net_rpc.rs b/src/eth_rpc/servers/net_rpc.rs index 105c1ea7b..d2363457d 100644 --- a/src/eth_rpc/servers/net_rpc.rs +++ b/src/eth_rpc/servers/net_rpc.rs @@ -1,30 +1,25 @@ -use crate::starknet_client::constants::CHAIN_ID; -use crate::starknet_client::errors::EthApiError; -use crate::starknet_client::KakarotClient; +use crate::eth_provider::provider::EthereumProvider; use jsonrpsee::core::{async_trait, RpcResult as Result}; use reth_primitives::U64; use reth_rpc_types::PeerCount; -use starknet::providers::Provider; -use std::sync::Arc; - use crate::eth_rpc::api::net_api::NetApiServer; /// The RPC module for the implementing Net api -pub struct NetRpc { - pub kakarot_client: Arc>, +pub struct NetRpc { + eth_provider: P, } -impl NetRpc

{ - pub fn new(kakarot_client: Arc>) -> Self { - Self { kakarot_client } +impl NetRpc

{ + pub fn new(eth_provider: P) -> Self { + Self { eth_provider } } } #[async_trait] -impl NetApiServer for NetRpc

{ - fn version(&self) -> Result { - Ok(CHAIN_ID.into()) +impl NetApiServer for NetRpc

{ + async fn version(&self) -> Result { + Ok(self.eth_provider.chain_id().await?.unwrap_or_default()) } fn peer_count(&self) -> Result { @@ -39,7 +34,7 @@ impl NetApiServer for NetRpc

{ async fn health(&self) -> Result { // Calls starknet block_number method to check if it resolves - self.kakarot_client.starknet_provider().block_number().await.map_err(EthApiError::from)?; + let _ = self.eth_provider.block_number().await?; Ok(true) } diff --git a/src/eth_rpc/servers/web3_rpc.rs b/src/eth_rpc/servers/web3_rpc.rs index 933622349..e6a0d608a 100644 --- a/src/eth_rpc/servers/web3_rpc.rs +++ b/src/eth_rpc/servers/web3_rpc.rs @@ -1,4 +1,3 @@ -use crate::starknet_client::constants::KAKAROT_CLIENT_VERSION; use jsonrpsee::core::{async_trait, RpcResult as Result}; use reth_primitives::{keccak256, Bytes, H256}; @@ -17,7 +16,7 @@ impl Web3Rpc { #[async_trait] impl Web3ApiServer for Web3Rpc { fn client_version(&self) -> Result { - Ok((*KAKAROT_CLIENT_VERSION).clone()) + Ok(format!("kakarot_{}", env!("CARGO_PKG_VERSION"))) } fn sha3(&self, input: Bytes) -> Result { diff --git a/src/lib.rs b/src/lib.rs index 5b2e727ec..bf64f3009 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,6 @@ -pub mod contracts; +pub mod config; pub mod eth_provider; pub mod eth_rpc; pub mod models; -pub mod starknet_client; +#[cfg(feature = "testing")] +pub mod test_utils; diff --git a/src/main.rs b/src/main.rs index bcf745e5c..d2fbdb46f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,16 +1,14 @@ +use std::env::var; use std::sync::Arc; use dotenv::dotenv; use eyre::Result; +use kakarot_rpc::config::{JsonRpcClientBuilder, KakarotRpcConfig, Network, SequencerGatewayProviderBuilder}; use kakarot_rpc::eth_provider::database::Database; use kakarot_rpc::eth_provider::provider::EthDataProvider; use kakarot_rpc::eth_rpc::config::RPCConfig; use kakarot_rpc::eth_rpc::rpc::KakarotRpcModuleBuilder; use kakarot_rpc::eth_rpc::run_server; -use kakarot_rpc::starknet_client::config::{ - env_var, JsonRpcClientBuilder, KakarotRpcConfig, Network, SequencerGatewayProviderBuilder, -}; -use kakarot_rpc::starknet_client::KakarotClient; use mongodb::options::{DatabaseOptions, ReadConcern, WriteConcern}; use starknet::providers::jsonrpc::HttpTransport; use starknet::providers::{JsonRpcClient, SequencerGatewayProvider}; @@ -44,27 +42,24 @@ async fn main() -> Result<()> { ), }; - let db_client = mongodb::Client::with_uri_str( - env_var("MONGO_CONNECTION_STRING").expect("Missing MONGO_CONNECTION_STRING .env"), - ) - .await?; + let db_client = + mongodb::Client::with_uri_str(var("MONGO_CONNECTION_STRING").expect("Missing MONGO_CONNECTION_STRING .env")) + .await?; let db = Database::new(db_client.database_with_options( - &env_var("MONGO_DATABASE_NAME").expect("Missing MONGO_DATABASE_NAME from .env"), + &var("MONGO_DATABASE_NAME").expect("Missing MONGO_DATABASE_NAME from .env"), DatabaseOptions::builder().read_concern(ReadConcern::MAJORITY).write_concern(WriteConcern::MAJORITY).build(), )); let kakarot_rpc_module = match starknet_provider { StarknetProvider::JsonRpcClient(starknet_provider) => { let starknet_provider = Arc::new(starknet_provider); - let kakarot_client = Arc::new(KakarotClient::new(starknet_config, starknet_provider.clone())); let eth_provider = EthDataProvider::new(db, starknet_provider); - KakarotRpcModuleBuilder::new(kakarot_client, eth_provider).rpc_module() + KakarotRpcModuleBuilder::new(eth_provider).rpc_module() } StarknetProvider::SequencerGatewayProvider(starknet_provider) => { let starknet_provider = Arc::new(starknet_provider); - let kakarot_client = Arc::new(KakarotClient::new(starknet_config, starknet_provider.clone())); let eth_provider = EthDataProvider::new(db, starknet_provider); - KakarotRpcModuleBuilder::new(kakarot_client, eth_provider).rpc_module() + KakarotRpcModuleBuilder::new(eth_provider).rpc_module() } }?; diff --git a/src/models/balance.rs b/src/models/balance.rs index 0eea57489..e5e8f8926 100644 --- a/src/models/balance.rs +++ b/src/models/balance.rs @@ -5,7 +5,7 @@ use futures::{Future, FutureExt}; use reth_primitives::{Address, U256}; use serde::{Deserialize, Serialize}; -use crate::starknet_client::errors::EthApiError; +use crate::eth_provider::error::EthProviderError; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct TokenBalance { @@ -20,7 +20,7 @@ pub struct TokenBalances { pub token_balances: Vec, } -type BalanceOfResult = Result; +type BalanceOfResult = Result; pub struct FutureTokenBalance> { pub balance: F, diff --git a/src/models/block.rs b/src/models/block.rs index 3511f142d..7930ad589 100644 --- a/src/models/block.rs +++ b/src/models/block.rs @@ -1,16 +1,9 @@ -use reth_primitives::{Address, BlockId as EthereumBlockId, BlockNumberOrTag, Bloom, Bytes, H256, H64, U256}; -use reth_rpc_types::{Block, BlockTransactions, Header, RichBlock}; -use starknet::core::types::{ - BlockId as StarknetBlockId, BlockTag, FieldElement, MaybePendingBlockWithTxHashes, MaybePendingBlockWithTxs, - Transaction, -}; -use starknet::providers::Provider; +use reth_primitives::{BlockId as EthereumBlockId, BlockNumberOrTag}; +use starknet::core::types::{BlockId as StarknetBlockId, BlockTag}; use super::felt::Felt252Wrapper; use crate::into_via_try_wrapper; use crate::models::errors::ConversionError; -use crate::starknet_client::constants::{EARLIEST_BLOCK_NUMBER, GAS_LIMIT, GAS_USED, SIZE}; -use crate::starknet_client::KakarotClient; pub struct EthBlockId(EthereumBlockId); @@ -60,183 +53,9 @@ impl From for StarknetBlockId { BlockNumberOrTag::Safe | BlockNumberOrTag::Latest | BlockNumberOrTag::Finalized => { Self::Tag(BlockTag::Latest) } - BlockNumberOrTag::Earliest => Self::Number(EARLIEST_BLOCK_NUMBER), + BlockNumberOrTag::Earliest => Self::Number(0), BlockNumberOrTag::Pending => Self::Tag(BlockTag::Pending), BlockNumberOrTag::Number(number) => Self::Number(number), } } } - -/// Implement getters for fields that are present in Starknet Blocks, both in pending and validated -/// state. For example, `parent_hash` is present in both `PendingBlock` and `Block`. -macro_rules! implement_starknet_block_getters { - ($(($enum:ident, $field:ident, $field_type:ty)),*) => { - $(pub fn $field(&self) -> $field_type { - match &self.0 { - $enum::PendingBlock(pending_block_with_tx_hashes) => { - pending_block_with_tx_hashes.$field.clone() - } - $enum::Block(block_with_tx_hashes) => { - block_with_tx_hashes.$field.clone() - } - } - })* - }; -} - -/// Implement getters for fields that are only present in Starknet Blocks that are not pending. -/// For example, `block_hash` is only present in `Block` and not in `PendingBlock`. -macro_rules! implement_starknet_block_getters_not_pending { - ($(($enum:ident, $field:ident, $field_type:ty)),*) => { - $(pub fn $field(&self) -> Option<$field_type> { - match &self.0 { - $enum::PendingBlock(_) => { - None - } - $enum::Block(block_with_txs) => { - Some(block_with_txs.$field.clone()) - } - } - })* - }; -} - -pub struct BlockWithTxHashes(MaybePendingBlockWithTxHashes); - -impl BlockWithTxHashes { - pub const fn new(block: MaybePendingBlockWithTxHashes) -> Self { - Self(block) - } - - implement_starknet_block_getters!( - (MaybePendingBlockWithTxHashes, parent_hash, FieldElement), - (MaybePendingBlockWithTxHashes, sequencer_address, FieldElement), - (MaybePendingBlockWithTxHashes, timestamp, u64), - (MaybePendingBlockWithTxHashes, transactions, Vec) - ); - - implement_starknet_block_getters_not_pending!( - (MaybePendingBlockWithTxHashes, block_hash, FieldElement), - (MaybePendingBlockWithTxHashes, block_number, u64) - ); -} - -impl From for BlockWithTxHashes { - fn from(block: MaybePendingBlockWithTxHashes) -> Self { - Self(block) - } -} - -pub struct BlockWithTxs(MaybePendingBlockWithTxs); - -impl BlockWithTxs { - pub const fn new(block: MaybePendingBlockWithTxs) -> Self { - Self(block) - } - - implement_starknet_block_getters!( - (MaybePendingBlockWithTxs, parent_hash, FieldElement), - (MaybePendingBlockWithTxs, sequencer_address, FieldElement), - (MaybePendingBlockWithTxs, timestamp, u64), - (MaybePendingBlockWithTxs, transactions, Vec) - ); - - implement_starknet_block_getters_not_pending!( - (MaybePendingBlockWithTxs, block_hash, FieldElement), - (MaybePendingBlockWithTxs, block_number, u64) - ); -} - -impl From for BlockWithTxs { - fn from(block: MaybePendingBlockWithTxs) -> Self { - Self(block) - } -} - -macro_rules! to_eth_block { - ($self:ident, $client: ident, $transactions: expr) => {{ - // TODO: Fetch real data - let gas_limit = *GAS_LIMIT; - - // TODO: Fetch real data - let gas_used = *GAS_USED; - - // TODO: Fetch real data - let nonce: Option = Some(H64::zero()); - - // TODO: - // Aggregate all the logs from the transactions - // Create a bloom filter from the logs and add it to the block - let logs_bloom = Bloom::default(); - let extra_data = Bytes::default(); - - // TODO: Fetch real data - let base_fee_per_gas = $client.base_fee_per_gas(); - - let parent_hash = H256::from_slice(&$self.parent_hash().to_bytes_be()); - let sequencer = Address::from_slice(&$self.sequencer_address().to_bytes_be()[12..]); - let timestamp = U256::from($self.timestamp()); - - let hash = $self.block_hash().as_ref().map(|hash| H256::from_slice(&hash.to_bytes_be())); - let number = $self.block_number().map(U256::from); - - // TODO: Add filter to tx_hashes - let transactions = $transactions; - - let header = Header { - // PendingBlockWithTxHashes doesn't have a block hash - hash, - parent_hash, - uncles_hash: parent_hash, - miner: sequencer, - // PendingBlockWithTxHashes doesn't have a state root - state_root: H256::zero(), - // PendingBlockWithTxHashes doesn't have a transactions root - transactions_root: H256::zero(), - // PendingBlockWithTxHashes doesn't have a receipts root - receipts_root: H256::zero(), - // PendingBlockWithTxHashes doesn't have a block number - number, - gas_used, - gas_limit, - extra_data, - logs_bloom, - timestamp, - difficulty: U256::ZERO, - nonce, - base_fee_per_gas: Some(base_fee_per_gas), - mix_hash: H256::zero(), - withdrawals_root: Some(H256::zero()), - blob_gas_used: None, - excess_blob_gas: None, - parent_beacon_block_root: None, - }; - let block = Block { - header, - total_difficulty: None, - uncles: vec![], - transactions, - size: Some(*SIZE), - withdrawals: Some(vec![]), - }; - Into::::into(block) - }}; -} - -impl BlockWithTxHashes { - pub async fn to_eth_block(&self, client: &KakarotClient

) -> RichBlock { - let transactions = BlockTransactions::Hashes( - self.transactions().iter().map(|tx| H256::from_slice(&tx.to_bytes_be())).collect(), - ); - to_eth_block!(self, client, transactions) - } -} - -impl BlockWithTxs { - pub async fn to_eth_block(&self, client: &KakarotClient

) -> RichBlock { - let hash = self.block_hash().as_ref().map(|hash| H256::from_slice(&hash.to_bytes_be())); - let number = self.block_number().map(U256::from); - let transactions = client.filter_starknet_into_eth_txs(self.transactions(), hash, number).await; - to_eth_block!(self, client, transactions) - } -} diff --git a/src/models/call.rs b/src/models/call.rs deleted file mode 100644 index ffd8bb53c..000000000 --- a/src/models/call.rs +++ /dev/null @@ -1,217 +0,0 @@ -use std::slice::SliceIndex; - -use bytes::BytesMut; -use reth_primitives::{Signature, Transaction, TransactionSigned}; -use reth_rlp::Decodable; -use starknet::accounts::Call as StarknetCall; -use starknet_crypto::FieldElement; - -use crate::models::errors::ConversionError; -use crate::starknet_client::helpers::{try_from_u8_iterator, DataDecodingError}; - -#[derive(Clone)] -pub struct Call(StarknetCall); - -impl From for StarknetCall { - fn from(call: Call) -> Self { - call.0 - } -} - -impl From for Call { - fn from(call: StarknetCall) -> Self { - Self(call) - } -} - -impl From for Vec { - fn from(call: Call) -> Self { - let mut c = vec![ - FieldElement::ONE, - call.0.to, - call.0.selector, - FieldElement::ZERO, - FieldElement::from(call.0.calldata.len()), - FieldElement::from(call.0.calldata.len()), - ]; - c.extend(call.0.calldata); - c - } -} - -pub struct Calls(Vec); - -impl From for Vec { - fn from(calls: Calls) -> Self { - calls.0 - } -} - -impl From> for Calls { - fn from(calls: Vec) -> Self { - Self(calls) - } -} - -/// Converts a raw starknet transaction calldata to a vector of starknet calls. -impl TryFrom> for Calls { - type Error = ConversionError; - - fn try_from(value: Vec) -> Result { - // in account calls, the calldata is first each call as {contract address, selector, data offset, - // data length} and then all the calldata of each call, so each call takes 4 felts, and - // eventually the calldata of the first call is at offset = 1 (for call_len) + 4 * call_len + 1 - // (for calldata_len) - let calls_len = u32::try_from(value[0]) - .map_err(|e| ConversionError::ValueOutOfRange(format!("{}: call array length > u32::MAX", e)))? - as usize; - - let mut offset = calls_len * 4 + 2; - - let mut calls = vec![]; - for i in 0..calls_len { - let calldata_len = - u32::try_from(value[i * 4 + 4]).map_err(|e| ConversionError::ValueOutOfRange(e.to_string()))? as usize; - let call = StarknetCall { - to: value[i * 4 + 2], - selector: value[i * 4 + 3], - calldata: value[offset..offset + calldata_len].to_vec(), - }; - offset += calldata_len; - calls.push(call); - } - Ok(Self(calls)) - } -} - -impl TryFrom for Transaction { - type Error = DataDecodingError; - - fn try_from(value: Call) -> std::result::Result { - let mut call: Vec = try_from_u8_iterator(value.0.calldata.into_iter()); - // Append a default RLP encoded signature in order to - // be able to decode the transaction as a TransactionSigned. - let mut buf = BytesMut::new(); - Signature::default().encode(&mut buf); - call.append(&mut buf.to_vec()); - - let tx = TransactionSigned::decode(&mut call.as_slice())?; - Ok(tx.transaction) - } -} - -impl Calls { - pub fn len(&self) -> usize { - self.0.len() - } - - pub fn is_empty(&self) -> bool { - self.len() == 0 - } - - pub fn get(&self, index: I) -> Option<&I::Output> - where - I: SliceIndex<[StarknetCall]>, - { - self.0.get(index) - } -} - -#[cfg(test)] -mod tests { - use std::str::FromStr; - - use reth_primitives::Address; - use serde::Deserialize; - use starknet::macros::felt; - - use super::*; - use crate::starknet_client::constants::selectors::ETH_CALL; - - #[derive(Debug, Deserialize)] - pub struct TestCall { - pub to: FieldElement, - pub selector: FieldElement, - pub calldata: Vec, - } - - // Impl From for TestCall into StarknetCall - impl From for StarknetCall { - fn from(call: TestCall) -> Self { - Self { to: call.to, selector: call.selector, calldata: call.calldata } - } - } - - #[test] - fn test_from_call() { - // Given - let call: Call = StarknetCall { - to: felt!("0xdead"), - selector: ETH_CALL, - calldata: vec![1u8, 2u8, 3u8, 4u8, 5u8, 6u8].into_iter().map(FieldElement::from).collect(), - } - .into(); - - // When - let raw_calldata = Vec::::from(call); - - // Then - let expected = vec![ - FieldElement::ONE, - felt!("0xdead"), - ETH_CALL, - FieldElement::ZERO, - FieldElement::from(6u8), - FieldElement::from(6u8), - FieldElement::from(1u8), - FieldElement::from(2u8), - FieldElement::from(3u8), - FieldElement::from(4u8), - FieldElement::from(5u8), - FieldElement::from(6u8), - ]; - assert_eq!(expected, raw_calldata); - } - - #[test] - fn test_calls_get_to_eip1559() { - // Given - let raw: TestCall = serde_json::from_str(include_str!("test_data/call/eip1559.json")).unwrap(); - let starknet_call: StarknetCall = raw.into(); - let call = Call::from(starknet_call); - - // When - let to = TryInto::::try_into(call).unwrap().to(); - - // Then - assert_eq!(to, Some(Address::from_str("0x1f9840a85d5af5bf1d1762f925bdaddc4201f984").unwrap())); - } - - #[test] - fn test_calls_get_to_eip2930() { - // Given - let raw: TestCall = serde_json::from_str(include_str!("test_data/call/eip2930.json")).unwrap(); - let starknet_call: StarknetCall = raw.into(); - let call = Call::from(starknet_call); - - // When - let to = TryInto::::try_into(call).unwrap().to(); - - // Then - assert_eq!(to, Some(Address::from_str("0x0000006f746865725f65766d5f61646472657373").unwrap())); - } - - #[test] - fn test_calls_get_to_legacy() { - // Given - let raw: TestCall = serde_json::from_str(include_str!("test_data/call/legacy.json")).unwrap(); - let starknet_call: StarknetCall = raw.into(); - let call = Call::from(starknet_call); - - // When - let to = TryInto::::try_into(call).unwrap().to(); - - // Then - assert_eq!(to, Some(Address::from_str("0x1f9840a85d5af5bf1d1762f925bdaddc4201f984").unwrap())); - } -} diff --git a/src/models/errors.rs b/src/models/errors.rs index 48d8ee2af..3bcd48835 100644 --- a/src/models/errors.rs +++ b/src/models/errors.rs @@ -1,28 +1,21 @@ -use jsonrpsee::types::ErrorObject; use ruint::FromUintError; use starknet::core::types::FromByteArrayError; use thiserror::Error; -use crate::starknet_client::{errors::EthApiError, helpers::DataDecodingError}; - #[derive(Debug, Error)] /// Conversion error pub enum ConversionError { - /// Ethereum to Starknet transaction conversion error - #[error("transaction conversion error: {0}")] - TransactionConversionError(String), /// Felt252Wrapper conversion error #[error(transparent)] Felt252WrapperConversionError(#[from] FromByteArrayError), - /// Data decoding error - #[error(transparent)] - DataDecodingError(#[from] DataDecodingError), /// Felt252Wrapper to Ethereum address conversion error #[error( "failed to convert Felt252Wrapper to Ethereum address: the value exceeds the maximum size of an Ethereum \ address" )] ToEthereumAddressError, + #[error("Failed to convert Ethereum transaction to Starknet transaction: {0}")] + ToStarknetTransactionError(String), /// Value out of range error #[error("value out of range: {0}")] ValueOutOfRange(String), @@ -39,10 +32,3 @@ impl From> for ConversionError { Self::UintConversionError(err.to_string()) } } - -impl From for ErrorObject<'static> { - fn from(err: ConversionError) -> Self { - let err = EthApiError::from(err); - err.into() - } -} diff --git a/src/models/event.rs b/src/models/event.rs deleted file mode 100644 index e4ce1cb13..000000000 --- a/src/models/event.rs +++ /dev/null @@ -1,76 +0,0 @@ -use reth_primitives::{Bytes, H256, U256}; -use reth_rpc_types::Log; -use starknet::core::types::Event; -use starknet::providers::Provider; - -use super::felt::Felt252Wrapper; -use crate::starknet_client::errors::EthApiError; -use crate::starknet_client::helpers::try_from_u8_iterator; -use crate::starknet_client::KakarotClient; -use crate::{into_via_wrapper, try_into_via_wrapper}; - -#[derive(Debug, Clone)] -pub struct StarknetEvent(Event); - -impl StarknetEvent { - pub const fn new(sn_event: Event) -> Self { - Self(sn_event) - } -} - -impl From for StarknetEvent { - fn from(event: Event) -> Self { - Self::new(event) - } -} - -impl StarknetEvent { - pub fn to_eth_log( - self, - client: &KakarotClient

, - block_hash: Option, - block_number: Option, - transaction_hash: Option, - log_index: Option, - transaction_index: Option, - ) -> Result { - // If event `from_address` does not equal kakarot address, return early - if self.0.from_address != client.kakarot_address() { - return Err(EthApiError::KakarotDataFilteringError("Event".into())); - } - - // Derive the evm address from the first item in the `event.keys` vector and remove it - let (evm_contract_address, keys) = - self.0.keys.split_first().ok_or_else(|| EthApiError::KakarotDataFilteringError("Event".into()))?; - - let address = try_into_via_wrapper!(*evm_contract_address); - - if keys.len() % 2 != 0 { - return Err(anyhow::anyhow!("Not a convertible event: Keys length is not even").into()); - } - - let topics: Vec = keys - .chunks(2) - .map(|chunk| { - let low: U256 = into_via_wrapper!(chunk[0]); - let high: U256 = into_via_wrapper!(chunk[1]); - let val = low | (high << 128); - H256::from(val) - }) - .collect(); - - let data = Bytes::from(try_from_u8_iterator::<_, Vec<_>>(self.0.data.into_iter())); - - Ok(Log { - address, - topics, - data, - block_hash, - block_number, - transaction_hash, - log_index, - transaction_index, - removed: false, - }) - } -} diff --git a/src/models/event_filter.rs b/src/models/event_filter.rs deleted file mode 100644 index 450416a02..000000000 --- a/src/models/event_filter.rs +++ /dev/null @@ -1,104 +0,0 @@ -use reth_primitives::U256; -use reth_rpc_types::{Filter, ValueOrArray}; -use starknet::core::types::{BlockId, EventFilter}; -use starknet::providers::Provider; -use starknet_crypto::FieldElement; -use tracing::debug; - -use super::block::EthBlockNumberOrTag; -use super::felt::Felt252Wrapper; -use crate::starknet_client::errors::EthApiError; -use crate::starknet_client::helpers::split_u256; -use crate::starknet_client::KakarotClient; -use crate::{into_via_try_wrapper, into_via_wrapper}; - -pub struct EthEventFilter(Filter); - -impl From for EthEventFilter { - fn from(filter: Filter) -> Self { - Self(filter) - } -} - -impl From for Filter { - fn from(filter: EthEventFilter) -> Self { - filter.0 - } -} - -impl EthEventFilter { - #[tracing::instrument(skip_all, level = "debug")] - pub fn to_starknet_event_filter( - self, - client: &KakarotClient

, - ) -> Result { - let filter: Filter = self.into(); - let block_hash = filter.get_block_hash(); - - debug!("ethereum event filter: {:?}", filter); - - // Extract keys into topics - let keys: Vec = filter - .topics - .into_iter() - .flat_map(|filter| match filter.to_value_or_array() { - None => vec![], - Some(ValueOrArray::Value(value)) => { - let topic = U256::from_be_bytes(value.to_fixed_bytes()); - split_u256(topic).to_vec() - }, - Some(ValueOrArray::Array(topics)) => topics - .iter() - .flat_map(|topic| { - let topic = U256::from_be_bytes(topic.to_fixed_bytes()); - split_u256(topic).to_vec() - }) - .collect(), - }) - .take(8) // take up to 4 topics split into 2 field elements - .collect(); - - // Get the filter address if any (added as first key) - let address = filter.address.to_value_or_array().and_then(|a| match a { - ValueOrArray::Array(addresses) => addresses.first().copied(), - ValueOrArray::Value(address) => Some(address), - }); - - // Convert to expected format Vec> or None if no keys and no address - let keys = if !keys.is_empty() | address.is_some() { - let keys = keys.into_iter().map(|key| vec![key]).collect(); - // If address is present add it as first key, otherwise add an empty key - let keys = [address.map_or(vec![vec![]], |a| vec![vec![into_via_wrapper!(a)]]), keys].concat(); - Some(keys) - } else { - None - }; - - debug!("starknet event filter keys: {:?}", keys); - - // Add filter block range - let starknet_filter = if let Some(block_hash) = block_hash { - let block_hash = into_via_try_wrapper!(block_hash); - - EventFilter { - from_block: Some(BlockId::Hash(block_hash)), - to_block: Some(BlockId::Hash(block_hash)), - address: Some(client.kakarot_address()), - keys, - } - } else { - let from_block = filter.block_option.get_from_block().copied().map(Into::::into); - let to_block = filter.block_option.get_to_block().copied().map(Into::::into); - EventFilter { - from_block: from_block.map(Into::::into), - to_block: to_block.map(Into::::into), - address: Some(client.kakarot_address()), - keys, - } - }; - - debug!("starknet event filter: {:?}", starknet_filter); - - Ok(starknet_filter) - } -} diff --git a/src/models/mod.rs b/src/models/mod.rs index bba6a149d..f76e9992e 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,10 +1,4 @@ pub mod balance; pub mod block; -pub mod call; pub mod errors; -pub mod event; -pub mod event_filter; pub mod felt; -pub mod signature; -pub mod transaction; -pub mod transaction_receipt; diff --git a/src/models/signature.rs b/src/models/signature.rs deleted file mode 100644 index bef26e4bb..000000000 --- a/src/models/signature.rs +++ /dev/null @@ -1,100 +0,0 @@ -use crate::into_via_wrapper; -use reth_primitives::U256; -use reth_rpc_types::{Parity, Signature as EthSignature}; -use starknet::core::types::FieldElement; -use thiserror::Error; - -use super::felt::Felt252Wrapper; - -#[derive(Debug, Error, PartialEq, Eq)] -pub enum StarknetSignatureError { - #[error("missing Starknet signature param {0}")] - MissingSignatureParamsError(String), -} - -pub struct StarknetSignature(Vec); - -impl From> for StarknetSignature { - fn from(value: Vec) -> Self { - Self(value) - } -} - -impl TryFrom for EthSignature { - type Error = StarknetSignatureError; - - fn try_from(value: StarknetSignature) -> Result { - macro_rules! signature_param { - ($index: expr, $param: expr) => { - into_via_wrapper!(*value.0.get($index).ok_or_else(|| { - StarknetSignatureError::MissingSignatureParamsError(format!("Starknet signature param {}", $param)) - })?) - }; - } - let r_low: U256 = signature_param!(0, "r"); - let r_high: U256 = signature_param!(1, "r"); - let r = r_low + (r_high << 128); - let s_low: U256 = signature_param!(2, "s"); - let s_high: U256 = signature_param!(3, "s"); - let s = s_low + (s_high << 128); - let v: U256 = signature_param!(4, "v"); - let y_parity = if v == U256::from(0u8) { - Some(Parity(false)) - } else if v == U256::from(1u8) { - Some(Parity(true)) - } else { - None - }; - Ok(Self { r, s, v, y_parity }) - } -} - -#[cfg(test)] -mod tests { - use starknet::core::crypto::pedersen_hash; - use starknet_crypto::{sign, ExtendedSignature}; - - use crate::starknet_client::helpers::split_u256; - - use super::*; - - pub const PRIVATE_KEY: &str = "0x0684e179baf957906d4a0e33bd28066778659964f6b5477e2483b72419a6b874"; - - fn get_signature() -> (Vec, ExtendedSignature) { - let tx_hash = pedersen_hash(&FieldElement::from(1u8), &FieldElement::from(2u8)); - let private_key = FieldElement::from_hex_be(PRIVATE_KEY).unwrap(); - - let signature = sign(&private_key, &tx_hash, &FieldElement::from(1u8)).unwrap(); - let r = Felt252Wrapper::from(signature.r); - let r: U256 = r.into(); - let [r_low, r_high] = split_u256(r); - - let s = Felt252Wrapper::from(signature.s); - let s: U256 = s.into(); - let [s_low, s_high] = split_u256(s); - - (vec![r_low, r_high, s_low, s_high, signature.v], signature) - } - - #[test] - fn test_starknet_to_eth_signature_passes() { - let (starknet_signature, raw_signature) = get_signature(); - - let eth_signature = EthSignature::try_from(StarknetSignature::from(starknet_signature)).unwrap(); - - let y_parity = if raw_signature.v == FieldElement::ONE || raw_signature.v == FieldElement::ZERO { - Some(Parity(raw_signature.v == FieldElement::ONE)) - } else { - None - }; - - let expected_signature = EthSignature { - r: Felt252Wrapper::from(raw_signature.r).into(), - s: Felt252Wrapper::from(raw_signature.s).into(), - v: Felt252Wrapper::from(raw_signature.v).into(), - y_parity, - }; - - assert_eq!(eth_signature, expected_signature); - } -} diff --git a/src/models/transaction/mod.rs b/src/models/transaction/mod.rs deleted file mode 100644 index eea9538a6..000000000 --- a/src/models/transaction/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -#[allow(clippy::module_inception)] -pub mod transaction; -pub mod transaction_signed; diff --git a/src/models/transaction/transaction.rs b/src/models/transaction/transaction.rs deleted file mode 100644 index e764c5238..000000000 --- a/src/models/transaction/transaction.rs +++ /dev/null @@ -1,137 +0,0 @@ -use reth_primitives::{Transaction as TransactionType, H256, U128, U256, U64}; -use reth_rpc_types::{Signature, Transaction as EthTransaction}; -use starknet::core::types::{BlockId as StarknetBlockId, FieldElement, InvokeTransaction, StarknetError, Transaction}; -use starknet::providers::{MaybeUnknownErrorCode, Provider, ProviderError, StarknetErrorWithMessage}; - -use crate::into_via_try_wrapper; -use crate::models::call::{Call, Calls}; -use crate::models::errors::ConversionError; -use crate::models::felt::Felt252Wrapper; -use crate::models::signature::StarknetSignature; -use crate::starknet_client::constants::CHAIN_ID; -use crate::starknet_client::errors::EthApiError; -use crate::starknet_client::KakarotClient; - -pub struct StarknetTransaction(Transaction); - -impl From for StarknetTransaction { - fn from(tx: Transaction) -> Self { - Self(tx) - } -} - -impl From for Transaction { - fn from(tx: StarknetTransaction) -> Self { - tx.0 - } -} - -macro_rules! invoke_transaction_field { - (($field_v0:ident, $field_v1:ident), $type:ty) => { - pub fn $field_v1(&self) -> Result<$type, ConversionError> { - match &self.0 { - Transaction::Invoke(tx) => match tx { - InvokeTransaction::V0(tx) => Ok(tx.$field_v0.clone().into()), - InvokeTransaction::V1(tx) => Ok(tx.$field_v1.clone().into()), - }, - _ => Err(ConversionError::TransactionConversionError( - "L1Handler, Declare, Deploy and DeployAccount transactions unsupported".to_string(), - )), - } - } - }; -} - -impl StarknetTransaction { - invoke_transaction_field!((calldata, calldata), Vec); - invoke_transaction_field!((contract_address, sender_address), Felt252Wrapper); - invoke_transaction_field!((signature, signature), Vec); - - pub fn transaction_hash(&self) -> H256 { - H256::from_slice(&self.0.transaction_hash().to_bytes_be()) - } -} - -impl StarknetTransaction { - pub async fn to_eth_transaction( - &self, - client: &KakarotClient

, - block_hash: Option, - block_number: Option, - transaction_index: Option, - ) -> Result { - let sender_address: FieldElement = self.sender_address()?.into(); - - let hash = self.transaction_hash(); - - let starknet_block_id = match block_hash { - Some(block_hash) => StarknetBlockId::Hash(into_via_try_wrapper!(block_hash)), - None => match block_number { - Some(block_number) => StarknetBlockId::Number(TryInto::::try_into(block_number)?), - None => { - return Err(EthApiError::RequestError(ProviderError::StarknetError(StarknetErrorWithMessage { - code: MaybeUnknownErrorCode::Known(StarknetError::BlockNotFound), - message: "Block hash or block number must be provided".into(), - }))); - } - }, - }; - let nonce: Felt252Wrapper = match &self.0 { - Transaction::Invoke(invoke_tx) => match invoke_tx { - InvokeTransaction::V0(_) => { - client.starknet_provider().get_nonce(starknet_block_id, sender_address).await?.into() - } - InvokeTransaction::V1(v1) => v1.nonce.into(), - }, - _ => return Err(EthApiError::KakarotDataFilteringError("Transaction".into())), - }; - let nonce: U64 = u64::try_from(nonce)?.into(); - - let from = client.get_evm_address(&sender_address).await?; - - let max_priority_fee_per_gas = Some(client.max_priority_fee_per_gas()); - - let calls: Calls = self.calldata()?.try_into()?; - - if calls.len() != 1 { - return Err(EthApiError::ConversionError("Call length is {calls.len()}, expected 1".to_string())); - } - - let call = - calls.get(0).ok_or(EthApiError::ConversionError("Call array length != 1 is not supported".to_string()))?; - - let tx: TransactionType = Call::from(call.clone()).try_into()?; - let input = tx.input().to_owned(); - let signature: Signature = StarknetSignature::from(self.signature()?) - .try_into() - .map_err(|_| EthApiError::KakarotDataFilteringError("Transaction Signature".into()))?; - let to = tx.to(); - let value = U256::from(tx.value()); - let max_fee_per_gas = Some(U128::from(tx.max_fee_per_gas())); - let transaction_type = Some(U64::from(Into::::into(tx.tx_type()))); - - let signature = Some(signature); - - Ok(EthTransaction { - hash, - nonce, - block_hash, - block_number, - transaction_index, - from, - to, - value, - gas_price: None, // TODO fetch the gas price - gas: U256::from(100), // TODO fetch the gas amount - max_fee_per_gas, - max_priority_fee_per_gas, - input, - signature, - chain_id: Some(CHAIN_ID.into()), - access_list: None, // TODO fetch the access list - transaction_type, - max_fee_per_blob_gas: None, - blob_versioned_hashes: Vec::new(), - }) - } -} diff --git a/src/models/transaction/transaction_signed.rs b/src/models/transaction/transaction_signed.rs deleted file mode 100644 index 5a983457e..000000000 --- a/src/models/transaction/transaction_signed.rs +++ /dev/null @@ -1,95 +0,0 @@ -use reth_primitives::{Bytes, TransactionSigned}; -use reth_rlp::Decodable as _; -use starknet::core::types::BroadcastedInvokeTransaction; -use starknet::providers::Provider; -use starknet_crypto::FieldElement; -use tracing::debug; - -use crate::starknet_client::constants::{CHAIN_ID, MAX_FEE}; -use crate::starknet_client::errors::EthApiError; -use crate::starknet_client::helpers::{prepare_kakarot_eth_send_transaction, split_u256, DataDecodingError}; -use crate::starknet_client::KakarotClient; - -use reth_primitives::Transaction as TransactionType; - -pub struct StarknetTransactionSigned(Bytes); - -impl From for StarknetTransactionSigned { - fn from(tx: Bytes) -> Self { - Self(tx) - } -} - -impl StarknetTransactionSigned { - #[tracing::instrument(skip_all, level = "debug")] - pub async fn to_broadcasted_invoke_transaction( - &self, - client: &KakarotClient

, - ) -> Result { - let mut data = self.0.as_ref(); - - let transaction = TransactionSigned::decode(&mut data).map_err(DataDecodingError::TransactionDecodingError)?; - - let evm_address = transaction.recover_signer().ok_or_else(|| { - EthApiError::Other(anyhow::anyhow!("Kakarot send_transaction: signature ecrecover failed")) - })?; - - let starknet_address = client.compute_starknet_address(&evm_address).await?; - - let nonce = FieldElement::from(transaction.nonce()); - - // Get estimated_fee from Starknet - let max_fee = (*MAX_FEE).into(); - - // Step: Signature - // Extract the signature from the Ethereum Transaction - // and place it in the Starknet signature InvokeTransaction vector - let mut signature: Vec = { - let r = split_u256(transaction.signature().r); - let s = split_u256(transaction.signature().s); - let signature = vec![r[0], r[1], s[0], s[1]]; - signature - }; - // Push the last element of the signature - // In case of a Legacy Transaction, it is v := {0, 1} + chain_id * 2 + 35 - // Else, it is odd_y_parity - if let TransactionType::Legacy(_) = transaction.transaction { - let chain_id = CHAIN_ID; - // TODO(elias): replace by dynamic chain_id when Kakarot supports it - // let chain_id: u64 = client - // .starknet_provider() - // .chain_id() - // .await? - // .try_into() - // .map_err(|e: ValueOutOfRangeError| ConversionError::ValueOutOfRange(e.to_string()))?; - signature.push(transaction.signature().v(Some(chain_id)).into()); - } else { - signature.push((transaction.signature().odd_y_parity as u64).into()); - } - - // Step: Calldata - // RLP encode the transaction without the signature - // Example: For Legacy Transactions: rlp([nonce, gas_price, gas_limit, to, value, data, chain_id, 0, 0]) - let mut signed_data = Vec::new(); - - debug!("ethereum transaction: {:?}", transaction); - - transaction.transaction.encode_without_signature(&mut signed_data); - - let calldata = prepare_kakarot_eth_send_transaction( - client.kakarot_address(), - signed_data.into_iter().map(FieldElement::from).collect(), - ); - - debug!("starknet calldata: {:?}", calldata); - - Ok(BroadcastedInvokeTransaction { - max_fee, - signature, - nonce, - sender_address: starknet_address, - calldata, - is_query: false, - }) - } -} diff --git a/src/models/transaction_receipt.rs b/src/models/transaction_receipt.rs deleted file mode 100644 index 97180fce2..000000000 --- a/src/models/transaction_receipt.rs +++ /dev/null @@ -1,137 +0,0 @@ -use reth_primitives::bloom::logs_bloom; -use reth_primitives::contract::create_address; -use reth_primitives::{U128, U256, U64, U8}; -use reth_rpc_types::{Log, TransactionReceipt as EthTransactionReceipt}; -use starknet::core::types::{ - ExecutionResult, InvokeTransactionReceipt, MaybePendingTransactionReceipt, TransactionReceipt, -}; -use starknet::providers::Provider; -use tracing::{debug, error}; - -use super::event::StarknetEvent; -use super::felt::Felt252Wrapper; -use super::transaction::transaction::StarknetTransaction; -use crate::into_via_wrapper; -use crate::starknet_client::errors::EthApiError; -use crate::starknet_client::KakarotClient; - -pub struct StarknetTransactionReceipt(MaybePendingTransactionReceipt); - -impl From for StarknetTransactionReceipt { - fn from(receipt: MaybePendingTransactionReceipt) -> Self { - Self(receipt) - } -} - -impl From for MaybePendingTransactionReceipt { - fn from(receipt: StarknetTransactionReceipt) -> Self { - receipt.0 - } -} - -impl StarknetTransactionReceipt { - #[tracing::instrument(skip_all, level = "debug")] - pub async fn to_eth_transaction_receipt( - self, - client: &KakarotClient

, - ) -> Result, EthApiError> { - let starknet_tx_receipt: MaybePendingTransactionReceipt = self.into(); - - debug!("starknet transaction receipt: {:?}", starknet_tx_receipt); - - let res_receipt = match starknet_tx_receipt { - MaybePendingTransactionReceipt::Receipt(receipt) => match receipt { - TransactionReceipt::Invoke(InvokeTransactionReceipt { - transaction_hash, - execution_result, - block_hash, - block_number, - events, - .. - }) => { - let starknet_tx: StarknetTransaction = - client.starknet_provider().get_transaction_by_hash(transaction_hash).await?.into(); - - let transaction_hash = Some(into_via_wrapper!(transaction_hash)); - let block_hash = Some(into_via_wrapper!(block_hash)); - let block_number = Some(into_via_wrapper!(block_number)); - - let eth_tx = starknet_tx.to_eth_transaction(client, block_hash, block_number, None).await?; - let from = eth_tx.from; - let to = eth_tx.to; - let contract_address = match execution_result { - ExecutionResult::Succeeded => { - match to { - // If to is Some, means contract_address should be None as it is a normal transaction - Some(_) => None, - // If to is None, is a contract creation transaction so contract_address should be Some - None => Some(create_address(eth_tx.from, eth_tx.nonce.as_u64())), - } - } - ExecutionResult::Reverted { ref reason } => { - error!("Transaction reverted with {reason}"); - None - } - }; - - let status_code = match execution_result { - ExecutionResult::Succeeded => Some(U64::from(1)), - ExecutionResult::Reverted { .. } => Some(U64::from(0)), - }; - - let logs: Vec = events - .into_iter() - .map(StarknetEvent::new) - .filter_map(|event| { - event.to_eth_log(client, block_hash, block_number, transaction_hash, None, None).ok() - }) - .collect(); - - // Reth note: - // This bloom operation is slow and should be cached if possible. - let bloom = { - let logs: Vec = logs - .iter() - .map(|log| reth_primitives::Log { - data: log.data.clone(), - topics: log.topics.clone(), - address: log.address, - }) - .collect(); - logs_bloom(logs.iter()) - }; - - EthTransactionReceipt { - transaction_hash, - transaction_index: U64::from(0), // TODO: Fetch real data - block_hash, - block_number, - from, - to, - cumulative_gas_used: U256::from(1_000_000), // TODO: Fetch real data - gas_used: Some(U256::from(500_000)), - contract_address, - logs, - state_root: None, - logs_bloom: bloom, - status_code, - effective_gas_price: U128::from(1_000_000), // TODO: Fetch real data - transaction_type: U8::from(0), // TODO: Fetch real data - blob_gas_price: None, - blob_gas_used: None, - } - } - // L1Handler, Declare, Deploy and DeployAccount transactions unsupported for now in - // Kakarot - _ => return Ok(None), - }, - MaybePendingTransactionReceipt::PendingReceipt(_) => { - return Ok(None); - } - }; - - debug!("ethereum transaction receipt: {:?}", res_receipt); - - Ok(Some(res_receipt)) - } -} diff --git a/src/starknet_client/constants.rs b/src/starknet_client/constants.rs deleted file mode 100644 index b63ceb854..000000000 --- a/src/starknet_client/constants.rs +++ /dev/null @@ -1,119 +0,0 @@ -use lazy_static::lazy_static; -use reth_primitives::{Address, U128, U256}; -use starknet::accounts::Call as StarknetCall; -use starknet::core::types::FieldElement; -use starknet::macros::selector; - -use crate::models::call::Call; - -/// CHAIN_ID = KKRT (0x4b4b5254) in ASCII -pub const CHAIN_ID: u64 = 1_263_227_476; - -pub const STARKNET_NATIVE_TOKEN: &str = "0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7"; - -pub const EARLIEST_BLOCK_NUMBER: u64 = 0; - -/// Current chunk limit for pathfinder https://github.com/eqlabs/pathfinder/blob/main/crates/storage/src/connection/event.rs#L11 -pub const CHUNK_SIZE_LIMIT: u64 = 1024; - -pub const MADARA_RPC_URL: &str = "http://127.0.0.1:9944"; - -pub const KATANA_RPC_URL: &str = "http://0.0.0.0:5050"; - -pub mod selectors { - use starknet::core::types::FieldElement; - use starknet::macros::selector; - - pub const BYTECODE: FieldElement = selector!("bytecode"); - pub const STORAGE: FieldElement = selector!("storage"); - pub const GET_IMPLEMENTATION: FieldElement = selector!("get_implementation"); - pub const GET_NONCE: FieldElement = selector!("get_nonce"); - - pub const ETH_CALL: FieldElement = selector!("eth_call"); - pub const ETH_SEND_TRANSACTION: FieldElement = selector!("eth_send_transaction"); - pub const DEPLOY_EXTERNALLY_OWNED_ACCOUNT: FieldElement = selector!("deploy_externally_owned_account"); - - pub const GET_EVM_ADDRESS: FieldElement = selector!("get_evm_address"); - - pub const BALANCE_OF: FieldElement = selector!("balanceOf"); - - pub const EVM_CONTRACT_DEPLOYED: FieldElement = selector!("evm_contract_deployed"); -} - -/// This module contains constants related to EVM gas fees. -pub mod gas { - use reth_primitives::U128; - - /// The base fee for a transaction in gwei. - /// - /// Since Starknet does not currently have a market for gas fees - /// TODO: Get Starknet "historical" Gas Price instead - pub const BASE_FEE_PER_GAS: u64 = 1; - - /// The maximum priority fee for a transaction in gwei. - /// - /// This fee is the maximum amount a user is willing to pay to have their transaction - /// included in a block quickly. - /// - /// Since Starknet does not currently have a market for gas fees, transactions are processed - /// on a "first come first served" basis by the Sequencer. - /// As a result, the priority fee is set to 0. - pub const MAX_PRIORITY_FEE_PER_GAS: U128 = U128::ZERO; - - /// The minimum gas fee for a transaction - /// - /// This minimum of 21,000 (see https://ethereum.stackexchange.com/questions/34674/where-does-the-number-21000-come-from-for-the-base-gas-consumption-in-ethereum/34675#34675) - /// is used if the returned fee estimate is lower, otherwise wallets such as Metamask will not - /// allow the transaction to be sent. - pub const MINIMUM_GAS_FEE: u64 = 21_000; -} - -// This module contains constants which are being used in place of real data that should be fetched -// in production. -lazy_static! { - pub static ref GAS_LIMIT: U256 = U256::from(1_000_000u64); - pub static ref GAS_USED: U256 = U256::from(500_000u64); - pub static ref CUMULATIVE_GAS_USED: U256 = U256::from(1_000_000u64); - pub static ref EFFECTIVE_GAS_PRICE: U128 = U128::from(1_000_000u64); - pub static ref SIZE: U256 = U256::from(1_000_000u64); - pub static ref MAX_FEE: u64 = 100_000_000_000_000_000u64; - pub static ref ESTIMATE_GAS: u64 = 100_000_000_000_000_000u64; - pub static ref TX_ORIGIN_ZERO: Address = Address::zero(); -} - -lazy_static! { - pub static ref KAKAROT_CLIENT_VERSION: String = format!("kakarot_{}", env!("CARGO_PKG_VERSION")); -} - -// This module contains constants related to transactions used to calculate -// Starknet gas price -lazy_static! { - /// The address of the argent account used to calculate the gas price. - /// (code: https://github.com/argentlabs/argent-contracts-starknet/blob/develop/contracts/account/src/argent_account.cairo) - /// This account is ONLY used for the gasPrice JSON RPC route, to send a simulate_transaction payload for a dummy transaction on Starknet - /// Thus recovering the current gas_price - pub static ref DUMMY_ARGENT_GAS_PRICE_ACCOUNT_ADDRESS: FieldElement = - FieldElement::from_hex_be("0x07142FbF6E8C9C07b079D47727C6D2ff49970203bfd5Bd6ED0D740e0f5a344E7").unwrap(); - pub static ref INC_SELECTOR: FieldElement = selector!("inc"); - /// The address of the counter contract used to calculate the gas price on mainnet - /// (code: https://gist.github.com/greged93/78b58f85cba6cf76eefaedab87f1b645) - pub static ref COUNTER_ADDRESS_MAINNET: FieldElement = - FieldElement::from_hex_be("0x02786c4cdfb2ee39727cb00695cf136710e2c3bfc5cb09315101be3d37c2c557").unwrap(); - /// The address of the counter contract used to calculate the gas price on goerli 1 - pub static ref COUNTER_ADDRESS_TESTNET1: FieldElement = - FieldElement::from_hex_be("0x03c12643f0e9f0b41de95a87e4f03f5fa69601930e9354a206a0b82a02119f2b").unwrap(); - /// The address of the counter contract used to calculate the gas price on goerli 2 - pub static ref COUNTER_ADDRESS_TESTNET2: FieldElement = - FieldElement::from_hex_be("0x00e438661a4775fdf10cf132cc50730f40e59f3d040b15e64cd292add25eb01b").unwrap(); - pub static ref COUNTER_CALL_MAINNET: Call = - StarknetCall { to: *COUNTER_ADDRESS_MAINNET, selector: *INC_SELECTOR, calldata: vec![] }.into(); - pub static ref COUNTER_CALL_TESTNET1: Call = - StarknetCall { to: *COUNTER_ADDRESS_TESTNET1, selector: *INC_SELECTOR, calldata: vec![] }.into(); - pub static ref COUNTER_CALL_TESTNET2: Call = - StarknetCall { to: *COUNTER_ADDRESS_TESTNET2, selector: *INC_SELECTOR, calldata: vec![] }.into(); -} - -// This module contains constants to be used for deployment of Kakarot System -lazy_static! { - pub static ref DEPLOY_FEE: FieldElement = FieldElement::from(100_000_u64); -} diff --git a/src/starknet_client/errors.rs b/src/starknet_client/errors.rs deleted file mode 100644 index 87ba06170..000000000 --- a/src/starknet_client/errors.rs +++ /dev/null @@ -1,179 +0,0 @@ -use jsonrpsee::types::ErrorObject; -use ruint::FromUintError; -use starknet::core::types::{FromByteSliceError, StarknetError}; -use starknet::providers::{MaybeUnknownErrorCode, ProviderError}; -use thiserror::Error; - -use super::helpers::DataDecodingError; -use crate::models::errors::ConversionError; - -use starknet_abigen_parser::cairo_types::Error as AbigenError; - -/// List of JSON-RPC error codes from ETH rpc spec. -/// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1474.md -#[derive(Debug, Copy, PartialEq, Eq, Clone)] -pub enum EthRpcErrorCode { - /// Custom geth error code, - Unknown, - ExecutionError = 3, - ParseError = -32700, - InvalidRequest = -32600, - MethodNotFound = -32601, - InvalidParams = -32602, - InternalError = -32603, - InvalidInput = -32000, - ResourceNotFound = -32001, - ResourceUnavailable = -32002, - TransactionRejected = -32003, - MethodNotSupported = -32004, - RequestLimitExceeded = -32005, - JsonRpcVersionUnsupported = -32006, -} - -/// Error that can occur when preparing configuration. -#[derive(Debug, Error)] -pub enum ConfigError { - /// Missing mandatory environment variable error. - #[error("Missing mandatory environment variable: {0}")] - EnvironmentVariableMissing(String), - /// Environment variable set wrong error. - #[error("Environment variable {0} set wrong: {1}")] - EnvironmentVariableSetWrong(String, String), - /// Invalid URL error. - #[error("Invalid URL: {0}")] - InvalidUrl(#[from] url::ParseError), - /// Invalid network error. - #[error("Invalid network: {0}")] - InvalidNetwork(String), -} - -/// Error that can accure when interacting with the Kakarot ETH API. -#[derive(Debug, Error)] -pub enum EthApiError { - /// Request to the Starknet provider failed. - #[error(transparent)] - RequestError(#[from] ProviderError), - /// Contract call with abigen failed. - #[error(transparent)] - AbigenError(#[from] AbigenError), - /// Conversion between Starknet types and ETH failed. - #[error("conversion error: {0}")] - ConversionError(String), - /// Data decoding into ETH types failed. - #[error(transparent)] - DataDecodingError(#[from] DataDecodingError), - /// Data not part of Kakarot. - #[error("{0} not from Kakarot")] - KakarotDataFilteringError(String), - /// Feeder gateway error. - #[error("Feeder gateway error: {0}")] - FeederGatewayError(String), - /// Missing parameter error. - #[error("Missing parameter: {0}")] - MissingParameterError(String), - /// Configuration error. - #[error(transparent)] - ConfigError(#[from] ConfigError), - /// Method not supported error. - #[error("Method not supported: {0}")] - MethodNotSupported(String), - /// Execution Error - #[error("Kakarot Execution Error, Reverted With: {0}")] - EVMExecutionError(String), - /// Other error. - #[error(transparent)] - Other(#[from] anyhow::Error), -} - -impl From for EthApiError { - fn from(err: ConversionError) -> Self { - Self::ConversionError(err.to_string()) - } -} - -impl From for EthApiError { - fn from(err: FromByteSliceError) -> Self { - Self::ConversionError(format!("Failed to convert from byte slice: {}", err)) - } -} - -impl From> for EthApiError { - fn from(err: FromUintError) -> Self { - Self::ConversionError(err.to_string()) - } -} - -impl From for ErrorObject<'static> { - fn from(error: EthApiError) -> Self { - match error { - EthApiError::RequestError(err_provider) => match err_provider { - ProviderError::StarknetError(err_with_msg) => match err_with_msg.code { - MaybeUnknownErrorCode::Known(err) => match err { - StarknetError::BlockNotFound - | StarknetError::ClassHashNotFound - | StarknetError::ContractNotFound - | StarknetError::NoBlocks - | StarknetError::TransactionHashNotFound - | StarknetError::InvalidBlockHash - | StarknetError::InvalidTransactionHash - | StarknetError::NoTraceAvailable => { - rpc_err(EthRpcErrorCode::ResourceNotFound, format!("{err}: {}", err_with_msg.message)) - } - StarknetError::ContractError => { - rpc_err(EthRpcErrorCode::ExecutionError, format!("{err}: {}", err_with_msg.message)) - } - StarknetError::InvalidTransactionNonce - | StarknetError::InvalidContinuationToken - | StarknetError::InvalidTransactionIndex - | StarknetError::PageSizeTooBig - | StarknetError::TooManyKeysInFilter - | StarknetError::InsufficientAccountBalance - | StarknetError::InsufficientMaxFee - | StarknetError::ClassAlreadyDeclared - | StarknetError::UnsupportedTxVersion - | StarknetError::CompilationFailed => { - rpc_err(EthRpcErrorCode::InvalidInput, format!("{err}: {}", err_with_msg.message)) - } - StarknetError::FailedToReceiveTransaction - | StarknetError::DuplicateTx - | StarknetError::NonAccount - | StarknetError::ValidationFailure - | StarknetError::UnsupportedContractClassVersion - | StarknetError::ContractClassSizeIsTooLarge - | StarknetError::CompiledClassHashMismatch - | StarknetError::UnexpectedError => { - rpc_err(EthRpcErrorCode::TransactionRejected, format!("{err}: {}", err_with_msg.message)) - } - }, - MaybeUnknownErrorCode::Unknown(code) => { - rpc_err(EthRpcErrorCode::Unknown, format!("got code {} with: {}", code, err_with_msg.message)) - } - }, - ProviderError::ArrayLengthMismatch => rpc_err(EthRpcErrorCode::InvalidParams, err_provider.to_string()), - ProviderError::RateLimited => rpc_err(EthRpcErrorCode::RequestLimitExceeded, err_provider.to_string()), - ProviderError::Other(_) => rpc_err(EthRpcErrorCode::InternalError, err_provider.to_string()), - }, - EthApiError::AbigenError(err) => rpc_err(EthRpcErrorCode::InternalError, err.to_string()), - EthApiError::ConversionError(err) => rpc_err(EthRpcErrorCode::InternalError, err), - EthApiError::DataDecodingError(err) => rpc_err(EthRpcErrorCode::InternalError, err.to_string()), - EthApiError::KakarotDataFilteringError(err) => rpc_err(EthRpcErrorCode::InternalError, err), - EthApiError::FeederGatewayError(err) => rpc_err(EthRpcErrorCode::InternalError, err), - EthApiError::MissingParameterError(err) => rpc_err(EthRpcErrorCode::InvalidParams, err), - EthApiError::ConfigError(err) => rpc_err(EthRpcErrorCode::InternalError, err.to_string()), - EthApiError::MethodNotSupported(err) => rpc_err(EthRpcErrorCode::MethodNotSupported, err), - EthApiError::EVMExecutionError(err) => rpc_err(EthRpcErrorCode::ExecutionError, err), - EthApiError::Other(err) => rpc_err(EthRpcErrorCode::InternalError, err.to_string()), - } - } -} - -impl From for jsonrpsee::core::Error { - fn from(err: EthApiError) -> Self { - Self::Call(err.into()) - } -} - -/// Constructs a JSON-RPC error object, consisting of `code` and `message`. -pub fn rpc_err(code: EthRpcErrorCode, msg: impl Into) -> jsonrpsee::types::error::ErrorObject<'static> { - jsonrpsee::types::error::ErrorObject::owned(code as i32, msg.into(), None::<()>) -} diff --git a/src/starknet_client/helpers.rs b/src/starknet_client/helpers.rs deleted file mode 100644 index 303af4278..000000000 --- a/src/starknet_client/helpers.rs +++ /dev/null @@ -1,77 +0,0 @@ -use reth_primitives::{U128, U256}; -use reth_rlp::DecodeError; -use starknet::core::types::FieldElement; -use thiserror::Error; - -use crate::starknet_client::constants::selectors::ETH_SEND_TRANSACTION; - -#[derive(Debug, Error)] -pub enum DataDecodingError { - #[error("failed to decode signature {0}")] - SignatureDecodingError(String), - #[error("failed to decode calldata {0}")] - CalldataDecodingError(String), - #[error("failed to decode transaction")] - TransactionDecodingError(#[from] DecodeError), - #[error("{entrypoint} returned invalid array length, expected {expected}, got {actual}")] - InvalidReturnArrayLength { entrypoint: String, expected: usize, actual: usize }, -} - -/// Constructs the calldata for a raw Starknet invoke transaction call -pub fn prepare_kakarot_eth_send_transaction( - kakarot_address: FieldElement, - mut calldata: Vec, -) -> Vec { - let mut execute_calldata: Vec = vec![ - FieldElement::ONE, // call array length - kakarot_address, // contract address - ETH_SEND_TRANSACTION, // selector - FieldElement::ZERO, // data offset - FieldElement::from(calldata.len()), // data length - FieldElement::from(calldata.len()), // calldata length - ]; - execute_calldata.append(&mut calldata); - - execute_calldata -} - -/// Helper function to split a U256 value into two generic values -/// implementing the From trait -pub fn split_u256>(value: U256) -> [T; 2] { - let low: u128 = (value & U256::from(U128::MAX)).try_into().unwrap(); // safe to unwrap - let high: U256 = value >> 128; - let high: u128 = high.try_into().unwrap(); // safe to unwrap - [T::from(low), T::from(high)] -} - -pub fn try_from_u8_iterator, T: FromIterator>(it: impl Iterator) -> T { - it.filter_map(|x| TryInto::::try_into(x).ok()).collect() -} - -#[cfg(test)] -mod tests { - - use rstest::*; - - use super::*; - - #[rstest] - #[test] - #[case("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")] - #[case("0x0000000000000000000000000000000000000000000000000000000000000000")] - #[case("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff")] - fn test_split_u256(#[case] input: U256) { - // When - let result = split_u256::(input); - - // Then - // Recalculate the U256 values using the resulting FieldElements - // The first is the low 128 bits of the U256 value - // The second is the high 128 bits of the U256 value and is left shifted by 128 bits - let result: U256 = - U256::from_be_bytes(result[1].to_bytes_be()) << 128 | U256::from_be_bytes(result[0].to_bytes_be()); - - // Assert that the input and recombined U256 values are equal - assert_eq!(input, result); - } -} diff --git a/src/starknet_client/mod.rs b/src/starknet_client/mod.rs deleted file mode 100644 index f17c2f7a8..000000000 --- a/src/starknet_client/mod.rs +++ /dev/null @@ -1,677 +0,0 @@ -pub mod config; -pub mod constants; -pub mod errors; -pub mod helpers; - -use crate::starknet_client::Uint256 as CairoUint256; -use crate::{into_via_try_wrapper, into_via_wrapper, try_into_via_wrapper}; -use eyre::Result; -use futures::future::join_all; -use reqwest::Client; -use reth_primitives::{ - AccessList, Address, BlockId, BlockNumberOrTag, Bytes, Transaction, TransactionKind, TxEip1559, H256, U128, U256, - U64, -}; -use reth_rpc_types::{BlockTransactions, CallRequest, FeeHistory, Index, RichBlock, Transaction as EtherTransaction}; -use starknet::core::types::{ - BlockId as StarknetBlockId, BlockTag, BroadcastedInvokeTransaction, EmittedEvent, EventFilterWithPage, EventsPage, - FieldElement, MaybePendingBlockWithTxHashes, MaybePendingBlockWithTxs, MaybePendingTransactionReceipt, - StarknetError, Transaction as TransactionType, TransactionReceipt as StarknetTransactionReceipt, -}; -use starknet::core::utils::{get_contract_address, get_storage_var_address}; -use starknet::providers::sequencer::models::{FeeEstimate, FeeUnit, TransactionSimulationInfo, TransactionTrace}; -use starknet::providers::{MaybeUnknownErrorCode, Provider, ProviderError, StarknetErrorWithMessage}; -use starknet_abigen_parser::cairo_types::CairoArrayLegacy; -use std::sync::Arc; -use tracing::debug; - -use self::config::{KakarotRpcConfig, Network}; -use self::constants::gas::{BASE_FEE_PER_GAS, MAX_PRIORITY_FEE_PER_GAS, MINIMUM_GAS_FEE}; -use self::constants::{ - CHAIN_ID, COUNTER_CALL_MAINNET, COUNTER_CALL_TESTNET1, COUNTER_CALL_TESTNET2, - DUMMY_ARGENT_GAS_PRICE_ACCOUNT_ADDRESS, ESTIMATE_GAS, MAX_FEE, STARKNET_NATIVE_TOKEN, -}; -use self::errors::EthApiError; -use self::helpers::{prepare_kakarot_eth_send_transaction, split_u256, try_from_u8_iterator}; -use crate::contracts::erc20::EthereumErc20; -use crate::contracts::kakarot_contract::KakarotContract; -use crate::contracts::kakarot_contract::Uint256 as KakarotUint256; -use crate::models::balance::{FutureTokenBalance, TokenBalances}; -use crate::models::block::{BlockWithTxHashes, BlockWithTxs, EthBlockId}; -use crate::models::errors::ConversionError; -use crate::models::felt::Felt252Wrapper; -use crate::models::transaction::transaction::StarknetTransaction; -use crate::models::transaction::transaction_signed::StarknetTransactionSigned; - -use starknet_abigen_macros::abigen_legacy; -use starknet_abigen_parser; - -use crate::contracts::kakarot_contract::KakarotCoreReader; - -abigen_legacy!(ContractAccount, "./artifacts/contract_account.json"); -abigen_legacy!(Proxy, "./artifacts/proxy.json"); - -mod erc20 { - use starknet::core::types::FieldElement; - use starknet_abigen_macros::abigen_legacy; - use starknet_abigen_parser; - - abigen_legacy!(ERC20, "./artifacts/fixtures/ERC20.json"); -} - -pub struct KakarotClient { - starknet_provider: Arc

, - kakarot_contract: KakarotContract

, - network: Network, -} - -impl KakarotClient

{ - /// Create a new `KakarotClient`. - pub fn new(starknet_config: KakarotRpcConfig, starknet_provider: Arc

) -> Self { - let KakarotRpcConfig { - kakarot_address, - proxy_account_class_hash, - externally_owned_account_class_hash, - contract_account_class_hash, - network, - } = starknet_config; - - let provider = starknet_provider.clone(); - let contract_reader = KakarotCoreReader::new(kakarot_address, provider); - - let kakarot_contract = KakarotContract::new( - proxy_account_class_hash, - externally_owned_account_class_hash, - contract_account_class_hash, - contract_reader, - ); - - Self { starknet_provider, network, kakarot_contract } - } - - /// Returns the result of executing a call on a ethereum address for a given calldata and block - /// without creating a transaction. - #[tracing::instrument(skip_all, level = "debug")] - pub async fn call(&self, request: CallRequest, block_id: BlockId) -> Result { - let starknet_block_id: StarknetBlockId = EthBlockId::new(block_id).try_into()?; - - // unwrap option or return jsonrpc error - let to = request - .to - .ok_or_else(|| EthApiError::MissingParameterError("Missing `to` field in CallRequest".to_string()))?; - let to = into_via_wrapper!(to); - - // Here we check if CallRequest.origin is None, if so, we insert origin = address(0) - let origin = into_via_wrapper!(request.from.unwrap_or_default()); - - let calldata = request - .input - .data - .ok_or_else(|| EthApiError::MissingParameterError("Missing `data` field in CallRequest".to_string()))?; - let calldata: Vec<_> = calldata.to_vec().into_iter().map(FieldElement::from).collect(); - - let gas_limit = into_via_try_wrapper!(request.gas.unwrap_or_default()); - - let gas_price = into_via_try_wrapper!(request.gas_price.unwrap_or_default()); - - let value = { - let value = request.value.unwrap_or_default(); - split_u256::(value) - }; - - debug!("origin: {:?}", origin); - debug!("to: {:?}", to); - debug!("gas_limit: {:?}", gas_limit); - debug!("gas_price: {:?}", gas_price); - debug!("value: {:?}", value); - debug!("calldata: {:?}", calldata); - - let (_, return_data, success, _gas_used) = self - .kakarot_contract - .reader - .eth_call( - &origin, - &to, - &gas_limit, - &gas_price, - &KakarotUint256 { low: value[0], high: value[1] }, - &calldata.len().into(), - &CairoArrayLegacy(calldata), - ) - .block_id(starknet_block_id) - .call() - .await?; - - if success == FieldElement::ZERO { - let revert_reason = - return_data.0.into_iter().filter_map(|x| u8::try_from(x).ok()).map(|x| x as char).collect::(); - return Err(EthApiError::EVMExecutionError(revert_reason)); - } - - Ok(Bytes::from(try_from_u8_iterator::<_, Vec<_>>(return_data.0.into_iter()))) - } - - /// Returns the number of transactions in a block given a block id. - pub async fn get_transaction_count_by_block(&self, block_id: BlockId) -> Result { - let starknet_block_id: StarknetBlockId = EthBlockId::new(block_id).try_into()?; - let starknet_block = self.starknet_provider.get_block_with_txs(starknet_block_id).await?; - - let block_transactions = match starknet_block { - MaybePendingBlockWithTxs::PendingBlock(pending_block_with_txs) => { - self.filter_starknet_into_eth_txs(pending_block_with_txs.transactions, None, None).await - } - MaybePendingBlockWithTxs::Block(block_with_txs) => { - let block_hash = Some(into_via_wrapper!(block_with_txs.block_hash)); - let block_number = Some(into_via_wrapper!(block_with_txs.block_number)); - self.filter_starknet_into_eth_txs(block_with_txs.transactions, block_hash, block_number).await - } - }; - let len = match block_transactions { - BlockTransactions::Full(transactions) => transactions.len(), - BlockTransactions::Hashes(_) => 0, - BlockTransactions::Uncle => 0, - }; - Ok(U64::from(len)) - } - - /// Returns the transaction for a given block id and transaction index. - #[tracing::instrument(skip_all, level = "debug")] - pub async fn transaction_by_block_id_and_index( - &self, - block_id: BlockId, - tx_index: Index, - ) -> Result { - let index: u64 = usize::from(tx_index) as u64; - let starknet_block_id: StarknetBlockId = EthBlockId::new(block_id).try_into()?; - - let starknet_tx = - self.starknet_provider.get_transaction_by_block_id_and_index(starknet_block_id, index).await?; - - let tx_hash = starknet_tx.transaction_hash(); - - let tx_receipt = self.starknet_provider.get_transaction_receipt(tx_hash).await?; - let (block_hash, block_num) = match tx_receipt { - MaybePendingTransactionReceipt::Receipt(StarknetTransactionReceipt::Invoke(tr)) => { - (Some(into_via_wrapper!(tr.block_hash)), Some(U256::from(tr.block_number))) - } - _ => (None, None), // skip all transactions other than Invoke, covers the pending case - }; - - debug!("starknet transaction: {:?}", starknet_tx); - let eth_tx = StarknetTransaction::from(starknet_tx) - .to_eth_transaction(self, block_hash, block_num, Some(U256::from(index))) - .await?; - debug!("ethereum transaction: {:?}", eth_tx); - - Ok(eth_tx) - } - - /// Returns the nonce for a given ethereum address - /// if it's an Eoa, use native nonce and if it's a contract account, use managed nonce - /// if ethereum -> stark mapping doesn't exist in the starknet provider, we translate - /// ContractNotFound errors into zeros - #[tracing::instrument(skip_all, level = "debug")] - pub async fn nonce(&self, ethereum_address: Address, block_id: BlockId) -> Result { - let starknet_block_id: StarknetBlockId = EthBlockId::new(block_id).try_into()?; - let starknet_address = self.compute_starknet_address(ðereum_address).await?; - - let proxy = ProxyReader::new(starknet_address, &self.starknet_provider); - let class_hash = proxy.get_implementation().call().await.map_or(FieldElement::ZERO, |class_hash| class_hash); - - if class_hash == self.kakarot_contract.contract_account_class_hash { - debug!("contract account"); - // Get the nonce of the contract account -> a storage variable - let contract_account = ContractAccountReader::new(starknet_address, &self.starknet_provider); - Ok(into_via_wrapper!(contract_account.get_nonce().block_id(starknet_block_id).call().await?)) - } else { - debug!("eoa"); - // Get the nonce of the Eoa -> the protocol level nonce - let nonce = self.starknet_provider.get_nonce(starknet_block_id, starknet_address).await; - let nonce = match nonce { - Ok(nonce) => nonce, - // In EVM, if the contract does not exist, its nonce is zero - Err(ProviderError::StarknetError(StarknetErrorWithMessage { - code: MaybeUnknownErrorCode::Known(StarknetError::ContractNotFound), - .. - })) => { - debug!("StarknetError::ContractNotFound"); - FieldElement::ZERO - } - Err(err) => return Err(EthApiError::RequestError(err)), - }; - Ok(into_via_wrapper!(nonce)) - } - } - - /// Returns the balance in Starknet's native token of a specific EVM address. - #[tracing::instrument(skip_all, level = "debug")] - pub async fn balance(&self, ethereum_address: Address, block_id: BlockId) -> Result { - let starknet_block_id: StarknetBlockId = EthBlockId::new(block_id).try_into()?; - let starknet_address = self.compute_starknet_address(ðereum_address).await?; - - let native_token_address = FieldElement::from_hex_be(STARKNET_NATIVE_TOKEN).unwrap(); - let provider = self.starknet_provider(); - let native_token = erc20::ERC20Reader::new(native_token_address, &provider); - let balance = native_token.balanceOf(&starknet_address).block_id(starknet_block_id).call().await?; - - debug!("starknet address {} has balance: {:?}", starknet_address, balance); - - let low: U256 = into_via_wrapper!(balance.low); - let high: U256 = into_via_wrapper!(balance.high); - Ok(low + (high << 128)) - } - - /// Returns the storage value at a specific index of a contract given its address and a block - /// id. - #[tracing::instrument(skip_all, level = "debug")] - pub async fn storage_at(&self, address: Address, index: U256, block_id: BlockId) -> Result { - let starknet_block_id: StarknetBlockId = EthBlockId::new(block_id).try_into()?; - - let starknet_contract_address = self.compute_starknet_address(&address).await?; - - let provider = self.starknet_provider(); - let contract_account = ContractAccountReader::new(starknet_contract_address, &provider); - - // Convert a Uint256 to a Starknet storage key - let keys = split_u256::(index); - let storage_address = - get_storage_var_address("storage_", &keys).map_err(|e| EthApiError::ConversionError(e.to_string()))?; - - let storage: CairoUint256 = - contract_account.storage(&storage_address).block_id(starknet_block_id).call().await?; - - debug!( - "storage at starknet address {} key slot {} is {:?}", - starknet_contract_address, storage_address, storage - ); - - let low: U256 = into_via_wrapper!(storage.low); - let high: U256 = into_via_wrapper!(storage.high); - Ok(low + (high << 128)) - } - - /// Returns token balances for a specific address given a list of contracts addresses. - pub async fn token_balances( - &self, - address: Address, - token_addresses: Vec

, - ) -> Result { - let block_id = BlockId::Number(BlockNumberOrTag::Latest); - - let handles = token_addresses.into_iter().map(|token_address| { - let token_addr: Felt252Wrapper = token_address.into(); - let token = EthereumErc20::new(token_addr.into(), self.starknet_provider(), self.kakarot_address()); - let balance = token.balance_of(address.into(), block_id); - - FutureTokenBalance::new(Box::pin(balance), token_address) - }); - - let token_balances = join_all(handles).await; - - Ok(TokenBalances { address, token_balances }) - } - - /// Sends raw Ethereum transaction bytes to Kakarot - pub async fn send_transaction(&self, bytes: Bytes) -> Result { - let transaction: StarknetTransactionSigned = bytes.into(); - - let invoke_transaction = transaction.to_broadcasted_invoke_transaction(self).await?; - - let transaction_result = self.starknet_provider.add_invoke_transaction(&invoke_transaction).await?; - - Ok(H256::from_slice(&transaction_result.transaction_hash.to_bytes_be())) - } - - /// Returns the fixed base_fee_per_gas of Kakarot - /// Since Starknet works on a FCFS basis (FIFO queue), it is not possible to tip miners to - /// incentivize faster transaction inclusion - /// As a result, in Kakarot, gas_price := base_fee_per_gas - pub fn base_fee_per_gas(&self) -> U256 { - U256::from(BASE_FEE_PER_GAS) - } - - /// Returns the max_priority_fee_per_gas of Kakarot - pub const fn max_priority_fee_per_gas(&self) -> U128 { - MAX_PRIORITY_FEE_PER_GAS - } - - /// Returns the fee history of Kakarot ending at the newest block and going back `block_count` - pub async fn fee_history( - &self, - block_count: U256, - newest_block: BlockNumberOrTag, - _reward_percentiles: Option>, - ) -> Result { - if block_count == U256::ZERO { - return Ok(FeeHistory::default()); - } - - let block_count_usize = - usize::try_from(block_count).map_err(|e| ConversionError::ValueOutOfRange(e.to_string()))?; - - let base_fee = self.base_fee_per_gas(); - - let base_fee_per_gas: Vec = vec![base_fee; block_count_usize]; - let newest_block = match newest_block { - BlockNumberOrTag::Number(n) => n, - // TODO: Add Genesis block number - BlockNumberOrTag::Earliest => 1_u64, - // TODO: Add block hash lookup - _ => self.starknet_provider().block_number().await?, - }; - - let gas_used_ratio: Vec = vec![1.0; block_count_usize]; - let newest_block = U256::from(newest_block); - let oldest_block: U256 = if newest_block + U256::from(1) >= block_count { - newest_block + U256::from(1) - block_count - } else { - U256::ZERO - }; - - Ok(FeeHistory { base_fee_per_gas, gas_used_ratio, oldest_block, reward: Some(vec![]) }) - } - - /// Returns the estimated gas for a transaction - #[tracing::instrument(skip_all, level = "debug")] - pub async fn estimate_gas(&self, request: CallRequest, block_id: BlockId) -> Result { - match self.network { - Network::MainnetGateway | Network::Goerli1Gateway | Network::Goerli2Gateway => (), - _ => { - return Ok(U256::from(*ESTIMATE_GAS)); - } - }; - - let chain_id = request.chain_id.unwrap_or_else(|| CHAIN_ID.into()); - - let from = request.from.ok_or_else(|| EthApiError::MissingParameterError("from for estimate_gas".into()))?; - let nonce = self.nonce(from, block_id).await?.try_into().map_err(ConversionError::from)?; - - let gas_limit = request.gas.unwrap_or_default().try_into().map_err(ConversionError::from)?; - let max_fee_per_gas = request - .max_fee_per_gas - .unwrap_or_else(|| U256::from(BASE_FEE_PER_GAS)) - .try_into() - .map_err(ConversionError::from)?; - let max_priority_fee_per_gas = request - .max_priority_fee_per_gas - .unwrap_or_else(|| U256::from(self.max_priority_fee_per_gas())) - .try_into() - .map_err(ConversionError::from)?; - - let to = request.to.map_or(TransactionKind::Create, TransactionKind::Call); - - let value = request.value.unwrap_or_default().try_into().map_err(ConversionError::from)?; - - let data = request.input.data.unwrap_or_default(); - - let tx = Transaction::Eip1559(TxEip1559 { - chain_id: chain_id.low_u64(), - nonce, - gas_limit, - max_fee_per_gas, - max_priority_fee_per_gas, - to, - value, - access_list: AccessList(vec![]), - input: data, - }); - - debug!("ethereum transaction: {:?}", tx); - - let starknet_block_id: StarknetBlockId = EthBlockId::new(block_id).try_into()?; - let block_number = self.map_block_id_to_block_number(&starknet_block_id).await?; - - let sender_address = self.compute_starknet_address(&from).await?; - - let mut data = vec![]; - tx.encode_without_signature(&mut data); - let data = data.into_iter().map(FieldElement::from).collect(); - let calldata = prepare_kakarot_eth_send_transaction(self.kakarot_address(), data); - - debug!("starknet calldata: {:?}", calldata); - - let tx = BroadcastedInvokeTransaction { - max_fee: FieldElement::ZERO, - signature: vec![], - sender_address, - nonce: nonce.into(), - calldata, - is_query: false, - }; - - let fee_estimate = self.simulate_transaction(tx, block_number, true).await?.fee_estimation; - if fee_estimate.gas_usage < MINIMUM_GAS_FEE { - return Ok(U256::from(MINIMUM_GAS_FEE)); - } - Ok(U256::from(fee_estimate.gas_usage)) - } - - /// Returns the gas price on the network - pub async fn gas_price(&self) -> Result { - let call = match self.network { - Network::MainnetGateway => COUNTER_CALL_MAINNET.clone(), - Network::Goerli1Gateway => COUNTER_CALL_TESTNET1.clone(), - Network::Goerli2Gateway => COUNTER_CALL_TESTNET2.clone(), - _ => return Ok(self.base_fee_per_gas()), - }; - - let raw_calldata: Vec = call.into(); - - let block_id = StarknetBlockId::Tag(BlockTag::Latest); - let nonce = self.starknet_provider.get_nonce(block_id, *DUMMY_ARGENT_GAS_PRICE_ACCOUNT_ADDRESS).await?; - - let tx = BroadcastedInvokeTransaction { - max_fee: FieldElement::ZERO, - signature: vec![], - sender_address: *DUMMY_ARGENT_GAS_PRICE_ACCOUNT_ADDRESS, - nonce, - calldata: raw_calldata, - is_query: true, - }; - - let block_number = self.starknet_provider().block_number().await?; - let fee_estimate = self.simulate_transaction(tx, block_number, true).await?.fee_estimation; - - Ok(U256::from(fee_estimate.gas_price)) - } - /// Returns the Kakarot contract address. - pub const fn kakarot_address(&self) -> FieldElement { - self.kakarot_contract.reader.address - } - - /// Returns the Kakarot externally owned account class hash. - pub const fn externally_owned_account_class_hash(&self) -> FieldElement { - self.kakarot_contract.externally_owned_account_class_hash - } - - /// Returns the Kakarot contract account class hash. - pub const fn contract_account_class_hash(&self) -> FieldElement { - self.kakarot_contract.contract_account_class_hash - } - - /// Returns the Kakarot proxy account class hash. - pub const fn proxy_account_class_hash(&self) -> FieldElement { - self.kakarot_contract.proxy_account_class_hash - } - - /// Returns a reference to the Starknet provider. - pub fn starknet_provider(&self) -> Arc

{ - Arc::clone(&self.starknet_provider) - } - - /// Returns the Starknet block number for a given block id. - pub async fn map_block_id_to_block_number(&self, block_id: &StarknetBlockId) -> Result { - match block_id { - StarknetBlockId::Number(n) => Ok(*n), - StarknetBlockId::Tag(_) => Ok(self.starknet_provider().block_number().await?), - StarknetBlockId::Hash(_) => { - let block = self.starknet_provider.get_block_with_tx_hashes(block_id).await?; - match block { - MaybePendingBlockWithTxHashes::Block(block_with_tx_hashes) => Ok(block_with_tx_hashes.block_number), - _ => Err(ProviderError::StarknetError(StarknetErrorWithMessage { - code: MaybeUnknownErrorCode::Known(StarknetError::BlockNotFound), - message: "".to_string(), - }) - .into()), - } - } - } - } - - /// Returns the EVM address associated with a given Starknet address for a given block id - /// by calling the `get_evm_address` function on the Kakarot contract. - pub async fn get_evm_address(&self, starknet_address: &FieldElement) -> Result { - let contract_account = ContractAccountReader::new(*starknet_address, &self.starknet_provider); - let evm_address = contract_account.get_evm_address().call().await?; - Ok(try_into_via_wrapper!(evm_address)) - } - - /// Returns the EVM address associated with a given Starknet address for a given block id - /// by calling the `compute_starknet_address` function on the Kakarot contract. - #[tracing::instrument(skip_all, level = "debug")] - pub async fn compute_starknet_address(&self, ethereum_address: &Address) -> Result { - let ethereum_address = into_via_wrapper!(*ethereum_address); - debug!("ethereum address: {:?}", ethereum_address); - let starknet_address = - get_contract_address(ethereum_address, self.proxy_account_class_hash(), &[], self.kakarot_address()); - debug!("starknet address: {:?}", starknet_address); - Ok(starknet_address) - } - - /// Returns the Ethereum transactions executed by the Kakarot contract by filtering the provided - /// Starknet transaction. - #[tracing::instrument(skip_all, level = "debug")] - pub async fn filter_starknet_into_eth_txs( - &self, - transactions: Vec, - block_hash: Option, - block_number: Option, - ) -> BlockTransactions { - debug!("starknet transactions: {:?}", transactions); - let handles = transactions.into_iter().map(|tx| async move { - let tx = Into::::into(tx); - tx.to_eth_transaction(self, block_hash, block_number, None).await - }); - let transactions = join_all(handles).await.into_iter().filter_map(|transaction| transaction.ok()).collect(); - debug!("ethereum transactions: {:?}", transactions); - BlockTransactions::Full(transactions) - } - - /// Get the Kakarot eth block provided a Starknet block id. - pub async fn get_eth_block_from_starknet_block( - &self, - block_id: StarknetBlockId, - hydrated_tx: bool, - ) -> Result { - if hydrated_tx { - let block = self.starknet_provider.get_block_with_txs(block_id).await?; - let starknet_block = BlockWithTxs::new(block); - Ok(starknet_block.to_eth_block(self).await) - } else { - let block = self.starknet_provider.get_block_with_tx_hashes(block_id).await?; - let starknet_block = BlockWithTxHashes::new(block); - Ok(starknet_block.to_eth_block(self).await) - } - } - - /// Get the simulation of the BroadcastedInvokeTransactionV1 result - /// FIXME 306: make simulate_transaction agnostic of the provider (rn only works for - /// a SequencerGatewayProvider on testnets and mainnet) - #[tracing::instrument(skip_all, level = "debug")] - pub async fn simulate_transaction( - &self, - request: BroadcastedInvokeTransaction, - block_number: u64, - skip_validate: bool, - ) -> Result { - let client = Client::new(); - - // build the url for simulate transaction - let url = self.network.gateway_url(); - - // if the url is invalid, return an empty simulation (allows to call simulate_transaction on Kakana, - // Madara, etc.) - if url.is_err() { - debug!("invalid url, returning empty simulation"); - let gas_usage = *ESTIMATE_GAS; - let gas_price = *MAX_FEE; - let overall_fee = (gas_usage * gas_price) as u128; - return Ok(TransactionSimulationInfo { - trace: TransactionTrace { - function_invocation: None, - fee_transfer_invocation: None, - validate_invocation: None, - signature: vec![], - revert_error: None, - }, - fee_estimation: FeeEstimate { - gas_usage, - gas_price, - overall_fee: overall_fee as u64, - unit: FeeUnit::Wei, - }, - }); - } - - let mut url = url - .unwrap() // safe unwrap because we checked for error above - .join("simulate_transaction") - .map_err(|e| EthApiError::FeederGatewayError(format!("gateway url parsing error: {:?}", e)))?; - - // add the block number and skipValidate query params - url.query_pairs_mut() - .append_pair("blockNumber", &block_number.to_string()) - .append_pair("skipValidate", &skip_validate.to_string()); - - // serialize the request - let mut request = serde_json::to_value(request) - .map_err(|e| EthApiError::FeederGatewayError(format!("request serializing error: {:?}", e)))?; - // BroadcastedInvokeTransactionV1 gets serialized with type="INVOKE" but the simulate endpoint takes - // type="INVOKE_FUNCTION" - request["type"] = "INVOKE_FUNCTION".into(); - debug!("request: {:?}", request); - - // post to the gateway - let response = client - .post(url) - .json(&request) - .send() - .await - .map_err(|e| EthApiError::FeederGatewayError(format!("gateway post error: {:?}", e)))?; - - // decode the response to a `TransactionSimulationInfo` - let resp: TransactionSimulationInfo = response - .error_for_status() - .map_err(|e| EthApiError::FeederGatewayError(format!("http error: {:?}", e)))? - .json() - .await - .map_err(|e| { - EthApiError::FeederGatewayError(format!( - "error while decoding response body to TransactionSimulationInfo: {:?}", - e - )) - })?; - - Ok(resp) - } - - pub async fn filter_events(&self, filter: EventFilterWithPage) -> Result, EthApiError> { - let chunk_size = filter.result_page_request.chunk_size; - let continuation_token = filter.result_page_request.continuation_token; - let filter = filter.event_filter; - - let mut result = EventsPage { events: Vec::new(), continuation_token }; - let mut events = vec![]; - - loop { - result = self.starknet_provider.get_events(filter.clone(), result.continuation_token, chunk_size).await?; - events.append(&mut result.events); - - if result.continuation_token.is_none() { - break; - } - } - - Ok(events) - } -} diff --git a/tests/test_utils/constants.rs b/src/test_utils/constants.rs similarity index 100% rename from tests/test_utils/constants.rs rename to src/test_utils/constants.rs diff --git a/tests/test_utils/eoa.rs b/src/test_utils/eoa.rs similarity index 72% rename from tests/test_utils/eoa.rs rename to src/test_utils/eoa.rs index eed2ac778..922dcad9a 100644 --- a/tests/test_utils/eoa.rs +++ b/src/test_utils/eoa.rs @@ -1,20 +1,19 @@ +use std::sync::Arc; + use async_trait::async_trait; -use bytes::BytesMut; use ethers::abi::Tokenize; use ethers::signers::{LocalWallet, Signer}; -use kakarot_rpc::models::felt::Felt252Wrapper; -use kakarot_rpc::starknet_client::constants::CHAIN_ID; -use kakarot_rpc::starknet_client::KakarotClient; use reth_primitives::{ - sign_message, Address, BlockId, BlockNumberOrTag, Bytes, Transaction, TransactionKind, TransactionSigned, - TxEip1559, H256, U256, + sign_message, Address, Bytes, Transaction, TransactionKind, TransactionSigned, TxEip1559, H256, U256, }; use starknet::core::types::{MaybePendingTransactionReceipt, TransactionReceipt}; use starknet::core::utils::get_selector_from_name; use starknet::providers::Provider; use starknet_crypto::FieldElement; -use std::sync::Arc; +use crate::eth_provider::provider::{EthDataProvider, EthereumProvider}; +use crate::eth_provider::starknet::kakarot_core::starknet_address; +use crate::models::felt::Felt252Wrapper; use crate::test_utils::evm_contract::EvmContract; use crate::test_utils::evm_contract::KakarotEvmContract; use crate::test_utils::tx_waiter::watch_tx; @@ -22,23 +21,21 @@ use crate::test_utils::tx_waiter::watch_tx; /// EOA is an Ethereum-like Externally Owned Account (EOA) that can sign transactions and send them to the underlying Starknet provider. #[async_trait] pub trait Eoa { - async fn starknet_address(&self) -> Result { - let client: &KakarotClient

= self.client(); - Ok(client.compute_starknet_address(&self.evm_address()?).await?) + fn starknet_address(&self) -> Result { + Ok(starknet_address(self.evm_address()?)) } fn evm_address(&self) -> Result { let wallet = LocalWallet::from_bytes(self.private_key().as_bytes())?; Ok(Address::from_slice(wallet.address().as_bytes())) } fn private_key(&self) -> H256; - fn provider(&self) -> Arc

; - fn client(&self) -> &KakarotClient

; + fn eth_provider(&self) -> &EthDataProvider

; async fn nonce(&self) -> Result { - let client = self.client(); + let eth_provider = self.eth_provider(); let evm_address = self.evm_address()?; - Ok(client.nonce(evm_address, BlockId::Number(BlockNumberOrTag::Latest)).await?) + Ok(eth_provider.transaction_count(evm_address, None).await?) } fn sign_transaction(&self, tx: Transaction) -> Result { @@ -48,21 +45,21 @@ pub trait Eoa { } async fn send_transaction(&self, tx: TransactionSigned) -> Result { - let client = self.client(); - let mut buffer = BytesMut::new(); - tx.encode_enveloped(&mut buffer); - Ok(client.send_transaction(buffer.to_vec().into()).await?) + let eth_provider = self.eth_provider(); + let mut v = Vec::new(); + tx.encode_enveloped(&mut v); + Ok(eth_provider.send_raw_transaction(v.into()).await?) } } -pub struct KakarotEOA { +pub struct KakarotEOA { pub private_key: H256, - pub client: KakarotClient

, + pub eth_provider: Arc>, } -impl KakarotEOA

{ - pub const fn new(private_key: H256, client: KakarotClient

) -> Self { - Self { private_key, client } +impl KakarotEOA

{ + pub const fn new(private_key: H256, eth_provider: Arc>) -> Self { + Self { private_key, eth_provider } } } @@ -71,15 +68,16 @@ impl Eoa

for KakarotEOA

{ fn private_key(&self) -> H256 { self.private_key } - fn provider(&self) -> Arc

{ - self.client.starknet_provider() - } - fn client(&self) -> &KakarotClient

{ - &self.client + fn eth_provider(&self) -> &EthDataProvider

{ + &self.eth_provider } } impl KakarotEOA

{ + fn starknet_provider(&self) -> &P { + self.eth_provider.starknet_provider() + } + pub async fn deploy_evm_contract( &self, contract_name: &str, @@ -87,20 +85,31 @@ impl KakarotEOA

{ ) -> Result { let nonce = self.nonce().await?; let nonce: u64 = nonce.try_into()?; + let chain_id = self.eth_provider.chain_id().await?.unwrap_or_default(); let bytecode = ::load_contract_bytecode(contract_name)?; - let tx = ::prepare_create_transaction(&bytecode, constructor_args, nonce)?; + let tx = ::prepare_create_transaction( + &bytecode, + constructor_args, + nonce, + chain_id.as_u64(), + )?; let tx_signed = self.sign_transaction(tx)?; let tx_hash = self.send_transaction(tx_signed).await?; let tx_hash: Felt252Wrapper = tx_hash.try_into().expect("Tx Hash should fit into Felt252Wrapper"); - watch_tx(self.provider(), tx_hash.clone().into(), std::time::Duration::from_millis(100)) - .await - .expect("Tx polling failed"); + watch_tx( + self.eth_provider.starknet_provider(), + tx_hash.clone().into(), + std::time::Duration::from_millis(100), + 60, + ) + .await + .expect("Tx polling failed"); let maybe_receipt = self - .provider() + .starknet_provider() .get_transaction_receipt(FieldElement::from(tx_hash)) .await .expect("Failed to get transaction receipt after retries"); @@ -137,15 +146,16 @@ impl KakarotEOA

{ ) -> Result { let nonce = self.nonce().await?; let nonce: u64 = nonce.try_into()?; + let chain_id = self.eth_provider.chain_id().await?.unwrap_or_default(); - let tx = contract.prepare_call_transaction(function, args, nonce, value)?; + let tx = contract.prepare_call_transaction(function, args, nonce, value, chain_id.as_u64())?; let tx_signed = self.sign_transaction(tx)?; let tx_hash = self.send_transaction(tx_signed).await?; let bytes = tx_hash.to_fixed_bytes(); let starknet_tx_hash = FieldElement::from_bytes_be(&bytes).unwrap(); - watch_tx(self.provider(), starknet_tx_hash, std::time::Duration::from_millis(100)) + watch_tx(self.eth_provider.starknet_provider(), starknet_tx_hash, std::time::Duration::from_millis(100), 60) .await .expect("Tx polling failed"); @@ -163,7 +173,7 @@ impl KakarotEOA

{ let nonce: u64 = nonce.try_into()?; let tx = Transaction::Eip1559(TxEip1559 { - chain_id: CHAIN_ID, + chain_id: self.eth_provider.chain_id().await?.unwrap_or_default().as_u64(), nonce, max_priority_fee_per_gas: Default::default(), max_fee_per_gas: Default::default(), diff --git a/tests/test_utils/evm_contract.rs b/src/test_utils/evm_contract.rs similarity index 83% rename from tests/test_utils/evm_contract.rs rename to src/test_utils/evm_contract.rs index 94018c78a..b94df9871 100644 --- a/tests/test_utils/evm_contract.rs +++ b/src/test_utils/evm_contract.rs @@ -4,11 +4,10 @@ use std::path::Path; use ethers::abi::Tokenize; use ethers_solc::artifacts::CompactContractBytecode; use foundry_config::{find_project_root_path, load_config}; -use kakarot_rpc::models::felt::Felt252Wrapper; -use kakarot_rpc::starknet_client::constants::CHAIN_ID; use reth_primitives::{Transaction, TransactionKind, TxEip1559}; use starknet_crypto::FieldElement; +use crate::models::felt::Felt252Wrapper; use crate::root_project_path; pub trait EvmContract { @@ -28,7 +27,34 @@ pub trait EvmContract { contract_bytecode: &CompactContractBytecode, constructor_args: T, nonce: u64, - ) -> Result; + chain_id: u64, + ) -> Result { + let abi = contract_bytecode.abi.as_ref().ok_or_else(|| eyre::eyre!("No ABI found"))?; + let bytecode = contract_bytecode + .bytecode + .as_ref() + .ok_or_else(|| eyre::eyre!("No bytecode found"))? + .object + .as_bytes() + .cloned() + .unwrap_or_default(); + let params = constructor_args.into_tokens(); + + let deploy_data = match abi.constructor() { + Some(constructor) => constructor.encode_input(bytecode.to_vec(), ¶ms)?, + None => bytecode.to_vec(), + }; + + Ok(Transaction::Eip1559(TxEip1559 { + chain_id, + nonce, + gas_limit: u64::MAX, + to: TransactionKind::Create, + value: 0, + input: deploy_data.into(), + ..Default::default() + })) + } fn prepare_call_transaction( &self, @@ -36,6 +62,7 @@ pub trait EvmContract { constructor_args: T, nonce: u64, value: u128, + chain_id: u64, ) -> Result; } @@ -57,46 +84,13 @@ impl KakarotEvmContract { } impl EvmContract for KakarotEvmContract { - fn prepare_create_transaction( - contract_bytecode: &CompactContractBytecode, - constructor_args: T, - nonce: u64, - ) -> Result { - let abi = contract_bytecode.abi.as_ref().ok_or_else(|| eyre::eyre!("No ABI found"))?; - let bytecode = contract_bytecode - .bytecode - .as_ref() - .ok_or_else(|| eyre::eyre!("No bytecode found"))? - .object - .as_bytes() - .cloned() - .unwrap_or_default(); - let params = constructor_args.into_tokens(); - - let deploy_data = match abi.constructor() { - Some(constructor) => constructor.encode_input(bytecode.to_vec(), ¶ms)?, - None => bytecode.to_vec(), - }; - - Ok(Transaction::Eip1559(TxEip1559 { - chain_id: CHAIN_ID, - nonce, - max_priority_fee_per_gas: Default::default(), - max_fee_per_gas: Default::default(), - gas_limit: u64::MAX, - to: TransactionKind::Create, - value: 0, - input: deploy_data.into(), - access_list: Default::default(), - })) - } - fn prepare_call_transaction( &self, selector: &str, args: T, nonce: u64, value: u128, + chain_id: u64, ) -> Result { let abi = self.bytecode.abi.as_ref().ok_or_else(|| eyre::eyre!("No ABI found"))?; let params = args.into_tokens(); @@ -104,17 +98,14 @@ impl EvmContract for KakarotEvmContract { let data = abi.function(selector).and_then(|function| function.encode_input(¶ms))?; let evm_address: Felt252Wrapper = self.evm_address.try_into()?; - Ok(Transaction::Eip1559(TxEip1559 { - chain_id: CHAIN_ID, + chain_id, nonce, - max_priority_fee_per_gas: Default::default(), - max_fee_per_gas: Default::default(), gas_limit: u64::MAX, to: TransactionKind::Call(evm_address.try_into()?), value, input: data.into(), - access_list: Default::default(), + ..Default::default() })) } } diff --git a/tests/test_utils/fixtures.rs b/src/test_utils/fixtures.rs similarity index 74% rename from tests/test_utils/fixtures.rs rename to src/test_utils/fixtures.rs index 6493bbbfa..43d75ed2b 100644 --- a/tests/test_utils/fixtures.rs +++ b/src/test_utils/fixtures.rs @@ -1,5 +1,6 @@ use ethers::abi::Token; use rstest::*; +use tracing_subscriber::{filter, FmtSubscriber}; use super::sequencer::Katana; use crate::test_utils::evm_contract::KakarotEvmContract; @@ -36,3 +37,13 @@ pub async fn katana() -> Katana { // Create a new test environment on Katana Katana::new().await } + +/// This fixture configures the tests. The following setup +/// is used: +/// - The log level is set to `info` +#[fixture] +pub fn setup() { + let filter = filter::EnvFilter::new("info"); + let subscriber = FmtSubscriber::builder().with_env_filter(filter).finish(); + let _ = tracing::subscriber::set_global_default(subscriber); +} diff --git a/tests/test_utils/macros.rs b/src/test_utils/macros.rs similarity index 100% rename from tests/test_utils/macros.rs rename to src/test_utils/macros.rs diff --git a/tests/test_utils/mod.rs b/src/test_utils/mod.rs similarity index 86% rename from tests/test_utils/mod.rs rename to src/test_utils/mod.rs index 15c6d33ee..efb486126 100644 --- a/tests/test_utils/mod.rs +++ b/src/test_utils/mod.rs @@ -1,9 +1,9 @@ pub mod constants; pub mod eoa; -pub mod eth_provider; pub mod evm_contract; pub mod fixtures; pub mod macros; +pub mod mongo; pub mod rpc; pub mod sequencer; pub mod tx_waiter; diff --git a/src/test_utils/mongo/mod.rs b/src/test_utils/mongo/mod.rs new file mode 100644 index 000000000..447f26c66 --- /dev/null +++ b/src/test_utils/mongo/mod.rs @@ -0,0 +1,217 @@ +use crate::eth_provider::database::Database; +use lazy_static::lazy_static; +use mongodb::{ + bson::{doc, Document}, + options::{DatabaseOptions, ReadConcern, UpdateModifications, UpdateOptions, WriteConcern}, + Client, Collection, +}; +use reth_primitives::H256; +use testcontainers::{ + clients::{self, Cli}, + core::WaitFor, + Container, GenericImage, +}; + +lazy_static! { + static ref DOCKER_CLI: Cli = clients::Cli::default(); + static ref IMAGE: GenericImage = GenericImage::new("mongo", "6.0.13") + .with_wait_for(WaitFor::message_on_stdout("server is ready")) + .with_env_var("MONGO_INITDB_DATABASE", "kakarot") + .with_env_var("MONGO_INITDB_ROOT_USERNAME", "root") + .with_env_var("MONGO_INITDB_ROOT_PASSWORD", "root") + .with_exposed_port(27017); + // The container is made static to avoid dropping it before the tests are finished. + static ref CONTAINER: Container<'static, GenericImage> = DOCKER_CLI.run(IMAGE.clone()); + + pub static ref BLOCK_HASH: H256 = H256::from_low_u64_be(0x1234); + pub static ref BLOCK_NUMBER: u64 = 0x1234; +} + +pub async fn mock_database() -> Database { + let port = CONTAINER.get_host_port_ipv4(27017); + + let mongo_client = Client::with_uri_str(format!("mongodb://root:root@localhost:{}", port)) + .await + .expect("Failed to init mongo Client"); + + let mongodb = mongo_client.database_with_options( + "kakarot", + DatabaseOptions::builder().read_concern(ReadConcern::MAJORITY).write_concern(WriteConcern::MAJORITY).build(), + ); + + // Insert one document to create collection + let hash_256_zero = format!("0x{:064x}", 0); + let address_zero = format!("0x{:040x}", 0); + let bloom_zero = format!("0x{:0512x}", 0); + + let zero = format!("0x{:064x}", 0); + let one = format!("0x{:064x}", 1); + let two = format!("0x{:064x}", 2); + let three = format!("0x{:064x}", 3); + + update_many( + "header".to_string(), + "number".to_string(), + mongodb.collection("headers"), + vec![ + doc! {"header": doc! { + "hash": format!("0x{:064x}", *BLOCK_HASH), + "parentHash": &hash_256_zero, + "sha3Uncles": &hash_256_zero, + "miner": &address_zero, + "stateRoot": &hash_256_zero, + "transactionsRoot": &hash_256_zero, + "receiptsRoot": &hash_256_zero, + "logsBloom": &bloom_zero, + "difficulty": &hash_256_zero, + "number": &hash_256_zero, + "gasLimit": &one, + "gasUsed": &one, + "timestamp": &hash_256_zero, + "extraData": "0x", + "mixHash": &hash_256_zero, + }}, + doc! {"header": doc! { + "parentHash": &hash_256_zero, + "sha3Uncles": &hash_256_zero, + "miner": &address_zero, + "stateRoot": &hash_256_zero, + "transactionsRoot": &hash_256_zero, + "receiptsRoot": &hash_256_zero, + "logsBloom": &bloom_zero, + "difficulty": &hash_256_zero, + "number": &one, + "gasLimit": &one, + "gasUsed": &one, + "timestamp": &hash_256_zero, + "extraData": "0x", + "mixHash": &hash_256_zero, + "baseFeePerGas": &one, + }}, + doc! {"header": doc! { + "parentHash": &hash_256_zero, + "sha3Uncles": &hash_256_zero, + "miner": &address_zero, + "stateRoot": &hash_256_zero, + "transactionsRoot": &hash_256_zero, + "receiptsRoot": &hash_256_zero, + "logsBloom": &bloom_zero, + "difficulty": &hash_256_zero, + "number": &two, + "gasLimit": &one, + "gasUsed": &one, + "timestamp": &hash_256_zero, + "extraData": "0x", + "mixHash": &hash_256_zero, + "baseFeePerGas": &one, + }}, + doc! {"header": doc! { + "parentHash": &hash_256_zero, + "sha3Uncles": &hash_256_zero, + "miner": &address_zero, + "stateRoot": &hash_256_zero, + "transactionsRoot": &hash_256_zero, + "receiptsRoot": &hash_256_zero, + "logsBloom": &bloom_zero, + "difficulty": &hash_256_zero, + "number": &three, + "gasLimit": &one, + "gasUsed": &one, + "timestamp": &hash_256_zero, + "extraData": "0x", + "mixHash": &hash_256_zero, + "baseFeePerGas": &one, + }}, + doc! {"header": doc! { + "parentHash": &hash_256_zero, + "sha3Uncles": &hash_256_zero, + "miner": &address_zero, + "stateRoot": &hash_256_zero, + "transactionsRoot": &hash_256_zero, + "receiptsRoot": &hash_256_zero, + "logsBloom": &bloom_zero, + "difficulty": &hash_256_zero, + "number": format!("0x{:064x}", *BLOCK_NUMBER), + "gasLimit": &one, + "gasUsed": &one, + "timestamp": &hash_256_zero, + "extraData": "0x", + "mixHash": &hash_256_zero, + "baseFeePerGas": &one, + }}, + ], + ) + .await; + + update_many( + "tx".to_string(), + "hash".to_string(), + mongodb.collection("transactions"), + vec![ + doc! {"tx": doc! { + "hash": &zero, + "nonce": &zero, + "blockHash": format!("0x{:064x}", *BLOCK_HASH), + "blockNumber": format!("0x{:064x}", *BLOCK_NUMBER), + "transactionIndex": &zero, + "from": &address_zero, + "value": &zero, + "gas": &zero, + "input": "0x", + }}, + doc! {"tx": doc! { + "hash": &one, + "nonce": &zero, + "blockHash": format!("0x{:064x}", *BLOCK_HASH), + "blockNumber": format!("0x{:064x}", *BLOCK_NUMBER), + "transactionIndex": &zero, + "from": &address_zero, + "value": &zero, + "gas": &zero, + "input": "0x", + }}, + doc! {"tx": doc! { + "hash": &two, + "nonce": &zero, + "blockHash": format!("0x{:064x}", *BLOCK_HASH), + "blockNumber": format!("0x{:064x}", *BLOCK_NUMBER), + "transactionIndex": &zero, + "from": &address_zero, + "value": &zero, + "gas": &zero, + "input": "0x", + }}, + ], + ) + .await; + + Database::new(mongodb) +} + +async fn update_many(doc: String, value: String, collection: Collection, updates: Vec) { + let key = [doc.as_str(), value.as_str()].join("."); + for u in updates { + collection + .update_one( + doc! {&key: u.get_document(&doc).unwrap().get_str(&value).unwrap()}, + UpdateModifications::Document(doc! {"$set": u}), + UpdateOptions::builder().upsert(true).build(), + ) + .await + .expect("Failed to insert documents"); + } +} + +#[cfg(test)] +mod tests { + use crate::eth_provider::database::types::header::StoredHeader; + + use super::*; + + #[tokio::test] + async fn test_mongo_connection() { + let database = mock_database().await; + + let _ = database.get_one::("headers", None, None).await.unwrap(); + } +} diff --git a/tests/test_utils/rpc/mod.rs b/src/test_utils/rpc/mod.rs similarity index 68% rename from tests/test_utils/rpc/mod.rs rename to src/test_utils/rpc/mod.rs index 8ad722ec8..09c7705ca 100644 --- a/tests/test_utils/rpc/mod.rs +++ b/src/test_utils/rpc/mod.rs @@ -1,17 +1,11 @@ use std::net::SocketAddr; -use std::sync::Arc; use jsonrpsee::server::ServerHandle; -use kakarot_rpc::eth_rpc::config::RPCConfig; -use kakarot_rpc::eth_rpc::rpc::KakarotRpcModuleBuilder; -use kakarot_rpc::eth_rpc::run_server; -use kakarot_rpc::starknet_client::config::{KakarotRpcConfig, Network}; -use kakarot_rpc::starknet_client::KakarotClient; -use starknet::providers::jsonrpc::HttpTransport; -use starknet::providers::JsonRpcClient; -use super::eth_provider::mock_ethereum_provider; use super::sequencer::Katana; +use crate::eth_rpc::config::RPCConfig; +use crate::eth_rpc::rpc::KakarotRpcModuleBuilder; +use crate::eth_rpc::run_server; /// Sets up the environment for Kakarot RPC integration tests by deploying the Kakarot contracts /// and starting the Kakarot RPC server. @@ -64,23 +58,8 @@ use super::sequencer::Katana; /// and each test is compiled separately, so the compiler thinks this function is unused #[allow(dead_code)] pub async fn start_kakarot_rpc_server(katana: &Katana) -> Result<(SocketAddr, ServerHandle), eyre::Report> { - let sequencer = katana.sequencer(); - let client = katana.client(); - - let provider = Arc::new(JsonRpcClient::new(HttpTransport::new(sequencer.url()))); - let starknet_config = KakarotRpcConfig::new( - Network::JsonRpcProvider(sequencer.url()), - client.kakarot_address(), - client.proxy_account_class_hash(), - client.externally_owned_account_class_hash(), - client.contract_account_class_hash(), - ); - - let kakarot_client = Arc::new(KakarotClient::new(starknet_config, provider)); - let eth_db = mock_ethereum_provider(); - // Create and run Kakarot RPC module. - let kakarot_rpc_module = KakarotRpcModuleBuilder::new(kakarot_client, eth_db).rpc_module()?; + let kakarot_rpc_module = KakarotRpcModuleBuilder::new(katana.eth_provider()).rpc_module()?; let rpc_config = RPCConfig::from_env()?; let (server_addr, server_handle) = run_server(kakarot_rpc_module, rpc_config).await?; diff --git a/tests/test_utils/sequencer/mod.rs b/src/test_utils/sequencer/mod.rs similarity index 50% rename from tests/test_utils/sequencer/mod.rs rename to src/test_utils/sequencer/mod.rs index 968a02301..678983413 100644 --- a/tests/test_utils/sequencer/mod.rs +++ b/src/test_utils/sequencer/mod.rs @@ -1,21 +1,20 @@ -use std::collections::HashMap; use std::path::Path; use std::str::FromStr; use std::sync::Arc; -use crate::test_utils::eoa::{Eoa, KakarotEOA}; +use crate::eth_provider::provider::EthDataProvider; +use crate::test_utils::eoa::KakarotEOA; use dojo_test_utils::sequencer::{Environment, SequencerConfig, StarknetConfig, TestSequencer}; use ethers::types::H256; use foundry_config::utils::find_project_root_path; -use kakarot_rpc::starknet_client::config::{KakarotRpcConfig, Network}; -use kakarot_rpc::starknet_client::KakarotClient; use katana_core::db::serde::state::SerializableState; use starknet::providers::jsonrpc::HttpTransport; use starknet::providers::JsonRpcClient; -use starknet_crypto::FieldElement; use crate::root_project_path; +use super::mongo::mock_database; + /// Returns the dumped Katana state with deployed Kakarot. pub fn load_katana_state() -> SerializableState { // Get dump path @@ -32,7 +31,7 @@ pub fn katana_config() -> StarknetConfig { StarknetConfig { disable_fee: true, env: Environment { - chain_id: "SN_GOERLI".into(), + chain_id: "KKRT".to_string(), invoke_max_steps: max_steps, validate_max_steps: max_steps, gas_price: 1, @@ -49,7 +48,7 @@ async fn katana_sequencer() -> TestSequencer { pub struct Katana { pub sequencer: TestSequencer, - pub eoa: KakarotEOA>, + pub eoa: KakarotEOA>>, } impl Katana { @@ -57,58 +56,31 @@ impl Katana { let sequencer = katana_sequencer().await; let starknet_provider = Arc::new(JsonRpcClient::new(HttpTransport::new(sequencer.url()))); - Self::initialize(sequencer, starknet_provider) + Self::initialize(sequencer, starknet_provider).await } /// Initializes the Katana test environment. - fn initialize(sequencer: TestSequencer, starknet_provider: Arc>) -> Self { - // Load deployments - let deployments_path = root_project_path!("lib/kakarot/deployments/katana/deployments.json"); - let deployments = std::fs::read_to_string(deployments_path).expect("Failed to read deployment file"); - let deployments: HashMap<&str, serde_json::Value> = - serde_json::from_str(&deployments).expect("Failed to deserialize deployments"); - - let kakarot_address = deployments["kakarot"]["address"].as_str().expect("Failed to get Kakarot address"); - let kakarot_address = FieldElement::from_hex_be(kakarot_address).expect("Failed to parse Kakarot address"); - - // Load declarations - let declaration_path = root_project_path!("lib/kakarot/deployments/katana/declarations.json"); - let declarations = std::fs::read_to_string(declaration_path).expect("Failed to read declaration file"); - let declarations: HashMap<&str, FieldElement> = - serde_json::from_str(&declarations).expect("Failed to deserialize declarations"); - - let proxy_class_hash = declarations["proxy"]; - let externally_owned_account_class_hash = declarations["externally_owned_account"]; - let contract_account_class_hash = declarations["contract_account"]; - + async fn initialize(sequencer: TestSequencer, starknet_provider: Arc>) -> Self { // Load PK dotenv::dotenv().expect("Failed to load .env file"); let pk = std::env::var("EVM_PRIVATE_KEY").expect("Failed to get EVM private key"); let pk = H256::from_str(&pk).expect("Failed to parse EVM private key").into(); // Create a Kakarot client - let kakarot_client = KakarotClient::new( - KakarotRpcConfig::new( - Network::JsonRpcProvider(sequencer.url()), - kakarot_address, - proxy_class_hash, - externally_owned_account_class_hash, - contract_account_class_hash, - ), - starknet_provider, - ); - - let eoa = KakarotEOA::new(pk, kakarot_client); + let database = mock_database().await; + let eth_provider = Arc::new(EthDataProvider::new(database, starknet_provider)); + + let eoa = KakarotEOA::new(pk, eth_provider); Self { sequencer, eoa } } - pub const fn eoa(&self) -> &KakarotEOA> { - &self.eoa + pub fn eth_provider(&self) -> Arc>>> { + self.eoa.eth_provider.clone() } - pub fn client(&self) -> &KakarotClient> { - self.eoa.client() + pub const fn eoa(&self) -> &KakarotEOA>> { + &self.eoa } /// allow(dead_code) is used because this function is used in tests, diff --git a/tests/test_utils/tx_waiter.rs b/src/test_utils/tx_waiter.rs similarity index 84% rename from tests/test_utils/tx_waiter.rs rename to src/test_utils/tx_waiter.rs index c30130f1e..87f4af892 100644 --- a/tests/test_utils/tx_waiter.rs +++ b/src/test_utils/tx_waiter.rs @@ -10,11 +10,20 @@ use tracing::info; /// Code taken from /// /// Credits to Jonathan Lei -pub async fn watch_tx

(provider: P, transaction_hash: FieldElement, poll_interval: Duration) -> Result<()> +pub async fn watch_tx

( + provider: P, + transaction_hash: FieldElement, + poll_interval: Duration, + count: usize, +) -> Result<()> where P: Provider, { + let mut i = 0usize; loop { + if i >= count { + return Err(anyhow::anyhow!("transaction not confirmed after {} tries", count)); + } match provider.get_transaction_receipt(transaction_hash).await { Ok(receipt) => match receipt.execution_result() { ExecutionResult::Succeeded => { @@ -43,5 +52,6 @@ where } tokio::time::sleep(poll_interval).await; + i += 1; } } diff --git a/tests/alchemy.rs b/tests/alchemy.rs new file mode 100644 index 000000000..9650c7981 --- /dev/null +++ b/tests/alchemy.rs @@ -0,0 +1,62 @@ +#![cfg(feature = "testing")] +use ethers::abi::Token; +use kakarot_rpc::models::{balance::TokenBalances, felt::Felt252Wrapper}; +use kakarot_rpc::test_utils::eoa::Eoa as _; +use kakarot_rpc::test_utils::evm_contract::KakarotEvmContract; +use kakarot_rpc::test_utils::fixtures::{erc20, setup}; +use kakarot_rpc::test_utils::rpc::start_kakarot_rpc_server; +use kakarot_rpc::test_utils::sequencer::Katana; +use reth_primitives::{Address, U256}; +use rstest::*; +use serde_json::{json, Value}; + +#[rstest] +#[awt] +#[tokio::test(flavor = "multi_thread")] +async fn test_token_balances(#[future] erc20: (Katana, KakarotEvmContract), _setup: ()) { + // Given + let katana = erc20.0; + let erc20 = erc20.1; + let eoa = katana.eoa(); + let eoa_address = eoa.evm_address().expect("Failed to get Eoa EVM address"); + let erc20_address: Address = + Into::::into(erc20.evm_address).try_into().expect("Failed to convert EVM address"); + + let (server_addr, server_handle) = + start_kakarot_rpc_server(&katana).await.expect("Error setting up Kakarot RPC server"); + + // When + let to = eoa.evm_address().unwrap(); + let amount = U256::from(10_000); + eoa.call_evm_contract(&erc20, "mint", (Token::Address(to.into()), Token::Uint(amount.into())), 0) + .await + .expect("Failed to mint ERC20 tokens"); + + // Then + let reqwest_client = reqwest::Client::new(); + let res = reqwest_client + .post(format!("http://localhost:{}", server_addr.port())) + .header("Content-Type", "application/json") + .body( + json!( + { + "jsonrpc":"2.0", + "method":"alchemy_getTokenBalances", + "params":[eoa_address, [erc20_address]], + "id":1, + } + ) + .to_string(), + ) + .send() + .await + .expect("Failed to call Alchemy RPC"); + let response = res.text().await.expect("Failed to get response body"); + let raw: Value = serde_json::from_str(&response).expect("Failed to deserialize response body"); + let balances: TokenBalances = + serde_json::from_value(raw.get("result").cloned().unwrap()).expect("Failed to deserialize response body"); + let erc20_balance = balances.token_balances[0].token_balance.expect("Failed to get ERC20 balance"); + + assert_eq!(amount, erc20_balance); + drop(server_handle); +} diff --git a/tests/api.rs b/tests/api.rs deleted file mode 100644 index ec673b3bf..000000000 --- a/tests/api.rs +++ /dev/null @@ -1,111 +0,0 @@ -mod test_utils; - -use ethers::abi::Token; -use kakarot_rpc::models::felt::Felt252Wrapper; -use reth_primitives::{Address, BlockId, BlockNumberOrTag, U256}; -use rstest::*; -use starknet::core::types::FieldElement; -use test_utils::eoa::Eoa; -use test_utils::evm_contract::KakarotEvmContract; -use test_utils::fixtures::{counter, erc20, katana}; -use test_utils::sequencer::Katana; - -#[rstest] -#[awt] -#[tokio::test(flavor = "multi_thread")] -async fn test_nonce_eoa(#[future] katana: Katana) { - // Given - let client = katana.client(); - - // When - let nonce = client.nonce(Address::zero(), BlockId::from(BlockNumberOrTag::Latest)).await.unwrap(); - - // Then - // Zero address shouldn't throw 'ContractNotFound', but return zero - assert_eq!(U256::from(0), nonce); -} - -#[rstest] -#[awt] -#[tokio::test(flavor = "multi_thread")] -async fn test_nonce_contract_account(#[future] counter: (Katana, KakarotEvmContract)) { - // Given - let katana = counter.0; - let counter = counter.1; - let client = katana.client(); - let counter_evm_address: Felt252Wrapper = counter.evm_address.into(); - - // When - let nonce_initial = - client.nonce(counter_evm_address.try_into().unwrap(), BlockId::from(BlockNumberOrTag::Latest)).await.unwrap(); - - // Then - assert_eq!(nonce_initial, U256::from(1)); -} - -#[rstest] -#[awt] -#[tokio::test(flavor = "multi_thread")] -async fn test_eoa_balance(#[future] katana: Katana) { - // Given - let client = katana.client(); - let eoa = katana.eoa(); - - // When - let eoa_balance = client - .balance(eoa.evm_address().unwrap(), BlockId::Number(reth_primitives::BlockNumberOrTag::Latest)) - .await - .unwrap(); - let eoa_balance = FieldElement::from_bytes_be(&eoa_balance.to_be_bytes()).unwrap(); - - // Then - assert!(eoa_balance > FieldElement::ZERO); -} - -#[rstest] -#[awt] -#[tokio::test(flavor = "multi_thread")] -async fn test_token_balances(#[future] erc20: (Katana, KakarotEvmContract)) { - // Given - let katana = erc20.0; - let erc20 = erc20.1; - let client = katana.client(); - let eoa = katana.eoa(); - let eoa_evm_address = eoa.evm_address().expect("Failed to get Eoa EVM address"); - let erc20_evm_address: Felt252Wrapper = erc20.evm_address.into(); - let erc20_evm_address = erc20_evm_address.try_into().expect("Failed to convert EVM address"); - - // When - let to = eoa.evm_address().unwrap(); - let amount = U256::from(10_000); - eoa.call_evm_contract(&erc20, "mint", (Token::Address(to.into()), Token::Uint(amount.into())), 0) - .await - .expect("Failed to mint ERC20 tokens"); - - // Then - let balances = client.token_balances(eoa_evm_address, vec![erc20_evm_address]).await.unwrap(); - let erc20_balance = balances.token_balances[0].token_balance.expect("Failed to get ERC20 balance"); - - assert_eq!(amount, erc20_balance); -} - -#[rstest] -#[awt] -#[tokio::test(flavor = "multi_thread")] -async fn test_storage_at(#[future] counter: (Katana, KakarotEvmContract)) { - // Given - let katana = counter.0; - let counter = counter.1; - let client = katana.client(); - let eoa = katana.eoa(); - let counter_evm_address: Felt252Wrapper = counter.evm_address.into(); - let counter_evm_address = counter_evm_address.try_into().expect("Failed to convert EVM address"); - - // When - eoa.call_evm_contract(&counter, "inc", (), 0).await.expect("Failed to increment counter"); - - // Then - let count = - client.storage_at(counter_evm_address, U256::from(0), BlockId::Number(BlockNumberOrTag::Latest)).await.unwrap(); - assert_eq!(U256::from(1), count); -} diff --git a/tests/block.rs b/tests/block.rs deleted file mode 100644 index 1ddf332da..000000000 --- a/tests/block.rs +++ /dev/null @@ -1,103 +0,0 @@ -mod test_utils; -use kakarot_rpc::models::block::{BlockWithTxHashes, BlockWithTxs}; -use kakarot_rpc::models::felt::Felt252Wrapper; -use reth_primitives::U256; -use reth_rpc_types::BlockTransactions; -use rstest::*; -use starknet::core::types::{BlockId, MaybePendingTransactionReceipt, TransactionReceipt}; -use starknet::providers::Provider; -use test_utils::evm_contract::KakarotEvmContract; -use test_utils::fixtures::counter; -use test_utils::sequencer::Katana; - -#[rstest] -#[awt] -#[tokio::test(flavor = "multi_thread")] -async fn test_to_eth_block_with_tx_hashes(#[future] counter: (Katana, KakarotEvmContract)) { - let katana: Katana = counter.0; - let counter = counter.1; - let client = katana.client(); - let eoa = katana.eoa(); - let starknet_tx_hash = eoa.call_evm_contract(&counter, "inc", (), 0).await.expect("Failed to increment counter"); - - let tx_receipt = client - .starknet_provider() - .get_transaction_receipt(starknet_tx_hash) - .await - .expect("Failed to query transaction receipt"); - let block_number = match tx_receipt { - MaybePendingTransactionReceipt::Receipt(tx_receipt) => { - if let TransactionReceipt::Invoke(tx_receipt) = tx_receipt { - tx_receipt.block_number - } else { - panic!("Transaction receipt is not an invoke transaction"); - } - } - MaybePendingTransactionReceipt::PendingReceipt(_) => panic!("Transaction receipt is pending"), - }; - - let block: BlockWithTxHashes = client - .starknet_provider() - .get_block_with_tx_hashes(BlockId::Number(block_number)) - .await - .expect("Failed to query block") - .into(); - - let eth_block = block.to_eth_block(client).await.inner; - - // TODO: Check that the block is valid - assert_eq!(ð_block.header.number.unwrap(), &U256::from(block_number)); - let tx_hashes = match eth_block.transactions { - BlockTransactions::Hashes(tx_hashes) => tx_hashes, - _ => panic!("Expected block transactions to be hashes"), - }; - assert_eq!(tx_hashes.len(), 1); - let tx_hash = Felt252Wrapper::from(starknet_tx_hash).into(); - assert_eq!(tx_hashes[0], tx_hash); -} - -#[rstest] -#[awt] -#[tokio::test(flavor = "multi_thread")] -async fn test_to_eth_block_with_txs(#[future] counter: (Katana, KakarotEvmContract)) { - let katana: Katana = counter.0; - let counter = counter.1; - let client = katana.client(); - let eoa = katana.eoa(); - let starknet_tx_hash = eoa.call_evm_contract(&counter, "inc", (), 0).await.expect("Failed to increment counter"); - - let tx_receipt = client - .starknet_provider() - .get_transaction_receipt(starknet_tx_hash) - .await - .expect("Failed to query transaction receipt"); - let block_number = match tx_receipt { - MaybePendingTransactionReceipt::Receipt(tx_receipt) => { - if let TransactionReceipt::Invoke(tx_receipt) = tx_receipt { - tx_receipt.block_number - } else { - panic!("Transaction receipt is not an invoke transaction"); - } - } - MaybePendingTransactionReceipt::PendingReceipt(_) => panic!("Transaction receipt is pending"), - }; - - let block: BlockWithTxs = client - .starknet_provider() - .get_block_with_txs(BlockId::Number(block_number)) - .await - .expect("Failed to query block") - .into(); - - let eth_block = block.to_eth_block(client).await.inner; - - // TODO: Check that the block is valid - assert_eq!(ð_block.header.number.unwrap(), &U256::from(block_number)); - let txs = match eth_block.transactions { - BlockTransactions::Full(txs) => txs, - _ => panic!("Expected block transactions to be full"), - }; - assert_eq!(txs.len(), 1); - let tx_hash = Felt252Wrapper::from(starknet_tx_hash).into(); - assert_eq!(txs[0].hash, tx_hash); -} diff --git a/tests/eth_provider.rs b/tests/eth_provider.rs new file mode 100644 index 000000000..fbce7c3a2 --- /dev/null +++ b/tests/eth_provider.rs @@ -0,0 +1,259 @@ +#![cfg(feature = "testing")] +use std::cmp::min; +use std::str::FromStr as _; + +use kakarot_rpc::eth_provider::provider::EthereumProvider; +use kakarot_rpc::models::felt::Felt252Wrapper; +use kakarot_rpc::test_utils::eoa::Eoa as _; +use kakarot_rpc::test_utils::evm_contract::EvmContract; +use kakarot_rpc::test_utils::fixtures::{counter, katana, setup}; +use kakarot_rpc::test_utils::mongo::{BLOCK_HASH, BLOCK_NUMBER}; +use kakarot_rpc::test_utils::{evm_contract::KakarotEvmContract, sequencer::Katana}; +use reth_rpc_types::{CallInput, CallRequest}; +use rstest::*; + +use reth_primitives::{Address, BlockNumberOrTag, Bytes, U256, U64}; + +#[rstest] +#[awt] +#[tokio::test(flavor = "multi_thread")] +async fn test_block_number(#[future] katana: Katana, _setup: ()) { + // Given + let eth_provider = katana.eth_provider(); + + // When + let block_number = eth_provider.block_number().await.unwrap(); + + // Then + // The block number is 3 because this is what we set in the mocked mongo database. + let expected = U64::from(*BLOCK_NUMBER); + assert_eq!(block_number, expected); +} + +#[rstest] +#[awt] +#[tokio::test(flavor = "multi_thread")] +async fn test_chain_id(#[future] katana: Katana, _setup: ()) { + // Given + let eth_provider = katana.eth_provider(); + + // When + let chain_id = eth_provider.chain_id().await.unwrap().unwrap_or_default(); + + // Then + // ASCII code for "KKRT" is 0x4b4b5254 + assert_eq!(chain_id, U64::from(0x4b4b5254)); +} + +#[rstest] +#[awt] +#[tokio::test(flavor = "multi_thread")] +async fn test_block_by_hash(#[future] katana: Katana, _setup: ()) { + // Given + let eth_provider = katana.eth_provider(); + + // When + let block = eth_provider.block_by_hash(*BLOCK_HASH, false).await.unwrap().unwrap(); + + // Then + assert_eq!(block.header.hash, Some(*BLOCK_HASH)); +} + +#[rstest] +#[awt] +#[tokio::test(flavor = "multi_thread")] +async fn test_block_by_number(#[future] katana: Katana, _setup: ()) { + // Given + let eth_provider = katana.eth_provider(); + + // When + let block = eth_provider.block_by_number(BlockNumberOrTag::Number(*BLOCK_NUMBER), false).await.unwrap().unwrap(); + + // Then + assert_eq!(block.header.number, Some(U256::from(*BLOCK_NUMBER))); +} + +#[rstest] +#[awt] +#[tokio::test(flavor = "multi_thread")] +async fn test_block_transaction_count_by_hash(#[future] katana: Katana, _setup: ()) { + // Given + let eth_provider = katana.eth_provider(); + + // When + let count = eth_provider.block_transaction_count_by_hash(*BLOCK_HASH).await.unwrap(); + + // Then + assert_eq!(count, U64::from(3)); +} + +#[rstest] +#[awt] +#[tokio::test(flavor = "multi_thread")] +async fn test_block_transaction_count_by_number(#[future] katana: Katana, _setup: ()) { + // Given + let eth_provider = katana.eth_provider(); + + // When + let count = eth_provider.block_transaction_count_by_number(BlockNumberOrTag::Number(*BLOCK_NUMBER)).await.unwrap(); + + // Then + assert_eq!(count, U64::from(3)); +} + +#[rstest] +#[awt] +#[tokio::test(flavor = "multi_thread")] +async fn test_balance(#[future] katana: Katana, _setup: ()) { + // Given + let eth_provider = katana.eth_provider(); + let eoa = katana.eoa(); + + // When + let eoa_balance = eth_provider.balance(eoa.evm_address().unwrap(), None).await.unwrap(); + + // Then + assert!(eoa_balance > U256::ZERO); +} + +#[rstest] +#[awt] +#[tokio::test(flavor = "multi_thread")] +async fn test_storage_at(#[future] counter: (Katana, KakarotEvmContract), _setup: ()) { + // Given + let katana = counter.0; + let counter = counter.1; + let eth_provider = katana.eth_provider(); + let eoa = katana.eoa(); + let counter_address: Felt252Wrapper = counter.evm_address.into(); + let counter_address = counter_address.try_into().expect("Failed to convert EVM address"); + + // When + eoa.call_evm_contract(&counter, "inc", (), 0).await.expect("Failed to increment counter"); + + // Then + let count = eth_provider.storage_at(counter_address, U256::from(0), None).await.unwrap(); + assert_eq!(U256::from(1), count); +} + +#[rstest] +#[awt] +#[tokio::test(flavor = "multi_thread")] +async fn test_nonce_eoa(#[future] katana: Katana, _setup: ()) { + // Given + let eth_provider = katana.eth_provider(); + + // When + let nonce = eth_provider.transaction_count(Address::zero(), None).await.unwrap(); + + // Then + // Zero address shouldn't throw 'ContractNotFound', but return zero + assert_eq!(U256::from(0), nonce); +} + +#[rstest] +#[awt] +#[tokio::test(flavor = "multi_thread")] +async fn test_nonce_contract_account(#[future] counter: (Katana, KakarotEvmContract), _setup: ()) { + // Given + let katana = counter.0; + let counter = counter.1; + let eth_provider = katana.eth_provider(); + let counter_address: Felt252Wrapper = counter.evm_address.into(); + let counter_address = counter_address.try_into().expect("Failed to convert EVM address"); + + // When + let nonce_initial = eth_provider.transaction_count(counter_address, None).await.unwrap(); + + // Then + assert_eq!(nonce_initial, U256::from(1)); +} + +#[rstest] +#[awt] +#[tokio::test(flavor = "multi_thread")] +async fn test_nonce(#[future] counter: (Katana, KakarotEvmContract), _setup: ()) { + // Given + let katana: Katana = counter.0; + let counter = counter.1; + let eth_provider = katana.eth_provider(); + let eoa = katana.eoa(); + + let nonce_before = eth_provider.transaction_count(eoa.evm_address().unwrap(), None).await.unwrap(); + + // When + eoa.call_evm_contract(&counter, "inc", (), 0).await.expect("Failed to increment counter"); + + // Then + let nonce_after = eth_provider.transaction_count(eoa.evm_address().unwrap(), None).await.unwrap(); + assert_eq!(nonce_before + U256::from(1), nonce_after); +} + +#[rstest] +#[awt] +#[tokio::test(flavor = "multi_thread")] +async fn test_get_code(#[future] counter: (Katana, KakarotEvmContract), _setup: ()) { + // Given + let katana: Katana = counter.0; + let counter = counter.1; + let eth_provider = katana.eth_provider(); + let counter_address: Felt252Wrapper = counter.evm_address.into(); + let counter_address = counter_address.try_into().expect("Failed to convert EVM address"); + + // When + let bytecode = eth_provider.get_code(counter_address, None).await.unwrap(); + + // Then + let counter_bytecode = ::load_contract_bytecode("Counter") + .expect("Failed to load counter bytecode"); + let expected = + counter_bytecode.deployed_bytecode.unwrap().bytecode.unwrap().object.into_bytes().unwrap().as_ref().to_vec(); + assert_eq!(bytecode, Bytes::from(expected)); +} + +#[rstest] +#[awt] +#[tokio::test(flavor = "multi_thread")] +async fn test_estimate_gas(#[future] counter: (Katana, KakarotEvmContract), _setup: ()) { + // Given + let eoa = counter.0.eoa(); + let eth_provider = counter.0.eth_provider(); + let counter = counter.1; + + let chain_id = eth_provider.chain_id().await.unwrap().unwrap_or_default(); + let counter_address: Felt252Wrapper = counter.evm_address.into(); + + let request = CallRequest { + from: Some(eoa.evm_address().unwrap()), + to: Some(counter_address.try_into().unwrap()), + input: CallInput { input: None, data: Some(Bytes::from_str("0x371303c0").unwrap()) }, // selector of "function inc()" + chain_id: Some(chain_id), + ..Default::default() + }; + + // When + let estimate = eth_provider.estimate_gas(request, None).await.unwrap(); + + // Then + assert!(estimate > U256::from(0)); +} + +#[rstest] +#[awt] +#[tokio::test(flavor = "multi_thread")] +async fn test_fee_history(#[future] katana: Katana, _setup: ()) { + // Given + let eth_provider = katana.eth_provider(); + let newest_block = 3; + let block_count = 100usize; + + // When + let fee_history = + eth_provider.fee_history(U256::from(block_count), BlockNumberOrTag::Number(newest_block), None).await.unwrap(); + + // Then + let actual_block_count = min(block_count, newest_block as usize + 1); + assert_eq!(fee_history.base_fee_per_gas.len(), actual_block_count + 1); + assert_eq!(fee_history.gas_used_ratio.len(), actual_block_count); + assert_eq!(fee_history.oldest_block, U256::ZERO); +} diff --git a/tests/ethereum_integration.rs b/tests/ethereum_integration.rs deleted file mode 100644 index b76ef4497..000000000 --- a/tests/ethereum_integration.rs +++ /dev/null @@ -1,152 +0,0 @@ -mod test_utils; -use std::convert::TryFrom; -use std::sync::Arc; -use std::time::Duration; - -use ethers::contract::ContractFactory; -use ethers::core::k256::ecdsa::SigningKey; -use ethers::middleware::SignerMiddleware; -use ethers::prelude::abigen; -use ethers::providers::{Http, Middleware, Provider}; -use ethers::signers::{LocalWallet, Signer}; -use ethers::types::{BlockId, BlockNumber, TransactionReceipt, H160, H256}; -use ethers::utils::keccak256; -use hex::FromHex; -use reth_primitives::{U256, U64}; -use rstest::*; -use test_utils::eoa::Eoa; -use test_utils::fixtures::katana; -use test_utils::rpc::start_kakarot_rpc_server; -use test_utils::sequencer::Katana; - -abigen!(ERC20, "tests/ERC20/IERC20.json"); - -// ⚠️ Only one test with `start_kakarot_rpc_server` -// When trying to run two tests with a server originating from `start_kakarot_rpc_server`, the -// second test will fail with: `thread 'test_erc20' panicked at 'Failed to start the server: Os -// { code: 98, kind: AddrInUse, message: "Address already in use" }'` -#[rstest] -#[awt] -// We ignore this test for now, because we need `send_transaction` to be implemented on the EthereumProvider -#[ignore] -#[tokio::test(flavor = "multi_thread")] -async fn test_erc20(#[future] katana: Katana) { - let (server_addr, server_handle) = - start_kakarot_rpc_server(&katana).await.expect("Error setting up Kakarot RPC server"); - - let reqwest_client = reqwest::Client::new(); - let _res = reqwest_client - .post(format!("http://localhost:{}/net_health", server_addr.port())) - .send() - .await - .expect("net_health: health check failed"); - - let wallet: LocalWallet = SigningKey::from_slice(katana.eoa().private_key().as_ref()) - .expect("Eoa Private Key should be used to init a LocalWallet") - .into(); - - let provider = Provider::::try_from(format!("http://localhost:{}", server_addr.port())) - .unwrap() - .interval(Duration::from_millis(10u64)); - - // get_chainid() returns a U256, which is a [u64; 4] - // We only need the first u64 - let chain_id = provider.get_chainid().await.unwrap().0[0]; - let client = Arc::new(SignerMiddleware::new(provider, wallet.with_chain_id(chain_id))); - - let block_number: U64 = client.get_block_number().await.unwrap(); - let params = BlockId::Number(BlockNumber::Number(block_number)); - let block = client.get_block(params).await; - assert!(block.is_ok()); - - let bytecode = include_str!("ERC20/bytecode.json"); - let bytecode: serde_json::Value = serde_json::from_str(bytecode).unwrap(); - // Deploy an ERC20 - let factory = ContractFactory::new( - ERC20_ABI.clone(), - ethers::types::Bytes::from_hex(bytecode["bytecode"].as_str().unwrap()).unwrap(), - client.clone(), - ); - - let contract = factory.deploy(()).unwrap().send().await.unwrap(); - let _: U64 = client.get_block_number().await.unwrap(); - let token = ERC20::new(contract.address(), client.clone()); - - // Assert initial balance is 0 - let balance = token - .balance_of(katana.eoa().evm_address().unwrap().into()) - .gas(U256::from(0xffffffffffffffffffffffffffffffff_u128)) - .call() - .await - .unwrap(); - assert_eq!(balance, 0u64.into()); - - // Mint some tokens - let tx_receipt: TransactionReceipt = token.mint(100u64.into()).send().await.unwrap().await.unwrap().unwrap(); - let block_number: U64 = client.get_block_number().await.unwrap(); - - // Assert balance is now 100 - let balance = token - .balance_of(katana.eoa().evm_address().unwrap().into()) - .gas(U256::from(0xffffffffffffffffffffffffffffffff_u128)) - .call() - .await - .unwrap(); - assert_eq!(balance, 100u64.into()); - - // Assert on the transaction receipt - assert_eq!(tx_receipt.status, Some(1u64.into())); - assert_eq!(tx_receipt.transaction_index, 0.into()); - assert_eq!(tx_receipt.block_number, Some(block_number)); - assert_eq!(tx_receipt.from, katana.eoa().evm_address().unwrap().into()); - assert_eq!(tx_receipt.to, Some(contract.address())); - // Assert on the logs - assert_eq!(tx_receipt.logs.len(), 1); - assert_eq!(tx_receipt.logs[0].topics.len(), 3); - assert_eq!(tx_receipt.logs[0].topics[0], H256::from_slice(&keccak256("Transfer(address,address,uint256)"))); - assert_eq!(tx_receipt.logs[0].topics[1], H256::zero()); - assert_eq!(tx_receipt.logs[0].topics[2], H160::from(katana.eoa().evm_address().unwrap().as_fixed_bytes()).into()); - assert_eq!( - tx_receipt.logs[0].data, - ethers::types::Bytes::from_hex("0x0000000000000000000000000000000000000000000000000000000000000064").unwrap() - ); - - // eth_getTransactionByHash - let tx = client.get_transaction(tx_receipt.transaction_hash).await.unwrap().unwrap(); - assert_eq!(tx.block_number, Some(block_number)); - assert_eq!(tx.from, katana.eoa().evm_address().unwrap().into()); - assert_eq!(tx.to, Some(contract.address())); - assert_eq!(tx.value, 0u64.into()); - assert_eq!(tx.gas, 100.into()); - // Gas Price is None in TxType == 2, i.e. EIP1559 - assert_eq!(tx.gas_price, None); - assert_eq!(tx.transaction_type, Some(2.into())); - // TODO: Fix inconsistent max_fee_per_gas and max_priority_fee_per_gas - assert_eq!(tx.max_fee_per_gas, Some(3000000002_u64.into())); - assert_eq!(tx.max_priority_fee_per_gas, Some(0.into())); - // ⚠️ Do not use Transaction::hash() to compare hashes - // As it computes the keccak256 of the RLP encoding of the transaction - // This is not the same as the transaction hash returned by the RPC (Starknet transaction hash) - assert_eq!(tx.hash, tx_receipt.transaction_hash); - - // eth_getBlockByNumber - let block = client.get_block(BlockId::Number(BlockNumber::Number(block_number))).await.unwrap().unwrap(); - assert_eq!(&block.number.unwrap(), &block_number); - // Check that our transaction is inside the block - assert_eq!(block.transactions.len(), 1); - assert_eq!(block.transactions[0], tx_receipt.transaction_hash); - - // eth_syncing - // TODO: Fix eth_syncing - // let syncing = client.syncing().await.unwrap(); - // assert_eq!(syncing, SyncingStatus::IsFalse); - // returns an error `MiddlewareError(JsonRpcClientError(JsonRpcError(JsonRpcError { - // code: 0, message: "got code -32601 with: Method not found", data: None })))` - - // eth_gasPrice - let gas_price = client.get_gas_price().await.unwrap(); - assert_eq!(gas_price, 1u64.into()); - - // Stop the server - server_handle.stop().expect("Failed to stop the server"); -} diff --git a/tests/event.rs b/tests/event.rs deleted file mode 100644 index 27c4a898c..000000000 --- a/tests/event.rs +++ /dev/null @@ -1,112 +0,0 @@ -mod test_utils; -use kakarot_rpc::models::event::StarknetEvent; -use reth_primitives::{H256, U256}; -use reth_rpc_types::Log; -use rstest::*; -use starknet::core::types::Event; -use test_utils::fixtures::katana; -use test_utils::sequencer::Katana; - -use crate::test_utils::constants::KAKAROT_ADDRESS; - -#[rstest] -#[awt] -#[tokio::test(flavor = "multi_thread")] -async fn test_to_eth_log_log3(#[future] katana: Katana) { - // Given - let mut event: Event = serde_json::from_str(include_str!("test_data/conversion/starknet/event_log3.json")).unwrap(); - event.from_address = *KAKAROT_ADDRESS; - let starknet_event = StarknetEvent::new(event); - - let client = katana.client(); - - // When - let eth_log = starknet_event.to_eth_log(client, None, None, None, None, None).unwrap(); - - // Then - let expected: Log = serde_json::from_str(include_str!("test_data/conversion/eth/event_log3.json")).unwrap(); - assert_eq!(expected, eth_log); -} - -#[rstest] -#[awt] -#[tokio::test(flavor = "multi_thread")] -async fn test_to_eth_log_log4(#[future] katana: Katana) { - // Given - let mut event: Event = serde_json::from_str(include_str!("test_data/conversion/starknet/event_log4.json")).unwrap(); - event.from_address = *KAKAROT_ADDRESS; - let starknet_event = StarknetEvent::new(event); - - let client = katana.client(); - - // When - let eth_log = starknet_event.to_eth_log(client, None, None, None, None, None).unwrap(); - - // Then - let expected: Log = serde_json::from_str(include_str!("test_data/conversion/eth/event_log4.json")).unwrap(); - assert_eq!(expected, eth_log); -} - -#[rstest] -#[awt] -#[tokio::test(flavor = "multi_thread")] -#[should_panic(expected = "KakarotDataFilteringError(\"Event\")")] -async fn test_to_eth_log_should_fail_on_from_address_not_kakarot_address(#[future] katana: Katana) { - // Given - let mut event: Event = - serde_json::from_str(include_str!("test_data/conversion/starknet/event_invalid_from_address.json")).unwrap(); - event.from_address = *KAKAROT_ADDRESS; - - let starknet_event = StarknetEvent::new(event); - - let client = katana.client(); - - // When - starknet_event.to_eth_log(client, None, None, None, None, None).unwrap(); -} - -#[rstest] -#[awt] -#[tokio::test(flavor = "multi_thread")] -#[should_panic(expected = "ConversionError(\"failed to convert Felt252Wrapper to Ethereum address: the value \ - exceeds the maximum size of an Ethereum address\")")] -async fn test_to_eth_log_should_fail_on_key_not_convertible_to_eth_address(#[future] katana: Katana) { - // Given - let mut event: Event = - serde_json::from_str(include_str!("test_data/conversion/starknet/event_invalid_key.json")).unwrap(); - event.from_address = *KAKAROT_ADDRESS; - - let starknet_event = StarknetEvent::new(event); - - let client = katana.client(); - - // When - starknet_event.to_eth_log(client, None, None, None, None, None).unwrap(); -} - -#[rstest] -#[awt] -#[tokio::test(flavor = "multi_thread")] -async fn test_to_eth_log_with_optional_parameters(#[future] katana: Katana) { - // Given - let mut event: Event = serde_json::from_str(include_str!("test_data/conversion/starknet/event_log3.json")).unwrap(); - event.from_address = *KAKAROT_ADDRESS; - let starknet_event = StarknetEvent::new(event); - - let client = katana.client(); - - // When - let block_hash = Some(H256::from_low_u64_be(0xdeadbeef)); - let block_number = Some(U256::from(0x1)); - let transaction_hash = Some(H256::from_low_u64_be(0x12)); - let transaction_index = Some(U256::from(0x123)); - let log_index = Some(U256::from(0x1234)); - let eth_event = starknet_event - .to_eth_log(client, block_hash, block_number, transaction_hash, log_index, transaction_index) - .unwrap(); - - // Then - let expected: Log = - serde_json::from_str(include_str!("test_data/conversion/eth/event_log3_with_optionals.json")).unwrap(); - assert_eq!(expected, eth_event); -} diff --git a/tests/event_filter.rs b/tests/event_filter.rs deleted file mode 100644 index f3b28368d..000000000 --- a/tests/event_filter.rs +++ /dev/null @@ -1,129 +0,0 @@ -mod test_utils; - -use kakarot_rpc::models::event_filter::EthEventFilter; -use reth_rpc_types::Filter; -use rstest::*; -use starknet::core::types::EventFilter; -use test_utils::fixtures::katana; -use test_utils::sequencer::Katana; - -use crate::test_utils::constants::KAKAROT_ADDRESS; - -fn assert_eq_event_filter(lhs: EventFilter, rhs: EventFilter) { - assert_eq!(lhs.from_block, rhs.from_block); - assert_eq!(lhs.to_block, rhs.to_block); - assert_eq!(lhs.address, Some(*KAKAROT_ADDRESS)); - assert_eq!(lhs.keys, rhs.keys); -} - -#[rstest] -#[awt] -#[tokio::test(flavor = "multi_thread")] -async fn test_to_starknet_event_filter_with_block_hash(#[future] katana: Katana) { - // Given - let eth_event_filter: Filter = - serde_json::from_str(include_str!("test_data/conversion/eth/event_filter_block_hash.json")).unwrap(); - let eth_event_filter: EthEventFilter = eth_event_filter.into(); - - let client = katana.client(); - - // When - let starknet_event_filter = eth_event_filter.to_starknet_event_filter(client).unwrap(); - - // Then - let mut expected: EventFilter = - serde_json::from_str(include_str!("test_data/conversion/starknet/event_filter_block_hash.json")).unwrap(); - // Workaround for the fact that the Kakarot contract address is not deterministic - expected.address = Some(*KAKAROT_ADDRESS); - assert_eq_event_filter(expected, starknet_event_filter); -} - -#[rstest] -#[awt] -#[tokio::test(flavor = "multi_thread")] -async fn test_to_starknet_event_filter_with_from_to(#[future] katana: Katana) { - // Given - let eth_event_filter: Filter = - serde_json::from_str(include_str!("test_data/conversion/eth/event_filter_from_to.json")).unwrap(); - let eth_event_filter: EthEventFilter = eth_event_filter.into(); - - let client = katana.client(); - - // When - let starknet_event_filter = eth_event_filter.to_starknet_event_filter(client).unwrap(); - - // Then - let mut expected: EventFilter = - serde_json::from_str(include_str!("test_data/conversion/starknet/event_filter_from_to.json")).unwrap(); - // Workaround for the fact that the Kakarot contract address is not deterministic - expected.address = Some(*KAKAROT_ADDRESS); - assert_eq_event_filter(expected, starknet_event_filter); -} - -#[rstest] -#[awt] -#[tokio::test(flavor = "multi_thread")] -async fn test_to_starknet_event_filter_without_topics(#[future] katana: Katana) { - // Given - let eth_event_filter: Filter = - serde_json::from_str(include_str!("test_data/conversion/eth/event_filter_without_topics.json")).unwrap(); - let eth_event_filter: EthEventFilter = eth_event_filter.into(); - - let client = katana.client(); - - // When - let starknet_event_filter = eth_event_filter.to_starknet_event_filter(client).unwrap(); - - // Then - let mut expected: EventFilter = - serde_json::from_str(include_str!("test_data/conversion/starknet/event_filter_without_topics.json")).unwrap(); - // Workaround for the fact that the Kakarot contract address is not deterministic - expected.address = Some(*KAKAROT_ADDRESS); - assert_eq_event_filter(expected, starknet_event_filter); -} - -#[rstest] -#[awt] -#[tokio::test(flavor = "multi_thread")] -async fn test_to_starknet_event_filter_without_address(#[future] katana: Katana) { - // Given - let eth_event_filter: Filter = - serde_json::from_str(include_str!("test_data/conversion/eth/event_filter_without_address.json")).unwrap(); - let eth_event_filter: EthEventFilter = eth_event_filter.into(); - - let client = katana.client(); - - // When - let starknet_event_filter = eth_event_filter.to_starknet_event_filter(client).unwrap(); - - // Then - let mut expected: EventFilter = - serde_json::from_str(include_str!("test_data/conversion/starknet/event_filter_without_address.json")).unwrap(); - // Workaround for the fact that the Kakarot contract address is not deterministic - expected.address = Some(*KAKAROT_ADDRESS); - assert_eq_event_filter(expected, starknet_event_filter); -} - -#[rstest] -#[awt] -#[tokio::test(flavor = "multi_thread")] -async fn test_to_starknet_event_filter_without_topics_or_address(#[future] katana: Katana) { - // Given - let eth_event_filter: Filter = - serde_json::from_str(include_str!("test_data/conversion/eth/event_filter_without_topics_or_address.json")) - .unwrap(); - let eth_event_filter: EthEventFilter = eth_event_filter.into(); - - let client = katana.client(); - - // When - let starknet_event_filter = eth_event_filter.to_starknet_event_filter(client).unwrap(); - - // Then - let mut expected: EventFilter = - serde_json::from_str(include_str!("test_data/conversion/starknet/event_filter_without_topics_or_address.json")) - .unwrap(); - // Workaround for the fact that the Kakarot contract address is not deterministic - expected.address = Some(*KAKAROT_ADDRESS); - assert_eq_event_filter(expected, starknet_event_filter); -} diff --git a/tests/setup.rs b/tests/setup.rs deleted file mode 100644 index d0169208f..000000000 --- a/tests/setup.rs +++ /dev/null @@ -1,9 +0,0 @@ -use ctor::ctor; -use tracing_subscriber::{filter, FmtSubscriber}; - -#[ctor] -fn setup() { - let filter = filter::EnvFilter::new("info"); - let subscriber = FmtSubscriber::builder().with_env_filter(filter).finish(); - tracing::subscriber::set_global_default(subscriber).expect("setting tracing default failed"); -} diff --git a/tests/starknet_client.rs b/tests/starknet_client.rs deleted file mode 100644 index 05d22f157..000000000 --- a/tests/starknet_client.rs +++ /dev/null @@ -1,116 +0,0 @@ -#[cfg(test)] -mod test_utils; -use kakarot_rpc::contracts::kakarot_contract::KakarotCoreReader; -use kakarot_rpc::models::felt::Felt252Wrapper; -use rstest::*; -use starknet::providers::Provider; -use starknet_crypto::FieldElement; -use test_utils::eoa::Eoa; -use test_utils::evm_contract::KakarotEvmContract; -use test_utils::fixtures::{counter, katana}; -use test_utils::sequencer::Katana; - -use reth_primitives::{Address, BlockId, BlockNumberOrTag, U256}; - -#[rstest] -#[awt] -#[tokio::test(flavor = "multi_thread")] -async fn test_nonce(#[future] counter: (Katana, KakarotEvmContract)) { - let katana: Katana = counter.0; - let counter = counter.1; - let client = katana.client(); - let eoa = katana.eoa(); - - // Check nonce of Eoa - let nonce_before = - client.nonce(eoa.evm_address().unwrap(), BlockId::Number(BlockNumberOrTag::Latest)).await.unwrap(); - - eoa.call_evm_contract(&counter, "inc", (), 0).await.expect("Failed to increment counter"); - - // Check nonce of Eoa - let nonce_after = - client.nonce(eoa.evm_address().unwrap(), BlockId::Number(BlockNumberOrTag::Latest)).await.unwrap(); - assert_eq!(nonce_before + U256::from(1), nonce_after); -} - -#[rstest] -#[awt] -#[tokio::test(flavor = "multi_thread")] -async fn test_get_evm_address(#[future] counter: (Katana, KakarotEvmContract)) { - let katana: Katana = counter.0; - let counter = counter.1; - let client = katana.client(); - - // Check counter EVM address - let counter_evm_address = client.get_evm_address(&counter.starknet_address).await.unwrap(); - assert_eq!(Address::try_from(Felt252Wrapper::from(counter.evm_address)).unwrap(), counter_evm_address); -} - -#[rstest] -#[awt] -#[tokio::test(flavor = "multi_thread")] -async fn test_fee_history(#[future] katana: Katana) { - let client = katana.client(); - - let newest_block = client.starknet_provider().block_number().await.unwrap(); - let block_count = newest_block + 1; - - // Check fee history - let fee_history = - client.fee_history(U256::from(block_count), BlockNumberOrTag::Number(newest_block), None).await.unwrap(); - assert_eq!(fee_history.base_fee_per_gas.len(), block_count as usize); - assert_eq!(fee_history.gas_used_ratio.len(), block_count as usize); - assert_eq!(fee_history.oldest_block, U256::ZERO); -} - -#[rstest] -#[awt] -#[tokio::test(flavor = "multi_thread")] -async fn test_compute_starknet_address(#[future] katana: Katana) { - let client = katana.client(); - let kakarot_contract = KakarotCoreReader::new(client.kakarot_address(), client.starknet_provider()); - let address = 0x1234u64; - - let starknet_address = client.compute_starknet_address(&Address::from(address)).await.unwrap(); - let expected_address = - kakarot_contract.compute_starknet_address(&FieldElement::from(address)).call().await.unwrap(); - - assert_eq!(starknet_address, expected_address); -} - -#[tokio::test] -// Ignore until #649 is fixed -#[ignore] -async fn test_estimate_gas() { - // // Given - // let client = init_testnet_client(); - - // let request = CallRequest { - // from: Some(*ACCOUNT_ADDRESS_EVM), // account address - // to: Some(*COUNTER_ADDRESS_EVM), // counter address - // input: CallInput { input: None, data: Some(Bytes::from_str(INC_DATA).unwrap()) }, // call to inc() - // chain_id: Some(U64::from(CHAIN_ID)), // "KKRT" chain id - // ..Default::default() - // }; - // let block_id = BlockId::Number(BlockNumberOrTag::Latest); - - // // When - // let estimate = client.estimate_gas(request, block_id).await.unwrap(); - - // // Then - // assert!(estimate > U256::from(0)); -} - -#[tokio::test] -// Ignore until #649 is fixed and test runs against Madara -#[ignore] -async fn test_gas_price() { - // // Given - // let client = init_testnet_client(); - - // // When - // let gas_price = client.gas_price().await.unwrap(); - - // // Then - // assert!(gas_price > U256::from(0)); -} diff --git a/tests/test_data/conversion/eth/event_filter_block_hash.json b/tests/test_data/conversion/eth/event_filter_block_hash.json deleted file mode 100644 index 76e2392d4..000000000 --- a/tests/test_data/conversion/eth/event_filter_block_hash.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "blockHash": "0x0449aa33ad836b65b10fa60082de99e24ac876ee2fd93e723a99190a530af0a9", - "address": "0x2b61c43a85bd35987c5311215e8288b823a6873e", - "topics": [ - "0x5998d146b8109b9444e9bb13ae9a548e7f38d2db6e0da72afe22cefa3065bc63", - "0x000000000000000000000000000000000000000000000000000000000000000a", - "0x000000000000000000000000000000000000000000000000000000000000000b" - ] -} diff --git a/tests/test_data/conversion/eth/event_filter_from_to.json b/tests/test_data/conversion/eth/event_filter_from_to.json deleted file mode 100644 index d9300258a..000000000 --- a/tests/test_data/conversion/eth/event_filter_from_to.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "fromBlock": "0x64", - "toBlock": "0x1f4", - "address": "0x2b61c43a85bd35987c5311215e8288b823a6873e", - "topics": [ - "0x5998d146b8109b9444e9bb13ae9a548e7f38d2db6e0da72afe22cefa3065bc63", - "0x000000000000000000000000000000000000000000000000000000000000000a", - "0x000000000000000000000000000000000000000000000000000000000000000b" - ] -} diff --git a/tests/test_data/conversion/eth/event_filter_without_address.json b/tests/test_data/conversion/eth/event_filter_without_address.json deleted file mode 100644 index 3400fac94..000000000 --- a/tests/test_data/conversion/eth/event_filter_without_address.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "fromBlock": "0x64", - "toBlock": "0x1f4", - "topics": [ - "0x5998d146b8109b9444e9bb13ae9a548e7f38d2db6e0da72afe22cefa3065bc63", - "0x000000000000000000000000000000000000000000000000000000000000000a", - "0x000000000000000000000000000000000000000000000000000000000000000b" - ] -} diff --git a/tests/test_data/conversion/eth/event_filter_without_topics.json b/tests/test_data/conversion/eth/event_filter_without_topics.json deleted file mode 100644 index a72b65e93..000000000 --- a/tests/test_data/conversion/eth/event_filter_without_topics.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "fromBlock": "0x64", - "toBlock": "0x1f4", - "address": "0x2b61c43a85bd35987c5311215e8288b823a6873e", - "topics": [] -} diff --git a/tests/test_data/conversion/eth/event_filter_without_topics_or_address.json b/tests/test_data/conversion/eth/event_filter_without_topics_or_address.json deleted file mode 100644 index 315070ae2..000000000 --- a/tests/test_data/conversion/eth/event_filter_without_topics_or_address.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "fromBlock": "0x64", - "toBlock": "0x1f4", - "topics": [] -} diff --git a/tests/test_data/conversion/eth/event_log3.json b/tests/test_data/conversion/eth/event_log3.json deleted file mode 100644 index e084a1f9d..000000000 --- a/tests/test_data/conversion/eth/event_log3.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "address": "0x2b61c43a85bd35987c5311215e8288b823a6873e", - "topics": [ - "0x5998d146b8109b9444e9bb13ae9a548e7f38d2db6e0da72afe22cefa3065bc63", - "0x000000000000000000000000000000000000000000000000000000000000000a", - "0x000000000000000000000000000000000000000000000000000000000000000b" - ], - "data": "0x000000000000000000000000000000000000000000000000000000000000000a", - "removed": false -} diff --git a/tests/test_data/conversion/eth/event_log3_with_optionals.json b/tests/test_data/conversion/eth/event_log3_with_optionals.json deleted file mode 100644 index 4941d5df5..000000000 --- a/tests/test_data/conversion/eth/event_log3_with_optionals.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "address": "0x2b61c43a85bd35987c5311215e8288b823a6873e", - "topics": [ - "0x5998d146b8109b9444e9bb13ae9a548e7f38d2db6e0da72afe22cefa3065bc63", - "0x000000000000000000000000000000000000000000000000000000000000000a", - "0x000000000000000000000000000000000000000000000000000000000000000b" - ], - "data": "0x000000000000000000000000000000000000000000000000000000000000000a", - "blockHash": "0x00000000000000000000000000000000000000000000000000000000deadbeef", - "blockNumber": "0x0000000000000000000000000000000000000000000000000000000000000001", - "transactionHash": "0x0000000000000000000000000000000000000000000000000000000000000012", - "transactionIndex": "0x0000000000000000000000000000000000000000000000000000000000000123", - "logIndex": "0x0000000000000000000000000000000000000000000000000000000000001234", - "removed": false -} diff --git a/tests/test_data/conversion/eth/event_log4.json b/tests/test_data/conversion/eth/event_log4.json deleted file mode 100644 index 957a85103..000000000 --- a/tests/test_data/conversion/eth/event_log4.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "address": "0x2b61c43a85bd35987c5311215e8288b823a6873e", - "topics": [ - "0x8106949def8a44172f54941ce774c774bf0a60652fafd614e9b6be2ca74a54a9", - "0x000000000000000000000000000000000000000000000000000000000000000a", - "0x000000000000000000000000000000000000000000000000000000000000000b", - "0x000000000000000000000000000000000000000000000000000000000000000a" - ], - "data": "0x", - "removed": false -} diff --git a/tests/test_data/conversion/starknet/event_filter_block_hash.json b/tests/test_data/conversion/starknet/event_filter_block_hash.json deleted file mode 100644 index 17962491a..000000000 --- a/tests/test_data/conversion/starknet/event_filter_block_hash.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "from_block": { - "block_hash": "0x0449aa33ad836b65b10fa60082de99e24ac876ee2fd93e723a99190a530af0a9" - }, - "to_block": { - "block_hash": "0x0449aa33ad836b65b10fa60082de99e24ac876ee2fd93e723a99190a530af0a9" - }, - "address": "0x7f1ec5adcbf9097a51b80bd3552a7d1ff8b45afb0e658fb80728ff77833a4f", - "keys": [ - ["0x2b61c43a85bd35987c5311215e8288b823a6873e"], - ["0x7f38d2db6e0da72afe22cefa3065bc63"], - ["0x5998d146b8109b9444e9bb13ae9a548e"], - ["0x0000000000000000000000000000000a"], - ["0x00000000000000000000000000000000"], - ["0x0000000000000000000000000000000b"], - ["0x00000000000000000000000000000000"] - ] -} diff --git a/tests/test_data/conversion/starknet/event_filter_from_to.json b/tests/test_data/conversion/starknet/event_filter_from_to.json deleted file mode 100644 index ac9f06553..000000000 --- a/tests/test_data/conversion/starknet/event_filter_from_to.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "from_block": { - "block_number": 100 - }, - "to_block": { - "block_number": 500 - }, - "address": "0x7f1ec5adcbf9097a51b80bd3552a7d1ff8b45afb0e658fb80728ff77833a4f", - "keys": [ - ["0x2b61c43a85bd35987c5311215e8288b823a6873e"], - ["0x7f38d2db6e0da72afe22cefa3065bc63"], - ["0x5998d146b8109b9444e9bb13ae9a548e"], - ["0x0000000000000000000000000000000a"], - ["0x00000000000000000000000000000000"], - ["0x0000000000000000000000000000000b"], - ["0x00000000000000000000000000000000"] - ] -} diff --git a/tests/test_data/conversion/starknet/event_filter_without_address.json b/tests/test_data/conversion/starknet/event_filter_without_address.json deleted file mode 100644 index 0f952fafe..000000000 --- a/tests/test_data/conversion/starknet/event_filter_without_address.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "from_block": { - "block_number": 100 - }, - "to_block": { - "block_number": 500 - }, - "address": "0x7f1ec5adcbf9097a51b80bd3552a7d1ff8b45afb0e658fb80728ff77833a4f", - "keys": [ - [], - ["0x7f38d2db6e0da72afe22cefa3065bc63"], - ["0x5998d146b8109b9444e9bb13ae9a548e"], - ["0x0000000000000000000000000000000a"], - ["0x00000000000000000000000000000000"], - ["0x0000000000000000000000000000000b"], - ["0x00000000000000000000000000000000"] - ] -} diff --git a/tests/test_data/conversion/starknet/event_filter_without_topics.json b/tests/test_data/conversion/starknet/event_filter_without_topics.json deleted file mode 100644 index 5acd71ea0..000000000 --- a/tests/test_data/conversion/starknet/event_filter_without_topics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "from_block": { - "block_number": 100 - }, - "to_block": { - "block_number": 500 - }, - "address": "0x7f1ec5adcbf9097a51b80bd3552a7d1ff8b45afb0e658fb80728ff77833a4f", - "keys": [["0x2b61c43a85bd35987c5311215e8288b823a6873e"]] -} diff --git a/tests/test_data/conversion/starknet/event_filter_without_topics_or_address.json b/tests/test_data/conversion/starknet/event_filter_without_topics_or_address.json deleted file mode 100644 index 78c5186b1..000000000 --- a/tests/test_data/conversion/starknet/event_filter_without_topics_or_address.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "from_block": { - "block_number": 100 - }, - "to_block": { - "block_number": 500 - }, - "address": "0x7f1ec5adcbf9097a51b80bd3552a7d1ff8b45afb0e658fb80728ff77833a4f" -} diff --git a/tests/test_data/conversion/starknet/event_invalid_from_address.json b/tests/test_data/conversion/starknet/event_invalid_from_address.json deleted file mode 100644 index 63241d8cb..000000000 --- a/tests/test_data/conversion/starknet/event_invalid_from_address.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "from_address": "0x0000000000000000000000000000000000000000000000000000000000000000", - "keys": [], - "data": [] -} diff --git a/tests/test_data/conversion/starknet/event_invalid_key.json b/tests/test_data/conversion/starknet/event_invalid_key.json deleted file mode 100644 index 121c9b63e..000000000 --- a/tests/test_data/conversion/starknet/event_invalid_key.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "from_address": "0x7f1ec5adcbf9097a51b80bd3552a7d1ff8b45afb0e658fb80728ff77833a4f", - "keys": [ - "0x2b61c43a85bd35987c5311215e8288b823a6873edeadbeef", - "0x7f38d2db6e0da72afe22cefa3065bc63", - "0x5998d146b8109b9444e9bb13ae9a548e", - "0xa", - "0x0", - "0xb", - "0x0" - ], - "data": [] -} diff --git a/tests/test_data/conversion/starknet/event_log3.json b/tests/test_data/conversion/starknet/event_log3.json deleted file mode 100644 index 1cd8f6e58..000000000 --- a/tests/test_data/conversion/starknet/event_log3.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "from_address": "0x7f1ec5adcbf9097a51b80bd3552a7d1ff8b45afb0e658fb80728ff77833a4f", - "keys": [ - "0x2b61c43a85bd35987c5311215e8288b823a6873e", - "0x7f38d2db6e0da72afe22cefa3065bc63", - "0x5998d146b8109b9444e9bb13ae9a548e", - "0xa", - "0x0", - "0xb", - "0x0" - ], - "data": [ - "0x0", - "0x0", - "0x0", - "0x0", - "0x0", - "0x0", - "0x0", - "0x0", - "0x0", - "0x0", - "0x0", - "0x0", - "0x0", - "0x0", - "0x0", - "0x0", - "0x0", - "0x0", - "0x0", - "0x0", - "0x0", - "0x0", - "0x0", - "0x0", - "0x0", - "0x0", - "0x0", - "0x0", - "0x0", - "0x0", - "0x0", - "0xa" - ] -} diff --git a/tests/test_data/conversion/starknet/event_log4.json b/tests/test_data/conversion/starknet/event_log4.json deleted file mode 100644 index 5d3df09f6..000000000 --- a/tests/test_data/conversion/starknet/event_log4.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "from_address": "0x7f1ec5adcbf9097a51b80bd3552a7d1ff8b45afb0e658fb80728ff77833a4f", - "keys": [ - "0x2b61c43a85bd35987c5311215e8288b823a6873e", - "0xbf0a60652fafd614e9b6be2ca74a54a9", - "0x8106949def8a44172f54941ce774c774", - "0xa", - "0x0", - "0xb", - "0x0", - "0xa", - "0x0" - ], - "data": [] -} diff --git a/tests/test_utils/eth_provider/mod.rs b/tests/test_utils/eth_provider/mod.rs deleted file mode 100644 index e13e6e4ac..000000000 --- a/tests/test_utils/eth_provider/mod.rs +++ /dev/null @@ -1,31 +0,0 @@ -use std::sync::Mutex; - -use kakarot_rpc::{eth_provider::provider::MockEthereumProvider, starknet_client::constants::CHAIN_ID}; -use lazy_static::lazy_static; -use reth_primitives::U64; - -lazy_static! { - /// A Mutex-wrapped U64 that is used to mock the block number. - pub static ref MOCK_BLOCK_NUMBER: Mutex = Mutex::new(U64::from(0)); -} - -/// Returns a MockEthereumProvider that can be used to mock the EthereumProvider. -pub fn mock_ethereum_provider() -> MockEthereumProvider { - let mut eth_db = MockEthereumProvider::new(); - - eth_db.expect_chain_id().returning(|| Box::pin(async { Ok(Some(U64::from(CHAIN_ID))) })); - - // In order to increment the block number, we use a Mutex to lock the block number and increment - // it by 1 each time the function is called. - eth_db.expect_block_number().returning(|| { - Box::pin(async { - let mut lock = MOCK_BLOCK_NUMBER.lock().unwrap(); - let block_number = *lock; - *lock += U64::from(1); - drop(lock); - Ok(block_number) - }) - }); - - eth_db -} diff --git a/tests/transaction.rs b/tests/transaction.rs deleted file mode 100644 index 93f6dc252..000000000 --- a/tests/transaction.rs +++ /dev/null @@ -1,66 +0,0 @@ -#[cfg(test)] -mod test_utils; - -use kakarot_rpc::models::felt::Felt252Wrapper; -use kakarot_rpc::models::transaction::transaction::StarknetTransaction; -use reth_primitives::U256; -use rstest::*; -use starknet::core::types::{BlockId, MaybePendingTransactionReceipt, TransactionReceipt}; -use starknet::providers::Provider; -use test_utils::evm_contract::KakarotEvmContract; -use test_utils::fixtures::counter; -use test_utils::sequencer::Katana; - -#[rstest] -#[awt] -#[tokio::test(flavor = "multi_thread")] -async fn test_to_eth_transaction(#[future] counter: (Katana, KakarotEvmContract)) { - // Increment a counter - let katana: Katana = counter.0; - let counter = counter.1; - let client = katana.client(); - let eoa = katana.eoa(); - let starknet_tx_hash = eoa.call_evm_contract(&counter, "inc", (), 0).await.expect("Failed to increment counter"); - - // Query transaction - let tx = client - .starknet_provider() - .get_transaction_by_hash(starknet_tx_hash) - .await - .expect("Failed to query transaction"); - let starknet_tx: StarknetTransaction = tx.into(); - - // Get additional tx information block_number, block_hash, transaction_index - let tx_receipt = client - .starknet_provider() - .get_transaction_receipt(starknet_tx_hash) - .await - .expect("Failed to query transaction receipt"); - let (block_number, block_hash, transaction_index) = match tx_receipt { - MaybePendingTransactionReceipt::Receipt(tx_receipt) => { - if let TransactionReceipt::Invoke(tx_receipt) = tx_receipt { - let block_number = tx_receipt.block_number; - let block_hash = tx_receipt.block_hash; - let transaction_index = client - .starknet_provider() - .get_block_with_tx_hashes(BlockId::Hash(block_hash)) - .await - .unwrap() - .transactions() - .binary_search(&starknet_tx_hash) - .unwrap(); - ( - Some(U256::from(block_number)), - Some(Felt252Wrapper::from(block_hash).into()), - Some(U256::from(transaction_index)), - ) - } else { - panic!("Transaction receipt not found or not invoke") - } - } - MaybePendingTransactionReceipt::PendingReceipt(_) => (None, None, None), - }; - let _eth_tx = starknet_tx.to_eth_transaction(client, block_hash, block_number, transaction_index).await.unwrap(); - - // TODO: Assert that the transaction is valid -} diff --git a/tests/transaction_receipt.rs b/tests/transaction_receipt.rs deleted file mode 100644 index 0e826ab32..000000000 --- a/tests/transaction_receipt.rs +++ /dev/null @@ -1,35 +0,0 @@ -#[cfg(test)] -mod test_utils; - -use crate::test_utils::evm_contract::KakarotEvmContract; -use kakarot_rpc::models::felt::Felt252Wrapper; -use kakarot_rpc::models::transaction_receipt::StarknetTransactionReceipt; -use rstest::*; -use starknet::providers::Provider; -use test_utils::fixtures::counter; -use test_utils::sequencer::Katana; - -#[rstest] -#[awt] -#[tokio::test(flavor = "multi_thread")] -async fn test_to_eth_transaction_receipt(#[future] counter: (Katana, KakarotEvmContract)) { - let katana: Katana = counter.0; - let counter = counter.1; - let client = katana.client(); - let eoa = katana.eoa(); - let starknet_tx_hash = eoa.call_evm_contract(&counter, "inc", (), 0).await.expect("Failed to increment counter"); - - // Get additional tx information block_number, block_hash, transaction_index - let tx_receipt = client - .starknet_provider() - .get_transaction_receipt(starknet_tx_hash) - .await - .expect("Failed to query transaction receipt"); - - let starknet_transaction_receipt: StarknetTransactionReceipt = tx_receipt.into(); - let eth_transaction_receipt = - starknet_transaction_receipt.to_eth_transaction_receipt(client).await.unwrap().unwrap(); - - // TODO: Assert that the transaction receipt is valid - assert_eq!(eth_transaction_receipt.transaction_hash.unwrap(), Felt252Wrapper::from(starknet_tx_hash).into()); -}