diff --git a/Cargo.lock b/Cargo.lock index 1bb69e2124..745ad8d2ef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1168,7 +1168,7 @@ dependencies = [ "quote", "serde_json", "starknet", - "syn 2.0.41", + "syn 2.0.47", "thiserror", ] @@ -1196,7 +1196,7 @@ dependencies = [ "quote", "serde_json", "starknet", - "syn 2.0.41", + "syn 2.0.47", "thiserror", ] @@ -2722,7 +2722,7 @@ checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" [[package]] name = "dojo-bindgen" -version = "0.5.0" +version = "0.5.1-alpha.0" dependencies = [ "async-trait", "cainome 0.1.5 (git+https://github.com/cartridge-gg/cainome?tag=v0.2.2)", @@ -2830,6 +2830,7 @@ dependencies = [ "katana-core", "katana-primitives", "katana-rpc", + "katana-rpc-api", "scarb", "scarb-ui", "serde", @@ -4623,6 +4624,26 @@ dependencies = [ "walkdir", ] +[[package]] +name = "gloo-net" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9902a044653b26b99f7e3693a42f171312d9be8b26b5697bd1e43ad1f8a35e10" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "gloo-timers" version = "0.2.6" @@ -4635,6 +4656,19 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "gloo-utils" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037fcb07216cb3a30f7292bd0176b050b7b9a052ba830ef7d5d65f6dc64ba58e" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "good_lp" version = "1.7.0" @@ -4957,9 +4991,12 @@ dependencies = [ "futures-util", "http", "hyper", + "log", "rustls 0.21.10", + "rustls-native-certs", "tokio", "tokio-rustls 0.24.1", + "webpki-roots", ] [[package]] @@ -5389,13 +5426,42 @@ version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "367a292944c07385839818bb71c8d76611138e2dedb0677d035b8da21d29c78b" dependencies = [ + "jsonrpsee-client-transport", "jsonrpsee-core", + "jsonrpsee-http-client", "jsonrpsee-proc-macros", "jsonrpsee-server", "jsonrpsee-types", + "jsonrpsee-wasm-client", + "jsonrpsee-ws-client", "tracing", ] +[[package]] +name = "jsonrpsee-client-transport" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8b3815d9f5d5de348e5f162b316dc9cdf4548305ebb15b4eb9328e66cf27d7a" +dependencies = [ + "anyhow", + "futures-channel", + "futures-timer", + "futures-util", + "gloo-net", + "http", + "jsonrpsee-core", + "jsonrpsee-types", + "pin-project", + "rustls-native-certs", + "soketto", + "thiserror", + "tokio", + "tokio-rustls 0.24.1", + "tokio-util", + "tracing", + "webpki-roots", +] + [[package]] name = "jsonrpsee-core" version = "0.16.3" @@ -5404,9 +5470,11 @@ checksum = "2b5dde66c53d6dcdc8caea1874a45632ec0fcf5b437789f1e45766a1512ce803" dependencies = [ "anyhow", "arrayvec", + "async-lock 2.8.0", "async-trait", "beef", "futures-channel", + "futures-timer", "futures-util", "globset", "hyper", @@ -5420,6 +5488,26 @@ dependencies = [ "thiserror", "tokio", "tracing", + "wasm-bindgen-futures", +] + +[[package]] +name = "jsonrpsee-http-client" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e5f9fabdd5d79344728521bb65e3106b49ec405a78b66fbff073b72b389fa43" +dependencies = [ + "async-trait", + "hyper", + "hyper-rustls 0.24.2", + "jsonrpsee-core", + "jsonrpsee-types", + "rustc-hash", + "serde", + "serde_json", + "thiserror", + "tokio", + "tracing", ] [[package]] @@ -5471,6 +5559,29 @@ dependencies = [ "tracing", ] +[[package]] +name = "jsonrpsee-wasm-client" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18e5df77c8f625d36e4cfb583c5a674eccebe32403fcfe42f7ceff7fac9324dd" +dependencies = [ + "jsonrpsee-client-transport", + "jsonrpsee-core", + "jsonrpsee-types", +] + +[[package]] +name = "jsonrpsee-ws-client" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e1b3975ed5d73f456478681a417128597acd6a2487855fdb7b4a3d4d195bf5e" +dependencies = [ + "http", + "jsonrpsee-client-transport", + "jsonrpsee-core", + "jsonrpsee-types", +] + [[package]] name = "jsonwebtoken" version = "8.3.0" @@ -5520,6 +5631,7 @@ dependencies = [ "katana-core", "katana-primitives", "katana-rpc", + "katana-rpc-api", "metrics 0.5.1-alpha.0", "metrics-process", "serde_json", @@ -5684,8 +5796,10 @@ dependencies = [ "katana-executor", "katana-primitives", "katana-provider", + "katana-rpc-api", "katana-rpc-types", "katana-rpc-types-builder", + "katana-tasks", "serde", "serde_json", "serde_with", @@ -5699,17 +5813,32 @@ dependencies = [ "url", ] +[[package]] +name = "katana-rpc-api" +version = "0.5.1-alpha.0" +dependencies = [ + "jsonrpsee", + "katana-core", + "katana-primitives", + "katana-rpc-types", + "starknet", +] + [[package]] name = "katana-rpc-types" version = "0.5.1-alpha.0" dependencies = [ "anyhow", "derive_more", + "jsonrpsee", + "katana-core", "katana-primitives", + "katana-provider", "serde", "serde_json", "serde_with", "starknet", + "thiserror", ] [[package]] @@ -5736,6 +5865,16 @@ dependencies = [ "url", ] +[[package]] +name = "katana-tasks" +version = "0.5.1-alpha.0" +dependencies = [ + "futures", + "rayon", + "thiserror", + "tokio", +] + [[package]] name = "keccak" version = "0.1.4" diff --git a/Cargo.toml b/Cargo.toml index b0a8ef8aba..76576ea096 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,8 @@ members = [ "crates/katana/core", "crates/katana/executor", "crates/katana/primitives", - "crates/katana/rpc", + "crates/katana/rpc/rpc", + "crates/katana/rpc/rpc-api", "crates/katana/rpc/rpc-types", "crates/katana/rpc/rpc-types-builder", "crates/katana/runner", @@ -89,6 +90,7 @@ futures = "0.3.28" hex = "0.4.3" indoc = "1.0.7" itertools = "0.10.3" +jsonrpsee = { version = "0.16.2", default-features = false } lazy_static = "1.4.0" metrics-process = "1.0.9" num-bigint = "0.4" diff --git a/crates/dojo-test-utils/Cargo.toml b/crates/dojo-test-utils/Cargo.toml index 5e549eb37c..a69649fd02 100644 --- a/crates/dojo-test-utils/Cargo.toml +++ b/crates/dojo-test-utils/Cargo.toml @@ -19,7 +19,8 @@ dojo-world = { path = "../dojo-world", features = [ "manifest", "migration" ] } jsonrpsee = { version = "0.16.2", features = [ "server" ] } katana-core = { path = "../katana/core" } katana-primitives = { path = "../katana/primitives" } -katana-rpc = { path = "../katana/rpc" } +katana-rpc = { path = "../katana/rpc/rpc" } +katana-rpc-api = { path = "../katana/rpc/rpc-api" } scarb-ui.workspace = true scarb.workspace = true serde.workspace = true diff --git a/crates/dojo-test-utils/src/sequencer.rs b/crates/dojo-test-utils/src/sequencer.rs index 9d44119b75..4726363183 100644 --- a/crates/dojo-test-utils/src/sequencer.rs +++ b/crates/dojo-test-utils/src/sequencer.rs @@ -5,9 +5,9 @@ pub use katana_core::backend::config::{Environment, StarknetConfig}; use katana_core::sequencer::KatanaSequencer; pub use katana_core::sequencer::SequencerConfig; use katana_primitives::chain::ChainId; -use katana_rpc::api::ApiKind; use katana_rpc::config::ServerConfig; use katana_rpc::{spawn, NodeHandle}; +use katana_rpc_api::ApiKind; use starknet::accounts::{ExecutionEncoding, SingleOwnerAccount}; use starknet::core::chain_id; use starknet::core::types::FieldElement; diff --git a/crates/katana/Cargo.toml b/crates/katana/Cargo.toml index 5196c53e50..992199c119 100644 --- a/crates/katana/Cargo.toml +++ b/crates/katana/Cargo.toml @@ -12,7 +12,8 @@ clap_complete.workspace = true console.workspace = true katana-core = { path = "core" } katana-primitives = { path = "primitives" } -katana-rpc = { path = "rpc" } +katana-rpc = { path = "rpc/rpc" } +katana-rpc-api = { path = "rpc/rpc-api" } metrics = { path = "../metrics" } metrics-process.workspace = true serde_json.workspace = true diff --git a/crates/katana/core/src/accounts.rs b/crates/katana/core/src/accounts.rs index f602a38e34..4f6c3d82f4 100644 --- a/crates/katana/core/src/accounts.rs +++ b/crates/katana/core/src/accounts.rs @@ -6,7 +6,7 @@ use katana_primitives::FieldElement; use katana_provider::traits::state::StateWriter; use rand::rngs::SmallRng; use rand::{RngCore, SeedableRng}; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use serde_with::serde_as; use starknet::core::serde::unsigned_field_element::UfeHex; use starknet::core::utils::{get_contract_address, get_storage_var_address}; @@ -15,7 +15,7 @@ use starknet::signers::SigningKey; use crate::constants::{FEE_TOKEN_ADDRESS, OZ_V1_ACCOUNT_CONTRACT_CLASS_HASH}; #[serde_as] -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct Account { #[serde_as(as = "UfeHex")] pub balance: FieldElement, diff --git a/crates/katana/core/src/sequencer.rs b/crates/katana/core/src/sequencer.rs index 1f2b85dbde..16c3ca9b7e 100644 --- a/crates/katana/core/src/sequencer.rs +++ b/crates/katana/core/src/sequencer.rs @@ -267,8 +267,9 @@ impl KatanaSequencer { self.backend.chain_id } - pub fn block_number(&self) -> BlockNumber { - BlockNumberProvider::latest_number(&self.backend.blockchain.provider()).unwrap() + pub fn block_number(&self) -> SequencerResult { + let num = BlockNumberProvider::latest_number(&self.backend.blockchain.provider())?; + Ok(num) } pub fn block_tx_count(&self, block_id: BlockIdOrTag) -> SequencerResult> { @@ -300,7 +301,7 @@ impl KatanaSequencer { Ok(count) } - pub async fn nonce_at( + pub fn nonce_at( &self, block_id: BlockIdOrTag, contract_address: ContractAddress, @@ -352,7 +353,7 @@ impl KatanaSequencer { Ok(tx) } - pub async fn events( + pub fn events( &self, from_block: BlockIdOrTag, to_block: BlockIdOrTag, diff --git a/crates/katana/rpc/rpc-api/Cargo.toml b/crates/katana/rpc/rpc-api/Cargo.toml new file mode 100644 index 0000000000..f9fac4650f --- /dev/null +++ b/crates/katana/rpc/rpc-api/Cargo.toml @@ -0,0 +1,18 @@ +[package] +description = "Katana RPC APIs" +edition.workspace = true +name = "katana-rpc-api" +version.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +katana-core = { path = "../../core" } +katana-primitives = { path = "../../primitives" } +katana-rpc-types = { path = "../rpc-types" } + +jsonrpsee = { workspace = true, features = [ "macros", "server" ] } +starknet.workspace = true + +[features] +client = [ "jsonrpsee/client" ] diff --git a/crates/katana/rpc/rpc-api/src/katana.rs b/crates/katana/rpc/rpc-api/src/katana.rs new file mode 100644 index 0000000000..9ef3dc99b5 --- /dev/null +++ b/crates/katana/rpc/rpc-api/src/katana.rs @@ -0,0 +1,31 @@ +use jsonrpsee::core::RpcResult; +use jsonrpsee::proc_macros::rpc; +use katana_core::accounts::Account; +use katana_primitives::FieldElement; + +#[cfg_attr(not(feature = "client"), rpc(server, namespace = "katana"))] +#[cfg_attr(feature = "client", rpc(client, server, namespace = "katana"))] +pub trait KatanaApi { + #[method(name = "generateBlock")] + async fn generate_block(&self) -> RpcResult<()>; + + #[method(name = "nextBlockTimestamp")] + async fn next_block_timestamp(&self) -> RpcResult; + + #[method(name = "setNextBlockTimestamp")] + async fn set_next_block_timestamp(&self, timestamp: u64) -> RpcResult<()>; + + #[method(name = "increaseNextBlockTimestamp")] + async fn increase_next_block_timestamp(&self, timestamp: u64) -> RpcResult<()>; + + #[method(name = "predeployedAccounts")] + async fn predeployed_accounts(&self) -> RpcResult>; + + #[method(name = "setStorageAt")] + async fn set_storage_at( + &self, + contract_address: FieldElement, + key: FieldElement, + value: FieldElement, + ) -> RpcResult<()>; +} diff --git a/crates/katana/rpc/src/api/mod.rs b/crates/katana/rpc/rpc-api/src/lib.rs similarity index 100% rename from crates/katana/rpc/src/api/mod.rs rename to crates/katana/rpc/rpc-api/src/lib.rs diff --git a/crates/katana/rpc/rpc-api/src/starknet.rs b/crates/katana/rpc/rpc-api/src/starknet.rs new file mode 100644 index 0000000000..cc9090014b --- /dev/null +++ b/crates/katana/rpc/rpc-api/src/starknet.rs @@ -0,0 +1,181 @@ +use jsonrpsee::core::RpcResult; +use jsonrpsee::proc_macros::rpc; +use katana_primitives::block::{BlockIdOrTag, BlockNumber}; +use katana_primitives::transaction::TxHash; +use katana_primitives::FieldElement; +use katana_rpc_types::block::{ + BlockHashAndNumber, BlockTxCount, MaybePendingBlockWithTxHashes, MaybePendingBlockWithTxs, +}; +use katana_rpc_types::event::{EventFilterWithPage, EventsPage}; +use katana_rpc_types::message::MsgFromL1; +use katana_rpc_types::receipt::MaybePendingTxReceipt; +use katana_rpc_types::state_update::StateUpdate; +use katana_rpc_types::transaction::{ + BroadcastedDeclareTx, BroadcastedDeployAccountTx, BroadcastedInvokeTx, BroadcastedTx, + DeclareTxResult, DeployAccountTxResult, InvokeTxResult, Tx, +}; +use katana_rpc_types::{ContractClass, FeeEstimate, FeltAsHex, FunctionCall, SyncingStatus}; +use starknet::core::types::TransactionStatus; + +/// Starknet JSON-RPC APIs: +#[cfg_attr(not(feature = "client"), rpc(server, namespace = "starknet"))] +#[cfg_attr(feature = "client", rpc(client, server, namespace = "starknet"))] +pub trait StarknetApi { + /// Returns the version of the Starknet JSON-RPC specification being used. + #[method(name = "specVersion")] + async fn spec_version(&self) -> RpcResult { + Ok("0.5.1".into()) + } + + /// Get block information with transaction hashes given the block id. + #[method(name = "getBlockWithTxHashes")] + async fn block_with_tx_hashes( + &self, + block_id: BlockIdOrTag, + ) -> RpcResult; + + /// Get block information with full transactions given the block id. + #[method(name = "getBlockWithTxs")] + async fn block_with_txs(&self, block_id: BlockIdOrTag) -> RpcResult; + + /// Get the information about the result of executing the requested block. + #[method(name = "getStateUpdate")] + async fn state_update(&self, block_id: BlockIdOrTag) -> RpcResult; + + /// Get the value of the storage at the given address and key + #[method(name = "getStorageAt")] + async fn storage_at( + &self, + contract_address: FieldElement, + key: FieldElement, + block_id: BlockIdOrTag, + ) -> RpcResult; + + /// Gets the transaction status (possibly reflecting that the tx is still in the mempool, or + /// dropped from it). + #[method(name = "getTransactionStatus")] + async fn transaction_status(&self, transaction_hash: TxHash) -> RpcResult; + + /// Get the details and status of a submitted transaction. + #[method(name = "getTransactionByHash")] + async fn transaction_by_hash(&self, transaction_hash: TxHash) -> RpcResult; + + /// Get the details of a transaction by a given block id and index. + #[method(name = "getTransactionByBlockIdAndIndex")] + async fn transaction_by_block_id_and_index( + &self, + block_id: BlockIdOrTag, + index: u64, + ) -> RpcResult; + + /// Get the transaction receipt by the transaction hash. + #[method(name = "getTransactionReceipt")] + async fn transaction_receipt( + &self, + transaction_hash: TxHash, + ) -> RpcResult; + + /// Get the contract class definition in the given block associated with the given hash. + #[method(name = "getClass")] + async fn class( + &self, + block_id: BlockIdOrTag, + class_hash: FieldElement, + ) -> RpcResult; + + /// Get the contract class hash in the given block for the contract deployed at the given + /// address. + #[method(name = "getClassHashAt")] + async fn class_hash_at( + &self, + block_id: BlockIdOrTag, + contract_address: FieldElement, + ) -> RpcResult; + + /// Get the contract class definition in the given block at the given address. + #[method(name = "getClassAt")] + async fn class_at( + &self, + block_id: BlockIdOrTag, + contract_address: FieldElement, + ) -> RpcResult; + + /// Get the number of transactions in a block given a block id. + #[method(name = "getBlockTransactionCount")] + async fn block_transaction_count(&self, block_id: BlockIdOrTag) -> RpcResult; + + /// Call a starknet function without creating a StarkNet transaction. + #[method(name = "call")] + async fn call( + &self, + request: FunctionCall, + block_id: BlockIdOrTag, + ) -> RpcResult>; + + /// Estimate the fee for of StarkNet transactions. + #[method(name = "estimateFee")] + async fn estimate_fee( + &self, + request: Vec, + block_id: BlockIdOrTag, + ) -> RpcResult>; + + /// Estimate the L2 fee of a message sent on L1. + #[method(name = "estimateMessageFee")] + async fn estimate_message_fee( + &self, + message: MsgFromL1, + block_id: BlockIdOrTag, + ) -> RpcResult; + + /// Get the most recent accepted block number. + #[method(name = "blockNumber")] + async fn block_number(&self) -> RpcResult; + + /// Get the most recent accepted block hash and number. + #[method(name = "blockHashAndNumber")] + async fn block_hash_and_number(&self) -> RpcResult; + + /// Return the currently configured StarkNet chain id. + #[method(name = "chainId")] + async fn chain_id(&self) -> RpcResult; + + /// Returns an object about the sync status, or false if the node is not synching. + #[method(name = "syncing")] + async fn syncing(&self) -> RpcResult { + Ok(SyncingStatus::False) + } + + /// Returns all event objects matching the conditions in the provided filter. + #[method(name = "getEvents")] + async fn events(&self, filter: EventFilterWithPage) -> RpcResult; + + /// Get the nonce associated with the given address in the given block. + #[method(name = "getNonce")] + async fn nonce( + &self, + block_id: BlockIdOrTag, + contract_address: FieldElement, + ) -> RpcResult; + + /// Submit a new transaction to be added to the chain. + #[method(name = "addInvokeTransaction")] + async fn add_invoke_transaction( + &self, + invoke_transaction: BroadcastedInvokeTx, + ) -> RpcResult; + + /// Submit a new class declaration transaction. + #[method(name = "addDeclareTransaction")] + async fn add_declare_transaction( + &self, + declare_transaction: BroadcastedDeclareTx, + ) -> RpcResult; + + /// Submit a new deploy account transaction. + #[method(name = "addDeployAccountTransaction")] + async fn add_deploy_account_transaction( + &self, + deploy_account_transaction: BroadcastedDeployAccountTx, + ) -> RpcResult; +} diff --git a/crates/katana/rpc/rpc-types/Cargo.toml b/crates/katana/rpc/rpc-types/Cargo.toml index 792d2cccc2..a8760330b0 100644 --- a/crates/katana/rpc/rpc-types/Cargo.toml +++ b/crates/katana/rpc/rpc-types/Cargo.toml @@ -7,13 +7,17 @@ version.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +katana-core = { path = "../../core" } katana-primitives = { path = "../../primitives" } +katana-provider = { path = "../../storage/provider" } anyhow.workspace = true derive_more.workspace = true +jsonrpsee = { workspace = true, features = [ "macros", "server" ] } serde.workspace = true serde_with.workspace = true starknet.workspace = true +thiserror.workspace = true [dev-dependencies] serde_json.workspace = true diff --git a/crates/katana/rpc/rpc-types/src/block.rs b/crates/katana/rpc/rpc-types/src/block.rs index bbfc2d7f30..9d5b49725b 100644 --- a/crates/katana/rpc/rpc-types/src/block.rs +++ b/crates/katana/rpc/rpc-types/src/block.rs @@ -1,11 +1,11 @@ use katana_primitives::block::{Block, BlockHash, BlockNumber, FinalityStatus, PartialHeader}; use katana_primitives::transaction::{TxHash, TxWithHash}; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use starknet::core::types::{BlockStatus, ResourcePrice}; pub type BlockTxCount = u64; -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(transparent)] pub struct BlockWithTxs(starknet::core::types::BlockWithTxs); @@ -37,7 +37,7 @@ impl BlockWithTxs { } } -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(transparent)] pub struct PendingBlockWithTxs(starknet::core::types::PendingBlockWithTxs); @@ -62,14 +62,14 @@ impl PendingBlockWithTxs { } } -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum MaybePendingBlockWithTxs { Pending(PendingBlockWithTxs), Block(BlockWithTxs), } -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(transparent)] pub struct BlockWithTxHashes(starknet::core::types::BlockWithTxHashes); @@ -102,7 +102,7 @@ impl BlockWithTxHashes { } } -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(transparent)] pub struct PendingBlockWithTxHashes(starknet::core::types::PendingBlockWithTxHashes); @@ -124,14 +124,14 @@ impl PendingBlockWithTxHashes { } } -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum MaybePendingBlockWithTxHashes { Pending(PendingBlockWithTxHashes), Block(BlockWithTxHashes), } -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(transparent)] pub struct BlockHashAndNumber(starknet::core::types::BlockHashAndNumber); diff --git a/crates/katana/rpc/rpc-types/src/error/katana.rs b/crates/katana/rpc/rpc-types/src/error/katana.rs new file mode 100644 index 0000000000..24013845bb --- /dev/null +++ b/crates/katana/rpc/rpc-types/src/error/katana.rs @@ -0,0 +1,20 @@ +use jsonrpsee::core::Error; +use jsonrpsee::types::error::CallError; +use jsonrpsee::types::ErrorObject; + +#[derive(thiserror::Error, Clone, Copy, Debug)] +#[allow(clippy::enum_variant_names)] +pub enum KatanaApiError { + #[error("Failed to change next block timestamp.")] + FailedToChangeNextBlockTimestamp = 1, + #[error("Failed to dump state.")] + FailedToDumpState = 2, + #[error("Failed to update storage.")] + FailedToUpdateStorage = 3, +} + +impl From for Error { + fn from(err: KatanaApiError) -> Self { + Error::Call(CallError::Custom(ErrorObject::owned(err as i32, err.to_string(), None::<()>))) + } +} diff --git a/crates/katana/rpc/rpc-types/src/error/mod.rs b/crates/katana/rpc/rpc-types/src/error/mod.rs new file mode 100644 index 0000000000..608feeefd5 --- /dev/null +++ b/crates/katana/rpc/rpc-types/src/error/mod.rs @@ -0,0 +1,2 @@ +pub mod katana; +pub mod starknet; diff --git a/crates/katana/rpc/src/api/starknet.rs b/crates/katana/rpc/rpc-types/src/error/starknet.rs similarity index 52% rename from crates/katana/rpc/src/api/starknet.rs rename to crates/katana/rpc/rpc-types/src/error/starknet.rs index b3833488ed..564f704bb3 100644 --- a/crates/katana/rpc/src/api/starknet.rs +++ b/crates/katana/rpc/rpc-types/src/error/starknet.rs @@ -1,26 +1,12 @@ use jsonrpsee::core::Error; -use jsonrpsee::proc_macros::rpc; -use jsonrpsee::types::error::{CallError, ErrorObject}; +use jsonrpsee::types::error::CallError; +use jsonrpsee::types::ErrorObject; use katana_core::sequencer_error::SequencerError; -use katana_primitives::block::{BlockIdOrTag, BlockNumber}; -use katana_primitives::transaction::TxHash; -use katana_primitives::FieldElement; use katana_provider::error::ProviderError; -use katana_rpc_types::block::{ - BlockHashAndNumber, BlockTxCount, MaybePendingBlockWithTxHashes, MaybePendingBlockWithTxs, -}; -use katana_rpc_types::event::{EventFilterWithPage, EventsPage}; -use katana_rpc_types::message::MsgFromL1; -use katana_rpc_types::receipt::MaybePendingTxReceipt; -use katana_rpc_types::state_update::StateUpdate; -use katana_rpc_types::transaction::{ - BroadcastedDeclareTx, BroadcastedDeployAccountTx, BroadcastedInvokeTx, BroadcastedTx, - DeclareTxResult, DeployAccountTxResult, InvokeTxResult, Tx, -}; -use katana_rpc_types::{ContractClass, FeeEstimate, FeltAsHex, FunctionCall}; -use starknet::core::types::{ContractErrorData, TransactionStatus}; +use starknet::core::types::ContractErrorData; -#[derive(thiserror::Error, Clone, Debug)] +/// Possible list of errors that can be returned by the Starknet API according to the spec: . +#[derive(Debug, thiserror::Error, Clone)] #[repr(i32)] pub enum StarknetApiError { #[error("Failed to write transaction")] @@ -119,6 +105,11 @@ impl StarknetApiError { } } +#[derive(serde::Serialize, serde::Deserialize)] +struct UnexpectedError { + reason: String, +} + impl From for StarknetApiError { fn from(value: ProviderError) -> Self { StarknetApiError::UnexpectedError { reason: value.to_string() } @@ -136,11 +127,6 @@ impl From for Error { } StarknetApiError::UnexpectedError { reason } => { - #[derive(serde::Serialize, serde::Deserialize)] - struct UnexpectedError { - reason: String, - } - ErrorObject::owned(code, message, Some(UnexpectedError { reason })) } @@ -166,142 +152,3 @@ impl From for StarknetApiError { } } } - -#[rpc(server, namespace = "starknet")] -pub trait StarknetApi { - // Read API - - #[method(name = "specVersion")] - async fn spec_version(&self) -> Result { - Ok("0.5.1".into()) - } - - #[method(name = "chainId")] - async fn chain_id(&self) -> Result; - - #[method(name = "getNonce")] - async fn nonce( - &self, - block_id: BlockIdOrTag, - contract_address: FieldElement, - ) -> Result; - - #[method(name = "blockNumber")] - async fn block_number(&self) -> Result; - - #[method(name = "getTransactionByHash")] - async fn transaction_by_hash(&self, transaction_hash: TxHash) -> Result; - - #[method(name = "getBlockTransactionCount")] - async fn block_transaction_count(&self, block_id: BlockIdOrTag) -> Result; - - #[method(name = "getClassAt")] - async fn class_at( - &self, - block_id: BlockIdOrTag, - contract_address: FieldElement, - ) -> Result; - - #[method(name = "blockHashAndNumber")] - async fn block_hash_and_number(&self) -> Result; - - #[method(name = "getBlockWithTxHashes")] - async fn block_with_tx_hashes( - &self, - block_id: BlockIdOrTag, - ) -> Result; - - #[method(name = "getTransactionByBlockIdOrTagAndIndex")] - async fn transaction_by_block_id_and_index( - &self, - block_id: BlockIdOrTag, - index: u64, - ) -> Result; - - #[method(name = "getBlockWithTxs")] - async fn block_with_txs( - &self, - block_id: BlockIdOrTag, - ) -> Result; - - #[method(name = "getStateUpdate")] - async fn state_update(&self, block_id: BlockIdOrTag) -> Result; - - #[method(name = "getTransactionReceipt")] - async fn transaction_receipt( - &self, - transaction_hash: TxHash, - ) -> Result; - - #[method(name = "getTransactionStatus")] - async fn transaction_status( - &self, - transaction_hash: TxHash, - ) -> Result; - - #[method(name = "getClassHashAt")] - async fn class_hash_at( - &self, - block_id: BlockIdOrTag, - contract_address: FieldElement, - ) -> Result; - - #[method(name = "getClass")] - async fn class( - &self, - block_id: BlockIdOrTag, - class_hash: FieldElement, - ) -> Result; - - #[method(name = "getEvents")] - async fn events(&self, filter: EventFilterWithPage) -> Result; - - #[method(name = "estimateFee")] - async fn estimate_fee( - &self, - request: Vec, - block_id: BlockIdOrTag, - ) -> Result, Error>; - - #[method(name = "estimateMessageFee")] - async fn estimate_message_fee( - &self, - message: MsgFromL1, - block_id: BlockIdOrTag, - ) -> Result; - - #[method(name = "call")] - async fn call( - &self, - request: FunctionCall, - block_id: BlockIdOrTag, - ) -> Result, Error>; - - #[method(name = "getStorageAt")] - async fn storage_at( - &self, - contract_address: FieldElement, - key: FieldElement, - block_id: BlockIdOrTag, - ) -> Result; - - // Write API - - #[method(name = "addDeployAccountTransaction")] - async fn add_deploy_account_transaction( - &self, - deploy_account_transaction: BroadcastedDeployAccountTx, - ) -> Result; - - #[method(name = "addDeclareTransaction")] - async fn add_declare_transaction( - &self, - declare_transaction: BroadcastedDeclareTx, - ) -> Result; - - #[method(name = "addInvokeTransaction")] - async fn add_invoke_transaction( - &self, - invoke_transaction: BroadcastedInvokeTx, - ) -> Result; -} diff --git a/crates/katana/rpc/rpc-types/src/lib.rs b/crates/katana/rpc/rpc-types/src/lib.rs index e5035af7ed..5afb4e5184 100644 --- a/crates/katana/rpc/rpc-types/src/lib.rs +++ b/crates/katana/rpc/rpc-types/src/lib.rs @@ -4,6 +4,7 @@ //! `starknet-rs`. pub mod block; +pub mod error; pub mod event; pub mod message; pub mod receipt; @@ -15,6 +16,7 @@ use std::ops::Deref; use serde::{Deserialize, Serialize}; use serde_with::serde_as; use starknet::core::serde::unsigned_field_element::UfeHex; +use starknet::core::types::SyncStatus; /// A wrapper around [`FieldElement`](katana_primitives::FieldElement) that serializes to hex as /// default. @@ -47,6 +49,15 @@ pub type FeeEstimate = starknet::core::types::FeeEstimate; pub type ContractClass = starknet::core::types::ContractClass; +/// The state of the node synchronization. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum SyncingStatus { + #[serde(rename = "FALSE")] + False, + #[serde(untagged)] + Status(SyncStatus), +} + #[cfg(test)] mod tests { use serde_json::json; diff --git a/crates/katana/rpc/rpc-types/src/message.rs b/crates/katana/rpc/rpc-types/src/message.rs index 3b6c37b446..e9f1e2981b 100644 --- a/crates/katana/rpc/rpc-types/src/message.rs +++ b/crates/katana/rpc/rpc-types/src/message.rs @@ -2,9 +2,9 @@ use katana_primitives::chain::ChainId; use katana_primitives::transaction::L1HandlerTx; use katana_primitives::utils::transaction::compute_l1_message_hash; use katana_primitives::FieldElement; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; -#[derive(Debug, Clone, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct MsgFromL1(starknet::core::types::MsgFromL1); impl MsgFromL1 { diff --git a/crates/katana/rpc/rpc-types/src/receipt.rs b/crates/katana/rpc/rpc-types/src/receipt.rs index 0d7ac49838..8c6f86fad3 100644 --- a/crates/katana/rpc/rpc-types/src/receipt.rs +++ b/crates/katana/rpc/rpc-types/src/receipt.rs @@ -1,7 +1,7 @@ use katana_primitives::block::{BlockHash, BlockNumber, FinalityStatus}; use katana_primitives::receipt::{MessageToL1, Receipt, TxExecutionResources}; use katana_primitives::transaction::TxHash; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use starknet::core::types::{ DeclareTransactionReceipt, DeployAccountTransactionReceipt, ExecutionResult, Hash256, InvokeTransactionReceipt, L1HandlerTransactionReceipt, PendingDeclareTransactionReceipt, @@ -10,7 +10,7 @@ use starknet::core::types::{ TransactionReceipt, }; -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(transparent)] pub struct TxReceipt(starknet::core::types::TransactionReceipt); @@ -123,7 +123,7 @@ impl TxReceipt { } } -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(transparent)] pub struct PendingTxReceipt(starknet::core::types::PendingTransactionReceipt); @@ -213,7 +213,7 @@ impl PendingTxReceipt { } } -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum MaybePendingTxReceipt { Receipt(TxReceipt), diff --git a/crates/katana/rpc/rpc-types/src/state_update.rs b/crates/katana/rpc/rpc-types/src/state_update.rs index 456cbf7f99..a14c61e4b6 100644 --- a/crates/katana/rpc/rpc-types/src/state_update.rs +++ b/crates/katana/rpc/rpc-types/src/state_update.rs @@ -1,20 +1,20 @@ -use serde::Serialize; +use serde::{Deserialize, Serialize}; use starknet::core::types::{ ContractStorageDiffItem, DeclaredClassItem, DeployedContractItem, NonceUpdate, StorageEntry, }; -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum MaybePendingStateUpdate { Pending(PendingStateUpdate), Update(StateUpdate), } -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(transparent)] pub struct StateUpdate(starknet::core::types::StateUpdate); -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(transparent)] pub struct PendingStateUpdate(starknet::core::types::PendingStateUpdate); diff --git a/crates/katana/rpc/rpc-types/src/transaction.rs b/crates/katana/rpc/rpc-types/src/transaction.rs index 00aab586bc..a7445ffcaf 100644 --- a/crates/katana/rpc/rpc-types/src/transaction.rs +++ b/crates/katana/rpc/rpc-types/src/transaction.rs @@ -21,7 +21,7 @@ use starknet::core::types::{ }; use starknet::core::utils::get_contract_address; -#[derive(Debug, Clone, Deserialize, Deref)] +#[derive(Debug, Clone, Serialize, Deserialize, Deref)] #[serde(transparent)] pub struct BroadcastedInvokeTx(BroadcastedInvokeTransaction); @@ -39,7 +39,7 @@ impl BroadcastedInvokeTx { } } -#[derive(Debug, Clone, Deserialize, Deref)] +#[derive(Debug, Clone, Serialize, Deserialize, Deref)] #[serde(transparent)] pub struct BroadcastedDeclareTx(BroadcastedDeclareTransaction); @@ -108,7 +108,7 @@ impl BroadcastedDeclareTx { } } -#[derive(Debug, Clone, Deserialize, Deref)] +#[derive(Debug, Clone, Serialize, Deserialize, Deref)] #[serde(transparent)] pub struct BroadcastedDeployAccountTx(BroadcastedDeployAccountTransaction); @@ -135,7 +135,7 @@ impl BroadcastedDeployAccountTx { } } -#[derive(Debug, Clone, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum BroadcastedTx { Invoke(BroadcastedInvokeTx), @@ -143,19 +143,19 @@ pub enum BroadcastedTx { DeployAccount(BroadcastedDeployAccountTx), } -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(transparent)] pub struct Tx(pub starknet::core::types::Transaction); -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(transparent)] pub struct DeployAccountTxResult(DeployAccountTransactionResult); -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(transparent)] pub struct DeclareTxResult(DeclareTransactionResult); -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(transparent)] pub struct InvokeTxResult(InvokeTransactionResult); diff --git a/crates/katana/rpc/Cargo.toml b/crates/katana/rpc/rpc/Cargo.toml similarity index 65% rename from crates/katana/rpc/Cargo.toml rename to crates/katana/rpc/rpc/Cargo.toml index 33d21f9570..99051c2006 100644 --- a/crates/katana/rpc/Cargo.toml +++ b/crates/katana/rpc/rpc/Cargo.toml @@ -7,11 +7,14 @@ repository.workspace = true version.workspace = true [dependencies] -katana-executor = { path = "../executor" } -katana-primitives = { path = "../primitives" } -katana-provider = { path = "../storage/provider" } -katana-rpc-types = { path = "rpc-types" } -katana-rpc-types-builder = { path = "rpc-types-builder" } +katana-core = { path = "../../core" } +katana-executor = { path = "../../executor" } +katana-primitives = { path = "../../primitives" } +katana-provider = { path = "../../storage/provider" } +katana-rpc-api = { path = "../rpc-api" } +katana-rpc-types = { path = "../rpc-types" } +katana-rpc-types-builder = { path = "../rpc-types-builder" } +katana-tasks = { path = "../../tasks" } anyhow.workspace = true cairo-lang-starknet = "2.3.1" @@ -20,7 +23,6 @@ futures.workspace = true hex = { version = "0.4.3", default-features = false } hyper = "0.14.20" jsonrpsee = { version = "0.16.2", features = [ "macros", "server" ] } -katana-core = { path = "../core" } serde.workspace = true serde_json.workspace = true serde_with.workspace = true @@ -34,5 +36,5 @@ tracing.workspace = true [dev-dependencies] assert_matches = "1.5.0" -dojo-test-utils = { path = "../../dojo-test-utils" } +dojo-test-utils = { path = "../../../dojo-test-utils" } url.workspace = true diff --git a/crates/katana/rpc/src/config.rs b/crates/katana/rpc/rpc/src/config.rs similarity index 90% rename from crates/katana/rpc/src/config.rs rename to crates/katana/rpc/rpc/src/config.rs index 8fe1fc58a1..fd8c0848b2 100644 --- a/crates/katana/rpc/src/config.rs +++ b/crates/katana/rpc/rpc/src/config.rs @@ -1,4 +1,4 @@ -use crate::api::ApiKind; +use katana_rpc_api::ApiKind; #[derive(Debug, Clone)] pub struct ServerConfig { diff --git a/crates/katana/rpc/src/katana.rs b/crates/katana/rpc/rpc/src/katana.rs similarity index 94% rename from crates/katana/rpc/src/katana.rs rename to crates/katana/rpc/rpc/src/katana.rs index 43be36fcec..ffcb1ca3f7 100644 --- a/crates/katana/rpc/src/katana.rs +++ b/crates/katana/rpc/rpc/src/katana.rs @@ -4,8 +4,8 @@ use jsonrpsee::core::{async_trait, Error}; use katana_core::accounts::Account; use katana_core::sequencer::KatanaSequencer; use katana_primitives::FieldElement; - -use crate::api::katana::{KatanaApiError, KatanaApiServer}; +use katana_rpc_api::katana::KatanaApiServer; +use katana_rpc_types::error::katana::KatanaApiError; pub struct KatanaApi { sequencer: Arc, diff --git a/crates/katana/rpc/src/lib.rs b/crates/katana/rpc/rpc/src/lib.rs similarity index 95% rename from crates/katana/rpc/src/lib.rs rename to crates/katana/rpc/rpc/src/lib.rs index 4c3aaa1c83..68383be7c6 100644 --- a/crates/katana/rpc/src/lib.rs +++ b/crates/katana/rpc/rpc/src/lib.rs @@ -1,4 +1,3 @@ -pub mod api; pub mod config; pub mod katana; pub mod starknet; @@ -8,7 +7,6 @@ use std::sync::Arc; use std::time::{Duration, Instant}; use anyhow::Result; -use api::ApiKind; use config::ServerConfig; use hyper::Method; use jsonrpsee::server::logger::{Logger, MethodKind, TransportProtocol}; @@ -18,10 +16,11 @@ use jsonrpsee::tracing::debug; use jsonrpsee::types::Params; use jsonrpsee::RpcModule; use katana_core::sequencer::KatanaSequencer; +use katana_rpc_api::katana::KatanaApiServer; +use katana_rpc_api::starknet::StarknetApiServer; +use katana_rpc_api::ApiKind; use tower_http::cors::{Any, CorsLayer}; -use crate::api::katana::KatanaApiServer; -use crate::api::starknet::StarknetApiServer; use crate::katana::KatanaApi; use crate::starknet::StarknetApi; @@ -50,7 +49,7 @@ pub async fn spawn(sequencer: Arc, config: ServerConfig) -> Res let middleware = tower::ServiceBuilder::new() .layer(cors) .layer(ProxyGetRequestLayer::new("/", "health")?) - .timeout(Duration::from_secs(2)); + .timeout(Duration::from_secs(20)); let server = ServerBuilder::new() .set_logger(RpcLogger) diff --git a/crates/katana/rpc/rpc/src/starknet.rs b/crates/katana/rpc/rpc/src/starknet.rs new file mode 100644 index 0000000000..85b273176a --- /dev/null +++ b/crates/katana/rpc/rpc/src/starknet.rs @@ -0,0 +1,664 @@ +use std::sync::Arc; + +use jsonrpsee::core::{async_trait, Error, RpcResult}; +use katana_core::backend::contract::StarknetContract; +use katana_core::sequencer::KatanaSequencer; +use katana_executor::blockifier::utils::EntryPointCall; +use katana_primitives::block::{ + BlockHashOrNumber, BlockIdOrTag, FinalityStatus, GasPrices, PartialHeader, +}; +use katana_primitives::conversion::rpc::legacy_inner_to_rpc_class; +use katana_primitives::transaction::{ExecutableTx, ExecutableTxWithHash, TxHash}; +use katana_primitives::version::CURRENT_STARKNET_VERSION; +use katana_primitives::FieldElement; +use katana_provider::traits::block::{BlockHashProvider, BlockIdReader, BlockNumberProvider}; +use katana_provider::traits::transaction::{ + ReceiptProvider, TransactionProvider, TransactionStatusProvider, +}; +use katana_rpc_api::starknet::StarknetApiServer; +use katana_rpc_types::block::{ + BlockHashAndNumber, MaybePendingBlockWithTxHashes, MaybePendingBlockWithTxs, + PendingBlockWithTxHashes, PendingBlockWithTxs, +}; +use katana_rpc_types::error::starknet::StarknetApiError; +use katana_rpc_types::event::{EventFilterWithPage, EventsPage}; +use katana_rpc_types::message::MsgFromL1; +use katana_rpc_types::receipt::{MaybePendingTxReceipt, PendingTxReceipt}; +use katana_rpc_types::state_update::StateUpdate; +use katana_rpc_types::transaction::{ + BroadcastedDeclareTx, BroadcastedDeployAccountTx, BroadcastedInvokeTx, BroadcastedTx, + DeclareTxResult, DeployAccountTxResult, InvokeTxResult, Tx, +}; +use katana_rpc_types::{ContractClass, FeeEstimate, FeltAsHex, FunctionCall}; +use katana_rpc_types_builder::ReceiptBuilder; +use katana_tasks::{BlockingTaskPool, TokioTaskSpawner}; +use starknet::core::types::{BlockTag, TransactionExecutionStatus, TransactionStatus}; + +#[derive(Clone)] +pub struct StarknetApi { + inner: Arc, +} + +struct StarknetApiInner { + sequencer: Arc, + blocking_task_pool: BlockingTaskPool, +} + +impl StarknetApi { + pub fn new(sequencer: Arc) -> Self { + let blocking_task_pool = + BlockingTaskPool::new().expect("failed to create blocking task pool"); + Self { inner: Arc::new(StarknetApiInner { sequencer, blocking_task_pool }) } + } + + async fn on_cpu_blocking_task(&self, func: F) -> T + where + F: FnOnce(Self) -> T + Send + 'static, + T: Send + 'static, + { + let this = self.clone(); + self.inner.blocking_task_pool.spawn(move || func(this)).await.unwrap() + } + + async fn on_io_blocking_task(&self, func: F) -> T + where + F: FnOnce(Self) -> T + Send + 'static, + T: Send + 'static, + { + let this = self.clone(); + TokioTaskSpawner::new().unwrap().spawn_blocking(move || func(this)).await.unwrap() + } +} +#[async_trait] +impl StarknetApiServer for StarknetApi { + async fn chain_id(&self) -> RpcResult { + Ok(FieldElement::from(self.inner.sequencer.chain_id()).into()) + } + + async fn nonce( + &self, + block_id: BlockIdOrTag, + contract_address: FieldElement, + ) -> RpcResult { + self.on_io_blocking_task(move |this| { + let nonce = this + .inner + .sequencer + .nonce_at(block_id, contract_address.into()) + .map_err(StarknetApiError::from)? + .ok_or(StarknetApiError::ContractNotFound)?; + Ok(nonce.into()) + }) + .await + } + + async fn block_number(&self) -> RpcResult { + self.on_io_blocking_task(move |this| { + let block_number = + this.inner.sequencer.block_number().map_err(StarknetApiError::from)?; + Ok(block_number) + }) + .await + } + + async fn transaction_by_hash(&self, transaction_hash: FieldElement) -> RpcResult { + self.on_io_blocking_task(move |this| { + let tx = this + .inner + .sequencer + .transaction(&transaction_hash) + .map_err(StarknetApiError::from)? + .ok_or(StarknetApiError::TxnHashNotFound)?; + Ok(tx.into()) + }) + .await + } + + async fn block_transaction_count(&self, block_id: BlockIdOrTag) -> RpcResult { + self.on_io_blocking_task(move |this| { + let count = this + .inner + .sequencer + .block_tx_count(block_id) + .map_err(StarknetApiError::from)? + .ok_or(StarknetApiError::BlockNotFound)?; + Ok(count) + }) + .await + } + + async fn class_at( + &self, + block_id: BlockIdOrTag, + contract_address: FieldElement, + ) -> RpcResult { + let class_hash = self + .on_io_blocking_task(move |this| { + this.inner + .sequencer + .class_hash_at(block_id, contract_address.into()) + .map_err(StarknetApiError::from)? + .ok_or(StarknetApiError::ContractNotFound) + }) + .await?; + self.class(block_id, class_hash).await + } + + async fn block_hash_and_number(&self) -> RpcResult { + let hash_and_num_pair = self + .on_io_blocking_task(move |this| this.inner.sequencer.block_hash_and_number()) + .await + .map_err(StarknetApiError::from)?; + Ok(hash_and_num_pair.into()) + } + + async fn block_with_tx_hashes( + &self, + block_id: BlockIdOrTag, + ) -> RpcResult { + self.on_io_blocking_task(move |this| { + let provider = this.inner.sequencer.backend.blockchain.provider(); + + if BlockIdOrTag::Tag(BlockTag::Pending) == block_id { + if let Some(pending_state) = this.inner.sequencer.pending_state() { + let block_env = pending_state.block_envs.read().0.clone(); + let latest_hash = + BlockHashProvider::latest_hash(provider).map_err(StarknetApiError::from)?; + + let gas_prices = GasPrices { + eth: block_env.l1_gas_prices.eth, + strk: block_env.l1_gas_prices.strk, + }; + + let header = PartialHeader { + gas_prices, + parent_hash: latest_hash, + version: CURRENT_STARKNET_VERSION, + timestamp: block_env.timestamp, + sequencer_address: block_env.sequencer_address, + }; + + let transactions = pending_state + .executed_txs + .read() + .iter() + .map(|(tx, _)| tx.hash) + .collect::>(); + + return Ok(MaybePendingBlockWithTxHashes::Pending( + PendingBlockWithTxHashes::new(header, transactions), + )); + } + } + + let block_num = BlockIdReader::convert_block_id(provider, block_id) + .map_err(StarknetApiError::from)? + .map(BlockHashOrNumber::Num) + .ok_or(StarknetApiError::BlockNotFound)?; + + katana_rpc_types_builder::BlockBuilder::new(block_num, provider) + .build_with_tx_hash() + .map_err(StarknetApiError::from)? + .map(MaybePendingBlockWithTxHashes::Block) + .ok_or(Error::from(StarknetApiError::BlockNotFound)) + }) + .await + } + + async fn transaction_by_block_id_and_index( + &self, + block_id: BlockIdOrTag, + index: u64, + ) -> RpcResult { + self.on_io_blocking_task(move |this| { + // TEMP: have to handle pending tag independently for now + let tx = if BlockIdOrTag::Tag(BlockTag::Pending) == block_id { + let Some(pending_state) = this.inner.sequencer.pending_state() else { + return Err(StarknetApiError::BlockNotFound.into()); + }; + + let pending_txs = pending_state.executed_txs.read(); + pending_txs.iter().nth(index as usize).map(|(tx, _)| tx.clone()) + } else { + let provider = &this.inner.sequencer.backend.blockchain.provider(); + + let block_num = BlockIdReader::convert_block_id(provider, block_id) + .map_err(StarknetApiError::from)? + .map(BlockHashOrNumber::Num) + .ok_or(StarknetApiError::BlockNotFound)?; + + TransactionProvider::transaction_by_block_and_idx(provider, block_num, index) + .map_err(StarknetApiError::from)? + }; + + Ok(tx.ok_or(StarknetApiError::InvalidTxnIndex)?.into()) + }) + .await + } + + async fn block_with_txs(&self, block_id: BlockIdOrTag) -> RpcResult { + self.on_io_blocking_task(move |this| { + let provider = this.inner.sequencer.backend.blockchain.provider(); + + if BlockIdOrTag::Tag(BlockTag::Pending) == block_id { + if let Some(pending_state) = this.inner.sequencer.pending_state() { + let block_env = pending_state.block_envs.read().0.clone(); + let latest_hash = + BlockHashProvider::latest_hash(provider).map_err(StarknetApiError::from)?; + + let gas_prices = GasPrices { + eth: block_env.l1_gas_prices.eth, + strk: block_env.l1_gas_prices.strk, + }; + + let header = PartialHeader { + gas_prices, + parent_hash: latest_hash, + version: CURRENT_STARKNET_VERSION, + timestamp: block_env.timestamp, + sequencer_address: block_env.sequencer_address, + }; + + let transactions = pending_state + .executed_txs + .read() + .iter() + .map(|(tx, _)| tx.clone()) + .collect::>(); + + return Ok(MaybePendingBlockWithTxs::Pending(PendingBlockWithTxs::new( + header, + transactions, + ))); + } + } + + let block_num = BlockIdReader::convert_block_id(provider, block_id) + .map_err(|e| StarknetApiError::UnexpectedError { reason: e.to_string() })? + .map(BlockHashOrNumber::Num) + .ok_or(StarknetApiError::BlockNotFound)?; + + katana_rpc_types_builder::BlockBuilder::new(block_num, provider) + .build() + .map_err(|e| StarknetApiError::UnexpectedError { reason: e.to_string() })? + .map(MaybePendingBlockWithTxs::Block) + .ok_or(Error::from(StarknetApiError::BlockNotFound)) + }) + .await + } + + async fn state_update(&self, block_id: BlockIdOrTag) -> RpcResult { + self.on_io_blocking_task(move |this| { + let provider = this.inner.sequencer.backend.blockchain.provider(); + + let block_id = match block_id { + BlockIdOrTag::Number(num) => BlockHashOrNumber::Num(num), + BlockIdOrTag::Hash(hash) => BlockHashOrNumber::Hash(hash), + + BlockIdOrTag::Tag(BlockTag::Latest) => BlockNumberProvider::latest_number(provider) + .map(BlockHashOrNumber::Num) + .map_err(|_| StarknetApiError::BlockNotFound)?, + + BlockIdOrTag::Tag(BlockTag::Pending) => { + return Err(StarknetApiError::BlockNotFound.into()); + } + }; + + katana_rpc_types_builder::StateUpdateBuilder::new(block_id, provider) + .build() + .map_err(|e| StarknetApiError::UnexpectedError { reason: e.to_string() })? + .ok_or(Error::from(StarknetApiError::BlockNotFound)) + }) + .await + } + + async fn transaction_receipt( + &self, + transaction_hash: FieldElement, + ) -> RpcResult { + self.on_io_blocking_task(move |this| { + let provider = this.inner.sequencer.backend.blockchain.provider(); + let receipt = ReceiptBuilder::new(transaction_hash, provider) + .build() + .map_err(|e| StarknetApiError::UnexpectedError { reason: e.to_string() })?; + + match receipt { + Some(receipt) => Ok(MaybePendingTxReceipt::Receipt(receipt)), + + None => { + let pending_receipt = this.inner.sequencer.pending_state().and_then(|s| { + s.executed_txs + .read() + .iter() + .find(|(tx, _)| tx.hash == transaction_hash) + .map(|(_, rct)| rct.receipt.clone()) + }); + + let Some(pending_receipt) = pending_receipt else { + return Err(StarknetApiError::TxnHashNotFound.into()); + }; + + Ok(MaybePendingTxReceipt::Pending(PendingTxReceipt::new( + transaction_hash, + pending_receipt, + ))) + } + } + }) + .await + } + + async fn class_hash_at( + &self, + block_id: BlockIdOrTag, + contract_address: FieldElement, + ) -> RpcResult { + self.on_io_blocking_task(move |this| { + let hash = this + .inner + .sequencer + .class_hash_at(block_id, contract_address.into()) + .map_err(StarknetApiError::from)? + .ok_or(StarknetApiError::ContractNotFound)?; + Ok(hash.into()) + }) + .await + } + + async fn class( + &self, + block_id: BlockIdOrTag, + class_hash: FieldElement, + ) -> RpcResult { + self.on_io_blocking_task(move |this| { + let class = + this.inner.sequencer.class(block_id, class_hash).map_err(StarknetApiError::from)?; + let Some(class) = class else { return Err(StarknetApiError::ClassHashNotFound.into()) }; + + match class { + StarknetContract::Legacy(c) => { + let contract = legacy_inner_to_rpc_class(c) + .map_err(|e| StarknetApiError::UnexpectedError { reason: e.to_string() })?; + Ok(contract) + } + StarknetContract::Sierra(c) => Ok(ContractClass::Sierra(c)), + } + }) + .await + } + + async fn events(&self, filter: EventFilterWithPage) -> RpcResult { + self.on_io_blocking_task(move |this| { + let from_block = filter.event_filter.from_block.unwrap_or(BlockIdOrTag::Number(0)); + let to_block = + filter.event_filter.to_block.unwrap_or(BlockIdOrTag::Tag(BlockTag::Latest)); + + let keys = filter.event_filter.keys; + let keys = keys.filter(|keys| !(keys.len() == 1 && keys.is_empty())); + + let events = this + .inner + .sequencer + .events( + from_block, + to_block, + filter.event_filter.address.map(|f| f.into()), + keys, + filter.result_page_request.continuation_token, + filter.result_page_request.chunk_size, + ) + .map_err(StarknetApiError::from)?; + + Ok(events) + }) + .await + } + + async fn call( + &self, + request: FunctionCall, + block_id: BlockIdOrTag, + ) -> RpcResult> { + self.on_io_blocking_task(move |this| { + let request = EntryPointCall { + calldata: request.calldata, + contract_address: request.contract_address.into(), + entry_point_selector: request.entry_point_selector, + }; + + let res = + this.inner.sequencer.call(request, block_id).map_err(StarknetApiError::from)?; + Ok(res.into_iter().map(|v| v.into()).collect()) + }) + .await + } + + async fn storage_at( + &self, + contract_address: FieldElement, + key: FieldElement, + block_id: BlockIdOrTag, + ) -> RpcResult { + self.on_io_blocking_task(move |this| { + let value = this + .inner + .sequencer + .storage_at(contract_address.into(), key, block_id) + .map_err(StarknetApiError::from)?; + + Ok(value.into()) + }) + .await + } + + async fn add_deploy_account_transaction( + &self, + deploy_account_transaction: BroadcastedDeployAccountTx, + ) -> RpcResult { + self.on_io_blocking_task(move |this| { + if deploy_account_transaction.is_query { + return Err(StarknetApiError::UnsupportedTransactionVersion.into()); + } + + let chain_id = this.inner.sequencer.chain_id(); + + let tx = deploy_account_transaction.into_tx_with_chain_id(chain_id); + let contract_address = tx.contract_address; + + let tx = ExecutableTxWithHash::new(ExecutableTx::DeployAccount(tx)); + let tx_hash = tx.hash; + + this.inner.sequencer.add_transaction_to_pool(tx); + + Ok((tx_hash, contract_address).into()) + }) + .await + } + + async fn estimate_fee( + &self, + request: Vec, + block_id: BlockIdOrTag, + ) -> RpcResult> { + self.on_cpu_blocking_task(move |this| { + let chain_id = this.inner.sequencer.chain_id(); + + let transactions = request + .into_iter() + .map(|tx| { + let tx = match tx { + BroadcastedTx::Invoke(tx) => { + let tx = tx.into_tx_with_chain_id(chain_id); + ExecutableTxWithHash::new_query(ExecutableTx::Invoke(tx)) + } + + BroadcastedTx::DeployAccount(tx) => { + let tx = tx.into_tx_with_chain_id(chain_id); + ExecutableTxWithHash::new_query(ExecutableTx::DeployAccount(tx)) + } + + BroadcastedTx::Declare(tx) => { + let tx = tx + .try_into_tx_with_chain_id(chain_id) + .map_err(|_| StarknetApiError::InvalidContractClass)?; + ExecutableTxWithHash::new_query(ExecutableTx::Declare(tx)) + } + }; + + Result::::Ok(tx) + }) + .collect::, _>>()?; + + let res = this + .inner + .sequencer + .estimate_fee(transactions, block_id) + .map_err(StarknetApiError::from)?; + + Ok(res) + }) + .await + } + + async fn estimate_message_fee( + &self, + message: MsgFromL1, + block_id: BlockIdOrTag, + ) -> RpcResult { + self.on_cpu_blocking_task(move |this| { + let chain_id = this.inner.sequencer.chain_id(); + + let tx = message.into_tx_with_chain_id(chain_id); + let hash = tx.calculate_hash(); + let tx: ExecutableTxWithHash = ExecutableTxWithHash { hash, transaction: tx.into() }; + + let res = this + .inner + .sequencer + .estimate_fee(vec![tx], block_id) + .map_err(StarknetApiError::from)? + .pop() + .expect("should have estimate result"); + + Ok(res) + }) + .await + } + + async fn add_declare_transaction( + &self, + declare_transaction: BroadcastedDeclareTx, + ) -> RpcResult { + self.on_io_blocking_task(move |this| { + if declare_transaction.is_query() { + return Err(StarknetApiError::UnsupportedTransactionVersion.into()); + } + + let chain_id = this.inner.sequencer.chain_id(); + + // // validate compiled class hash + // let is_valid = declare_transaction + // .validate_compiled_class_hash() + // .map_err(|_| StarknetApiError::InvalidContractClass)?; + + // if !is_valid { + // return Err(StarknetApiError::CompiledClassHashMismatch.into()); + // } + + let tx = declare_transaction + .try_into_tx_with_chain_id(chain_id) + .map_err(|_| StarknetApiError::InvalidContractClass)?; + + let class_hash = tx.class_hash(); + let tx = ExecutableTxWithHash::new(ExecutableTx::Declare(tx)); + let tx_hash = tx.hash; + + this.inner.sequencer.add_transaction_to_pool(tx); + + Ok((tx_hash, class_hash).into()) + }) + .await + } + + async fn add_invoke_transaction( + &self, + invoke_transaction: BroadcastedInvokeTx, + ) -> RpcResult { + self.on_io_blocking_task(move |this| { + if invoke_transaction.is_query { + return Err(StarknetApiError::UnsupportedTransactionVersion.into()); + } + + let chain_id = this.inner.sequencer.chain_id(); + + let tx = invoke_transaction.into_tx_with_chain_id(chain_id); + let tx = ExecutableTxWithHash::new(ExecutableTx::Invoke(tx)); + let tx_hash = tx.hash; + + this.inner.sequencer.add_transaction_to_pool(tx); + + Ok(tx_hash.into()) + }) + .await + } + + async fn transaction_status(&self, transaction_hash: TxHash) -> RpcResult { + self.on_io_blocking_task(move |this| { + let provider = this.inner.sequencer.backend.blockchain.provider(); + + let tx_status = + TransactionStatusProvider::transaction_status(provider, transaction_hash) + .map_err(StarknetApiError::from)?; + + if let Some(status) = tx_status { + if let Some(receipt) = ReceiptProvider::receipt_by_hash(provider, transaction_hash) + .map_err(StarknetApiError::from)? + { + let execution_status = if receipt.is_reverted() { + TransactionExecutionStatus::Reverted + } else { + TransactionExecutionStatus::Succeeded + }; + + return Ok(match status { + FinalityStatus::AcceptedOnL1 => { + TransactionStatus::AcceptedOnL1(execution_status) + } + FinalityStatus::AcceptedOnL2 => { + TransactionStatus::AcceptedOnL2(execution_status) + } + }); + } + } + + let pending_state = this.inner.sequencer.pending_state(); + let state = pending_state.ok_or(StarknetApiError::TxnHashNotFound)?; + let executed_txs = state.executed_txs.read(); + + // attemps to find in the valid transactions list first (executed_txs) + // if not found, then search in the rejected transactions list (rejected_txs) + if let Some(is_reverted) = executed_txs + .iter() + .find(|(tx, _)| tx.hash == transaction_hash) + .map(|(_, rct)| rct.receipt.is_reverted()) + { + let exec_status = if is_reverted { + TransactionExecutionStatus::Reverted + } else { + TransactionExecutionStatus::Succeeded + }; + + Ok(TransactionStatus::AcceptedOnL2(exec_status)) + } else { + let rejected_txs = state.rejected_txs.read(); + + rejected_txs + .iter() + .find(|(tx, _)| tx.hash == transaction_hash) + .map(|_| TransactionStatus::Rejected) + .ok_or(Error::from(StarknetApiError::TxnHashNotFound)) + } + }) + .await + } +} diff --git a/crates/katana/rpc/tests/starknet.rs b/crates/katana/rpc/rpc/tests/starknet.rs similarity index 100% rename from crates/katana/rpc/tests/starknet.rs rename to crates/katana/rpc/rpc/tests/starknet.rs diff --git a/crates/katana/rpc/tests/test_data/cairo0_contract.json b/crates/katana/rpc/rpc/tests/test_data/cairo0_contract.json similarity index 100% rename from crates/katana/rpc/tests/test_data/cairo0_contract.json rename to crates/katana/rpc/rpc/tests/test_data/cairo0_contract.json diff --git a/crates/katana/rpc/tests/test_data/cairo1_contract.json b/crates/katana/rpc/rpc/tests/test_data/cairo1_contract.json similarity index 100% rename from crates/katana/rpc/tests/test_data/cairo1_contract.json rename to crates/katana/rpc/rpc/tests/test_data/cairo1_contract.json diff --git a/crates/katana/rpc/src/api/katana.rs b/crates/katana/rpc/src/api/katana.rs deleted file mode 100644 index 4414494e92..0000000000 --- a/crates/katana/rpc/src/api/katana.rs +++ /dev/null @@ -1,49 +0,0 @@ -use jsonrpsee::core::Error; -use jsonrpsee::proc_macros::rpc; -use jsonrpsee::types::error::CallError; -use jsonrpsee::types::ErrorObject; -use katana_core::accounts::Account; -use starknet::core::types::FieldElement; - -#[derive(thiserror::Error, Clone, Copy, Debug)] -#[allow(clippy::enum_variant_names)] -pub enum KatanaApiError { - #[error("Failed to change next block timestamp.")] - FailedToChangeNextBlockTimestamp = 1, - #[error("Failed to dump state.")] - FailedToDumpState = 2, - #[error("Failed to update storage.")] - FailedToUpdateStorage = 3, -} - -impl From for Error { - fn from(err: KatanaApiError) -> Self { - Error::Call(CallError::Custom(ErrorObject::owned(err as i32, err.to_string(), None::<()>))) - } -} - -#[rpc(server, namespace = "katana")] -pub trait KatanaApi { - #[method(name = "generateBlock")] - async fn generate_block(&self) -> Result<(), Error>; - - #[method(name = "nextBlockTimestamp")] - async fn next_block_timestamp(&self) -> Result; - - #[method(name = "setNextBlockTimestamp")] - async fn set_next_block_timestamp(&self, timestamp: u64) -> Result<(), Error>; - - #[method(name = "increaseNextBlockTimestamp")] - async fn increase_next_block_timestamp(&self, timestamp: u64) -> Result<(), Error>; - - #[method(name = "predeployedAccounts")] - async fn predeployed_accounts(&self) -> Result, Error>; - - #[method(name = "setStorageAt")] - async fn set_storage_at( - &self, - contract_address: FieldElement, - key: FieldElement, - value: FieldElement, - ) -> Result<(), Error>; -} diff --git a/crates/katana/rpc/src/starknet.rs b/crates/katana/rpc/src/starknet.rs deleted file mode 100644 index 737139d262..0000000000 --- a/crates/katana/rpc/src/starknet.rs +++ /dev/null @@ -1,568 +0,0 @@ -use std::sync::Arc; - -use jsonrpsee::core::{async_trait, Error}; -use katana_core::backend::contract::StarknetContract; -use katana_core::sequencer::KatanaSequencer; -use katana_executor::blockifier::utils::EntryPointCall; -use katana_primitives::block::{ - BlockHashOrNumber, BlockIdOrTag, FinalityStatus, GasPrices, PartialHeader, -}; -use katana_primitives::conversion::rpc::legacy_inner_to_rpc_class; -use katana_primitives::transaction::{ExecutableTx, ExecutableTxWithHash, TxHash}; -use katana_primitives::version::CURRENT_STARKNET_VERSION; -use katana_primitives::FieldElement; -use katana_provider::traits::block::{BlockHashProvider, BlockIdReader, BlockNumberProvider}; -use katana_provider::traits::transaction::{ - ReceiptProvider, TransactionProvider, TransactionStatusProvider, -}; -use katana_rpc_types::block::{ - BlockHashAndNumber, MaybePendingBlockWithTxHashes, MaybePendingBlockWithTxs, - PendingBlockWithTxHashes, PendingBlockWithTxs, -}; -use katana_rpc_types::event::{EventFilterWithPage, EventsPage}; -use katana_rpc_types::message::MsgFromL1; -use katana_rpc_types::receipt::{MaybePendingTxReceipt, PendingTxReceipt}; -use katana_rpc_types::state_update::StateUpdate; -use katana_rpc_types::transaction::{ - BroadcastedDeclareTx, BroadcastedDeployAccountTx, BroadcastedInvokeTx, BroadcastedTx, - DeclareTxResult, DeployAccountTxResult, InvokeTxResult, Tx, -}; -use katana_rpc_types::{ContractClass, FeeEstimate, FeltAsHex, FunctionCall}; -use katana_rpc_types_builder::ReceiptBuilder; -use starknet::core::types::{BlockTag, TransactionExecutionStatus, TransactionStatus}; - -use crate::api::starknet::{StarknetApiError, StarknetApiServer}; - -pub struct StarknetApi { - sequencer: Arc, -} - -impl StarknetApi { - pub fn new(sequencer: Arc) -> Self { - Self { sequencer } - } -} -#[async_trait] -impl StarknetApiServer for StarknetApi { - async fn chain_id(&self) -> Result { - Ok(FieldElement::from(self.sequencer.chain_id()).into()) - } - - async fn nonce( - &self, - block_id: BlockIdOrTag, - contract_address: FieldElement, - ) -> Result { - let nonce = self - .sequencer - .nonce_at(block_id, contract_address.into()) - .await - .map_err(StarknetApiError::from)? - .ok_or(StarknetApiError::ContractNotFound)?; - - Ok(nonce.into()) - } - - async fn block_number(&self) -> Result { - Ok(self.sequencer.block_number()) - } - - async fn transaction_by_hash(&self, transaction_hash: FieldElement) -> Result { - let tx = self - .sequencer - .transaction(&transaction_hash) - .map_err(StarknetApiError::from)? - .ok_or(StarknetApiError::TxnHashNotFound)?; - Ok(tx.into()) - } - - async fn block_transaction_count(&self, block_id: BlockIdOrTag) -> Result { - let count = self - .sequencer - .block_tx_count(block_id) - .map_err(StarknetApiError::from)? - .ok_or(StarknetApiError::BlockNotFound)?; - Ok(count) - } - - async fn class_at( - &self, - block_id: BlockIdOrTag, - contract_address: FieldElement, - ) -> Result { - let class_hash = self - .sequencer - .class_hash_at(block_id, contract_address.into()) - .map_err(StarknetApiError::from)? - .ok_or(StarknetApiError::ContractNotFound)?; - - self.class(block_id, class_hash).await - } - - async fn block_hash_and_number(&self) -> Result { - let hash_and_num_pair = - self.sequencer.block_hash_and_number().map_err(StarknetApiError::from)?; - Ok(hash_and_num_pair.into()) - } - - async fn block_with_tx_hashes( - &self, - block_id: BlockIdOrTag, - ) -> Result { - let provider = self.sequencer.backend.blockchain.provider(); - - if BlockIdOrTag::Tag(BlockTag::Pending) == block_id { - if let Some(pending_state) = self.sequencer.pending_state() { - let block_env = pending_state.block_envs.read().0.clone(); - let latest_hash = - BlockHashProvider::latest_hash(provider).map_err(StarknetApiError::from)?; - - let gas_prices = GasPrices { - eth: block_env.l1_gas_prices.eth, - strk: block_env.l1_gas_prices.strk, - }; - - let header = PartialHeader { - gas_prices, - parent_hash: latest_hash, - version: CURRENT_STARKNET_VERSION, - timestamp: block_env.timestamp, - sequencer_address: block_env.sequencer_address, - }; - - let transactions = pending_state - .executed_txs - .read() - .iter() - .map(|(tx, _)| tx.hash) - .collect::>(); - - return Ok(MaybePendingBlockWithTxHashes::Pending(PendingBlockWithTxHashes::new( - header, - transactions, - ))); - } - } - - let block_num = BlockIdReader::convert_block_id(provider, block_id) - .map_err(StarknetApiError::from)? - .map(BlockHashOrNumber::Num) - .ok_or(StarknetApiError::BlockNotFound)?; - - katana_rpc_types_builder::BlockBuilder::new(block_num, provider) - .build_with_tx_hash() - .map_err(StarknetApiError::from)? - .map(MaybePendingBlockWithTxHashes::Block) - .ok_or(Error::from(StarknetApiError::BlockNotFound)) - } - - async fn transaction_by_block_id_and_index( - &self, - block_id: BlockIdOrTag, - index: u64, - ) -> Result { - // TEMP: have to handle pending tag independently for now - let tx = if BlockIdOrTag::Tag(BlockTag::Pending) == block_id { - let Some(pending_state) = self.sequencer.pending_state() else { - return Err(StarknetApiError::BlockNotFound.into()); - }; - - let pending_txs = pending_state.executed_txs.read(); - pending_txs.iter().nth(index as usize).map(|(tx, _)| tx.clone()) - } else { - let provider = &self.sequencer.backend.blockchain.provider(); - - let block_num = BlockIdReader::convert_block_id(provider, block_id) - .map_err(StarknetApiError::from)? - .map(BlockHashOrNumber::Num) - .ok_or(StarknetApiError::BlockNotFound)?; - - TransactionProvider::transaction_by_block_and_idx(provider, block_num, index) - .map_err(StarknetApiError::from)? - }; - - Ok(tx.ok_or(StarknetApiError::InvalidTxnIndex)?.into()) - } - - async fn block_with_txs( - &self, - block_id: BlockIdOrTag, - ) -> Result { - let provider = self.sequencer.backend.blockchain.provider(); - - if BlockIdOrTag::Tag(BlockTag::Pending) == block_id { - if let Some(pending_state) = self.sequencer.pending_state() { - let block_env = pending_state.block_envs.read().0.clone(); - let latest_hash = - BlockHashProvider::latest_hash(provider).map_err(StarknetApiError::from)?; - - let gas_prices = GasPrices { - eth: block_env.l1_gas_prices.eth, - strk: block_env.l1_gas_prices.strk, - }; - - let header = PartialHeader { - gas_prices, - parent_hash: latest_hash, - version: CURRENT_STARKNET_VERSION, - timestamp: block_env.timestamp, - sequencer_address: block_env.sequencer_address, - }; - - let transactions = pending_state - .executed_txs - .read() - .iter() - .map(|(tx, _)| tx.clone()) - .collect::>(); - - return Ok(MaybePendingBlockWithTxs::Pending(PendingBlockWithTxs::new( - header, - transactions, - ))); - } - } - - let block_num = BlockIdReader::convert_block_id(provider, block_id) - .map_err(|e| StarknetApiError::UnexpectedError { reason: e.to_string() })? - .map(BlockHashOrNumber::Num) - .ok_or(StarknetApiError::BlockNotFound)?; - - katana_rpc_types_builder::BlockBuilder::new(block_num, provider) - .build() - .map_err(|e| StarknetApiError::UnexpectedError { reason: e.to_string() })? - .map(MaybePendingBlockWithTxs::Block) - .ok_or(Error::from(StarknetApiError::BlockNotFound)) - } - - async fn state_update(&self, block_id: BlockIdOrTag) -> Result { - let provider = self.sequencer.backend.blockchain.provider(); - - let block_id = match block_id { - BlockIdOrTag::Number(num) => BlockHashOrNumber::Num(num), - BlockIdOrTag::Hash(hash) => BlockHashOrNumber::Hash(hash), - - BlockIdOrTag::Tag(BlockTag::Latest) => BlockNumberProvider::latest_number(provider) - .map(BlockHashOrNumber::Num) - .map_err(|_| StarknetApiError::BlockNotFound)?, - - BlockIdOrTag::Tag(BlockTag::Pending) => { - return Err(StarknetApiError::BlockNotFound.into()); - } - }; - - katana_rpc_types_builder::StateUpdateBuilder::new(block_id, provider) - .build() - .map_err(|e| StarknetApiError::UnexpectedError { reason: e.to_string() })? - .ok_or(Error::from(StarknetApiError::BlockNotFound)) - } - - async fn transaction_receipt( - &self, - transaction_hash: FieldElement, - ) -> Result { - let provider = self.sequencer.backend.blockchain.provider(); - let receipt = ReceiptBuilder::new(transaction_hash, provider) - .build() - .map_err(|e| StarknetApiError::UnexpectedError { reason: e.to_string() })?; - - match receipt { - Some(receipt) => Ok(MaybePendingTxReceipt::Receipt(receipt)), - - None => { - let pending_receipt = self.sequencer.pending_state().and_then(|s| { - s.executed_txs - .read() - .iter() - .find(|(tx, _)| tx.hash == transaction_hash) - .map(|(_, rct)| rct.receipt.clone()) - }); - - let Some(pending_receipt) = pending_receipt else { - return Err(StarknetApiError::TxnHashNotFound.into()); - }; - - Ok(MaybePendingTxReceipt::Pending(PendingTxReceipt::new( - transaction_hash, - pending_receipt, - ))) - } - } - } - - async fn class_hash_at( - &self, - block_id: BlockIdOrTag, - contract_address: FieldElement, - ) -> Result { - let hash = self - .sequencer - .class_hash_at(block_id, contract_address.into()) - .map_err(StarknetApiError::from)? - .ok_or(StarknetApiError::ContractNotFound)?; - - Ok(hash.into()) - } - - async fn class( - &self, - block_id: BlockIdOrTag, - class_hash: FieldElement, - ) -> Result { - let class = self.sequencer.class(block_id, class_hash).map_err(StarknetApiError::from)?; - let Some(class) = class else { return Err(StarknetApiError::ClassHashNotFound.into()) }; - - match class { - StarknetContract::Legacy(c) => { - let contract = legacy_inner_to_rpc_class(c) - .map_err(|e| StarknetApiError::UnexpectedError { reason: e.to_string() })?; - Ok(contract) - } - StarknetContract::Sierra(c) => Ok(ContractClass::Sierra(c)), - } - } - - async fn events(&self, filter: EventFilterWithPage) -> Result { - let from_block = filter.event_filter.from_block.unwrap_or(BlockIdOrTag::Number(0)); - let to_block = filter.event_filter.to_block.unwrap_or(BlockIdOrTag::Tag(BlockTag::Latest)); - - let keys = filter.event_filter.keys; - let keys = keys.filter(|keys| !(keys.len() == 1 && keys.is_empty())); - - let events = self - .sequencer - .events( - from_block, - to_block, - filter.event_filter.address.map(|f| f.into()), - keys, - filter.result_page_request.continuation_token, - filter.result_page_request.chunk_size, - ) - .await - .map_err(StarknetApiError::from)?; - - Ok(events) - } - - async fn call( - &self, - request: FunctionCall, - block_id: BlockIdOrTag, - ) -> Result, Error> { - let request = EntryPointCall { - calldata: request.calldata, - contract_address: request.contract_address.into(), - entry_point_selector: request.entry_point_selector, - }; - - let res = self.sequencer.call(request, block_id).map_err(StarknetApiError::from)?; - - Ok(res.into_iter().map(|v| v.into()).collect()) - } - - async fn storage_at( - &self, - contract_address: FieldElement, - key: FieldElement, - block_id: BlockIdOrTag, - ) -> Result { - let value = self - .sequencer - .storage_at(contract_address.into(), key, block_id) - .map_err(StarknetApiError::from)?; - - Ok(value.into()) - } - - async fn add_deploy_account_transaction( - &self, - deploy_account_transaction: BroadcastedDeployAccountTx, - ) -> Result { - if deploy_account_transaction.is_query { - return Err(StarknetApiError::UnsupportedTransactionVersion.into()); - } - - let chain_id = self.sequencer.chain_id(); - - let tx = deploy_account_transaction.into_tx_with_chain_id(chain_id); - let contract_address = tx.contract_address; - - let tx = ExecutableTxWithHash::new(ExecutableTx::DeployAccount(tx)); - let tx_hash = tx.hash; - - self.sequencer.add_transaction_to_pool(tx); - - Ok((tx_hash, contract_address).into()) - } - - async fn estimate_fee( - &self, - request: Vec, - block_id: BlockIdOrTag, - ) -> Result, Error> { - let chain_id = self.sequencer.chain_id(); - - let transactions = request - .into_iter() - .map(|tx| { - let tx = match tx { - BroadcastedTx::Invoke(tx) => { - let tx = tx.into_tx_with_chain_id(chain_id); - ExecutableTxWithHash::new_query(ExecutableTx::Invoke(tx)) - } - - BroadcastedTx::DeployAccount(tx) => { - let tx = tx.into_tx_with_chain_id(chain_id); - ExecutableTxWithHash::new_query(ExecutableTx::DeployAccount(tx)) - } - - BroadcastedTx::Declare(tx) => { - let tx = tx - .try_into_tx_with_chain_id(chain_id) - .map_err(|_| StarknetApiError::InvalidContractClass)?; - ExecutableTxWithHash::new_query(ExecutableTx::Declare(tx)) - } - }; - - Result::::Ok(tx) - }) - .collect::, _>>()?; - - let res = - self.sequencer.estimate_fee(transactions, block_id).map_err(StarknetApiError::from)?; - - Ok(res) - } - - async fn estimate_message_fee( - &self, - message: MsgFromL1, - block_id: BlockIdOrTag, - ) -> Result { - let chain_id = self.sequencer.chain_id(); - - let tx = message.into_tx_with_chain_id(chain_id); - let hash = tx.calculate_hash(); - let tx: ExecutableTxWithHash = ExecutableTxWithHash { hash, transaction: tx.into() }; - - let res = self - .sequencer - .estimate_fee(vec![tx], block_id) - .map_err(StarknetApiError::from)? - .pop() - .expect("should have estimate result"); - - Ok(res) - } - - async fn add_declare_transaction( - &self, - declare_transaction: BroadcastedDeclareTx, - ) -> Result { - if declare_transaction.is_query() { - return Err(StarknetApiError::UnsupportedTransactionVersion.into()); - } - - let chain_id = self.sequencer.chain_id(); - - // // validate compiled class hash - // let is_valid = declare_transaction - // .validate_compiled_class_hash() - // .map_err(|_| StarknetApiError::InvalidContractClass)?; - - // if !is_valid { - // return Err(StarknetApiError::CompiledClassHashMismatch.into()); - // } - - let tx = declare_transaction - .try_into_tx_with_chain_id(chain_id) - .map_err(|_| StarknetApiError::InvalidContractClass)?; - - let class_hash = tx.class_hash(); - let tx = ExecutableTxWithHash::new(ExecutableTx::Declare(tx)); - let tx_hash = tx.hash; - - self.sequencer.add_transaction_to_pool(tx); - - Ok((tx_hash, class_hash).into()) - } - - async fn add_invoke_transaction( - &self, - invoke_transaction: BroadcastedInvokeTx, - ) -> Result { - if invoke_transaction.is_query { - return Err(StarknetApiError::UnsupportedTransactionVersion.into()); - } - - let chain_id = self.sequencer.chain_id(); - - let tx = invoke_transaction.into_tx_with_chain_id(chain_id); - let tx = ExecutableTxWithHash::new(ExecutableTx::Invoke(tx)); - let tx_hash = tx.hash; - - self.sequencer.add_transaction_to_pool(tx); - - Ok(tx_hash.into()) - } - - async fn transaction_status( - &self, - transaction_hash: TxHash, - ) -> Result { - let provider = self.sequencer.backend.blockchain.provider(); - - let tx_status = TransactionStatusProvider::transaction_status(provider, transaction_hash) - .map_err(StarknetApiError::from)?; - - if let Some(status) = tx_status { - if let Some(receipt) = ReceiptProvider::receipt_by_hash(provider, transaction_hash) - .map_err(StarknetApiError::from)? - { - let execution_status = if receipt.is_reverted() { - TransactionExecutionStatus::Reverted - } else { - TransactionExecutionStatus::Succeeded - }; - - return Ok(match status { - FinalityStatus::AcceptedOnL1 => { - TransactionStatus::AcceptedOnL1(execution_status) - } - FinalityStatus::AcceptedOnL2 => { - TransactionStatus::AcceptedOnL2(execution_status) - } - }); - } - } - - let pending_state = self.sequencer.pending_state(); - let state = pending_state.ok_or(StarknetApiError::TxnHashNotFound)?; - let executed_txs = state.executed_txs.read(); - - // attemps to find in the valid transactions list first (executed_txs) - // if not found, then search in the rejected transactions list (rejected_txs) - if let Some(is_reverted) = executed_txs - .iter() - .find(|(tx, _)| tx.hash == transaction_hash) - .map(|(_, rct)| rct.receipt.is_reverted()) - { - let exec_status = if is_reverted { - TransactionExecutionStatus::Reverted - } else { - TransactionExecutionStatus::Succeeded - }; - - Ok(TransactionStatus::AcceptedOnL2(exec_status)) - } else { - let rejected_txs = state.rejected_txs.read(); - - rejected_txs - .iter() - .find(|(tx, _)| tx.hash == transaction_hash) - .map(|_| TransactionStatus::Rejected) - .ok_or(Error::from(StarknetApiError::TxnHashNotFound)) - } - } -} diff --git a/crates/katana/src/args.rs b/crates/katana/src/args.rs index aa5e345866..f8dc48623c 100644 --- a/crates/katana/src/args.rs +++ b/crates/katana/src/args.rs @@ -21,8 +21,8 @@ use katana_core::constants::{ }; use katana_core::sequencer::SequencerConfig; use katana_primitives::chain::ChainId; -use katana_rpc::api::ApiKind; use katana_rpc::config::ServerConfig; +use katana_rpc_api::ApiKind; use metrics::utils::parse_socket_address; use tracing::Subscriber; use tracing_subscriber::{fmt, EnvFilter}; diff --git a/crates/katana/tasks/Cargo.toml b/crates/katana/tasks/Cargo.toml new file mode 100644 index 0000000000..fd03a40729 --- /dev/null +++ b/crates/katana/tasks/Cargo.toml @@ -0,0 +1,12 @@ +[package] +edition.workspace = true +name = "katana-tasks" +version.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +futures.workspace = true +rayon.workspace = true +thiserror.workspace = true +tokio.workspace = true diff --git a/crates/katana/tasks/src/lib.rs b/crates/katana/tasks/src/lib.rs new file mode 100644 index 0000000000..148630ade0 --- /dev/null +++ b/crates/katana/tasks/src/lib.rs @@ -0,0 +1,181 @@ +use std::any::Any; +use std::future::Future; +use std::panic::{self, AssertUnwindSafe}; +use std::pin::Pin; +use std::sync::Arc; +use std::task::Poll; + +use futures::channel::oneshot; +use rayon::ThreadPoolBuilder; +use tokio::runtime::Handle; +use tokio::task::JoinHandle; + +/// This `struct` is created by the [TokioTaskSpawner::new] method. +#[derive(Debug, thiserror::Error)] +#[error("Failed to initialize task spawner: {0}")] +pub struct TaskSpawnerInitError(tokio::runtime::TryCurrentError); + +/// A task spawner for spawning tasks on a tokio runtime. This is simple wrapper around a tokio's +/// runtime [Handle] to easily spawn tasks on the runtime. +/// +/// For running expensive CPU-bound tasks, use [BlockingTaskPool] instead. +#[derive(Debug, Clone)] +pub struct TokioTaskSpawner { + /// Handle to the tokio runtime. + tokio_handle: Handle, +} + +impl TokioTaskSpawner { + /// Creates a new [TokioTaskSpawner] over the currently running tokio runtime. + /// + /// ## Errors + /// + /// Returns an error if no tokio runtime has been started. + pub fn new() -> Result { + Ok(Self { tokio_handle: Handle::try_current().map_err(TaskSpawnerInitError)? }) + } + + /// Creates a new [TokioTaskSpawner] with the given tokio runtime [Handle]. + pub fn new_with_handle(tokio_handle: Handle) -> Self { + Self { tokio_handle } + } +} + +impl TokioTaskSpawner { + pub fn spawn(&self, future: F) -> JoinHandle + where + F: Future + Send + 'static, + F::Output: Send + 'static, + { + self.tokio_handle.spawn(future) + } + + pub fn spawn_blocking(&self, func: F) -> JoinHandle + where + F: FnOnce() -> R + Send + 'static, + R: Send + 'static, + { + self.tokio_handle.spawn_blocking(func) + } +} + +/// This `struct` is created by the [BlockingTaskPool::new] method. +#[derive(Debug, thiserror::Error)] +#[error("Failed to initialize blocking thread pool: {0}")] +pub struct BlockingTaskPoolInitError(rayon::ThreadPoolBuildError); + +type BlockingTaskResult = Result>; + +#[derive(Debug)] +#[must_use = "BlockingTaskHandle does nothing unless polled"] +pub struct BlockingTaskHandle(oneshot::Receiver>); + +impl Future for BlockingTaskHandle { + type Output = BlockingTaskResult; + + fn poll(self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll { + match Pin::new(&mut self.get_mut().0).poll(cx) { + Poll::Ready(Ok(res)) => Poll::Ready(res), + Poll::Ready(Err(_)) => panic!("blocking task cancelled"), + Poll::Pending => Poll::Pending, + } + } +} + +/// A thread-pool for spawning blocking tasks . This is a simple wrapper around *rayon*'s +/// thread-pool. This is mainly for executing expensive CPU-bound tasks. For spawing blocking +/// IO-bound tasks, use [TokioTaskSpawner::spawn_blocking] instead. +/// +/// Refer to the [CPU-bound tasks and blocking code] section of the *tokio* docs and this [blog +/// post] for more information. +/// +/// [CPU-bound tasks and blocking code]: https://docs.rs/tokio/latest/tokio/index.html#cpu-bound-tasks-and-blocking-code +/// [blog post]: https://ryhl.io/blog/async-what-is-blocking/ +#[derive(Debug, Clone)] +pub struct BlockingTaskPool { + pool: Arc, +} + +impl BlockingTaskPool { + /// Returns *rayon*'s [ThreadPoolBuilder] which can be used to build a new [BlockingTaskPool]. + pub fn build() -> ThreadPoolBuilder { + ThreadPoolBuilder::new().thread_name(|i| format!("blocking-thread-pool-{i}")) + } + + /// Creates a new [BlockingTaskPool] with the default configuration. + pub fn new() -> Result { + Self::build() + .build() + .map(|pool| Self { pool: Arc::new(pool) }) + .map_err(BlockingTaskPoolInitError) + } + + /// Creates a new [BlockingTaskPool] with the given *rayon* thread pool. + pub fn new_with_pool(rayon_pool: rayon::ThreadPool) -> Self { + Self { pool: Arc::new(rayon_pool) } + } + + /// Spawns an asynchronous task in this thread-pool, returning a handle for waiting on the + /// result asynchronously. + pub fn spawn(&self, func: F) -> BlockingTaskHandle + where + F: FnOnce() -> R + Send + 'static, + R: Send + 'static, + { + let (tx, rx) = oneshot::channel(); + self.pool.spawn(move || { + let _ = tx.send(panic::catch_unwind(AssertUnwindSafe(func))); + }); + BlockingTaskHandle(rx) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn tokio_task_spawner() { + let rt = tokio::runtime::Runtime::new().unwrap(); + + { + rt.block_on(async { + assert!( + TokioTaskSpawner::new().is_ok(), + "TokioTaskSpawner::new() should return Ok if within a tokio runtime" + ) + }); + } + + { + let tokio_handle = rt.handle().clone(); + rt.block_on(async move { + let spawner = TokioTaskSpawner::new_with_handle(tokio_handle); + let res = spawner.spawn(async { 1 + 1 }).await; + assert!(res.is_ok()); + }) + } + + { + assert!( + TokioTaskSpawner::new() + .unwrap_err() + .to_string() + .contains("Failed to initialize task spawner:"), + "TokioTaskSpawner::new() should return an error if not within a tokio runtime" + ); + } + } + + #[test] + fn blocking_task_pool() { + let rt = tokio::runtime::Runtime::new().unwrap(); + let blocking_pool = BlockingTaskPool::new().unwrap(); + rt.block_on(async { + let res = blocking_pool.spawn(|| 1 + 1).await; + assert!(res.is_ok()); + let res = blocking_pool.spawn(|| panic!("test")).await; + assert!(res.is_err(), "panic'd task should be caught"); + }) + } +}