diff --git a/.tool-versions b/.tool-versions index 1a66fa783..66a0643ff 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,2 @@ -scarb 2.8.2 +scarb 2.15.0 katana 1.7.0 diff --git a/Cargo.lock b/Cargo.lock index 22aa3755d..aef3df865 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6072,8 +6072,26 @@ dependencies = [ name = "katana-grpc" version = "1.7.0" dependencies = [ + "cairo-lang-starknet-classes", + "hex", + "http 0.2.12", + "katana-pool", + "katana-primitives", + "katana-provider", + "katana-rpc-api", + "katana-rpc-server", + "katana-rpc-types", + "num-bigint", + "prost 0.12.6", + "serde_json", + "starknet", + "thiserror 1.0.69", + "tokio", "tonic 0.11.0", "tonic-build", + "tonic-reflection", + "tower-service", + "tracing", ] [[package]] @@ -6138,6 +6156,7 @@ dependencies = [ "katana-gateway-client", "katana-gateway-server", "katana-gateway-types", + "katana-grpc", "katana-messaging", "katana-metrics", "katana-pipeline", @@ -11038,6 +11057,19 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "tonic-reflection" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "548c227bd5c0fae5925812c4ec6c66ffcfced23ea370cb823f4d18f0fc1cb6a7" +dependencies = [ + "prost 0.12.6", + "prost-types 0.12.6", + "tokio", + "tokio-stream", + "tonic 0.11.0", +] + [[package]] name = "tower" version = "0.4.13" diff --git a/Cargo.toml b/Cargo.toml index 575c969f7..227b7dcfb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -88,6 +88,7 @@ katana-gateway-server = { path = "crates/gateway/gateway-server" } katana-fork = { path = "crates/storage/fork" } katana-gas-price-oracle = { path = "crates/oracle/gas" } katana-genesis = { path = "crates/genesis" } +katana-grpc = { path = "crates/grpc" } katana-messaging = { path = "crates/messaging" } katana-metrics = { path = "crates/metrics" } katana-node = { path = "crates/node" } @@ -194,6 +195,7 @@ tower-service = "0.3" prost = "0.12" tonic = { version = "0.11", features = [ "gzip", "tls", "tls-roots", "tls-webpki-roots" ] } tonic-build = "0.11" +tonic-reflection = "0.11" # benchmark criterion = "0.5.1" diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 415c8a838..28962e809 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -42,7 +42,8 @@ cartridge = [ "katana-node/cartridge", "katana-rpc-server/cartridge", ] -default = ["cartridge", "server", "tee"] +grpc = [ "katana-node/grpc" ] +default = ["cartridge", "server", "tee", "grpc"] explorer = ["katana-node/explorer", "katana-utils/explorer"] native = ["katana-node/native"] server = [] diff --git a/crates/cli/src/args.rs b/crates/cli/src/args.rs index b27f7529a..7500e3b1e 100644 --- a/crates/cli/src/args.rs +++ b/crates/cli/src/args.rs @@ -20,6 +20,8 @@ use katana_node::config::execution::ExecutionConfig; use katana_node::config::fork::ForkingConfig; #[cfg(feature = "server")] use katana_node::config::gateway::GatewayConfig; +#[cfg(all(feature = "server", feature = "grpc"))] +use katana_node::config::grpc::GrpcConfig; use katana_node::config::metrics::MetricsConfig; #[cfg(feature = "cartridge")] use katana_node::config::paymaster::PaymasterConfig; @@ -135,6 +137,10 @@ pub struct SequencerNodeArgs { #[cfg(feature = "tee")] #[command(flatten)] pub tee: TeeOptions, + + #[cfg(all(feature = "server", feature = "grpc"))] + #[command(flatten)] + pub grpc: GrpcOptions, } impl SequencerNodeArgs { @@ -209,6 +215,8 @@ impl SequencerNodeArgs { let (chain, cs_messaging) = self.chain_spec()?; let metrics = self.metrics_config(); let gateway = self.gateway_config(); + #[cfg(all(feature = "server", feature = "grpc"))] + let grpc = self.grpc_config(); let forking = self.forking_config()?; let execution = self.execution_config(); let sequencing = self.sequencer_config(); @@ -222,6 +230,8 @@ impl SequencerNodeArgs { db, dev, rpc, + #[cfg(feature = "grpc")] + grpc, chain, metrics, gateway, @@ -448,6 +458,21 @@ impl SequencerNodeArgs { None } + #[cfg(all(feature = "server", feature = "grpc"))] + fn grpc_config(&self) -> Option { + if self.grpc.grpc_enable { + use std::time::Duration; + + Some(GrpcConfig { + addr: self.grpc.grpc_addr, + port: self.grpc.grpc_port, + timeout: self.grpc.grpc_timeout.map(Duration::from_secs), + }) + } else { + None + } + } + #[cfg(feature = "cartridge")] fn cartridge_config(&self) -> Option { if self.cartridge.paymaster { @@ -508,6 +533,11 @@ impl SequencerNodeArgs { } } + #[cfg(all(feature = "server", feature = "grpc"))] + { + self.grpc.merge(config.grpc.as_ref()); + } + self.starknet.merge(config.starknet.as_ref()); self.development.merge(config.development.as_ref()); @@ -881,13 +911,13 @@ explorer = true ControllerV108, ControllerV109, }; - assert!(config.chain.genesis().classes.get(&ControllerV104::HASH).is_some()); - assert!(config.chain.genesis().classes.get(&ControllerV105::HASH).is_some()); - assert!(config.chain.genesis().classes.get(&ControllerV106::HASH).is_some()); - assert!(config.chain.genesis().classes.get(&ControllerV107::HASH).is_some()); - assert!(config.chain.genesis().classes.get(&ControllerV108::HASH).is_some()); - assert!(config.chain.genesis().classes.get(&ControllerV109::HASH).is_some()); - assert!(config.chain.genesis().classes.get(&ControllerLatest::HASH).is_some()); + assert!(config.chain.genesis().classes.contains_key(&ControllerV104::HASH)); + assert!(config.chain.genesis().classes.contains_key(&ControllerV105::HASH)); + assert!(config.chain.genesis().classes.contains_key(&ControllerV106::HASH)); + assert!(config.chain.genesis().classes.contains_key(&ControllerV107::HASH)); + assert!(config.chain.genesis().classes.contains_key(&ControllerV108::HASH)); + assert!(config.chain.genesis().classes.contains_key(&ControllerV109::HASH)); + assert!(config.chain.genesis().classes.contains_key(&ControllerLatest::HASH)); // Test without paymaster enabled let args = SequencerNodeArgs::parse_from(["katana"]); @@ -896,12 +926,12 @@ explorer = true // Verify cartridge module is not enabled by default assert!(!config.rpc.apis.contains(&RpcModuleKind::Cartridge)); - assert!(config.chain.genesis().classes.get(&ControllerV104::HASH).is_none()); - assert!(config.chain.genesis().classes.get(&ControllerV105::HASH).is_none()); - assert!(config.chain.genesis().classes.get(&ControllerV106::HASH).is_none()); - assert!(config.chain.genesis().classes.get(&ControllerV107::HASH).is_none()); - assert!(config.chain.genesis().classes.get(&ControllerV108::HASH).is_none()); - assert!(config.chain.genesis().classes.get(&ControllerV109::HASH).is_none()); - assert!(config.chain.genesis().classes.get(&ControllerLatest::HASH).is_none()); + assert!(!config.chain.genesis().classes.contains_key(&ControllerV104::HASH)); + assert!(!config.chain.genesis().classes.contains_key(&ControllerV105::HASH)); + assert!(!config.chain.genesis().classes.contains_key(&ControllerV106::HASH)); + assert!(!config.chain.genesis().classes.contains_key(&ControllerV107::HASH)); + assert!(!config.chain.genesis().classes.contains_key(&ControllerV108::HASH)); + assert!(!config.chain.genesis().classes.contains_key(&ControllerV109::HASH)); + assert!(!config.chain.genesis().classes.contains_key(&ControllerLatest::HASH)); } } diff --git a/crates/cli/src/file.rs b/crates/cli/src/file.rs index 92fa87886..a8e2eee91 100644 --- a/crates/cli/src/file.rs +++ b/crates/cli/src/file.rs @@ -26,6 +26,8 @@ pub struct NodeArgsConfig { pub server: Option, #[cfg(feature = "server")] pub metrics: Option, + #[cfg(all(feature = "server", feature = "grpc"))] + pub grpc: Option, #[cfg(feature = "cartridge")] pub cartridge: Option, #[cfg(feature = "explorer")] @@ -76,6 +78,12 @@ impl TryFrom for NodeArgsConfig { if args.metrics == MetricsOptions::default() { None } else { Some(args.metrics) }; } + #[cfg(all(feature = "server", feature = "grpc"))] + { + node_config.grpc = + if args.grpc == GrpcOptions::default() { None } else { Some(args.grpc) }; + } + #[cfg(feature = "cartridge")] { node_config.cartridge = if args.cartridge == CartridgeOptions::default() { diff --git a/crates/cli/src/options.rs b/crates/cli/src/options.rs index e36fd0cc0..5e9f4c257 100644 --- a/crates/cli/src/options.rs +++ b/crates/cli/src/options.rs @@ -835,3 +835,77 @@ impl TeeOptions { } } } + +#[cfg(all(feature = "server", feature = "grpc"))] +#[derive(Debug, Args, Clone, Serialize, Deserialize, PartialEq)] +#[command(next_help_heading = "gRPC server options")] +pub struct GrpcOptions { + /// Enable the gRPC server. + /// + /// When enabled, the gRPC server will start alongside the JSON-RPC server, + /// providing high-performance endpoints for Starknet operations. + #[arg(long = "grpc")] + #[serde(default)] + pub grpc_enable: bool, + + /// gRPC server listening interface. + #[arg(requires = "grpc_enable")] + #[arg(long = "grpc.addr", value_name = "ADDRESS")] + #[arg(default_value_t = default_grpc_addr())] + #[serde(default = "default_grpc_addr")] + pub grpc_addr: IpAddr, + + /// gRPC server listening port. + #[arg(requires = "grpc_enable")] + #[arg(long = "grpc.port", value_name = "PORT")] + #[arg(default_value_t = default_grpc_port())] + #[serde(default = "default_grpc_port")] + pub grpc_port: u16, + + /// gRPC request timeout in seconds. + #[arg(requires = "grpc_enable")] + #[arg(long = "grpc.timeout", value_name = "TIMEOUT")] + pub grpc_timeout: Option, +} + +#[cfg(all(feature = "server", feature = "grpc"))] +impl Default for GrpcOptions { + fn default() -> Self { + GrpcOptions { + grpc_enable: false, + grpc_addr: default_grpc_addr(), + grpc_port: default_grpc_port(), + grpc_timeout: None, + } + } +} + +#[cfg(all(feature = "server", feature = "grpc"))] +impl GrpcOptions { + pub fn merge(&mut self, other: Option<&Self>) { + if let Some(other) = other { + if !self.grpc_enable { + self.grpc_enable = other.grpc_enable; + } + if self.grpc_addr == default_grpc_addr() { + self.grpc_addr = other.grpc_addr; + } + if self.grpc_port == default_grpc_port() { + self.grpc_port = other.grpc_port; + } + if self.grpc_timeout.is_none() { + self.grpc_timeout = other.grpc_timeout; + } + } + } +} + +#[cfg(all(feature = "server", feature = "grpc"))] +fn default_grpc_addr() -> IpAddr { + katana_node::config::grpc::DEFAULT_GRPC_ADDR +} + +#[cfg(all(feature = "server", feature = "grpc"))] +fn default_grpc_port() -> u16 { + katana_node::config::grpc::DEFAULT_GRPC_PORT +} diff --git a/crates/grpc/Cargo.toml b/crates/grpc/Cargo.toml index fb9ade66e..c8e8e9c67 100644 --- a/crates/grpc/Cargo.toml +++ b/crates/grpc/Cargo.toml @@ -6,9 +6,44 @@ repository.workspace = true version.workspace = true [dependencies] +# Internal dependencies +katana-primitives.workspace = true +katana-provider.workspace = true +katana-pool.workspace = true +katana-rpc-types.workspace = true +katana-rpc-server.workspace = true +katana-rpc-api.workspace = true + +# gRPC dependencies tonic.workspace = true +tonic-reflection = "0.11" +prost.workspace = true +http = "0.2" + +# Async runtime +tokio.workspace = true + +# Logging +tracing.workspace = true + +# Error handling +thiserror.workspace = true + +# Serialization +serde_json.workspace = true + +# Starknet types +starknet.workspace = true + +# Cairo types +cairo-lang-starknet-classes.workspace = true +num-bigint.workspace = true + +tower-service.workspace = true [dev-dependencies] +tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } +hex = "0.4" [build-dependencies] tonic-build.workspace = true diff --git a/crates/grpc/build.rs b/crates/grpc/build.rs index 361736f8b..f8607f9a5 100644 --- a/crates/grpc/build.rs +++ b/crates/grpc/build.rs @@ -3,13 +3,13 @@ use std::path::PathBuf; fn main() -> Result<(), Box> { let out_dir = PathBuf::from(std::env::var("OUT_DIR").expect("OUT_DIR environment variable not set")); - let feature_client = std::env::var("CARGO_FEATURE_CLIENT"); - let feature_server = std::env::var("CARGO_FEATURE_SERVER"); tonic_build::configure() - .build_server(feature_server.is_ok()) - .build_client(feature_client.is_ok()) + .build_server(true) + .build_client(true) .file_descriptor_set_path(out_dir.join("starknet_descriptor.bin")) + // Allow clippy lints on generated code for enum variant naming and size patterns + .type_attribute(".", "#[allow(clippy::enum_variant_names, clippy::large_enum_variant)]") .compile(&["proto/starknet.proto"], &["proto"])?; println!("cargo:rerun-if-changed=proto"); diff --git a/crates/grpc/proto/common.proto b/crates/grpc/proto/common.proto new file mode 100644 index 000000000..83018d16e --- /dev/null +++ b/crates/grpc/proto/common.proto @@ -0,0 +1,7 @@ +syntax = "proto3"; + +package common; + +message Felt { + bytes value = 1; +} diff --git a/crates/grpc/proto/starknet.proto b/crates/grpc/proto/starknet.proto index 12e326511..2ce026b17 100644 --- a/crates/grpc/proto/starknet.proto +++ b/crates/grpc/proto/starknet.proto @@ -2,9 +2,10 @@ syntax = "proto3"; package starknet; +import "common.proto"; import "types.proto"; -// The Starknet service provides methods for interacting with Starknet. +// The Starknet service provides read methods for interacting with Starknet. service Starknet { // Returns the version of the Starknet JSON-RPC specification being used rpc SpecVersion(SpecVersionRequest) returns (SpecVersionResponse); @@ -74,6 +75,36 @@ service Starknet { // Get the nonce associated with the given address in the given block rpc GetNonce(GetNonceRequest) returns (GetNonceResponse); + + // Get the compiled CASM for a given class hash + rpc GetCompiledCasm(GetCompiledCasmRequest) returns (GetCompiledCasmResponse); + + // Get Merkle paths in the state tries for a set of classes, contracts, and storage keys + rpc GetStorageProof(GetStorageProofRequest) returns (GetStorageProofResponse); +} + +// The StarknetWrite service provides write methods for submitting transactions. +service StarknetWrite { + // Submit a new invoke transaction + rpc AddInvokeTransaction(AddInvokeTransactionRequest) returns (AddInvokeTransactionResponse); + + // Submit a new declare transaction + rpc AddDeclareTransaction(AddDeclareTransactionRequest) returns (AddDeclareTransactionResponse); + + // Submit a new deploy account transaction + rpc AddDeployAccountTransaction(AddDeployAccountTransactionRequest) returns (AddDeployAccountTransactionResponse); +} + +// The StarknetTrace service provides trace methods for debugging and simulation. +service StarknetTrace { + // Get the trace for a specific transaction + rpc TraceTransaction(TraceTransactionRequest) returns (TraceTransactionResponse); + + // Simulate a list of transactions and return their execution traces and fee estimations + rpc SimulateTransactions(SimulateTransactionsRequest) returns (SimulateTransactionsResponse); + + // Get the traces for all transactions in a block + rpc TraceBlockTransactions(TraceBlockTransactionsRequest) returns (TraceBlockTransactionsResponse); } message SpecVersionRequest {} @@ -116,16 +147,16 @@ message GetStateUpdateResponse { message GetStorageAtRequest { types.BlockID block_id = 1; - types.Felt contract_address = 2; - types.Felt key = 3; + common.Felt contract_address = 2; + common.Felt key = 3; } message GetStorageAtResponse { - types.Felt value = 1; + common.Felt value = 1; } message GetTransactionStatusRequest { - types.Felt transaction_hash = 1; + common.Felt transaction_hash = 1; } message GetTransactionStatusResponse { @@ -134,7 +165,7 @@ message GetTransactionStatusResponse { } message GetTransactionByHashRequest { - types.Felt transaction_hash = 1; + common.Felt transaction_hash = 1; } message GetTransactionByHashResponse { @@ -151,7 +182,7 @@ message GetTransactionByBlockIdAndIndexResponse { } message GetTransactionReceiptRequest { - types.Felt transaction_hash = 1; + common.Felt transaction_hash = 1; } message GetTransactionReceiptResponse { @@ -160,7 +191,7 @@ message GetTransactionReceiptResponse { message GetClassRequest { types.BlockID block_id = 1; - types.Felt class_hash = 2; + common.Felt class_hash = 2; } message GetClassResponse { @@ -172,16 +203,16 @@ message GetClassResponse { message GetClassHashAtRequest { types.BlockID block_id = 1; - types.Felt contract_address = 2; + common.Felt contract_address = 2; } message GetClassHashAtResponse { - types.Felt class_hash = 1; + common.Felt class_hash = 1; } message GetClassAtRequest { types.BlockID block_id = 1; - types.Felt contract_address = 2; + common.Felt contract_address = 2; } message GetClassAtResponse { @@ -201,7 +232,7 @@ message CallRequest { } message CallResponse { - repeated types.Felt result = 1; + repeated common.Felt result = 1; } message EstimateFeeRequest { @@ -228,7 +259,7 @@ message BlockNumberResponse { message BlockHashAndNumberRequest {} message BlockHashAndNumberResponse { - types.Felt block_hash = 1; + common.Felt block_hash = 1; uint64 block_number = 2; } @@ -260,9 +291,97 @@ message GetEventsResponse { message GetNonceRequest { types.BlockID block_id = 1; - types.Felt contract_address = 2; + common.Felt contract_address = 2; } message GetNonceResponse { - types.Felt nonce = 1; + common.Felt nonce = 1; +} + +// ============================================================ +// Additional Read Methods +// ============================================================ + +message GetCompiledCasmRequest { + common.Felt class_hash = 1; +} + +message GetCompiledCasmResponse { + types.CasmContractClass casm_class = 1; +} + +message GetStorageProofRequest { + types.BlockID block_id = 1; + repeated common.Felt class_hashes = 2; + repeated common.Felt contract_addresses = 3; + repeated types.ContractStorageKeysRequest contracts_storage_keys = 4; +} + +message GetStorageProofResponse { + types.StorageProof proof = 1; +} + +// ============================================================ +// Write Methods (StarknetWrite service) +// ============================================================ + +message AddInvokeTransactionRequest { + types.BroadcastedInvokeTransaction transaction = 1; +} + +message AddInvokeTransactionResponse { + common.Felt transaction_hash = 1; +} + +message AddDeclareTransactionRequest { + types.BroadcastedDeclareTransaction transaction = 1; +} + +message AddDeclareTransactionResponse { + common.Felt transaction_hash = 1; + common.Felt class_hash = 2; +} + +message AddDeployAccountTransactionRequest { + types.BroadcastedDeployAccountTransaction transaction = 1; +} + +message AddDeployAccountTransactionResponse { + common.Felt transaction_hash = 1; + common.Felt contract_address = 2; +} + +// ============================================================ +// Trace Methods (StarknetTrace service) +// ============================================================ + +message TraceTransactionRequest { + common.Felt transaction_hash = 1; +} + +message TraceTransactionResponse { + types.TransactionTrace trace = 1; +} + +message SimulateTransactionsRequest { + types.BlockID block_id = 1; + repeated types.Transaction transactions = 2; + repeated string simulation_flags = 3; +} + +message SimulateTransactionsResponse { + repeated types.SimulatedTransaction simulated_transactions = 1; +} + +message TraceBlockTransactionsRequest { + types.ConfirmedBlockId block_id = 1; +} + +message TraceBlockTransactionsResponse { + repeated TransactionTraceWithHash traces = 1; +} + +message TransactionTraceWithHash { + common.Felt transaction_hash = 1; + types.TransactionTrace trace_root = 2; } diff --git a/crates/grpc/proto/types.proto b/crates/grpc/proto/types.proto index ef07757c9..cfb4fce50 100644 --- a/crates/grpc/proto/types.proto +++ b/crates/grpc/proto/types.proto @@ -2,15 +2,36 @@ syntax = "proto3"; package types; -message Felt { - bytes value = 1; +import "common.proto"; + +// Block tag for identifying special blocks. +enum BlockTag { + BLOCK_TAG_LATEST = 0; + BLOCK_TAG_PRE_CONFIRMED = 1; + BLOCK_TAG_L1_ACCEPTED = 2; } message BlockID { oneof identifier { uint64 number = 1 [json_name = "block_number"]; - Felt hash = 2 [json_name = "block_hash"]; - string tag = 3 [json_name = "block_tag"]; + common.Felt hash = 2 [json_name = "block_hash"]; + BlockTag tag = 3 [json_name = "block_tag"]; + } +} + +// Block tag for confirmed blocks only (no PRE_CONFIRMED). +enum ConfirmedBlockTag { + CONFIRMED_BLOCK_TAG_LATEST = 0; + CONFIRMED_BLOCK_TAG_L1_ACCEPTED = 1; +} + +// Block identifier for confirmed blocks only. +// Used by trace methods that cannot operate on pending blocks. +message ConfirmedBlockId { + oneof identifier { + uint64 number = 1 [json_name = "block_number"]; + common.Felt hash = 2 [json_name = "block_hash"]; + ConfirmedBlockTag tag = 3 [json_name = "block_tag"]; } } @@ -20,6 +41,19 @@ enum SimulationFlag { SKIP_VALIDATE = 2; } +// Finality status of a canonical block. +enum FinalityStatus { + FINALITY_STATUS_ACCEPTED_ON_L2 = 0; + FINALITY_STATUS_ACCEPTED_ON_L1 = 1; + FINALITY_STATUS_PRE_CONFIRMED = 2; +} + +// L1 data availability mode. +enum L1DataAvailabilityMode { + L1_DATA_AVAILABILITY_MODE_BLOB = 0; + L1_DATA_AVAILABILITY_MODE_CALLDATA = 1; +} + message Transaction { oneof transaction { InvokeTxnV1 invoke_v1 = 1; @@ -29,137 +63,161 @@ message Transaction { DeclareTxnV3 declare_v3 = 5; DeployAccountTxn deploy_account = 6; DeployAccountTxnV3 deploy_account_v3 = 7; + L1HandlerTxn l1_handler = 8; + DeployTxn deploy = 9; } } message InvokeTxnV1 { - Felt max_fee = 1 [json_name = "max_fee"]; + common.Felt max_fee = 1 [json_name = "max_fee"]; string version = 2 [json_name = "version"]; - repeated Felt signature = 3 [json_name = "signature"]; - Felt nonce = 4 [json_name = "nonce"]; + repeated common.Felt signature = 3 [json_name = "signature"]; + common.Felt nonce = 4 [json_name = "nonce"]; string type = 5 [json_name = "type"]; - Felt sender_address = 6 [json_name = "sender_address"]; - repeated Felt calldata = 7 [json_name = "calldata"]; + common.Felt sender_address = 6 [json_name = "sender_address"]; + repeated common.Felt calldata = 7 [json_name = "calldata"]; } message InvokeTxnV3 { string type = 1 [json_name = "type"]; - Felt sender_address = 2 [json_name = "sender_address"]; - repeated Felt calldata = 3 [json_name = "calldata"]; + common.Felt sender_address = 2 [json_name = "sender_address"]; + repeated common.Felt calldata = 3 [json_name = "calldata"]; string version = 4 [json_name = "version"]; - repeated Felt signature = 5 [json_name = "signature"]; - Felt nonce = 6 [json_name = "nonce"]; + repeated common.Felt signature = 5 [json_name = "signature"]; + common.Felt nonce = 6 [json_name = "nonce"]; ResourceBoundsMapping resource_bounds = 7 [json_name = "resource_bounds"]; - Felt tip = 8 [json_name = "tip"]; - repeated Felt paymaster_data = 9 [json_name = "paymaster_data"]; - repeated Felt account_deployment_data = 10 [json_name = "account_deployment_data"]; + common.Felt tip = 8 [json_name = "tip"]; + repeated common.Felt paymaster_data = 9 [json_name = "paymaster_data"]; + repeated common.Felt account_deployment_data = 10 [json_name = "account_deployment_data"]; string nonce_data_availability_mode = 11 [json_name = "nonce_data_availability_mode"]; string fee_data_availability_mode = 12 [json_name = "fee_data_availability_mode"]; } message DeclareTxnV1 { - Felt max_fee = 1 [json_name = "max_fee"]; + common.Felt max_fee = 1 [json_name = "max_fee"]; string version = 2 [json_name = "version"]; - repeated Felt signature = 3 [json_name = "signature"]; - Felt nonce = 4 [json_name = "nonce"]; + repeated common.Felt signature = 3 [json_name = "signature"]; + common.Felt nonce = 4 [json_name = "nonce"]; string type = 5 [json_name = "type"]; - Felt class_hash = 6 [json_name = "class_hash"]; - Felt sender_address = 7 [json_name = "sender_address"]; + common.Felt class_hash = 6 [json_name = "class_hash"]; + common.Felt sender_address = 7 [json_name = "sender_address"]; } message DeclareTxnV2 { string type = 1 [json_name = "type"]; - Felt sender_address = 2 [json_name = "sender_address"]; - Felt compiled_class_hash = 3 [json_name = "compiled_class_hash"]; - Felt max_fee = 4 [json_name = "max_fee"]; + common.Felt sender_address = 2 [json_name = "sender_address"]; + common.Felt compiled_class_hash = 3 [json_name = "compiled_class_hash"]; + common.Felt max_fee = 4 [json_name = "max_fee"]; string version = 5 [json_name = "version"]; - repeated Felt signature = 6 [json_name = "signature"]; - Felt nonce = 7 [json_name = "nonce"]; + repeated common.Felt signature = 6 [json_name = "signature"]; + common.Felt nonce = 7 [json_name = "nonce"]; bytes class = 8 [json_name = "contract_class"]; } message DeclareTxnV3 { string type = 1 [json_name = "type"]; - Felt sender_address = 2 [json_name = "sender_address"]; - Felt compiled_class_hash = 3 [json_name = "compiled_class_hash"]; + common.Felt sender_address = 2 [json_name = "sender_address"]; + common.Felt compiled_class_hash = 3 [json_name = "compiled_class_hash"]; string version = 4 [json_name = "version"]; - repeated Felt signature = 5 [json_name = "signature"]; - Felt nonce = 6 [json_name = "nonce"]; - Felt class_hash = 7 [json_name = "class_hash"]; + repeated common.Felt signature = 5 [json_name = "signature"]; + common.Felt nonce = 6 [json_name = "nonce"]; + common.Felt class_hash = 7 [json_name = "class_hash"]; ResourceBoundsMapping resource_bounds = 8 [json_name = "resource_bounds"]; - Felt tip = 9 [json_name = "tip"]; - repeated Felt paymaster_data = 10 [json_name = "paymaster_data"]; - repeated Felt account_deployment_data = 11 [json_name = "account_deployment_data"]; + common.Felt tip = 9 [json_name = "tip"]; + repeated common.Felt paymaster_data = 10 [json_name = "paymaster_data"]; + repeated common.Felt account_deployment_data = 11 [json_name = "account_deployment_data"]; string nonce_data_availability_mode = 12 [json_name = "nonce_data_availability_mode"]; string fee_data_availability_mode = 13 [json_name = "fee_data_availability_mode"]; } message DeployAccountTxn { - Felt max_fee = 1 [json_name = "max_fee"]; + common.Felt max_fee = 1 [json_name = "max_fee"]; string version = 2 [json_name = "version"]; - repeated Felt signature = 3 [json_name = "signature"]; - Felt nonce = 4 [json_name = "nonce"]; + repeated common.Felt signature = 3 [json_name = "signature"]; + common.Felt nonce = 4 [json_name = "nonce"]; string type = 5 [json_name = "type"]; - Felt class_hash = 6 [json_name = "class_hash"]; - Felt contract_address_salt = 7 [json_name = "contract_address_salt"]; - repeated Felt constructor_calldata = 8 [json_name = "constructor_calldata"]; + common.Felt class_hash = 6 [json_name = "class_hash"]; + common.Felt contract_address_salt = 7 [json_name = "contract_address_salt"]; + repeated common.Felt constructor_calldata = 8 [json_name = "constructor_calldata"]; } message DeployAccountTxnV3 { string type = 1 [json_name = "type"]; string version = 2 [json_name = "version"]; - repeated Felt signature = 3 [json_name = "signature"]; - Felt nonce = 4 [json_name = "nonce"]; - Felt contract_address_salt = 5 [json_name = "contract_address_salt"]; - repeated Felt constructor_calldata = 6 [json_name = "constructor_calldata"]; - Felt class_hash = 7 [json_name = "class_hash"]; + repeated common.Felt signature = 3 [json_name = "signature"]; + common.Felt nonce = 4 [json_name = "nonce"]; + common.Felt contract_address_salt = 5 [json_name = "contract_address_salt"]; + repeated common.Felt constructor_calldata = 6 [json_name = "constructor_calldata"]; + common.Felt class_hash = 7 [json_name = "class_hash"]; ResourceBoundsMapping resource_bounds = 8 [json_name = "resource_bounds"]; - Felt tip = 9 [json_name = "tip"]; - repeated Felt paymaster_data = 10 [json_name = "paymaster_data"]; + common.Felt tip = 9 [json_name = "tip"]; + repeated common.Felt paymaster_data = 10 [json_name = "paymaster_data"]; string nonce_data_availability_mode = 11 [json_name = "nonce_data_availability_mode"]; string fee_data_availability_mode = 12 [json_name = "fee_data_availability_mode"]; } +// L1 Handler transaction (from L1 to L2 messages) +message L1HandlerTxn { + string type = 1 [json_name = "type"]; + string version = 2 [json_name = "version"]; + common.Felt nonce = 3 [json_name = "nonce"]; + common.Felt contract_address = 4 [json_name = "contract_address"]; + common.Felt entry_point_selector = 5 [json_name = "entry_point_selector"]; + repeated common.Felt calldata = 6 [json_name = "calldata"]; +} + +// Legacy deploy transaction +message DeployTxn { + string type = 1 [json_name = "type"]; + string version = 2 [json_name = "version"]; + common.Felt class_hash = 3 [json_name = "class_hash"]; + common.Felt contract_address_salt = 4 [json_name = "contract_address_salt"]; + repeated common.Felt constructor_calldata = 5 [json_name = "constructor_calldata"]; +} + message ResourceBoundsMapping { - ResourceBounds l1_gas = 1 [json_name = "L1_GAS"]; - ResourceBounds l2_gas = 2 [json_name = "L2_GAS"]; + ResourceBounds l1_gas = 1 [json_name = "l1_gas"]; + ResourceBounds l2_gas = 2 [json_name = "l2_gas"]; + ResourceBounds l1_data_gas = 3 [json_name = "l1_data_gas"]; // For v0.14.0+ } message ResourceBounds { - Felt max_amount = 1 [json_name = "max_amount"]; - Felt max_price_per_unit = 2 [json_name = "max_price_per_unit"]; + common.Felt max_amount = 1 [json_name = "max_amount"]; + common.Felt max_price_per_unit = 2 [json_name = "max_price_per_unit"]; } +// Fee estimate matching JSON-RPC FeeEstimate message FeeEstimate { - Felt gas_consumed = 1 [json_name = "gas_consumed"]; - Felt gas_price = 2 [json_name = "gas_price"]; - Felt data_gas_consumed = 3 [json_name = "data_gas_consumed"]; - Felt data_gas_price = 4 [json_name = "data_gas_price"]; - Felt overall_fee = 5 [json_name = "overall_fee"]; - string unit = 6 [json_name = "unit"]; + common.Felt l1_gas_consumed = 1 [json_name = "l1_gas_consumed"]; + common.Felt l1_gas_price = 2 [json_name = "l1_gas_price"]; + common.Felt l2_gas_consumed = 3 [json_name = "l2_gas_consumed"]; + common.Felt l2_gas_price = 4 [json_name = "l2_gas_price"]; + common.Felt l1_data_gas_consumed = 5 [json_name = "l1_data_gas_consumed"]; + common.Felt l1_data_gas_price = 6 [json_name = "l1_data_gas_price"]; + common.Felt overall_fee = 7 [json_name = "overall_fee"]; } message BlockWithTxHashes { - string status = 1; + FinalityStatus status = 1; BlockHeader header = 2; - repeated Felt transactions = 3; + repeated common.Felt transactions = 3; } message BlockWithTxs { - string status = 1; + FinalityStatus status = 1; BlockHeader header = 2; repeated Transaction transactions = 3; } message BlockWithReceipts { - string status = 1; + FinalityStatus status = 1; BlockHeader header = 2; repeated TransactionWithReceipt transactions = 3; } message PendingBlockWithTxHashes { BlockHeader header = 1; - repeated Felt transactions = 2; + repeated common.Felt transactions = 2; } message PendingBlockWithTxs { @@ -173,20 +231,20 @@ message PendingBlockWithReceipts { } message StateUpdate { - Felt block_hash = 1; - Felt old_root = 2; - Felt new_root = 3; + common.Felt block_hash = 1; + common.Felt old_root = 2; + common.Felt new_root = 3; StateDiff state_diff = 4; } message PendingStateUpdate { - Felt old_root = 1; + common.Felt old_root = 1; StateDiff state_diff = 2; } message StateDiff { repeated StorageDiff storage_diffs = 1; - repeated Felt deprecated_declared_classes = 2; + repeated common.Felt deprecated_declared_classes = 2; repeated DeclaredClass declared_classes = 3; repeated DeployedContract deployed_contracts = 4; repeated ReplacedClass replaced_classes = 5; @@ -194,51 +252,52 @@ message StateDiff { } message StorageDiff { - Felt address = 1; + common.Felt address = 1; repeated StorageEntry storage_entries = 2; } message StorageEntry { - Felt key = 1; - Felt value = 2; + common.Felt key = 1; + common.Felt value = 2; } message DeclaredClass { - Felt class_hash = 1; - Felt compiled_class_hash = 2; + common.Felt class_hash = 1; + common.Felt compiled_class_hash = 2; } message DeployedContract { - Felt address = 1; - Felt class_hash = 2; + common.Felt address = 1; + common.Felt class_hash = 2; } message ReplacedClass { - Felt contract_address = 1; - Felt class_hash = 2; + common.Felt contract_address = 1; + common.Felt class_hash = 2; } message Nonce { - Felt contract_address = 1; - Felt nonce = 2; + common.Felt contract_address = 1; + common.Felt nonce = 2; } message BlockHeader { - Felt block_hash = 1; - Felt parent_hash = 2; + common.Felt block_hash = 1; + common.Felt parent_hash = 2; uint64 block_number = 3; - Felt new_root = 4; + common.Felt new_root = 4; uint64 timestamp = 5; - Felt sequencer_address = 6; + common.Felt sequencer_address = 6; ResourcePrice l1_gas_price = 7; ResourcePrice l1_data_gas_price = 8; - string l1_da_mode = 9; - string starknet_version = 10; + ResourcePrice l2_gas_price = 9; + L1DataAvailabilityMode l1_da_mode = 10; + string starknet_version = 11; } message ResourcePrice { - Felt price_in_wei = 1; - Felt price_in_fri = 2; + common.Felt price_in_wei = 1; + common.Felt price_in_fri = 2; } message TransactionWithReceipt { @@ -246,92 +305,89 @@ message TransactionWithReceipt { TransactionReceipt receipt = 2; } +// Transaction receipt matching JSON-RPC structure message TransactionReceipt { string type = 1; - Felt transaction_hash = 2; + common.Felt transaction_hash = 2; FeePayment actual_fee = 3; - string finality_status = 4; + FinalityStatus finality_status = 4; repeated MessageToL1 messages_sent = 5; repeated Event events = 6; ExecutionResources execution_resources = 7; string execution_status = 8; string revert_reason = 9; + // Type-specific fields + common.Felt contract_address = 10; // For Deploy and DeployAccount receipts + common.Felt message_hash = 11; // For L1Handler receipts + uint64 block_number = 12; + common.Felt block_hash = 13; } message FeePayment { - Felt amount = 1; + common.Felt amount = 1; string unit = 2; } message MessageToL1 { - Felt from_address = 1; - Felt to_address = 2; - repeated Felt payload = 3; + common.Felt from_address = 1; + common.Felt to_address = 2; + repeated common.Felt payload = 3; } message Event { - Felt from_address = 1; - repeated Felt keys = 2; - repeated Felt data = 3; + common.Felt from_address = 1; + repeated common.Felt keys = 2; + repeated common.Felt data = 3; } +// Execution resources matching JSON-RPC ExecutionResources (simple gas fields only) message ExecutionResources { - uint64 steps = 1; - uint64 memory_holes = 2; - uint64 range_check_builtin_applications = 3; - uint64 pedersen_builtin_applications = 4; - uint64 poseidon_builtin_applications = 5; - uint64 ec_op_builtin_applications = 6; - uint64 ecdsa_builtin_applications = 7; - uint64 bitwise_builtin_applications = 8; - uint64 keccak_builtin_applications = 9; - uint64 segment_arena_builtin = 10; - DataAvailability data_availability = 11; -} - -message DataAvailability { uint64 l1_gas = 1; uint64 l1_data_gas = 2; + uint64 l2_gas = 3; } message FunctionCall { - Felt contract_address = 1; - Felt entry_point_selector = 2; - repeated Felt calldata = 3; + common.Felt contract_address = 1; + common.Felt entry_point_selector = 2; + repeated common.Felt calldata = 3; } message MessageFromL1 { string from_address = 1; - Felt to_address = 2; - Felt entry_point_selector = 3; - repeated Felt payload = 4; + common.Felt to_address = 2; + common.Felt entry_point_selector = 3; + repeated common.Felt payload = 4; } +// Emitted event with index fields message EmittedEvent { - Event event = 1; - Felt block_hash = 2; - uint64 block_number = 3; - Felt transaction_hash = 4; + common.Felt from_address = 1; + repeated common.Felt keys = 2; + repeated common.Felt data = 3; + common.Felt block_hash = 4; + uint64 block_number = 5; + common.Felt transaction_hash = 6; } message EventFilter { BlockID from_block = 1; BlockID to_block = 2; - Felt address = 3; - repeated Felt keys = 4; + common.Felt address = 3; + repeated common.Felt keys = 4; } message SyncStatus { - Felt starting_block_hash = 1; + common.Felt starting_block_hash = 1; uint64 starting_block_num = 2; - Felt current_block_hash = 3; + common.Felt current_block_hash = 3; uint64 current_block_num = 4; - Felt highest_block_hash = 5; + common.Felt highest_block_hash = 5; uint64 highest_block_num = 6; } message ContractClass { - repeated Felt sierra_program = 1; + repeated common.Felt sierra_program = 1; string contract_class_version = 2; EntryPointsByType entry_points_by_type = 3; string abi = 4; @@ -356,11 +412,233 @@ message DeprecatedEntryPointsByType { } message SierraEntryPoint { - Felt selector = 1; + common.Felt selector = 1; uint64 function_idx = 2; } message DeprecatedCairoEntryPoint { string offset = 1; - Felt selector = 2; + common.Felt selector = 2; +} + +// ============================================================ +// Broadcasted Transaction Types (for write operations) +// ============================================================ + +// Broadcasted INVOKE transaction - flat structure matching RPC types +message BroadcastedInvokeTransaction { + common.Felt sender_address = 1; + repeated common.Felt calldata = 2; + repeated common.Felt signature = 3; + common.Felt nonce = 4; + repeated common.Felt paymaster_data = 5; + common.Felt tip = 6; + repeated common.Felt account_deployment_data = 7; + ResourceBoundsMapping resource_bounds = 8; + string fee_data_availability_mode = 9; + string nonce_data_availability_mode = 10; + common.Felt version = 11; +} + +// Broadcasted DECLARE transaction - flat structure matching RPC types +message BroadcastedDeclareTransaction { + common.Felt sender_address = 1; + common.Felt compiled_class_hash = 2; + repeated common.Felt signature = 3; + common.Felt nonce = 4; + ContractClass contract_class = 5; + repeated common.Felt paymaster_data = 6; + common.Felt tip = 7; + repeated common.Felt account_deployment_data = 8; + ResourceBoundsMapping resource_bounds = 9; + string fee_data_availability_mode = 10; + string nonce_data_availability_mode = 11; + common.Felt version = 12; +} + +// Broadcasted DEPLOY_ACCOUNT transaction - flat structure matching RPC types +message BroadcastedDeployAccountTransaction { + repeated common.Felt signature = 1; + common.Felt nonce = 2; + common.Felt contract_address_salt = 3; + repeated common.Felt constructor_calldata = 4; + common.Felt class_hash = 5; + repeated common.Felt paymaster_data = 6; + common.Felt tip = 7; + ResourceBoundsMapping resource_bounds = 8; + string fee_data_availability_mode = 9; + string nonce_data_availability_mode = 10; + common.Felt version = 11; +} + +// ============================================================ +// Transaction Trace Types (for trace operations) +// ============================================================ + +message TransactionTrace { + oneof trace { + InvokeTransactionTrace invoke_trace = 1; + DeclareTransactionTrace declare_trace = 2; + DeployAccountTransactionTrace deploy_account_trace = 3; + L1HandlerTransactionTrace l1_handler_trace = 4; + } +} + +message InvokeTransactionTrace { + FunctionInvocation execute_invocation = 1; + FunctionInvocation validate_invocation = 2; + FunctionInvocation fee_transfer_invocation = 3; + StateDiff state_diff = 4; + ExecutionResources execution_resources = 5; +} + +message DeclareTransactionTrace { + FunctionInvocation validate_invocation = 1; + FunctionInvocation fee_transfer_invocation = 2; + StateDiff state_diff = 3; + ExecutionResources execution_resources = 4; +} + +message DeployAccountTransactionTrace { + FunctionInvocation constructor_invocation = 1; + FunctionInvocation validate_invocation = 2; + FunctionInvocation fee_transfer_invocation = 3; + StateDiff state_diff = 4; + ExecutionResources execution_resources = 5; +} + +message L1HandlerTransactionTrace { + FunctionInvocation function_invocation = 1; + StateDiff state_diff = 2; + ExecutionResources execution_resources = 3; +} + +// Function invocation matching JSON-RPC FunctionInvocation +message FunctionInvocation { + common.Felt contract_address = 1; + common.Felt entry_point_selector = 2; + repeated common.Felt calldata = 3; + common.Felt caller_address = 4; + common.Felt class_hash = 5; + string entry_point_type = 6; + string call_type = 7; + repeated common.Felt result = 8; + repeated FunctionInvocation calls = 9; + repeated OrderedEvent events = 10; + repeated OrderedL2ToL1Message messages = 11; + ExecutionResources execution_resources = 12; + bool is_reverted = 13; +} + +// Ordered event (events in traces have order) +message OrderedEvent { + uint64 order = 1; + repeated common.Felt keys = 2; + repeated common.Felt data = 3; +} + +// Ordered L2 to L1 message (messages in traces have order) +message OrderedL2ToL1Message { + uint64 order = 1; + common.Felt from_address = 2; + common.Felt to_address = 3; + repeated common.Felt payload = 4; +} + +message SimulatedTransaction { + TransactionTrace transaction_trace = 1; + FeeEstimate fee_estimation = 2; +} + +// ============================================================ +// Storage Proof Types +// ============================================================ + +message StorageProof { + GlobalRoots global_roots = 1; + ClassesProof classes_proof = 2; + ContractsProof contracts_proof = 3; + ContractStorageProofs contracts_storage_proofs = 4; +} + +message GlobalRoots { + common.Felt block_hash = 1; + common.Felt classes_tree_root = 2; + common.Felt contracts_tree_root = 3; +} + +message ClassesProof { + repeated MerkleProofNode nodes = 1; +} + +message ContractsProof { + repeated MerkleProofNode nodes = 1; + repeated ContractLeafData contract_leaves_data = 2; +} + +message ContractLeafData { + common.Felt storage_root = 1; + common.Felt class_hash = 2; + common.Felt nonce = 3; +} + +message ContractStorageProofs { + repeated ContractStorageProof proofs = 1; +} + +message ContractStorageProof { + repeated MerkleProofNode nodes = 1; +} + +message MerkleProofNode { + oneof node { + BinaryNode binary = 1; + EdgeNode edge = 2; + } +} + +message BinaryNode { + common.Felt left = 1; + common.Felt right = 2; +} + +message EdgeNode { + common.Felt path = 1; + uint32 length = 2; + common.Felt child = 3; +} + +message ContractStorageKeysRequest { + common.Felt contract_address = 1; + repeated common.Felt keys = 2; +} + +// ============================================================ +// Compiled CASM Class Type +// ============================================================ + +message CasmContractClass { + string prime = 1; + string compiler_version = 2; + repeated CasmContractEntryPoint entry_points_by_type_external = 3; + repeated CasmContractEntryPoint entry_points_by_type_l1_handler = 4; + repeated CasmContractEntryPoint entry_points_by_type_constructor = 5; + repeated bytes bytecode = 6; + repeated BytecodeSegmentLength bytecode_segment_lengths = 7; + repeated Hint hints = 8; +} + +message CasmContractEntryPoint { + common.Felt selector = 1; + uint64 offset = 2; + repeated common.Felt builtins = 3; +} + +message BytecodeSegmentLength { + uint64 length = 1; +} + +message Hint { + uint64 id = 1; + string code = 2; } diff --git a/crates/grpc/src/client.rs b/crates/grpc/src/client.rs new file mode 100644 index 000000000..b2a8d7ede --- /dev/null +++ b/crates/grpc/src/client.rs @@ -0,0 +1,407 @@ +//! gRPC client implementation. + +use std::time::Duration; + +use tonic::transport::{Channel, Endpoint, Uri}; +use tonic::{Request, Response, Status}; + +use crate::protos::starknet::starknet_client::StarknetClient; +use crate::protos::starknet::starknet_trace_client::StarknetTraceClient; +use crate::protos::starknet::starknet_write_client::StarknetWriteClient; +use crate::protos::starknet::{ + AddDeclareTransactionRequest, AddDeclareTransactionResponse, + AddDeployAccountTransactionRequest, AddDeployAccountTransactionResponse, + AddInvokeTransactionRequest, AddInvokeTransactionResponse, BlockHashAndNumberRequest, + BlockHashAndNumberResponse, BlockNumberRequest, BlockNumberResponse, CallRequest, CallResponse, + ChainIdRequest, ChainIdResponse, EstimateFeeRequest, EstimateFeeResponse, + EstimateMessageFeeRequest, GetBlockRequest, GetBlockTransactionCountResponse, + GetBlockWithReceiptsResponse, GetBlockWithTxHashesResponse, GetBlockWithTxsResponse, + GetClassAtRequest, GetClassAtResponse, GetClassHashAtRequest, GetClassHashAtResponse, + GetClassRequest, GetClassResponse, GetCompiledCasmRequest, GetCompiledCasmResponse, + GetEventsRequest, GetEventsResponse, GetNonceRequest, GetNonceResponse, GetStateUpdateResponse, + GetStorageAtRequest, GetStorageAtResponse, GetStorageProofRequest, GetStorageProofResponse, + GetTransactionByBlockIdAndIndexRequest, GetTransactionByBlockIdAndIndexResponse, + GetTransactionByHashRequest, GetTransactionByHashResponse, GetTransactionReceiptRequest, + GetTransactionReceiptResponse, GetTransactionStatusRequest, GetTransactionStatusResponse, + SimulateTransactionsRequest, SimulateTransactionsResponse, SpecVersionRequest, + SpecVersionResponse, SyncingRequest, SyncingResponse, TraceBlockTransactionsRequest, + TraceBlockTransactionsResponse, TraceTransactionRequest, TraceTransactionResponse, +}; + +/// The default request timeout. +pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(20); + +/// The default connection timeout. +pub const DEFAULT_CONNECT_TIMEOUT: Duration = Duration::from_secs(5); + +/// Error type for gRPC client operations. +#[derive(Debug, thiserror::Error)] +pub enum Error { + /// Transport error from tonic. + #[error(transparent)] + Transport(#[from] tonic::transport::Error), + + /// Invalid URI. + #[error("Invalid URI: {0}")] + InvalidUri(#[from] http::uri::InvalidUri), +} + +/// Builder for creating a gRPC client. +/// +/// # Example +/// +/// ```ignore +/// use std::time::Duration; +/// use katana_grpc::GrpcClient; +/// +/// let client = GrpcClient::builder("http://localhost:5051") +/// .timeout(Duration::from_secs(30)) +/// .connect_timeout(Duration::from_secs(10)) +/// .connect() +/// .await?; +/// ``` +#[derive(Debug, Clone)] +pub struct GrpcClientBuilder { + endpoint: String, + timeout: Duration, + connect_timeout: Duration, +} + +impl GrpcClientBuilder { + /// Creates a new client builder for the specified endpoint. + fn new(endpoint: impl Into) -> Self { + Self { + endpoint: endpoint.into(), + timeout: DEFAULT_TIMEOUT, + connect_timeout: DEFAULT_CONNECT_TIMEOUT, + } + } + + /// Sets the request timeout. Default is 20 seconds. + pub fn timeout(mut self, timeout: Duration) -> Self { + self.timeout = timeout; + self + } + + /// Sets the connection timeout. Default is 5 seconds. + pub fn connect_timeout(mut self, timeout: Duration) -> Self { + self.connect_timeout = timeout; + self + } + + /// Connects to the gRPC server and returns a client. + pub async fn connect(self) -> Result { + let uri: Uri = self.endpoint.parse()?; + + let channel = Endpoint::from(uri) + .timeout(self.timeout) + .connect_timeout(self.connect_timeout) + .connect() + .await?; + + Ok(GrpcClient::from_channel(channel)) + } +} + +/// A client for interacting with Katana's gRPC endpoints. +#[derive(Debug, Clone)] +pub struct GrpcClient { + starknet: StarknetClient, + starknet_write: StarknetWriteClient, + starknet_trace: StarknetTraceClient, +} + +impl GrpcClient { + /// Creates a new client builder for the specified endpoint. + /// + /// # Arguments + /// + /// * `endpoint` - The URI of the gRPC server (e.g., "http://localhost:5051") + pub fn builder(endpoint: impl Into) -> GrpcClientBuilder { + GrpcClientBuilder::new(endpoint) + } + + /// Connects to the gRPC server with default configuration. + /// + /// This is a convenience method equivalent to `GrpcClient::builder(endpoint).connect()`. + /// + /// # Arguments + /// + /// * `endpoint` - The URI of the gRPC server (e.g., "http://localhost:5051") + pub async fn connect(endpoint: impl Into) -> Result { + Self::builder(endpoint).connect().await + } + + /// Creates a new gRPC client from an existing channel. + /// + /// This is useful when you want to share a channel between multiple clients + /// or have custom channel configuration. + pub fn from_channel(channel: Channel) -> Self { + Self { + starknet: StarknetClient::new(channel.clone()), + starknet_write: StarknetWriteClient::new(channel.clone()), + starknet_trace: StarknetTraceClient::new(channel), + } + } + + // ============================================================ + // Read Methods (Starknet service) + // ============================================================ + + /// Returns the version of the Starknet JSON-RPC specification being used. + pub async fn spec_version( + &mut self, + request: impl Into>, + ) -> Result, Status> { + self.starknet.spec_version(request.into()).await + } + + /// Get block information with transaction hashes given the block id. + pub async fn get_block_with_tx_hashes( + &mut self, + request: impl Into>, + ) -> Result, Status> { + self.starknet.get_block_with_tx_hashes(request.into()).await + } + + /// Get block information with full transactions given the block id. + pub async fn get_block_with_txs( + &mut self, + request: impl Into>, + ) -> Result, Status> { + self.starknet.get_block_with_txs(request.into()).await + } + + /// Get block information with full transactions and receipts given the block id. + pub async fn get_block_with_receipts( + &mut self, + request: impl Into>, + ) -> Result, Status> { + self.starknet.get_block_with_receipts(request.into()).await + } + + /// Get the information about the result of executing the requested block. + pub async fn get_state_update( + &mut self, + request: impl Into>, + ) -> Result, Status> { + self.starknet.get_state_update(request.into()).await + } + + /// Get the value of the storage at the given address and key. + pub async fn get_storage_at( + &mut self, + request: impl Into>, + ) -> Result, Status> { + self.starknet.get_storage_at(request.into()).await + } + + /// Gets the transaction status. + pub async fn get_transaction_status( + &mut self, + request: impl Into>, + ) -> Result, Status> { + self.starknet.get_transaction_status(request.into()).await + } + + /// Get the details and status of a submitted transaction. + pub async fn get_transaction_by_hash( + &mut self, + request: impl Into>, + ) -> Result, Status> { + self.starknet.get_transaction_by_hash(request.into()).await + } + + /// Get the details of a transaction by a given block id and index. + pub async fn get_transaction_by_block_id_and_index( + &mut self, + request: impl Into>, + ) -> Result, Status> { + self.starknet.get_transaction_by_block_id_and_index(request.into()).await + } + + /// Get the transaction receipt by the transaction hash. + pub async fn get_transaction_receipt( + &mut self, + request: impl Into>, + ) -> Result, Status> { + self.starknet.get_transaction_receipt(request.into()).await + } + + /// Get the contract class definition in the given block associated with the given hash. + pub async fn get_class( + &mut self, + request: impl Into>, + ) -> Result, Status> { + self.starknet.get_class(request.into()).await + } + + /// Get the contract class hash in the given block for the contract deployed at the given + /// address. + pub async fn get_class_hash_at( + &mut self, + request: impl Into>, + ) -> Result, Status> { + self.starknet.get_class_hash_at(request.into()).await + } + + /// Get the contract class definition in the given block at the given address. + pub async fn get_class_at( + &mut self, + request: impl Into>, + ) -> Result, Status> { + self.starknet.get_class_at(request.into()).await + } + + /// Get the number of transactions in a block given a block id. + pub async fn get_block_transaction_count( + &mut self, + request: impl Into>, + ) -> Result, Status> { + self.starknet.get_block_transaction_count(request.into()).await + } + + /// Call a starknet function without creating a Starknet transaction. + pub async fn call( + &mut self, + request: impl Into>, + ) -> Result, Status> { + self.starknet.call(request.into()).await + } + + /// Estimate the fee for Starknet transactions. + pub async fn estimate_fee( + &mut self, + request: impl Into>, + ) -> Result, Status> { + self.starknet.estimate_fee(request.into()).await + } + + /// Estimate the L2 fee of a message sent on L1. + pub async fn estimate_message_fee( + &mut self, + request: impl Into>, + ) -> Result, Status> { + self.starknet.estimate_message_fee(request.into()).await + } + + /// Get the most recent accepted block number. + pub async fn block_number( + &mut self, + request: impl Into>, + ) -> Result, Status> { + self.starknet.block_number(request.into()).await + } + + /// Get the most recent accepted block hash and number. + pub async fn block_hash_and_number( + &mut self, + request: impl Into>, + ) -> Result, Status> { + self.starknet.block_hash_and_number(request.into()).await + } + + /// Return the currently configured Starknet chain id. + pub async fn chain_id( + &mut self, + request: impl Into>, + ) -> Result, Status> { + self.starknet.chain_id(request.into()).await + } + + /// Returns an object about the sync status, or false if the node is not synching. + pub async fn syncing( + &mut self, + request: impl Into>, + ) -> Result, Status> { + self.starknet.syncing(request.into()).await + } + + /// Returns all events matching the given filter. + pub async fn get_events( + &mut self, + request: impl Into>, + ) -> Result, Status> { + self.starknet.get_events(request.into()).await + } + + /// Get the nonce associated with the given address in the given block. + pub async fn get_nonce( + &mut self, + request: impl Into>, + ) -> Result, Status> { + self.starknet.get_nonce(request.into()).await + } + + /// Get the compiled CASM for a given class hash. + pub async fn get_compiled_casm( + &mut self, + request: impl Into>, + ) -> Result, Status> { + self.starknet.get_compiled_casm(request.into()).await + } + + /// Get Merkle paths in the state tries for a set of classes, contracts, and storage keys. + pub async fn get_storage_proof( + &mut self, + request: impl Into>, + ) -> Result, Status> { + self.starknet.get_storage_proof(request.into()).await + } + + // ============================================================ + // Write Methods (StarknetWrite service) + // ============================================================ + + /// Submit a new invoke transaction. + pub async fn add_invoke_transaction( + &mut self, + request: impl Into>, + ) -> Result, Status> { + self.starknet_write.add_invoke_transaction(request.into()).await + } + + /// Submit a new declare transaction. + pub async fn add_declare_transaction( + &mut self, + request: impl Into>, + ) -> Result, Status> { + self.starknet_write.add_declare_transaction(request.into()).await + } + + /// Submit a new deploy account transaction. + pub async fn add_deploy_account_transaction( + &mut self, + request: impl Into>, + ) -> Result, Status> { + self.starknet_write.add_deploy_account_transaction(request.into()).await + } + + // ============================================================ + // Trace Methods (StarknetTrace service) + // ============================================================ + + /// Get the trace for a specific transaction. + pub async fn trace_transaction( + &mut self, + request: impl Into>, + ) -> Result, Status> { + self.starknet_trace.trace_transaction(request.into()).await + } + + /// Simulate a list of transactions and return their execution traces and fee estimations. + pub async fn simulate_transactions( + &mut self, + request: impl Into>, + ) -> Result, Status> { + self.starknet_trace.simulate_transactions(request.into()).await + } + + /// Get the traces for all transactions in a block. + pub async fn trace_block_transactions( + &mut self, + request: impl Into>, + ) -> Result, Status> { + self.starknet_trace.trace_block_transactions(request.into()).await + } +} diff --git a/crates/grpc/src/error.rs b/crates/grpc/src/error.rs new file mode 100644 index 000000000..f3cd52c3a --- /dev/null +++ b/crates/grpc/src/error.rs @@ -0,0 +1,154 @@ +//! Error handling for gRPC services. +//! +//! This module provides conversion from Starknet API errors to gRPC status codes. + +use katana_rpc_api::error::starknet::StarknetApiError; +use tonic::{Code, Status}; + +/// Converts a [`StarknetApiError`] to a [`tonic::Status`]. +/// +/// This mapping follows gRPC best practices for error codes: +/// - NOT_FOUND: Resource doesn't exist (block, transaction, contract, class) +/// - INVALID_ARGUMENT: Invalid input parameters +/// - FAILED_PRECONDITION: Operation cannot be performed in current state +/// - RESOURCE_EXHAUSTED: Limits exceeded +/// - INTERNAL: Unexpected internal errors +/// - UNIMPLEMENTED: Unsupported features +pub fn to_status(err: StarknetApiError) -> Status { + match err { + // Not found errors -> NOT_FOUND + StarknetApiError::BlockNotFound => Status::new(Code::NotFound, "Block not found"), + StarknetApiError::TxnHashNotFound => Status::new(Code::NotFound, "Transaction not found"), + StarknetApiError::ContractNotFound => Status::new(Code::NotFound, "Contract not found"), + StarknetApiError::ClassHashNotFound => Status::new(Code::NotFound, "Class hash not found"), + StarknetApiError::NoBlocks => Status::new(Code::NotFound, "No blocks"), + StarknetApiError::EntrypointNotFound => Status::new(Code::NotFound, "Entrypoint not found"), + + // Invalid argument errors -> INVALID_ARGUMENT + StarknetApiError::InvalidTxnIndex => { + Status::new(Code::InvalidArgument, "Invalid transaction index") + } + StarknetApiError::InvalidCallData => Status::new(Code::InvalidArgument, "Invalid calldata"), + StarknetApiError::InvalidContractClass => { + Status::new(Code::InvalidArgument, "Invalid contract class") + } + StarknetApiError::InvalidContinuationToken => { + Status::new(Code::InvalidArgument, "Invalid continuation token") + } + StarknetApiError::TooManyKeysInFilter => { + Status::new(Code::InvalidArgument, "Too many keys in filter") + } + StarknetApiError::TooManyAddressesInFilter => { + Status::new(Code::InvalidArgument, "Too many addresses in filter") + } + StarknetApiError::TooManyBlocksBack => { + Status::new(Code::InvalidArgument, "Too many blocks back") + } + StarknetApiError::InvalidSubscriptionId => { + Status::new(Code::InvalidArgument, "Invalid subscription id") + } + + // Resource exhausted errors -> RESOURCE_EXHAUSTED + StarknetApiError::PageSizeTooBig(data) => Status::new( + Code::ResourceExhausted, + format!("Page size too big: requested {}, max {}", data.requested, data.max_allowed), + ), + StarknetApiError::ProofLimitExceeded(data) => Status::new( + Code::ResourceExhausted, + format!("Proof limit exceeded: {} keys requested, limit is {}", data.total, data.limit), + ), + StarknetApiError::ContractClassSizeIsTooLarge => { + Status::new(Code::ResourceExhausted, "Contract class size is too large") + } + + // Transaction validation errors -> FAILED_PRECONDITION + StarknetApiError::InsufficientAccountBalance => { + Status::new(Code::FailedPrecondition, "Insufficient account balance") + } + StarknetApiError::InsufficientResourcesForValidate => { + Status::new(Code::FailedPrecondition, "Insufficient resources for validation") + } + StarknetApiError::InvalidTransactionNonce(data) => Status::new( + Code::FailedPrecondition, + format!("Invalid transaction nonce: {}", data.reason), + ), + StarknetApiError::ValidationFailure(data) => { + Status::new(Code::FailedPrecondition, format!("Validation failure: {}", data.reason)) + } + StarknetApiError::NonAccount => { + Status::new(Code::FailedPrecondition, "Sender address is not an account contract") + } + StarknetApiError::DuplicateTransaction => { + Status::new(Code::FailedPrecondition, "Transaction already exists in pool") + } + StarknetApiError::CompiledClassHashMismatch => { + Status::new(Code::FailedPrecondition, "Compiled class hash mismatch") + } + StarknetApiError::FailedToReceiveTxn => { + Status::new(Code::FailedPrecondition, "Failed to receive transaction") + } + StarknetApiError::FailedToFetchPendingTransactions => { + Status::new(Code::FailedPrecondition, "Failed to fetch pending transactions") + } + StarknetApiError::ReplacementTransactionUnderpriced => { + Status::new(Code::FailedPrecondition, "Replacement transaction underpriced") + } + StarknetApiError::FeeBelowMinimum => { + Status::new(Code::FailedPrecondition, "Fee below minimum") + } + + // Unsupported errors -> UNIMPLEMENTED + StarknetApiError::UnsupportedContractClassVersion => { + Status::new(Code::Unimplemented, "Unsupported contract class version") + } + StarknetApiError::UnsupportedTransactionVersion => { + Status::new(Code::Unimplemented, "Unsupported transaction version") + } + StarknetApiError::StorageProofNotSupported(data) => Status::new( + Code::Unimplemented, + format!( + "Storage proof not supported: oldest block {}, requested {}", + data.oldest_block, data.requested_block + ), + ), + + // Execution errors -> FAILED_PRECONDITION with details + StarknetApiError::ContractError(data) => { + Status::new(Code::FailedPrecondition, format!("Contract error: {}", data.revert_error)) + } + StarknetApiError::TransactionExecutionError(data) => Status::new( + Code::FailedPrecondition, + format!( + "Transaction execution error at index {}: {}", + data.transaction_index, data.execution_error + ), + ), + StarknetApiError::CompilationError(data) => Status::new( + Code::FailedPrecondition, + format!("Compilation failed: {}", data.compilation_error), + ), + + // Class already declared -> ALREADY_EXISTS + StarknetApiError::ClassAlreadyDeclared => { + Status::new(Code::AlreadyExists, "Class already declared") + } + + // Unexpected errors -> INTERNAL + StarknetApiError::UnexpectedError(data) => { + Status::new(Code::Internal, format!("Unexpected error: {}", data.reason)) + } + } +} + +/// Extension trait to easily convert Results with StarknetApiError to gRPC Results. +#[allow(clippy::result_large_err)] +pub trait IntoGrpcResult { + /// Converts the result to a gRPC result. + fn into_grpc_result(self) -> Result; +} + +impl IntoGrpcResult for Result { + fn into_grpc_result(self) -> Result { + self.map_err(to_status) + } +} diff --git a/crates/grpc/src/handlers/mod.rs b/crates/grpc/src/handlers/mod.rs new file mode 100644 index 000000000..df5ff7555 --- /dev/null +++ b/crates/grpc/src/handlers/mod.rs @@ -0,0 +1,8 @@ +//! gRPC service handlers. +//! +//! This module contains the implementations of the gRPC services defined in the proto files. +//! The handlers delegate to the underlying `StarknetApi` implementation for business logic. + +mod starknet; + +pub use self::starknet::StarknetService; diff --git a/crates/grpc/src/handlers/starknet.rs b/crates/grpc/src/handlers/starknet.rs new file mode 100644 index 000000000..d3bb7b96c --- /dev/null +++ b/crates/grpc/src/handlers/starknet.rs @@ -0,0 +1,590 @@ +//! Starknet service handler implementation. + +use katana_pool::TransactionPool; +use katana_primitives::transaction::TxHash; +use katana_primitives::Felt; +use katana_provider::{ProviderFactory, ProviderRO}; +use katana_rpc_api::starknet::RPC_SPEC_VERSION; +use katana_rpc_server::starknet::{PendingBlockProvider, StarknetApi}; +use katana_rpc_types::event::EventFilterWithPage; +use katana_rpc_types::trie::ContractStorageKeys; +use katana_rpc_types::{BroadcastedTxWithChainId, FunctionCall}; +use tonic::{Request, Response, Status}; + +use crate::conversion::{block_id_from_proto, confirmed_block_id_from_proto}; +use crate::error::IntoGrpcResult; +use crate::protos::starknet::starknet_server::Starknet; +use crate::protos::starknet::starknet_trace_server::StarknetTrace; +use crate::protos::starknet::starknet_write_server::StarknetWrite; +use crate::protos::starknet::{ + AddDeclareTransactionRequest, AddDeclareTransactionResponse, + AddDeployAccountTransactionRequest, AddDeployAccountTransactionResponse, + AddInvokeTransactionRequest, AddInvokeTransactionResponse, BlockHashAndNumberRequest, + BlockHashAndNumberResponse, BlockNumberRequest, BlockNumberResponse, CallRequest, CallResponse, + ChainIdRequest, ChainIdResponse, EstimateFeeRequest, EstimateFeeResponse, + EstimateMessageFeeRequest, GetBlockRequest, GetBlockTransactionCountResponse, + GetBlockWithReceiptsResponse, GetBlockWithTxHashesResponse, GetBlockWithTxsResponse, + GetClassAtRequest, GetClassAtResponse, GetClassHashAtRequest, GetClassHashAtResponse, + GetClassRequest, GetClassResponse, GetCompiledCasmRequest, GetCompiledCasmResponse, + GetEventsRequest, GetEventsResponse, GetNonceRequest, GetNonceResponse, GetStateUpdateResponse, + GetStorageAtRequest, GetStorageAtResponse, GetStorageProofRequest, GetStorageProofResponse, + GetTransactionByBlockIdAndIndexRequest, GetTransactionByBlockIdAndIndexResponse, + GetTransactionByHashRequest, GetTransactionByHashResponse, GetTransactionReceiptRequest, + GetTransactionReceiptResponse, GetTransactionStatusRequest, GetTransactionStatusResponse, + SimulateTransactionsRequest, SimulateTransactionsResponse, SpecVersionRequest, + SpecVersionResponse, SyncingRequest, SyncingResponse, TraceBlockTransactionsRequest, + TraceBlockTransactionsResponse, TraceTransactionRequest, TraceTransactionResponse, +}; +use crate::protos::types::{Transaction as ProtoTx, TransactionReceipt as ProtoTransactionReceipt}; + +/// The main handler for Starknet gRPC services. +/// +/// This struct wraps `StarknetApi` from `katana-rpc-server` and implements the gRPC +/// service traits by delegating to the underlying API. +pub struct StarknetService +where + Pool: TransactionPool, + PP: PendingBlockProvider, + PF: ProviderFactory, +{ + pub(crate) api: StarknetApi, +} + +impl StarknetService +where + Pool: TransactionPool, + PP: PendingBlockProvider, + PF: ProviderFactory, +{ + pub fn new(api: StarknetApi) -> Self { + Self { api } + } +} + +impl std::fmt::Debug for StarknetService +where + Pool: TransactionPool, + PP: PendingBlockProvider, + PF: ProviderFactory, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("StarknetService").finish_non_exhaustive() + } +} + +impl Clone for StarknetService +where + Pool: TransactionPool, + PP: PendingBlockProvider, + PF: ProviderFactory, +{ + fn clone(&self) -> Self { + Self { api: self.api.clone() } + } +} + +///////////////////////////////////////////////////////////////////////// +/// Starknet Read Service Implementation +///////////////////////////////////////////////////////////////////////// + +#[tonic::async_trait] +impl Starknet for StarknetService +where + Pool: TransactionPool + 'static, + PP: PendingBlockProvider, + PF: ProviderFactory, + ::Provider: ProviderRO, +{ + async fn spec_version( + &self, + _request: Request, + ) -> Result, Status> { + Ok(Response::new(SpecVersionResponse { version: RPC_SPEC_VERSION.to_string() })) + } + + async fn get_block_with_tx_hashes( + &self, + request: Request, + ) -> Result, Status> { + let block_id = block_id_from_proto(request.into_inner().block_id)?; + let result = self.api.block_with_tx_hashes(block_id).await.into_grpc_result()?; + Ok(Response::new(result.into())) + } + + async fn get_block_with_txs( + &self, + request: Request, + ) -> Result, Status> { + let block_id = block_id_from_proto(request.into_inner().block_id)?; + let result = self.api.block_with_txs(block_id).await.into_grpc_result()?; + Ok(Response::new(result.into())) + } + + async fn get_block_with_receipts( + &self, + request: Request, + ) -> Result, Status> { + let block_id = block_id_from_proto(request.into_inner().block_id)?; + let result = self.api.block_with_receipts(block_id).await.into_grpc_result()?; + Ok(Response::new(result.into())) + } + + async fn get_state_update( + &self, + request: Request, + ) -> Result, Status> { + let block_id = block_id_from_proto(request.into_inner().block_id)?; + let result = self.api.state_update(block_id).await.into_grpc_result()?; + Ok(Response::new(result.into())) + } + + async fn get_storage_at( + &self, + request: Request, + ) -> Result, Status> { + let GetStorageAtRequest { block_id, contract_address, key } = request.into_inner(); + + let block_id = block_id_from_proto(block_id)?; + let contract_address = contract_address + .ok_or_else(|| Status::invalid_argument("Missing `contract_address`"))? + .try_into()?; + let key = key.ok_or_else(|| Status::invalid_argument("Missing `key`"))?.try_into()?; + + let result = + self.api.storage_at(contract_address, key, block_id).await.into_grpc_result()?; + + Ok(Response::new(GetStorageAtResponse { value: Some(result.into()) })) + } + + async fn get_transaction_status( + &self, + request: Request, + ) -> Result, Status> { + let tx_hash = request + .into_inner() + .transaction_hash + .ok_or_else(|| Status::invalid_argument("Missing transaction_hash"))? + .try_into()?; + + let status = self.api.transaction_status(tx_hash).await.into_grpc_result()?; + + let (finality_status, execution_status) = match status { + katana_rpc_types::TxStatus::Received => ("RECEIVED".to_string(), String::new()), + katana_rpc_types::TxStatus::Candidate => ("CANDIDATE".to_string(), String::new()), + katana_rpc_types::TxStatus::PreConfirmed(exec) => { + ("PRE_CONFIRMED".to_string(), execution_result_to_string(&exec)) + } + katana_rpc_types::TxStatus::AcceptedOnL2(exec) => { + ("ACCEPTED_ON_L2".to_string(), execution_result_to_string(&exec)) + } + katana_rpc_types::TxStatus::AcceptedOnL1(exec) => { + ("ACCEPTED_ON_L1".to_string(), execution_result_to_string(&exec)) + } + }; + + Ok(Response::new(GetTransactionStatusResponse { finality_status, execution_status })) + } + + async fn get_transaction_by_hash( + &self, + request: Request, + ) -> Result, Status> { + let tx_hash = request + .into_inner() + .transaction_hash + .ok_or_else(|| Status::invalid_argument("Missing transaction_hash"))? + .try_into()?; + + let tx = self.api.transaction(tx_hash).await.into_grpc_result()?; + + Ok(Response::new(GetTransactionByHashResponse { transaction: Some(ProtoTx::from(tx)) })) + } + + async fn get_transaction_by_block_id_and_index( + &self, + request: Request, + ) -> Result, Status> { + let req = request.into_inner(); + let block_id = block_id_from_proto(req.block_id)?; + let index = req.index; + + let tx = + self.api.transaction_by_block_id_and_index(block_id, index).await.into_grpc_result()?; + + Ok(Response::new(GetTransactionByBlockIdAndIndexResponse { transaction: Some(tx.into()) })) + } + + async fn get_transaction_receipt( + &self, + request: Request, + ) -> Result, Status> { + let tx_hash = request + .into_inner() + .transaction_hash + .ok_or_else(|| Status::invalid_argument("Missing transaction_hash"))? + .try_into()?; + + let receipt = self.api.receipt(tx_hash).await.into_grpc_result()?; + + Ok(Response::new(GetTransactionReceiptResponse { + receipt: Some(ProtoTransactionReceipt::from(&receipt)), + })) + } + + async fn get_class( + &self, + request: Request, + ) -> Result, Status> { + let req = request.into_inner(); + let block_id = block_id_from_proto(req.block_id)?; + let class_hash = req + .class_hash + .ok_or_else(|| Status::invalid_argument("Missing class_hash"))? + .try_into()?; + + let class = self.api.class_at_hash(block_id, class_hash).await.into_grpc_result()?; + + // Convert class to proto - simplified for now + Ok(Response::new(GetClassResponse { + result: Some(crate::protos::starknet::get_class_response::Result::ContractClass( + crate::protos::types::ContractClass { + sierra_program: Vec::new(), // Would need full conversion + contract_class_version: String::new(), + entry_points_by_type: None, + abi: serde_json::to_string(&class).unwrap_or_default(), + }, + )), + })) + } + + async fn get_class_hash_at( + &self, + request: Request, + ) -> Result, Status> { + let req = request.into_inner(); + let block_id = block_id_from_proto(req.block_id)?; + let contract_address = req + .contract_address + .ok_or_else(|| Status::invalid_argument("Missing contract_address"))? + .try_into()?; + + let class_hash = + self.api.class_hash_at_address(block_id, contract_address).await.into_grpc_result()?; + + Ok(Response::new(GetClassHashAtResponse { class_hash: Some(class_hash.into()) })) + } + + async fn get_class_at( + &self, + request: Request, + ) -> Result, Status> { + let req = request.into_inner(); + let block_id = block_id_from_proto(req.block_id)?; + let contract_address = req + .contract_address + .ok_or_else(|| Status::invalid_argument("Missing contract_address"))? + .try_into()?; + + let class = + self.api.class_at_address(block_id, contract_address).await.into_grpc_result()?; + + // Convert class to proto - simplified for now + Ok(Response::new(GetClassAtResponse { + result: Some(crate::protos::starknet::get_class_at_response::Result::ContractClass( + crate::protos::types::ContractClass { + sierra_program: Vec::new(), + contract_class_version: String::new(), + entry_points_by_type: None, + abi: serde_json::to_string(&class).unwrap_or_default(), + }, + )), + })) + } + + async fn get_block_transaction_count( + &self, + request: Request, + ) -> Result, Status> { + let block_id = block_id_from_proto(request.into_inner().block_id)?; + let count = self.api.block_tx_count(block_id).await.into_grpc_result()?; + Ok(Response::new(GetBlockTransactionCountResponse { count })) + } + + async fn call(&self, request: Request) -> Result, Status> { + let req = request.into_inner(); + let block_id = block_id_from_proto(req.block_id)?; + + let function_call = + req.request.ok_or_else(|| Status::invalid_argument("Missing request"))?; + + let contract_address = function_call + .contract_address + .ok_or_else(|| Status::invalid_argument("Missing contract_address"))? + .try_into()?; + + let entry_point_selector = function_call + .entry_point_selector + .ok_or_else(|| Status::invalid_argument("Missing entry_point_selector"))? + .try_into()?; + + let calldata = function_call + .calldata + .into_iter() + .map(Felt::try_from) + .collect::, _>>()?; + + let response = self + .api + .call_contract( + FunctionCall { calldata, entry_point_selector, contract_address }, + block_id, + ) + .await + .into_grpc_result()?; + + Ok(Response::new(CallResponse { + result: response.result.into_iter().map(Into::into).collect(), + })) + } + + async fn estimate_fee( + &self, + _request: Request, + ) -> Result, Status> { + Err(Status::unimplemented("estimate_fee requires full transaction conversion")) + } + + async fn estimate_message_fee( + &self, + _request: Request, + ) -> Result, Status> { + Err(Status::unimplemented("estimate_message_fee requires full message conversion")) + } + + async fn block_number( + &self, + _: Request, + ) -> Result, Status> { + let result = self.api.latest_block_number().await.into_grpc_result()?; + Ok(Response::new(BlockNumberResponse { block_number: result.block_number })) + } + + async fn block_hash_and_number( + &self, + _: Request, + ) -> Result, Status> { + let result = self.api.block_hash_and_number().await.into_grpc_result()?; + Ok(Response::new(BlockHashAndNumberResponse { + block_hash: Some(result.block_hash.into()), + block_number: result.block_number, + })) + } + + async fn chain_id( + &self, + _request: Request, + ) -> Result, Status> { + let chain_id = self.api.chain_id(); + Ok(Response::new(ChainIdResponse { chain_id: format!("{chain_id:#x}") })) + } + + async fn syncing( + &self, + _request: Request, + ) -> Result, Status> { + // Katana doesn't support syncing status yet + Ok(Response::new(SyncingResponse { + result: Some(crate::protos::starknet::syncing_response::Result::NotSyncing(true)), + })) + } + + async fn get_events( + &self, + request: Request, + ) -> Result, Status> { + let filter = EventFilterWithPage::try_from(request.into_inner())?; + let result = self.api.events(filter).await.into_grpc_result()?; + Ok(Response::new(result.into())) + } + + async fn get_nonce( + &self, + request: Request, + ) -> Result, Status> { + let req = request.into_inner(); + let block_id = block_id_from_proto(req.block_id)?; + let contract_address = req + .contract_address + .ok_or_else(|| Status::invalid_argument("Missing contract_address"))? + .try_into()?; + + let nonce = self.api.nonce_at(block_id, contract_address).await.into_grpc_result()?; + + Ok(Response::new(GetNonceResponse { nonce: Some(nonce.into()) })) + } + + async fn get_compiled_casm( + &self, + _request: Request, + ) -> Result, Status> { + Err(Status::unimplemented("get_compiled_casm requires CASM conversion")) + } + + async fn get_storage_proof( + &self, + request: Request, + ) -> Result, Status> { + let req = request.into_inner(); + let block_id = block_id_from_proto(req.block_id)?; + + // Convert class_hashes + let class_hashes = if req.class_hashes.is_empty() { + None + } else { + Some(req.class_hashes.into_iter().map(Felt::try_from).collect::, _>>()?) + }; + + // Convert contract_addresses + let contract_addresses = if req.contract_addresses.is_empty() { + None + } else { + Some( + req.contract_addresses + .into_iter() + .map(|f| f.try_into()) + .collect::, _>>()?, + ) + }; + + // Convert contracts_storage_keys + let contracts_storage_keys = if req.contracts_storage_keys.is_empty() { + None + } else { + Some( + req.contracts_storage_keys + .into_iter() + .map(ContractStorageKeys::try_from) + .collect::, _>>()?, + ) + }; + + let result = self + .api + .get_proofs(block_id, class_hashes, contract_addresses, contracts_storage_keys) + .await + .into_grpc_result()?; + + Ok(Response::new(GetStorageProofResponse { proof: Some(result.into()) })) + } +} + +///////////////////////////////////////////////////////////////////////// +/// Starknet Write Service Implementation +///////////////////////////////////////////////////////////////////////// + +#[tonic::async_trait] +impl StarknetWrite for StarknetService +where + Pool: TransactionPool + Send + Sync + 'static, + PoolTx: From, + PP: PendingBlockProvider, + PF: ProviderFactory, +{ + async fn add_invoke_transaction( + &self, + request: Request, + ) -> Result, Status> { + let AddInvokeTransactionRequest { transaction } = request.into_inner(); + + let tx = transaction.ok_or(Status::invalid_argument("missing transaction"))?; + let response = self.api.add_invoke_tx(tx.try_into()?).await.into_grpc_result()?; + + Ok(Response::new(AddInvokeTransactionResponse { + transaction_hash: Some(response.transaction_hash.into()), + })) + } + + async fn add_declare_transaction( + &self, + request: Request, + ) -> Result, Status> { + let AddDeclareTransactionRequest { transaction } = request.into_inner(); + + let tx = transaction.ok_or(Status::invalid_argument("missing transaction"))?; + let response = self.api.add_declare_tx(tx.try_into()?).await.into_grpc_result()?; + + Ok(Response::new(AddDeclareTransactionResponse { + transaction_hash: Some(response.transaction_hash.into()), + class_hash: Some(response.class_hash.into()), + })) + } + + async fn add_deploy_account_transaction( + &self, + request: Request, + ) -> Result, Status> { + let AddDeployAccountTransactionRequest { transaction } = request.into_inner(); + + let tx = transaction.ok_or(Status::invalid_argument("missing transaction"))?; + let response = self.api.add_deploy_account_tx(tx.try_into()?).await.into_grpc_result()?; + + Ok(Response::new(AddDeployAccountTransactionResponse { + transaction_hash: Some(response.transaction_hash.into()), + contract_address: Some(Felt::from(response.contract_address).into()), + })) + } +} + +///////////////////////////////////////////////////////////////////////// +/// Starknet Trace Service Implementation +///////////////////////////////////////////////////////////////////////// + +#[tonic::async_trait] +impl StarknetTrace for StarknetService +where + Pool: TransactionPool + 'static, + PP: PendingBlockProvider, + PF: ProviderFactory, + ::Provider: ProviderRO, +{ + async fn trace_transaction( + &self, + request: Request, + ) -> Result, Status> { + let tx_hash: TxHash = request + .into_inner() + .transaction_hash + .ok_or_else(|| Status::invalid_argument("Missing transaction_hash"))? + .try_into()?; + + let result = self.api.trace(tx_hash).await.into_grpc_result()?; + + Ok(Response::new(result.into())) + } + + async fn simulate_transactions( + &self, + _request: Request, + ) -> Result, Status> { + Err(Status::unimplemented( + "simulate_transactions requires full transaction conversion from proto", + )) + } + + async fn trace_block_transactions( + &self, + request: Request, + ) -> Result, Status> { + let block_id = confirmed_block_id_from_proto(request.into_inner().block_id)?; + let traces = self.api.block_traces(block_id).await.into_grpc_result()?; + let response = katana_rpc_types::trace::TraceBlockTransactionsResponse { traces }; + Ok(Response::new(response.into())) + } +} + +fn execution_result_to_string(exec: &katana_rpc_types::ExecutionResult) -> String { + match exec { + katana_rpc_types::ExecutionResult::Succeeded => "SUCCEEDED".to_string(), + katana_rpc_types::ExecutionResult::Reverted { .. } => "REVERTED".to_string(), + } +} diff --git a/crates/grpc/src/lib.rs b/crates/grpc/src/lib.rs index d0dd5a0e6..b13d54e09 100644 --- a/crates/grpc/src/lib.rs +++ b/crates/grpc/src/lib.rs @@ -1 +1,68 @@ -//! gRPC implementations. +//! gRPC server and client implementation for Katana. +//! +//! This crate provides gRPC endpoints for interacting with the Katana Starknet sequencer, +//! offering high-performance alternatives to JSON-RPC for performance-critical applications. +//! +//! # Server +//! +//! Use [`GrpcServer`] to create and start a gRPC server: +//! +//! ```ignore +//! use katana_grpc::{GrpcServer, StarknetServer, StarknetService}; +//! +//! let service = StarknetService::new(/* ... */); +//! let server = GrpcServer::new() +//! .service(StarknetServer::new(service)) +//! .start("127.0.0.1:5051".parse()?) +//! .await?; +//! ``` +//! +//! # Client +//! +//! Use [`GrpcClient`] to connect to a gRPC server: +//! +//! ```ignore +//! use katana_grpc::{GrpcClient, proto::ChainIdRequest}; +//! +//! // Simple connection with defaults +//! let mut client = GrpcClient::connect("http://localhost:5051").await?; +//! +//! // Or use the builder for custom configuration +//! let mut client = GrpcClient::builder("http://localhost:5051") +//! .timeout(Duration::from_secs(30)) +//! .connect() +//! .await?; +//! +//! let response = client.chain_id(ChainIdRequest {}).await?; +//! println!("Chain ID: {}", response.into_inner().chain_id); +//! ``` + +#![cfg_attr(not(test), warn(unused_crate_dependencies))] + +mod client; +mod error; +mod handlers; +mod protos; +mod server; + +// Client exports +pub use client::{Error as ClientError, GrpcClient, GrpcClientBuilder}; +// Server exports +pub use handlers::StarknetService; +pub use protos::starknet::starknet_server::StarknetServer; +pub use protos::starknet::starknet_trace_server::StarknetTraceServer; +pub use protos::starknet::starknet_write_server::StarknetWriteServer; +pub use server::{GrpcServer, GrpcServerHandle}; + +/// Protocol buffer generated types. +/// +/// This module contains all the request/response types needed to interact with +/// the gRPC services. +pub mod proto { + pub use super::protos::starknet::*; + pub use super::protos::types::*; + pub use super::protos::*; +} + +// Re-export conversion module for internal use +pub(crate) use protos::types::conversion; diff --git a/crates/grpc/src/protos/common.rs b/crates/grpc/src/protos/common.rs new file mode 100644 index 000000000..cd63462b1 --- /dev/null +++ b/crates/grpc/src/protos/common.rs @@ -0,0 +1 @@ +tonic::include_proto!("common"); diff --git a/crates/grpc/src/protos/mod.rs b/crates/grpc/src/protos/mod.rs new file mode 100644 index 000000000..3a5a028c3 --- /dev/null +++ b/crates/grpc/src/protos/mod.rs @@ -0,0 +1,13 @@ +mod common; +pub mod types; + +pub use common::*; + +/// Starknet service definitions from starknet.proto +pub mod starknet { + tonic::include_proto!("starknet"); + + /// File descriptor set for gRPC reflection support. + pub const FILE_DESCRIPTOR_SET: &[u8] = + tonic::include_file_descriptor_set!("starknet_descriptor"); +} diff --git a/crates/grpc/src/protos/types/conversion/block.rs b/crates/grpc/src/protos/types/conversion/block.rs new file mode 100644 index 000000000..0e2c8c0f3 --- /dev/null +++ b/crates/grpc/src/protos/types/conversion/block.rs @@ -0,0 +1,365 @@ +//! Block type conversions. + +use katana_primitives::block::{BlockIdOrTag, ConfirmedBlockIdOrTag, FinalityStatus}; +use katana_primitives::da::L1DataAvailabilityMode; +use katana_primitives::Felt; +use tonic::Status; + +use super::FeltVecExt; +use crate::protos::common::Felt as ProtoFelt; +use crate::protos::starknet::{ + GetBlockWithReceiptsResponse, GetBlockWithTxHashesResponse, GetBlockWithTxsResponse, + GetStateUpdateResponse, +}; +use crate::protos::types::block_id::Identifier; +use crate::protos::types::{ + BlockHeader as ProtoBlockHeader, BlockTag as ProtoBlockTag, + BlockWithReceipts as ProtoBlockWithReceipts, BlockWithTxHashes as ProtoBlockWithTxHashes, + BlockWithTxs as ProtoBlockWithTxs, FinalityStatus as ProtoFinalityStatus, + L1DataAvailabilityMode as ProtoL1DataAvailabilityMode, + PendingBlockWithReceipts as ProtoPendingBlockWithReceipts, + PendingBlockWithTxHashes as ProtoPendingBlockWithTxHashes, + PendingBlockWithTxs as ProtoPendingBlockWithTxs, PendingStateUpdate as ProtoPendingStateUpdate, + ResourcePrice as ProtoResourcePrice, StateUpdate as ProtoStateUpdate, +}; + +/// Convert FinalityStatus to proto enum. +fn finality_status_to_proto(status: FinalityStatus) -> i32 { + match status { + FinalityStatus::AcceptedOnL2 => ProtoFinalityStatus::AcceptedOnL2 as i32, + FinalityStatus::AcceptedOnL1 => ProtoFinalityStatus::AcceptedOnL1 as i32, + FinalityStatus::PreConfirmed => ProtoFinalityStatus::PreConfirmed as i32, + } +} + +/// Convert L1DataAvailabilityMode to proto enum. +fn l1_da_mode_to_proto(mode: L1DataAvailabilityMode) -> i32 { + match mode { + L1DataAvailabilityMode::Blob => ProtoL1DataAvailabilityMode::Blob as i32, + L1DataAvailabilityMode::Calldata => ProtoL1DataAvailabilityMode::Calldata as i32, + } +} + +impl TryFrom for katana_primitives::block::BlockIdOrTag { + type Error = Status; + + fn try_from(proto: crate::proto::BlockId) -> Result { + let identifier = + proto.identifier.ok_or_else(|| Status::invalid_argument("Missing identifier"))?; + + match identifier { + Identifier::Number(num) => Ok(BlockIdOrTag::Number(num)), + Identifier::Hash(hash) => Ok(BlockIdOrTag::Hash(hash.try_into()?)), + Identifier::Tag(tag) => { + let tag = ProtoBlockTag::try_from(tag) + .map_err(|_| Status::invalid_argument(format!("Unknown block tag: {tag}")))?; + + match tag { + ProtoBlockTag::Latest => Ok(BlockIdOrTag::Latest), + ProtoBlockTag::L1Accepted => Ok(BlockIdOrTag::L1Accepted), + ProtoBlockTag::PreConfirmed => Ok(BlockIdOrTag::PreConfirmed), + } + } + } + } +} + +#[allow(clippy::result_large_err)] +pub fn block_id_from_proto(proto: Option) -> Result { + let proto = proto.ok_or_else(|| Status::invalid_argument("Missing block_id"))?; + BlockIdOrTag::try_from(proto) +} + +impl TryFrom for ConfirmedBlockIdOrTag { + type Error = Status; + + fn try_from(proto: crate::proto::ConfirmedBlockId) -> Result { + use crate::protos::types::confirmed_block_id::Identifier; + use crate::protos::types::ConfirmedBlockTag as ProtoConfirmedBlockTag; + + let identifier = + proto.identifier.ok_or_else(|| Status::invalid_argument("Missing identifier"))?; + + match identifier { + Identifier::Number(num) => Ok(ConfirmedBlockIdOrTag::Number(num)), + Identifier::Hash(hash) => Ok(ConfirmedBlockIdOrTag::Hash(hash.try_into()?)), + Identifier::Tag(tag) => { + let tag = ProtoConfirmedBlockTag::try_from(tag) + .map_err(|_| Status::invalid_argument(format!("Unknown block tag: {tag}")))?; + + match tag { + ProtoConfirmedBlockTag::Latest => Ok(ConfirmedBlockIdOrTag::Latest), + ProtoConfirmedBlockTag::L1Accepted => Ok(ConfirmedBlockIdOrTag::L1Accepted), + } + } + } + } +} + +#[allow(clippy::result_large_err)] +pub fn confirmed_block_id_from_proto( + proto: Option, +) -> Result { + let proto = proto.ok_or_else(|| Status::invalid_argument("Missing block_id"))?; + ConfirmedBlockIdOrTag::try_from(proto) +} + +/// Convert block with tx hashes response to proto. +impl From for GetBlockWithTxHashesResponse { + fn from(response: katana_rpc_types::block::GetBlockWithTxHashesResponse) -> Self { + use katana_rpc_types::block::GetBlockWithTxHashesResponse as RpcResponse; + + match response { + RpcResponse::Block(block) => GetBlockWithTxHashesResponse { + result: Some( + crate::protos::starknet::get_block_with_tx_hashes_response::Result::Block( + ProtoBlockWithTxHashes { + status: finality_status_to_proto(block.status), + header: Some(ProtoBlockHeader::from(&block)), + transactions: block.transactions.to_proto_felts(), + }, + ), + ), + }, + RpcResponse::PreConfirmed(pending) => GetBlockWithTxHashesResponse { + result: Some( + crate::protos::starknet::get_block_with_tx_hashes_response::Result::PendingBlock( + ProtoPendingBlockWithTxHashes { + header: Some(ProtoBlockHeader::from(&pending)), + transactions: pending.transactions.to_proto_felts(), + }, + ), + ), + }, + } + } +} + +/// Convert block with txs response to proto. +impl From for GetBlockWithTxsResponse { + fn from(response: katana_rpc_types::block::MaybePreConfirmedBlock) -> Self { + use katana_rpc_types::block::MaybePreConfirmedBlock as RpcResponse; + + use crate::protos::types::Transaction as ProtoTx; + + match response { + RpcResponse::Confirmed(block) => GetBlockWithTxsResponse { + result: Some(crate::protos::starknet::get_block_with_txs_response::Result::Block( + ProtoBlockWithTxs { + status: finality_status_to_proto(block.status), + header: Some(ProtoBlockHeader::from(&block)), + transactions: block.transactions.into_iter().map(ProtoTx::from).collect(), + }, + )), + }, + RpcResponse::PreConfirmed(pending) => GetBlockWithTxsResponse { + result: Some( + crate::protos::starknet::get_block_with_txs_response::Result::PendingBlock( + ProtoPendingBlockWithTxs { + header: Some(ProtoBlockHeader::from(&pending)), + transactions: pending + .transactions + .into_iter() + .map(ProtoTx::from) + .collect(), + }, + ), + ), + }, + } + } +} + +/// Convert block with receipts response to proto. +impl From for GetBlockWithReceiptsResponse { + fn from(response: katana_rpc_types::block::GetBlockWithReceiptsResponse) -> Self { + use katana_rpc_types::block::GetBlockWithReceiptsResponse as RpcResponse; + + use crate::protos::types::TransactionWithReceipt; + + match response { + RpcResponse::Block(block) => GetBlockWithReceiptsResponse { + result: Some( + crate::protos::starknet::get_block_with_receipts_response::Result::Block( + ProtoBlockWithReceipts { + status: finality_status_to_proto(block.status), + header: Some(ProtoBlockHeader::from(&block)), + transactions: block + .transactions + .into_iter() + .map(TransactionWithReceipt::from) + .collect(), + }, + ), + ), + }, + RpcResponse::PreConfirmed(pending) => GetBlockWithReceiptsResponse { + result: Some( + crate::protos::starknet::get_block_with_receipts_response::Result::PendingBlock( + ProtoPendingBlockWithReceipts { + header: Some(ProtoBlockHeader::from(&pending)), + transactions: pending + .transactions + .into_iter() + .map(TransactionWithReceipt::from) + .collect(), + }, + ), + ), + }, + } + } +} + +/// Convert state update response to proto. +impl From for GetStateUpdateResponse { + fn from(response: katana_rpc_types::state_update::StateUpdate) -> Self { + use katana_rpc_types::state_update::StateUpdate as RpcStateUpdate; + + use crate::protos::types::StateDiff as ProtoStateDiff; + + match response { + RpcStateUpdate::Confirmed(update) => GetStateUpdateResponse { + result: Some( + crate::protos::starknet::get_state_update_response::Result::StateUpdate( + ProtoStateUpdate { + block_hash: Some(update.block_hash.into()), + old_root: Some(update.old_root.into()), + new_root: Some(update.new_root.into()), + state_diff: Some(ProtoStateDiff::from(&update.state_diff)), + }, + ), + ), + }, + RpcStateUpdate::PreConfirmed(pending) => GetStateUpdateResponse { + result: Some( + crate::protos::starknet::get_state_update_response::Result::PendingStateUpdate( + ProtoPendingStateUpdate { + old_root: pending.old_root.map(|f| f.into()), + state_diff: Some(ProtoStateDiff::from(&pending.state_diff)), + }, + ), + ), + }, + } + } +} + +// Block header conversions + +impl From<&katana_rpc_types::block::BlockWithTxHashes> for ProtoBlockHeader { + fn from(block: &katana_rpc_types::block::BlockWithTxHashes) -> Self { + ProtoBlockHeader { + block_hash: Some(block.block_hash.into()), + parent_hash: Some(block.parent_hash.into()), + block_number: block.block_number, + new_root: Some(block.new_root.into()), + timestamp: block.timestamp, + sequencer_address: Some(ProtoFelt::from(Felt::from(block.sequencer_address))), + l1_gas_price: Some(ProtoResourcePrice::from(&block.l1_gas_price)), + l1_data_gas_price: Some(ProtoResourcePrice::from(&block.l1_data_gas_price)), + l2_gas_price: Some(ProtoResourcePrice::from(&block.l2_gas_price)), + l1_da_mode: l1_da_mode_to_proto(block.l1_da_mode), + starknet_version: block.starknet_version.clone(), + } + } +} + +impl From<&katana_rpc_types::block::BlockWithTxs> for ProtoBlockHeader { + fn from(block: &katana_rpc_types::block::BlockWithTxs) -> Self { + ProtoBlockHeader { + block_hash: Some(block.block_hash.into()), + parent_hash: Some(block.parent_hash.into()), + block_number: block.block_number, + new_root: Some(block.new_root.into()), + timestamp: block.timestamp, + sequencer_address: Some(ProtoFelt::from(Felt::from(block.sequencer_address))), + l1_gas_price: Some(ProtoResourcePrice::from(&block.l1_gas_price)), + l1_data_gas_price: Some(ProtoResourcePrice::from(&block.l1_data_gas_price)), + l2_gas_price: Some(ProtoResourcePrice::from(&block.l2_gas_price)), + l1_da_mode: l1_da_mode_to_proto(block.l1_da_mode), + starknet_version: block.starknet_version.clone(), + } + } +} + +impl From<&katana_rpc_types::block::BlockWithReceipts> for ProtoBlockHeader { + fn from(block: &katana_rpc_types::block::BlockWithReceipts) -> Self { + ProtoBlockHeader { + block_hash: Some(block.block_hash.into()), + parent_hash: Some(block.parent_hash.into()), + block_number: block.block_number, + new_root: Some(block.new_root.into()), + timestamp: block.timestamp, + sequencer_address: Some(ProtoFelt::from(Felt::from(block.sequencer_address))), + l1_gas_price: Some(ProtoResourcePrice::from(&block.l1_gas_price)), + l1_data_gas_price: Some(ProtoResourcePrice::from(&block.l1_data_gas_price)), + l2_gas_price: Some(ProtoResourcePrice::from(&block.l2_gas_price)), + l1_da_mode: l1_da_mode_to_proto(block.l1_da_mode), + starknet_version: block.starknet_version.clone(), + } + } +} + +impl From<&katana_rpc_types::block::PreConfirmedBlockWithTxHashes> for ProtoBlockHeader { + fn from(pending: &katana_rpc_types::block::PreConfirmedBlockWithTxHashes) -> Self { + ProtoBlockHeader { + block_hash: None, + parent_hash: None, // PreConfirmed blocks don't have parent_hash + block_number: pending.block_number, + new_root: None, + timestamp: pending.timestamp, + sequencer_address: Some(ProtoFelt::from(Felt::from(pending.sequencer_address))), + l1_gas_price: Some(ProtoResourcePrice::from(&pending.l1_gas_price)), + l1_data_gas_price: Some(ProtoResourcePrice::from(&pending.l1_data_gas_price)), + l2_gas_price: Some(ProtoResourcePrice::from(&pending.l2_gas_price)), + l1_da_mode: l1_da_mode_to_proto(pending.l1_da_mode), + starknet_version: pending.starknet_version.clone(), + } + } +} + +impl From<&katana_rpc_types::block::PreConfirmedBlockWithTxs> for ProtoBlockHeader { + fn from(pending: &katana_rpc_types::block::PreConfirmedBlockWithTxs) -> Self { + ProtoBlockHeader { + block_hash: None, + parent_hash: None, // PreConfirmed blocks don't have parent_hash + block_number: pending.block_number, + new_root: None, + timestamp: pending.timestamp, + sequencer_address: Some(ProtoFelt::from(Felt::from(pending.sequencer_address))), + l1_gas_price: Some(ProtoResourcePrice::from(&pending.l1_gas_price)), + l1_data_gas_price: Some(ProtoResourcePrice::from(&pending.l1_data_gas_price)), + l2_gas_price: Some(ProtoResourcePrice::from(&pending.l2_gas_price)), + l1_da_mode: l1_da_mode_to_proto(pending.l1_da_mode), + starknet_version: pending.starknet_version.clone(), + } + } +} + +impl From<&katana_rpc_types::block::PreConfirmedBlockWithReceipts> for ProtoBlockHeader { + fn from(pending: &katana_rpc_types::block::PreConfirmedBlockWithReceipts) -> Self { + ProtoBlockHeader { + block_hash: None, + parent_hash: None, // PreConfirmed blocks don't have parent_hash + block_number: pending.block_number, + new_root: None, + timestamp: pending.timestamp, + sequencer_address: Some(ProtoFelt::from(Felt::from(pending.sequencer_address))), + l1_gas_price: Some(ProtoResourcePrice::from(&pending.l1_gas_price)), + l1_data_gas_price: Some(ProtoResourcePrice::from(&pending.l1_data_gas_price)), + l2_gas_price: Some(ProtoResourcePrice::from(&pending.l2_gas_price)), + l1_da_mode: l1_da_mode_to_proto(pending.l1_da_mode), + starknet_version: pending.starknet_version.clone(), + } + } +} + +impl From<&starknet::core::types::ResourcePrice> for ProtoResourcePrice { + fn from(price: &starknet::core::types::ResourcePrice) -> Self { + ProtoResourcePrice { + price_in_wei: Some(price.price_in_wei.into()), + price_in_fri: Some(price.price_in_fri.into()), + } + } +} diff --git a/crates/grpc/src/protos/types/conversion/event.rs b/crates/grpc/src/protos/types/conversion/event.rs new file mode 100644 index 000000000..560d0666b --- /dev/null +++ b/crates/grpc/src/protos/types/conversion/event.rs @@ -0,0 +1,76 @@ +//! Event type conversions. + +use katana_primitives::block::BlockIdOrTag; +use katana_primitives::Felt; +use katana_rpc_types::event::{ + EmittedEvent, EventFilter, EventFilterWithPage, GetEventsResponse, ResultPageRequest, +}; +use tonic::Status; + +use super::FeltVecExt; +use crate::protos::common::Felt as ProtoFelt; +use crate::protos::starknet::{GetEventsRequest, GetEventsResponse as ProtoGetEventsResponse}; +use crate::protos::types::EmittedEvent as ProtoEmittedEvent; + +/// Convert GetEventsRequest to EventFilterWithPage +impl TryFrom for EventFilterWithPage { + type Error = Status; + + fn try_from(req: GetEventsRequest) -> Result { + let filter = req.filter.ok_or_else(|| Status::invalid_argument("Missing filter"))?; + + let from_block = filter.from_block.map(BlockIdOrTag::try_from).transpose()?; + + let to_block = filter.to_block.map(BlockIdOrTag::try_from).transpose()?; + + let address = filter.address.map(|f| f.try_into()).transpose()?; + + // Convert flat keys to nested structure (each key at position i matches exactly) + let keys = if filter.keys.is_empty() { + None + } else { + Some( + filter.keys.into_iter().map(|k| Ok(vec![Felt::try_from(k)?])).collect::>, + Status, + >>( + )?, + ) + }; + + let continuation_token = + if req.continuation_token.is_empty() { None } else { Some(req.continuation_token) }; + + Ok(EventFilterWithPage { + event_filter: EventFilter { from_block, to_block, address, keys }, + result_page_request: ResultPageRequest { + continuation_token, + chunk_size: req.chunk_size as u64, + }, + }) + } +} + +/// Convert GetEventsResponse to proto +impl From for ProtoGetEventsResponse { + fn from(response: GetEventsResponse) -> Self { + ProtoGetEventsResponse { + events: response.events.into_iter().map(ProtoEmittedEvent::from).collect(), + continuation_token: response.continuation_token.unwrap_or_default(), + } + } +} + +/// Convert EmittedEvent to proto +impl From for ProtoEmittedEvent { + fn from(event: EmittedEvent) -> Self { + ProtoEmittedEvent { + from_address: Some(ProtoFelt::from(Felt::from(event.from_address))), + keys: event.keys.to_proto_felts(), + data: event.data.to_proto_felts(), + block_hash: event.block_hash.map(ProtoFelt::from), + block_number: event.block_number.unwrap_or(0), + transaction_hash: Some(event.transaction_hash.into()), + } + } +} diff --git a/crates/grpc/src/protos/types/conversion/mod.rs b/crates/grpc/src/protos/types/conversion/mod.rs new file mode 100644 index 000000000..83cf8dde4 --- /dev/null +++ b/crates/grpc/src/protos/types/conversion/mod.rs @@ -0,0 +1,78 @@ +//! Type conversion utilities between proto types and internal Katana types. +//! +//! This module provides bidirectional conversion between: +//! - Proto types generated from .proto files +//! - Internal Katana types from katana-primitives and katana-rpc-types +//! +//! Conversions are implemented using `From` and `TryFrom` traits for idiomatic Rust. + +mod block; +mod event; +mod receipt; +mod state; +mod trace; +mod transaction; +mod trie; + +pub use block::*; +#[allow(unused_imports)] +pub use event::*; +use katana_primitives::Felt; +// These modules implement From/TryFrom traits for type conversions +// The types are used via .into() or TryFrom::try_from() calls +#[allow(unused_imports)] +pub use receipt::*; +#[allow(unused_imports)] +pub use state::*; +use tonic::Status; +#[allow(unused_imports)] +pub use trace::*; +#[allow(unused_imports)] +pub use transaction::*; +#[allow(unused_imports)] +pub use trie::*; + +use crate::proto; + +impl From for proto::Felt { + fn from(felt: katana_primitives::Felt) -> Self { + Self { value: felt.to_bytes_be().to_vec() } + } +} + +impl TryFrom for katana_primitives::Felt { + type Error = Status; + + fn try_from(proto: proto::Felt) -> Result { + if proto.value.len() > 32 { + return Err(Status::invalid_argument("Felt value exceeds 32 bytes")); + } + + Ok(Felt::from_bytes_be_slice(&proto.value)) + } +} + +impl From for proto::Felt { + fn from(address: katana_primitives::ContractAddress) -> Self { + Self { value: address.to_bytes_be().to_vec() } + } +} + +impl TryFrom for katana_primitives::ContractAddress { + type Error = Status; + + fn try_from(value: proto::Felt) -> Result { + Ok(Self::new(value.try_into()?)) + } +} + +/// Extension trait for converting vectors of Felts. +pub trait FeltVecExt { + fn to_proto_felts(&self) -> Vec; +} + +impl FeltVecExt for [Felt] { + fn to_proto_felts(&self) -> Vec { + self.iter().copied().map(proto::Felt::from).collect() + } +} diff --git a/crates/grpc/src/protos/types/conversion/receipt.rs b/crates/grpc/src/protos/types/conversion/receipt.rs new file mode 100644 index 000000000..8d1fa9b4a --- /dev/null +++ b/crates/grpc/src/protos/types/conversion/receipt.rs @@ -0,0 +1,146 @@ +//! Receipt type conversions. + +use katana_primitives::block::FinalityStatus; +use katana_primitives::fee::PriceUnit; +use katana_primitives::receipt::{Event, MessageToL1}; +use katana_primitives::Felt; + +use super::FeltVecExt; +use crate::protos::common::Felt as ProtoFelt; +use crate::protos::types::{ + Event as ProtoEvent, ExecutionResources as ProtoExecutionResources, + FeePayment as ProtoFeePayment, FinalityStatus as ProtoFinalityStatus, + MessageToL1 as ProtoMessageToL1, Transaction as ProtoTx, + TransactionReceipt as ProtoTransactionReceipt, TransactionWithReceipt, +}; + +/// Convert PriceUnit to string representation for proto. +fn price_unit_to_string(unit: PriceUnit) -> String { + match unit { + PriceUnit::Wei => "WEI".to_string(), + PriceUnit::Fri => "FRI".to_string(), + } +} + +/// Convert FinalityStatus to proto enum. +fn finality_status_to_proto(status: &FinalityStatus) -> i32 { + match status { + FinalityStatus::AcceptedOnL2 => ProtoFinalityStatus::AcceptedOnL2 as i32, + FinalityStatus::AcceptedOnL1 => ProtoFinalityStatus::AcceptedOnL1 as i32, + FinalityStatus::PreConfirmed => ProtoFinalityStatus::PreConfirmed as i32, + } +} + +/// Convert RPC transaction with receipt to proto. +impl From for TransactionWithReceipt { + fn from(tx_with_receipt: katana_rpc_types::block::RpcTxWithReceipt) -> Self { + TransactionWithReceipt { + transaction: Some(ProtoTx::from(katana_rpc_types::transaction::RpcTxWithHash { + transaction_hash: tx_with_receipt.receipt.transaction_hash, + transaction: tx_with_receipt.transaction, + })), + receipt: Some(ProtoTransactionReceipt::from(&tx_with_receipt.receipt)), + } + } +} + +/// Convert RPC receipt with block info to proto. +impl From<&katana_rpc_types::receipt::TxReceiptWithBlockInfo> for ProtoTransactionReceipt { + fn from(receipt: &katana_rpc_types::receipt::TxReceiptWithBlockInfo) -> Self { + let mut proto = receipt_from_rpc_receipt(receipt.transaction_hash, &receipt.receipt); + // TxReceiptWithBlockInfo has block info via the block field + proto.block_hash = receipt.block.block_hash().map(|h| h.into()); + proto.block_number = receipt.block.block_number(); + proto + } +} + +/// Convert RPC receipt with hash to proto. +impl From<&katana_rpc_types::receipt::RpcTxReceiptWithHash> for ProtoTransactionReceipt { + fn from(receipt: &katana_rpc_types::receipt::RpcTxReceiptWithHash) -> Self { + receipt_from_rpc_receipt(receipt.transaction_hash, &receipt.receipt) + } +} + +fn receipt_from_rpc_receipt( + transaction_hash: Felt, + receipt: &katana_rpc_types::receipt::RpcTxReceipt, +) -> ProtoTransactionReceipt { + use katana_rpc_types::receipt::RpcTxReceipt; + + let tx_type = match receipt { + RpcTxReceipt::Invoke(_) => "INVOKE", + RpcTxReceipt::Declare(_) => "DECLARE", + RpcTxReceipt::Deploy(_) => "DEPLOY", + RpcTxReceipt::DeployAccount(_) => "DEPLOY_ACCOUNT", + RpcTxReceipt::L1Handler(_) => "L1_HANDLER", + }; + + let (execution_status, revert_reason) = match receipt.execution_result() { + katana_rpc_types::receipt::ExecutionResult::Succeeded => { + ("SUCCEEDED".to_string(), String::new()) + } + katana_rpc_types::receipt::ExecutionResult::Reverted { reason } => { + ("REVERTED".to_string(), reason.clone()) + } + }; + + // Extract type-specific fields + let (contract_address, message_hash) = match receipt { + RpcTxReceipt::Deploy(r) => (Some(ProtoFelt::from(Felt::from(r.contract_address))), None), + RpcTxReceipt::DeployAccount(r) => { + (Some(ProtoFelt::from(Felt::from(r.contract_address))), None) + } + RpcTxReceipt::L1Handler(r) => { + // Convert B256 message_hash to Felt + let hash_bytes = r.message_hash.0; + let hash_felt = Felt::from_bytes_be_slice(&hash_bytes); + (None, Some(hash_felt.into())) + } + _ => (None, None), + }; + + ProtoTransactionReceipt { + r#type: tx_type.to_string(), + transaction_hash: Some(transaction_hash.into()), + actual_fee: Some(ProtoFeePayment { + amount: Some(receipt.actual_fee().amount.into()), + unit: price_unit_to_string(receipt.actual_fee().unit), + }), + finality_status: finality_status_to_proto(receipt.finality_status()), + messages_sent: messages_to_proto(receipt.messages_sent()), + events: events_to_proto(receipt.events()), + execution_resources: Some(ProtoExecutionResources::from(receipt.execution_resources())), + execution_status, + revert_reason, + contract_address, + message_hash, + block_number: 0, // Will be set by caller if available + block_hash: None, // Will be set by caller if available + } +} + +fn messages_to_proto(messages: &[MessageToL1]) -> Vec { + messages + .iter() + .map(|m| ProtoMessageToL1 { + from_address: Some(ProtoFelt::from(Felt::from(m.from_address))), + to_address: Some(m.to_address.into()), + payload: m.payload.to_proto_felts(), + }) + .collect() +} + +fn events_to_proto(events: &[Event]) -> Vec { + events + .iter() + .map(|e| ProtoEvent { + from_address: Some(ProtoFelt::from(Felt::from(e.from_address))), + keys: e.keys.to_proto_felts(), + data: e.data.to_proto_felts(), + }) + .collect() +} + +// Note: ExecutionResources conversion is in trace.rs since it's the same type +// re-exported in katana_rpc_types::receipt and katana_rpc_types::trace diff --git a/crates/grpc/src/protos/types/conversion/state.rs b/crates/grpc/src/protos/types/conversion/state.rs new file mode 100644 index 000000000..5fe4352ce --- /dev/null +++ b/crates/grpc/src/protos/types/conversion/state.rs @@ -0,0 +1,68 @@ +//! State update type conversions. + +use katana_primitives::Felt; + +use crate::protos::common::Felt as ProtoFelt; +use crate::protos::types::{ + DeclaredClass, DeployedContract, Nonce as ProtoNonce, ReplacedClass, + StateDiff as ProtoStateDiff, StorageDiff, StorageEntry, +}; + +/// Convert RPC state diff to proto. +impl From<&katana_rpc_types::state_update::StateDiff> for ProtoStateDiff { + fn from(diff: &katana_rpc_types::state_update::StateDiff) -> Self { + ProtoStateDiff { + storage_diffs: diff + .storage_diffs + .iter() + .map(|(address, entries)| StorageDiff { + address: Some(ProtoFelt::from(Felt::from(*address))), + storage_entries: entries + .iter() + .map(|(key, value)| StorageEntry { + key: Some((*key).into()), + value: Some((*value).into()), + }) + .collect(), + }) + .collect(), + deprecated_declared_classes: diff + .deprecated_declared_classes + .iter() + .map(|c| ProtoFelt::from(*c)) + .collect(), + declared_classes: diff + .declared_classes + .iter() + .map(|(class_hash, compiled_class_hash)| DeclaredClass { + class_hash: Some((*class_hash).into()), + compiled_class_hash: Some((*compiled_class_hash).into()), + }) + .collect(), + deployed_contracts: diff + .deployed_contracts + .iter() + .map(|(address, class_hash)| DeployedContract { + address: Some(ProtoFelt::from(Felt::from(*address))), + class_hash: Some((*class_hash).into()), + }) + .collect(), + replaced_classes: diff + .replaced_classes + .iter() + .map(|(contract_address, class_hash)| ReplacedClass { + contract_address: Some(ProtoFelt::from(Felt::from(*contract_address))), + class_hash: Some((*class_hash).into()), + }) + .collect(), + nonces: diff + .nonces + .iter() + .map(|(contract_address, nonce)| ProtoNonce { + contract_address: Some(ProtoFelt::from(Felt::from(*contract_address))), + nonce: Some((*nonce).into()), + }) + .collect(), + } + } +} diff --git a/crates/grpc/src/protos/types/conversion/trace.rs b/crates/grpc/src/protos/types/conversion/trace.rs new file mode 100644 index 000000000..1610d8a92 --- /dev/null +++ b/crates/grpc/src/protos/types/conversion/trace.rs @@ -0,0 +1,222 @@ +//! Trace type conversions. + +use katana_primitives::Felt; + +use super::FeltVecExt; +use crate::protos::common::Felt as ProtoFelt; +use crate::protos::starknet::{ + SimulateTransactionsResponse, TraceBlockTransactionsResponse, TraceTransactionResponse, + TransactionTraceWithHash, +}; +use crate::protos::types::transaction_trace::Trace as ProtoTraceVariant; +use crate::protos::types::{ + DeclareTransactionTrace, DeployAccountTransactionTrace, ExecutionResources, + FeeEstimate as ProtoFeeEstimate, FunctionInvocation, InvokeTransactionTrace, + L1HandlerTransactionTrace, OrderedEvent, OrderedL2ToL1Message, SimulatedTransaction, + TransactionTrace as ProtoTransactionTrace, +}; + +/// Convert RPC trace to proto. +impl From<&katana_rpc_types::trace::TxTrace> for ProtoTransactionTrace { + fn from(trace: &katana_rpc_types::trace::TxTrace) -> Self { + use katana_rpc_types::trace::TxTrace; + + let trace_variant = match trace { + TxTrace::Invoke(invoke) => { + let execute_invocation = match &invoke.execute_invocation { + katana_rpc_types::trace::ExecuteInvocation::Success(inv) => { + Some(FunctionInvocation::from(inv.as_ref())) + } + katana_rpc_types::trace::ExecuteInvocation::Reverted(r) => { + // For reverted executions, create invocation with revert info + Some(FunctionInvocation { + is_reverted: true, + result: vec![ProtoFelt::from(Felt::from_bytes_be_slice( + r.revert_reason.as_bytes(), + ))], + ..Default::default() + }) + } + }; + + ProtoTraceVariant::InvokeTrace(InvokeTransactionTrace { + execute_invocation, + validate_invocation: invoke + .validate_invocation + .as_ref() + .map(FunctionInvocation::from), + fee_transfer_invocation: invoke + .fee_transfer_invocation + .as_ref() + .map(FunctionInvocation::from), + state_diff: None, // State diff conversion would require more complex mapping + execution_resources: Some(ExecutionResources::from( + &invoke.execution_resources, + )), + }) + } + TxTrace::Declare(declare) => ProtoTraceVariant::DeclareTrace(DeclareTransactionTrace { + validate_invocation: declare + .validate_invocation + .as_ref() + .map(FunctionInvocation::from), + fee_transfer_invocation: declare + .fee_transfer_invocation + .as_ref() + .map(FunctionInvocation::from), + state_diff: None, + execution_resources: Some(ExecutionResources::from(&declare.execution_resources)), + }), + TxTrace::DeployAccount(deploy) => { + ProtoTraceVariant::DeployAccountTrace(DeployAccountTransactionTrace { + constructor_invocation: Some(FunctionInvocation::from( + &deploy.constructor_invocation, + )), + validate_invocation: deploy + .validate_invocation + .as_ref() + .map(FunctionInvocation::from), + fee_transfer_invocation: deploy + .fee_transfer_invocation + .as_ref() + .map(FunctionInvocation::from), + state_diff: None, + execution_resources: Some(ExecutionResources::from( + &deploy.execution_resources, + )), + }) + } + TxTrace::L1Handler(l1) => { + let function_invocation = match &l1.function_invocation { + katana_rpc_types::trace::ExecuteInvocation::Success(inv) => { + Some(FunctionInvocation::from(inv.as_ref())) + } + katana_rpc_types::trace::ExecuteInvocation::Reverted(r) => { + Some(FunctionInvocation { + is_reverted: true, + result: vec![ProtoFelt::from(Felt::from_bytes_be_slice( + r.revert_reason.as_bytes(), + ))], + ..Default::default() + }) + } + }; + + ProtoTraceVariant::L1HandlerTrace(L1HandlerTransactionTrace { + function_invocation, + state_diff: None, + execution_resources: Some(ExecutionResources::from(&l1.execution_resources)), + }) + } + }; + + ProtoTransactionTrace { trace: Some(trace_variant) } + } +} + +/// Convert trace transaction response to proto. +impl From for TraceTransactionResponse { + fn from(trace: katana_rpc_types::trace::TxTrace) -> Self { + TraceTransactionResponse { trace: Some(ProtoTransactionTrace::from(&trace)) } + } +} + +/// Convert simulated transactions response to proto. +impl From for SimulateTransactionsResponse { + fn from(response: katana_rpc_types::trace::SimulatedTransactionsResponse) -> Self { + SimulateTransactionsResponse { + simulated_transactions: response + .transactions + .into_iter() + .map(|sim| SimulatedTransaction { + transaction_trace: Some(ProtoTransactionTrace::from(&sim.transaction_trace)), + fee_estimation: Some(ProtoFeeEstimate::from(&sim.fee_estimation)), + }) + .collect(), + } + } +} + +/// Convert trace block transactions response to proto. +impl From + for TraceBlockTransactionsResponse +{ + fn from(response: katana_rpc_types::trace::TraceBlockTransactionsResponse) -> Self { + TraceBlockTransactionsResponse { + traces: response + .traces + .into_iter() + .map(|t| TransactionTraceWithHash { + transaction_hash: Some(t.transaction_hash.into()), + trace_root: Some(ProtoTransactionTrace::from(&t.trace_root)), + }) + .collect(), + } + } +} + +impl From<&katana_rpc_types::trace::FunctionInvocation> for FunctionInvocation { + fn from(inv: &katana_rpc_types::trace::FunctionInvocation) -> Self { + FunctionInvocation { + contract_address: Some(ProtoFelt::from(Felt::from(inv.contract_address))), + entry_point_selector: Some(inv.entry_point_selector.into()), + calldata: inv.calldata.to_proto_felts(), + caller_address: Some(ProtoFelt::from(Felt::from(inv.caller_address))), + class_hash: Some(inv.class_hash.into()), + entry_point_type: format!("{:?}", inv.entry_point_type), + call_type: format!("{:?}", inv.call_type), + result: inv.result.to_proto_felts(), + calls: inv.calls.iter().map(FunctionInvocation::from).collect(), + events: inv + .events + .iter() + .map(|e| OrderedEvent { + order: e.order, + keys: e.keys.to_proto_felts(), + data: e.data.to_proto_felts(), + }) + .collect(), + messages: inv + .messages + .iter() + .map(|m| OrderedL2ToL1Message { + order: m.order, + from_address: Some(ProtoFelt::from(Felt::from(m.from_address))), + to_address: Some(m.to_address.into()), + payload: m.payload.to_proto_felts(), + }) + .collect(), + execution_resources: Some(ExecutionResources { + l1_gas: inv.execution_resources.l1_gas, + l1_data_gas: 0, // InnerCallExecutionResources doesn't have this + l2_gas: inv.execution_resources.l2_gas, + }), + is_reverted: inv.is_reverted, + } + } +} + +/// Convert ExecutionResources (from receipt/trace) to proto ExecutionResources. +impl From<&katana_rpc_types::receipt::ExecutionResources> for ExecutionResources { + fn from(resources: &katana_rpc_types::receipt::ExecutionResources) -> Self { + ExecutionResources { + l1_gas: resources.l1_gas, + l1_data_gas: resources.l1_data_gas, + l2_gas: resources.l2_gas, + } + } +} + +impl From<&katana_rpc_types::FeeEstimate> for ProtoFeeEstimate { + fn from(estimate: &katana_rpc_types::FeeEstimate) -> Self { + ProtoFeeEstimate { + l1_gas_consumed: Some(ProtoFelt::from(Felt::from(estimate.l1_gas_consumed))), + l1_gas_price: Some(ProtoFelt::from(Felt::from(estimate.l1_gas_price))), + l2_gas_consumed: Some(ProtoFelt::from(Felt::from(estimate.l2_gas_consumed))), + l2_gas_price: Some(ProtoFelt::from(Felt::from(estimate.l2_gas_price))), + l1_data_gas_consumed: Some(ProtoFelt::from(Felt::from(estimate.l1_data_gas_consumed))), + l1_data_gas_price: Some(ProtoFelt::from(Felt::from(estimate.l1_data_gas_price))), + overall_fee: Some(ProtoFelt::from(Felt::from(estimate.overall_fee))), + } + } +} diff --git a/crates/grpc/src/protos/types/conversion/transaction.rs b/crates/grpc/src/protos/types/conversion/transaction.rs new file mode 100644 index 000000000..d73637303 --- /dev/null +++ b/crates/grpc/src/protos/types/conversion/transaction.rs @@ -0,0 +1,652 @@ +//! Transaction type conversions. + +use std::sync::Arc; + +use cairo_lang_starknet_classes::contract_class::{ContractEntryPoint, ContractEntryPoints}; +use katana_primitives::da::DataAvailabilityMode; +use katana_primitives::fee::{ + AllResourceBoundsMapping, L1GasResourceBoundsMapping, + ResourceBounds as PrimitiveResourceBounds, + ResourceBoundsMapping as PrimitiveResourceBoundsMapping, +}; +use katana_primitives::Felt; +use katana_rpc_types::broadcasted::{ + BroadcastedDeclareTx, BroadcastedDeployAccountTx, BroadcastedInvokeTx, QUERY_VERSION_OFFSET, +}; +use katana_rpc_types::class::RpcSierraContractClass; +use tonic::Status; + +use super::FeltVecExt; +use crate::proto; +use crate::proto::{ + BroadcastedDeclareTransaction, BroadcastedDeployAccountTransaction, + BroadcastedInvokeTransaction, ContractClass as ProtoContractClass, DeployAccountTxn, + DeployAccountTxnV3, DeployTxn, InvokeTxnV1, InvokeTxnV3, L1HandlerTxn, ResourceBounds, + ResourceBoundsMapping, SierraEntryPoint as ProtoSierraEntryPoint, Transaction as ProtoTx, +}; +use crate::protos::common::Felt as ProtoFelt; +use crate::protos::types::transaction::Transaction as ProtoTxVariant; + +/// Convert DataAvailabilityMode to string representation for proto. +fn da_mode_to_string(mode: DataAvailabilityMode) -> String { + match mode { + DataAvailabilityMode::L1 => "L1".to_string(), + DataAvailabilityMode::L2 => "L2".to_string(), + } +} + +/// Parse DataAvailabilityMode from string. +#[allow(clippy::result_large_err)] +fn da_mode_from_string(s: &str) -> Result { + match s.to_uppercase().as_str() { + "L1" => Ok(DataAvailabilityMode::L1), + "L2" => Ok(DataAvailabilityMode::L2), + _ => Err(Status::invalid_argument(format!("invalid data availability mode: {s}"))), + } +} + +/// Helper to extract a required Felt field from an Option. +#[allow(clippy::result_large_err)] +fn required_felt(field: Option<&ProtoFelt>, field_name: &str) -> Result { + field + .cloned() + .ok_or_else(|| Status::invalid_argument(format!("missing required field: {field_name}")))? + .try_into() +} + +/// Derive is_query flag from version field. +#[allow(clippy::result_large_err)] +fn is_query_from_version(version: Felt) -> Result { + if version == Felt::THREE { + Ok(false) + } else if version == Felt::THREE + QUERY_VERSION_OFFSET { + Ok(true) + } else { + Err(Status::invalid_argument(format!( + "invalid version {version:#x} for broadcasted transaction" + ))) + } +} + +/// Compute version from is_query flag. +fn version_from_is_query(is_query: bool) -> Felt { + if is_query { + Felt::THREE + QUERY_VERSION_OFFSET + } else { + Felt::THREE + } +} + +// ============================================================ +// Proto -> RPC Type Conversions +// ============================================================ + +/// Convert proto ResourceBoundsMapping to primitives ResourceBoundsMapping. +impl TryFrom<&proto::ResourceBoundsMapping> for PrimitiveResourceBoundsMapping { + type Error = Status; + + fn try_from(proto: &proto::ResourceBoundsMapping) -> Result { + let l1_gas = proto + .l1_gas + .as_ref() + .map(|rb| -> Result { + Ok(PrimitiveResourceBounds { + max_amount: required_felt(rb.max_amount.as_ref(), "l1_gas.max_amount")? + .try_into() + .map_err(|_| Status::invalid_argument("l1_gas.max_amount overflow"))?, + max_price_per_unit: required_felt( + rb.max_price_per_unit.as_ref(), + "l1_gas.max_price_per_unit", + )? + .try_into() + .map_err(|_| Status::invalid_argument("l1_gas.max_price_per_unit overflow"))?, + }) + }) + .transpose()? + .unwrap_or_default(); + + let l2_gas = proto + .l2_gas + .as_ref() + .map(|rb| -> Result { + Ok(PrimitiveResourceBounds { + max_amount: required_felt(rb.max_amount.as_ref(), "l2_gas.max_amount")? + .try_into() + .map_err(|_| Status::invalid_argument("l2_gas.max_amount overflow"))?, + max_price_per_unit: required_felt( + rb.max_price_per_unit.as_ref(), + "l2_gas.max_price_per_unit", + )? + .try_into() + .map_err(|_| Status::invalid_argument("l2_gas.max_price_per_unit overflow"))?, + }) + }) + .transpose()? + .unwrap_or_default(); + + // Check if l1_data_gas is present to determine the mapping type + if let Some(l1_data_gas_proto) = &proto.l1_data_gas { + let l1_data_gas = PrimitiveResourceBounds { + max_amount: required_felt( + l1_data_gas_proto.max_amount.as_ref(), + "l1_data_gas.max_amount", + )? + .try_into() + .map_err(|_| Status::invalid_argument("l1_data_gas.max_amount overflow"))?, + max_price_per_unit: required_felt( + l1_data_gas_proto.max_price_per_unit.as_ref(), + "l1_data_gas.max_price_per_unit", + )? + .try_into() + .map_err(|_| Status::invalid_argument("l1_data_gas.max_price_per_unit overflow"))?, + }; + + Ok(PrimitiveResourceBoundsMapping::All(AllResourceBoundsMapping { + l1_gas, + l2_gas, + l1_data_gas, + })) + } else { + Ok(PrimitiveResourceBoundsMapping::L1Gas(L1GasResourceBoundsMapping { l1_gas, l2_gas })) + } + } +} + +/// Convert proto BroadcastedInvokeTransaction to RPC BroadcastedInvokeTx. +impl TryFrom for BroadcastedInvokeTx { + type Error = Status; + + fn try_from(proto: BroadcastedInvokeTransaction) -> Result { + let sender_address = required_felt(proto.sender_address.as_ref(), "sender_address")?; + let nonce = required_felt(proto.nonce.as_ref(), "nonce")?; + let version = required_felt(proto.version.as_ref(), "version")?; + let is_query = is_query_from_version(version)?; + let tip = proto.tip.map(Felt::try_from).transpose()?.unwrap_or(Felt::ZERO); + + let resource_bounds = proto + .resource_bounds + .as_ref() + .ok_or_else(|| Status::invalid_argument("missing required field: resource_bounds"))?; + + Ok(BroadcastedInvokeTx { + sender_address: sender_address.into(), + calldata: proto + .calldata + .into_iter() + .map(Felt::try_from) + .collect::, _>>()?, + signature: proto + .signature + .into_iter() + .map(Felt::try_from) + .collect::, _>>()?, + nonce, + paymaster_data: proto + .paymaster_data + .into_iter() + .map(Felt::try_from) + .collect::, _>>()?, + tip: u64::try_from(tip).map_err(|_| Status::invalid_argument("tip overflow"))?.into(), + account_deployment_data: proto + .account_deployment_data + .into_iter() + .map(Felt::try_from) + .collect::, _>>()?, + resource_bounds: PrimitiveResourceBoundsMapping::try_from(resource_bounds)?, + fee_data_availability_mode: da_mode_from_string(&proto.fee_data_availability_mode)?, + nonce_data_availability_mode: da_mode_from_string(&proto.nonce_data_availability_mode)?, + is_query, + }) + } +} + +/// Convert proto BroadcastedDeployAccountTransaction to RPC BroadcastedDeployAccountTx. +impl TryFrom for BroadcastedDeployAccountTx { + type Error = Status; + + fn try_from(proto: BroadcastedDeployAccountTransaction) -> Result { + let nonce = required_felt(proto.nonce.as_ref(), "nonce")?; + let class_hash = required_felt(proto.class_hash.as_ref(), "class_hash")?; + let contract_address_salt = + required_felt(proto.contract_address_salt.as_ref(), "contract_address_salt")?; + let version = required_felt(proto.version.as_ref(), "version")?; + let is_query = is_query_from_version(version)?; + let tip = proto.tip.map(Felt::try_from).transpose()?.unwrap_or(Felt::ZERO); + + let resource_bounds = proto + .resource_bounds + .as_ref() + .ok_or_else(|| Status::invalid_argument("missing required field: resource_bounds"))?; + + Ok(BroadcastedDeployAccountTx { + signature: proto + .signature + .into_iter() + .map(Felt::try_from) + .collect::, _>>()?, + nonce, + contract_address_salt, + constructor_calldata: proto + .constructor_calldata + .into_iter() + .map(Felt::try_from) + .collect::, _>>()?, + class_hash, + paymaster_data: proto + .paymaster_data + .into_iter() + .map(Felt::try_from) + .collect::, _>>()?, + tip: u64::try_from(tip).map_err(|_| Status::invalid_argument("tip overflow"))?.into(), + resource_bounds: PrimitiveResourceBoundsMapping::try_from(resource_bounds)?, + fee_data_availability_mode: da_mode_from_string(&proto.fee_data_availability_mode)?, + nonce_data_availability_mode: da_mode_from_string(&proto.nonce_data_availability_mode)?, + is_query, + }) + } +} + +/// Convert proto ContractClass to RPC RpcSierraContractClass. +impl TryFrom for RpcSierraContractClass { + type Error = Status; + + fn try_from(proto: ProtoContractClass) -> Result { + let sierra_program = proto + .sierra_program + .into_iter() + .map(Felt::try_from) + .collect::, _>>()?; + + let entry_points = proto + .entry_points_by_type + .as_ref() + .ok_or_else(|| Status::invalid_argument("missing entry_points_by_type"))?; + + let convert_entry_points = + |eps: &[ProtoSierraEntryPoint]| -> Result, Status> { + eps.iter() + .map(|ep| { + let selector = required_felt(ep.selector.as_ref(), "entry_point.selector")?; + // Convert Felt to BigUint via bytes + let selector_bytes = selector.to_bytes_be(); + let selector_biguint = num_bigint::BigUint::from_bytes_be(&selector_bytes); + Ok(ContractEntryPoint { + selector: selector_biguint, + function_idx: ep.function_idx as usize, + }) + }) + .collect() + }; + + let entry_points_by_type = ContractEntryPoints { + external: convert_entry_points(&entry_points.external)?, + l1_handler: convert_entry_points(&entry_points.l1_handler)?, + constructor: convert_entry_points(&entry_points.constructor)?, + }; + + Ok(RpcSierraContractClass { + sierra_program, + contract_class_version: proto.contract_class_version.clone(), + entry_points_by_type, + abi: if proto.abi.is_empty() { None } else { Some(proto.abi.clone()) }, + }) + } +} + +/// Convert proto BroadcastedDeclareTransaction to RPC BroadcastedDeclareTx. +impl TryFrom for BroadcastedDeclareTx { + type Error = Status; + + fn try_from(proto: BroadcastedDeclareTransaction) -> Result { + let sender_address = required_felt(proto.sender_address.as_ref(), "sender_address")?; + let nonce = required_felt(proto.nonce.as_ref(), "nonce")?; + let compiled_class_hash = + required_felt(proto.compiled_class_hash.as_ref(), "compiled_class_hash")?; + let version = required_felt(proto.version.as_ref(), "version")?; + let is_query = is_query_from_version(version)?; + let tip = proto.tip.map(Felt::try_from).transpose()?.unwrap_or(Felt::ZERO); + + let resource_bounds = proto + .resource_bounds + .as_ref() + .ok_or_else(|| Status::invalid_argument("missing required field: resource_bounds"))?; + + let contract_class = proto + .contract_class + .ok_or_else(|| Status::invalid_argument("missing required field: contract_class"))?; + + let rpc_contract_class = RpcSierraContractClass::try_from(contract_class)?; + + Ok(BroadcastedDeclareTx { + sender_address: sender_address.into(), + compiled_class_hash, + signature: proto + .signature + .into_iter() + .map(Felt::try_from) + .collect::, _>>()?, + nonce, + contract_class: Arc::new(rpc_contract_class), + paymaster_data: proto + .paymaster_data + .into_iter() + .map(Felt::try_from) + .collect::, _>>()?, + tip: u64::try_from(tip).map_err(|_| Status::invalid_argument("tip overflow"))?.into(), + account_deployment_data: proto + .account_deployment_data + .into_iter() + .map(Felt::try_from) + .collect::, _>>()?, + resource_bounds: PrimitiveResourceBoundsMapping::try_from(resource_bounds)?, + fee_data_availability_mode: da_mode_from_string(&proto.fee_data_availability_mode)?, + nonce_data_availability_mode: da_mode_from_string(&proto.nonce_data_availability_mode)?, + is_query, + }) + } +} + +// ============================================================ +// RPC Type -> Proto Conversions +// ============================================================ + +/// Convert RPC BroadcastedInvokeTx to proto BroadcastedInvokeTransaction. +impl From<&BroadcastedInvokeTx> for BroadcastedInvokeTransaction { + fn from(tx: &BroadcastedInvokeTx) -> Self { + BroadcastedInvokeTransaction { + sender_address: Some(ProtoFelt::from(Felt::from(tx.sender_address))), + calldata: tx.calldata.to_proto_felts(), + signature: tx.signature.to_proto_felts(), + nonce: Some(tx.nonce.into()), + paymaster_data: tx.paymaster_data.to_proto_felts(), + tip: Some(ProtoFelt::from(Felt::from(u64::from(tx.tip)))), + account_deployment_data: tx.account_deployment_data.to_proto_felts(), + resource_bounds: Some(ResourceBoundsMapping::from(&tx.resource_bounds)), + fee_data_availability_mode: da_mode_to_string(tx.fee_data_availability_mode), + nonce_data_availability_mode: da_mode_to_string(tx.nonce_data_availability_mode), + version: Some(version_from_is_query(tx.is_query).into()), + } + } +} + +/// Convert RPC BroadcastedDeployAccountTx to proto BroadcastedDeployAccountTransaction. +impl From<&BroadcastedDeployAccountTx> for BroadcastedDeployAccountTransaction { + fn from(tx: &BroadcastedDeployAccountTx) -> Self { + BroadcastedDeployAccountTransaction { + signature: tx.signature.to_proto_felts(), + nonce: Some(tx.nonce.into()), + contract_address_salt: Some(tx.contract_address_salt.into()), + constructor_calldata: tx.constructor_calldata.to_proto_felts(), + class_hash: Some(tx.class_hash.into()), + paymaster_data: tx.paymaster_data.to_proto_felts(), + tip: Some(ProtoFelt::from(Felt::from(u64::from(tx.tip)))), + resource_bounds: Some(ResourceBoundsMapping::from(&tx.resource_bounds)), + fee_data_availability_mode: da_mode_to_string(tx.fee_data_availability_mode), + nonce_data_availability_mode: da_mode_to_string(tx.nonce_data_availability_mode), + version: Some(version_from_is_query(tx.is_query).into()), + } + } +} + +/// Convert RPC BroadcastedDeclareTx to proto BroadcastedDeclareTransaction. +impl From<&BroadcastedDeclareTx> for BroadcastedDeclareTransaction { + fn from(tx: &BroadcastedDeclareTx) -> Self { + // Convert RpcSierraContractClass to proto ContractClass + let contract_class = ProtoContractClass { + sierra_program: tx.contract_class.sierra_program.to_proto_felts(), + contract_class_version: tx.contract_class.contract_class_version.clone(), + entry_points_by_type: Some(proto::EntryPointsByType { + constructor: tx + .contract_class + .entry_points_by_type + .constructor + .iter() + .map(|ep| ProtoSierraEntryPoint { + selector: Some(ProtoFelt::from(Felt::from_bytes_be( + &ep.selector.to_bytes_be().try_into().unwrap_or([0u8; 32]), + ))), + function_idx: ep.function_idx as u64, + }) + .collect(), + external: tx + .contract_class + .entry_points_by_type + .external + .iter() + .map(|ep| ProtoSierraEntryPoint { + selector: Some(ProtoFelt::from(Felt::from_bytes_be( + &ep.selector.to_bytes_be().try_into().unwrap_or([0u8; 32]), + ))), + function_idx: ep.function_idx as u64, + }) + .collect(), + l1_handler: tx + .contract_class + .entry_points_by_type + .l1_handler + .iter() + .map(|ep| ProtoSierraEntryPoint { + selector: Some(ProtoFelt::from(Felt::from_bytes_be( + &ep.selector.to_bytes_be().try_into().unwrap_or([0u8; 32]), + ))), + function_idx: ep.function_idx as u64, + }) + .collect(), + }), + abi: tx.contract_class.abi.clone().unwrap_or_default(), + }; + + BroadcastedDeclareTransaction { + sender_address: Some(ProtoFelt::from(Felt::from(tx.sender_address))), + compiled_class_hash: Some(tx.compiled_class_hash.into()), + signature: tx.signature.to_proto_felts(), + nonce: Some(tx.nonce.into()), + contract_class: Some(contract_class), + paymaster_data: tx.paymaster_data.to_proto_felts(), + tip: Some(ProtoFelt::from(Felt::from(u64::from(tx.tip)))), + account_deployment_data: tx.account_deployment_data.to_proto_felts(), + resource_bounds: Some(ResourceBoundsMapping::from(&tx.resource_bounds)), + fee_data_availability_mode: da_mode_to_string(tx.fee_data_availability_mode), + nonce_data_availability_mode: da_mode_to_string(tx.nonce_data_availability_mode), + version: Some(version_from_is_query(tx.is_query).into()), + } + } +} + +/// Convert RPC transaction to proto Transaction. +impl From for ProtoTx { + fn from(tx: katana_rpc_types::transaction::RpcTxWithHash) -> Self { + use katana_rpc_types::transaction::{RpcDeclareTx, RpcDeployAccountTx, RpcInvokeTx, RpcTx}; + + let transaction = match tx.transaction { + RpcTx::Invoke(invoke) => match invoke { + RpcInvokeTx::V0(v0) => ProtoTxVariant::InvokeV1(InvokeTxnV1 { + max_fee: Some(Felt::from(v0.max_fee).into()), + version: "0x0".to_string(), + signature: v0.signature.to_proto_felts(), + nonce: Some(Felt::ZERO.into()), + r#type: "INVOKE".to_string(), + sender_address: Some(ProtoFelt::from(Felt::from(v0.contract_address))), + calldata: v0.calldata.to_proto_felts(), + }), + RpcInvokeTx::V1(v1) => ProtoTxVariant::InvokeV1(InvokeTxnV1 { + max_fee: Some(Felt::from(v1.max_fee).into()), + version: "0x1".to_string(), + signature: v1.signature.to_proto_felts(), + nonce: Some(v1.nonce.into()), + r#type: "INVOKE".to_string(), + sender_address: Some(ProtoFelt::from(Felt::from(v1.sender_address))), + calldata: v1.calldata.to_proto_felts(), + }), + RpcInvokeTx::V3(v3) => ProtoTxVariant::InvokeV3(InvokeTxnV3 { + r#type: "INVOKE".to_string(), + sender_address: Some(ProtoFelt::from(Felt::from(v3.sender_address))), + calldata: v3.calldata.to_proto_felts(), + version: "0x3".to_string(), + signature: v3.signature.to_proto_felts(), + nonce: Some(v3.nonce.into()), + resource_bounds: Some(ResourceBoundsMapping::from(&v3.resource_bounds)), + tip: Some(ProtoFelt::from(Felt::from(u64::from(v3.tip)))), + paymaster_data: v3.paymaster_data.to_proto_felts(), + account_deployment_data: v3.account_deployment_data.to_proto_felts(), + nonce_data_availability_mode: da_mode_to_string( + v3.nonce_data_availability_mode, + ), + fee_data_availability_mode: da_mode_to_string(v3.fee_data_availability_mode), + }), + }, + RpcTx::Declare(declare) => match declare { + RpcDeclareTx::V0(v0) => { + ProtoTxVariant::DeclareV1(crate::protos::types::DeclareTxnV1 { + max_fee: Some(Felt::from(v0.max_fee).into()), + version: "0x0".to_string(), + signature: v0.signature.to_proto_felts(), + nonce: Some(Felt::ZERO.into()), + r#type: "DECLARE".to_string(), + class_hash: Some(v0.class_hash.into()), + sender_address: Some(ProtoFelt::from(Felt::from(v0.sender_address))), + }) + } + RpcDeclareTx::V1(v1) => { + ProtoTxVariant::DeclareV1(crate::protos::types::DeclareTxnV1 { + max_fee: Some(Felt::from(v1.max_fee).into()), + version: "0x1".to_string(), + signature: v1.signature.to_proto_felts(), + nonce: Some(v1.nonce.into()), + r#type: "DECLARE".to_string(), + class_hash: Some(v1.class_hash.into()), + sender_address: Some(ProtoFelt::from(Felt::from(v1.sender_address))), + }) + } + RpcDeclareTx::V2(v2) => { + ProtoTxVariant::DeclareV2(crate::protos::types::DeclareTxnV2 { + r#type: "DECLARE".to_string(), + sender_address: Some(ProtoFelt::from(Felt::from(v2.sender_address))), + compiled_class_hash: Some(v2.compiled_class_hash.into()), + max_fee: Some(Felt::from(v2.max_fee).into()), + version: "0x2".to_string(), + signature: v2.signature.to_proto_felts(), + nonce: Some(v2.nonce.into()), + class: Vec::new(), // Contract class is not included in transaction response + }) + } + RpcDeclareTx::V3(v3) => { + ProtoTxVariant::DeclareV3(crate::protos::types::DeclareTxnV3 { + r#type: "DECLARE".to_string(), + sender_address: Some(ProtoFelt::from(Felt::from(v3.sender_address))), + compiled_class_hash: Some(v3.compiled_class_hash.into()), + version: "0x3".to_string(), + signature: v3.signature.to_proto_felts(), + nonce: Some(v3.nonce.into()), + class_hash: Some(v3.class_hash.into()), + resource_bounds: Some(ResourceBoundsMapping::from(&v3.resource_bounds)), + tip: Some(ProtoFelt::from(Felt::from(u64::from(v3.tip)))), + paymaster_data: v3.paymaster_data.to_proto_felts(), + account_deployment_data: v3.account_deployment_data.to_proto_felts(), + nonce_data_availability_mode: da_mode_to_string( + v3.nonce_data_availability_mode, + ), + fee_data_availability_mode: da_mode_to_string( + v3.fee_data_availability_mode, + ), + }) + } + }, + RpcTx::DeployAccount(deploy) => match deploy { + RpcDeployAccountTx::V1(v1) => ProtoTxVariant::DeployAccount(DeployAccountTxn { + max_fee: Some(Felt::from(v1.max_fee).into()), + version: "0x1".to_string(), + signature: v1.signature.to_proto_felts(), + nonce: Some(v1.nonce.into()), + r#type: "DEPLOY_ACCOUNT".to_string(), + class_hash: Some(v1.class_hash.into()), + contract_address_salt: Some(v1.contract_address_salt.into()), + constructor_calldata: v1.constructor_calldata.to_proto_felts(), + }), + RpcDeployAccountTx::V3(v3) => ProtoTxVariant::DeployAccountV3(DeployAccountTxnV3 { + r#type: "DEPLOY_ACCOUNT".to_string(), + version: "0x3".to_string(), + signature: v3.signature.to_proto_felts(), + nonce: Some(v3.nonce.into()), + contract_address_salt: Some(v3.contract_address_salt.into()), + constructor_calldata: v3.constructor_calldata.to_proto_felts(), + class_hash: Some(v3.class_hash.into()), + resource_bounds: Some(ResourceBoundsMapping::from(&v3.resource_bounds)), + tip: Some(ProtoFelt::from(Felt::from(u64::from(v3.tip)))), + paymaster_data: v3.paymaster_data.to_proto_felts(), + nonce_data_availability_mode: da_mode_to_string( + v3.nonce_data_availability_mode, + ), + fee_data_availability_mode: da_mode_to_string(v3.fee_data_availability_mode), + }), + }, + RpcTx::L1Handler(l1) => { + // Use dedicated L1HandlerTxn type + ProtoTxVariant::L1Handler(L1HandlerTxn { + r#type: "L1_HANDLER".to_string(), + version: format!("{:#x}", l1.version), + nonce: Some(l1.nonce.into()), + contract_address: Some(ProtoFelt::from(Felt::from(l1.contract_address))), + entry_point_selector: Some(l1.entry_point_selector.into()), + calldata: l1.calldata.to_proto_felts(), + }) + } + RpcTx::Deploy(deploy) => { + // Use dedicated DeployTxn type for legacy deploy transactions + ProtoTxVariant::Deploy(DeployTxn { + r#type: "DEPLOY".to_string(), + version: format!("{:#x}", deploy.version), + class_hash: Some(deploy.class_hash.into()), + contract_address_salt: Some(deploy.contract_address_salt.into()), + constructor_calldata: deploy.constructor_calldata.to_proto_felts(), + }) + } + }; + + ProtoTx { transaction: Some(transaction) } + } +} + +impl From<&PrimitiveResourceBoundsMapping> for ResourceBoundsMapping { + fn from(bounds: &PrimitiveResourceBoundsMapping) -> Self { + match bounds { + PrimitiveResourceBoundsMapping::L1Gas(l1_gas_bounds) => ResourceBoundsMapping { + l1_gas: Some(ResourceBounds { + max_amount: Some(ProtoFelt::from(Felt::from(l1_gas_bounds.l1_gas.max_amount))), + max_price_per_unit: Some(ProtoFelt::from(Felt::from( + l1_gas_bounds.l1_gas.max_price_per_unit, + ))), + }), + l2_gas: Some(ResourceBounds { + max_amount: Some(ProtoFelt::from(Felt::from(l1_gas_bounds.l2_gas.max_amount))), + max_price_per_unit: Some(ProtoFelt::from(Felt::from( + l1_gas_bounds.l2_gas.max_price_per_unit, + ))), + }), + l1_data_gas: None, // L1Gas variant doesn't have l1_data_gas + }, + PrimitiveResourceBoundsMapping::All(all_bounds) => ResourceBoundsMapping { + l1_gas: Some(ResourceBounds { + max_amount: Some(ProtoFelt::from(Felt::from(all_bounds.l1_gas.max_amount))), + max_price_per_unit: Some(ProtoFelt::from(Felt::from( + all_bounds.l1_gas.max_price_per_unit, + ))), + }), + l2_gas: Some(ResourceBounds { + max_amount: Some(ProtoFelt::from(Felt::from(all_bounds.l2_gas.max_amount))), + max_price_per_unit: Some(ProtoFelt::from(Felt::from( + all_bounds.l2_gas.max_price_per_unit, + ))), + }), + l1_data_gas: Some(ResourceBounds { + max_amount: Some(ProtoFelt::from(Felt::from( + all_bounds.l1_data_gas.max_amount, + ))), + max_price_per_unit: Some(ProtoFelt::from(Felt::from( + all_bounds.l1_data_gas.max_price_per_unit, + ))), + }), + }, + } + } +} diff --git a/crates/grpc/src/protos/types/conversion/trie.rs b/crates/grpc/src/protos/types/conversion/trie.rs new file mode 100644 index 000000000..c8a71613e --- /dev/null +++ b/crates/grpc/src/protos/types/conversion/trie.rs @@ -0,0 +1,123 @@ +//! Storage proof type conversions. + +use tonic::Status; + +use crate::protos::common::Felt as ProtoFelt; +use crate::protos::types::{ + BinaryNode, ClassesProof as ProtoClassesProof, ContractLeafData as ProtoContractLeafData, + ContractStorageKeysRequest, ContractStorageProof, + ContractStorageProofs as ProtoContractStorageProofs, ContractsProof as ProtoContractsProof, + EdgeNode, GlobalRoots as ProtoGlobalRoots, MerkleProofNode, StorageProof as ProtoStorageProof, +}; + +// ============================================================ +// Request type conversions (Proto → Internal) +// ============================================================ + +impl TryFrom for katana_rpc_types::trie::ContractStorageKeys { + type Error = Status; + + fn try_from(proto: ContractStorageKeysRequest) -> Result { + let address = proto + .contract_address + .ok_or_else(|| Status::invalid_argument("Missing contract_address"))? + .try_into()?; + + let keys = proto.keys.into_iter().map(|f| f.try_into()).collect::, _>>()?; + + Ok(Self { address, keys }) + } +} + +// ============================================================ +// Response type conversions (Internal → Proto) +// ============================================================ + +impl From for ProtoStorageProof { + fn from(response: katana_rpc_types::trie::GetStorageProofResponse) -> Self { + ProtoStorageProof { + global_roots: Some(response.global_roots.into()), + classes_proof: Some(response.classes_proof.into()), + contracts_proof: Some(response.contracts_proof.into()), + contracts_storage_proofs: Some(response.contracts_storage_proofs.into()), + } + } +} + +impl From for ProtoGlobalRoots { + fn from(roots: katana_rpc_types::trie::GlobalRoots) -> Self { + ProtoGlobalRoots { + block_hash: Some(roots.block_hash.into()), + classes_tree_root: Some(roots.classes_tree_root.into()), + contracts_tree_root: Some(roots.contracts_tree_root.into()), + } + } +} + +impl From for ProtoClassesProof { + fn from(proof: katana_rpc_types::trie::ClassesProof) -> Self { + ProtoClassesProof { nodes: nodes_to_proto(proof.nodes) } + } +} + +impl From for ProtoContractsProof { + fn from(proof: katana_rpc_types::trie::ContractsProof) -> Self { + ProtoContractsProof { + nodes: nodes_to_proto(proof.nodes), + contract_leaves_data: proof + .contract_leaves_data + .into_iter() + .map(ProtoContractLeafData::from) + .collect(), + } + } +} + +impl From for ProtoContractLeafData { + fn from(leaf: katana_rpc_types::trie::ContractLeafData) -> Self { + ProtoContractLeafData { + storage_root: Some(leaf.storage_root.into()), + class_hash: Some(leaf.class_hash.into()), + nonce: Some(leaf.nonce.into()), + } + } +} + +impl From for ProtoContractStorageProofs { + fn from(proofs: katana_rpc_types::trie::ContractStorageProofs) -> Self { + ProtoContractStorageProofs { + proofs: proofs + .nodes + .into_iter() + .map(|nodes| ContractStorageProof { nodes: nodes_to_proto(nodes) }) + .collect(), + } + } +} + +/// Convert internal Nodes to proto MerkleProofNode list. +/// +/// Note: The proto `MerkleProofNode` doesn't include the `node_hash` field. +/// When converting from `NodeWithHash`, only the node structure is preserved. +/// Clients can recompute the hash if needed using the node data. +fn nodes_to_proto(nodes: katana_rpc_types::trie::Nodes) -> Vec { + nodes.0.into_iter().map(|nwh| merkle_node_to_proto(&nwh.node)).collect() +} + +fn merkle_node_to_proto(node: &katana_rpc_types::trie::MerkleNode) -> MerkleProofNode { + match node { + katana_rpc_types::trie::MerkleNode::Binary { left, right } => MerkleProofNode { + node: Some(crate::protos::types::merkle_proof_node::Node::Binary(BinaryNode { + left: Some(ProtoFelt::from(*left)), + right: Some(ProtoFelt::from(*right)), + })), + }, + katana_rpc_types::trie::MerkleNode::Edge { path, length, child } => MerkleProofNode { + node: Some(crate::protos::types::merkle_proof_node::Node::Edge(EdgeNode { + path: Some(ProtoFelt::from(*path)), + length: *length as u32, + child: Some(ProtoFelt::from(*child)), + })), + }, + } +} diff --git a/crates/grpc/src/protos/types/mod.rs b/crates/grpc/src/protos/types/mod.rs new file mode 100644 index 000000000..88f246ed5 --- /dev/null +++ b/crates/grpc/src/protos/types/mod.rs @@ -0,0 +1,3 @@ +pub mod conversion; + +tonic::include_proto!("types"); diff --git a/crates/grpc/src/server.rs b/crates/grpc/src/server.rs new file mode 100644 index 000000000..0c61a90e6 --- /dev/null +++ b/crates/grpc/src/server.rs @@ -0,0 +1,141 @@ +//! gRPC server implementation. + +use std::net::SocketAddr; +use std::sync::Arc; +use std::time::Duration; + +use tokio::sync::watch; +use tonic::transport::server::Routes; +use tonic::transport::Server; +use tracing::{error, info}; + +use crate::protos::starknet::FILE_DESCRIPTOR_SET; + +/// The default timeout for an request. +pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(20); + +/// Error type for gRPC server operations. +#[derive(Debug, thiserror::Error)] +pub enum Error { + /// Transport error from tonic. + #[error(transparent)] + Transport(#[from] tonic::transport::Error), + + /// Reflection service build error. + #[error("Failed to build reflection service: {0}")] + ReflectionBuild(String), + + /// Server has already been stopped. + #[error("gRPC server has already been stopped")] + AlreadyStopped, +} + +/// Handle to a running gRPC server. +/// +/// This handle can be used to get the server's address and to stop the server. +#[derive(Debug, Clone)] +pub struct GrpcServerHandle { + /// The actual address that the server is bound to. + addr: SocketAddr, + /// Sender to signal server shutdown. + shutdown_tx: Arc>, +} + +impl GrpcServerHandle { + /// Returns the socket address the server is listening on. + pub fn addr(&self) -> &SocketAddr { + &self.addr + } + + /// Stops the server without waiting for it to fully stop. + pub fn stop(&self) -> Result<(), Error> { + self.shutdown_tx.send(()).map_err(|_| Error::AlreadyStopped) + } + + /// Wait until the server has stopped. + pub async fn stopped(&self) { + self.shutdown_tx.closed().await + } + + /// Returns true if the server has stopped. + pub fn is_stopped(&self) -> bool { + self.shutdown_tx.is_closed() + } +} + +/// Builder for the gRPC server. +#[derive(Debug, Clone)] +pub struct GrpcServer { + routes: Routes, + /// Request timeout. + timeout: Duration, +} + +impl GrpcServer { + /// Creates a new gRPC server builder with the given configuration. + pub fn new() -> Self { + Self { routes: Routes::default(), timeout: DEFAULT_TIMEOUT } + } + + /// Set the timeout for the server. Default is 20 seconds. + pub fn timeout(mut self, timeout: Duration) -> Self { + self.timeout = timeout; + self + } + + pub fn service(mut self, service: S) -> Self + where + S: tower_service::Service< + http::Request, + Response = http::Response, + Error = std::convert::Infallible, + > + tonic::server::NamedService + + Clone + + Send + + 'static, + S::Future: Send + 'static, + S::Error: Into> + Send, + { + self.routes = self.routes.add_service(service); + self + } + + /// Starts the gRPC server. + /// + /// This method spawns the server on a new Tokio task and returns a handle + /// that can be used to manage the server. + pub async fn start(&self, addr: SocketAddr) -> Result { + // Build reflection service for tooling support (grpcurl, Postman, etc.) + let reflection_service = tonic_reflection::server::Builder::configure() + .register_encoded_file_descriptor_set(FILE_DESCRIPTOR_SET) + .build() + .map_err(|e| Error::ReflectionBuild(e.to_string()))?; + + // Create shutdown channel + let (shutdown_tx, mut shutdown_rx) = watch::channel(()); + + let mut builder = Server::builder().timeout(self.timeout); + let server = builder.add_routes(self.routes.clone()).add_service(reflection_service); + + // Start the server with graceful shutdown + let server_future = server.serve_with_shutdown(addr, async move { + let _ = shutdown_rx.changed().await; + }); + + tokio::spawn(async move { + if let Err(error) = server_future.await { + error!(target: "grpc", %error, "gRPC server error"); + } + }); + + info!(target: "grpc", %addr, "gRPC server started."); + + Ok(GrpcServerHandle { addr, shutdown_tx: Arc::new(shutdown_tx) }) + } +} + +impl Default for GrpcServer { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/node/Cargo.toml b/crates/node/Cargo.toml index 2d85c1a64..f4b45cef5 100644 --- a/crates/node/Cargo.toml +++ b/crates/node/Cargo.toml @@ -14,6 +14,7 @@ katana-gateway-server.workspace = true katana-gateway-client.workspace = true katana-gateway-types.workspace = true katana-gas-price-oracle.workspace = true +katana-grpc = { workspace = true, optional = true } katana-messaging.workspace = true katana-metrics.workspace = true katana-pipeline.workspace = true @@ -49,6 +50,7 @@ tokio = { workspace = true, features = [ "time" ] } [features] cartridge = ["katana-rpc-api/cartridge", "katana-rpc-server/cartridge"] explorer = ["katana-rpc-server/explorer"] +grpc = ["dep:katana-grpc"] native = ["katana-executor/native"] tee = ["dep:katana-tee", "katana-rpc-api/tee", "katana-rpc-server/tee"] tee-snp = ["tee", "katana-tee/snp"] diff --git a/crates/node/src/config/grpc.rs b/crates/node/src/config/grpc.rs new file mode 100644 index 000000000..cbde6a8dd --- /dev/null +++ b/crates/node/src/config/grpc.rs @@ -0,0 +1,41 @@ +//! gRPC server configuration for the node. + +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::time::Duration; + +/// Default gRPC server listening address. +pub const DEFAULT_GRPC_ADDR: IpAddr = IpAddr::V4(Ipv4Addr::LOCALHOST); + +/// Default gRPC server listening port. +pub const DEFAULT_GRPC_PORT: u16 = 5051; + +/// Default gRPC request timeout in seconds. +pub const DEFAULT_GRPC_TIMEOUT_SECS: u64 = 30; + +/// Configuration for the gRPC server. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct GrpcConfig { + /// The IP address to bind the server to. + pub addr: IpAddr, + /// The port to listen on. + pub port: u16, + /// Request timeout in seconds. + pub timeout: Option, +} + +impl GrpcConfig { + /// Returns the socket address for the server. + pub fn socket_addr(&self) -> SocketAddr { + SocketAddr::new(self.addr, self.port) + } +} + +impl Default for GrpcConfig { + fn default() -> Self { + Self { + addr: DEFAULT_GRPC_ADDR, + port: DEFAULT_GRPC_PORT, + timeout: Some(Duration::from_secs(DEFAULT_GRPC_TIMEOUT_SECS)), + } + } +} diff --git a/crates/node/src/config/mod.rs b/crates/node/src/config/mod.rs index 8e03bc9c4..f8b9e2dc9 100644 --- a/crates/node/src/config/mod.rs +++ b/crates/node/src/config/mod.rs @@ -6,18 +6,25 @@ pub mod execution; pub mod fork; pub mod gateway; pub mod metrics; -#[cfg(feature = "cartridge")] -pub mod paymaster; pub mod rpc; pub mod sequencing; + +#[cfg(feature = "cartridge")] +pub mod paymaster; + #[cfg(feature = "tee")] pub mod tee; +#[cfg(feature = "grpc")] +pub mod grpc; + use db::DbConfig; use dev::DevConfig; use execution::ExecutionConfig; use fork::ForkingConfig; use gateway::GatewayConfig; +#[cfg(feature = "grpc")] +use grpc::GrpcConfig; use katana_chain_spec::ChainSpec; use katana_messaging::MessagingConfig; use metrics::MetricsConfig; @@ -66,4 +73,8 @@ pub struct Config { /// TEE attestation options. #[cfg(feature = "tee")] pub tee: Option, + + /// gRPC options. + #[cfg(feature = "grpc")] + pub grpc: Option, } diff --git a/crates/node/src/exit.rs b/crates/node/src/exit.rs index fa3020795..15eb9436d 100644 --- a/crates/node/src/exit.rs +++ b/crates/node/src/exit.rs @@ -11,38 +11,47 @@ use crate::LaunchedNode; /// A Future that is resolved once the node has been stopped including all of its running tasks. #[must_use = "futures do nothing unless polled"] -pub struct NodeStoppedFuture<'a, P> { +pub struct NodeStoppedFuture<'a> { fut: BoxFuture<'a, Result<()>>, - _phantom: std::marker::PhantomData

, } -impl<'a, P> NodeStoppedFuture<'a, P> -where - P: ProviderFactory, -

::Provider: ProviderRO, -

::ProviderMut: ProviderRW, -{ - pub(crate) fn new(handle: &'a LaunchedNode

) -> Self { - let fut = Box::pin(async { - handle.node.task_manager.wait_for_shutdown().await; - handle.rpc.stop()?; - - if let Some(handle) = handle.gateway.as_ref() { - handle.stop()?; +impl<'a> NodeStoppedFuture<'a> { + pub(crate) fn new

(handle: &'a LaunchedNode

) -> Self + where + P: ProviderFactory, +

::Provider: ProviderRO, +

::ProviderMut: ProviderRW, + { + // Clone the handles we need so we can move them into the async block. + // This avoids capturing `&LaunchedNode

` which isn't Sync. + + let rpc = handle.rpc.clone(); + #[cfg(feature = "grpc")] + let grpc = handle.grpc.clone(); + let gateway = handle.gateway.clone(); + let task_manager = handle.node.task_manager.clone(); + + let fut = Box::pin(async move { + task_manager.wait_for_shutdown().await; + rpc.stop()?; + + #[cfg(feature = "grpc")] + if let Some(grpc) = grpc { + grpc.stop()?; + } + + if let Some(gw) = gateway { + gw.stop()?; } Ok(()) }); - Self { fut, _phantom: std::marker::PhantomData } + + Self { fut } } } -impl

Future for NodeStoppedFuture<'_, P> -where - P: ProviderFactory + Unpin, -

::Provider: ProviderRO, -

::ProviderMut: ProviderRW, -{ +impl Future for NodeStoppedFuture<'_> { type Output = Result<()>; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { @@ -51,13 +60,8 @@ where } } -impl

core::fmt::Debug for NodeStoppedFuture<'_, P> -where - P: ProviderFactory, -

::Provider: ProviderRO, -

::ProviderMut: ProviderRW, -{ +impl core::fmt::Debug for NodeStoppedFuture<'_> { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - f.debug_struct("NodeStoppedFuture").field("fut", &"...").finish() + f.debug_struct("NodeStoppedFuture").finish_non_exhaustive() } } diff --git a/crates/node/src/lib.rs b/crates/node/src/lib.rs index f425e80bb..191269075 100644 --- a/crates/node/src/lib.rs +++ b/crates/node/src/lib.rs @@ -23,6 +23,8 @@ use katana_executor::implementation::blockifier::BlockifierFactory; use katana_executor::{ExecutionFlags, ExecutorFactory}; use katana_gas_price_oracle::{FixedPriceOracle, GasPriceOracle}; use katana_gateway_server::{GatewayServer, GatewayServerHandle}; +#[cfg(feature = "grpc")] +use katana_grpc::{GrpcServer, GrpcServerHandle}; use katana_metrics::exporters::prometheus::{Prometheus, PrometheusRecorder}; use katana_metrics::sys::DiskReporter; use katana_metrics::{MetricsServer, MetricsServerHandle, Report}; @@ -77,6 +79,8 @@ where config: Arc, pool: TxPool, rpc_server: RpcServer, + #[cfg(feature = "grpc")] + grpc_server: Option, task_manager: TaskManager, backend: Arc>, block_producer: BlockProducer, @@ -330,6 +334,32 @@ where rpc_server = rpc_server.max_response_body_size(max_response_body_size); } + // --- build gRPC server (optional) + + #[cfg(feature = "grpc")] + let grpc_server = if let Some(grpc_config) = &config.grpc { + use katana_grpc::{ + StarknetServer, StarknetService, StarknetTraceServer, StarknetWriteServer, + }; + + let mut server = GrpcServer::new(); + + if let Some(timeout) = grpc_config.timeout { + server = server.timeout(timeout); + } + + let svc = StarknetService::new(starknet_api.clone()); + + server = server + .service(StarknetServer::new(svc.clone())) + .service(StarknetTraceServer::new(svc.clone())) + .service(StarknetWriteServer::new(svc.clone())); + + Some(server) + } else { + None + }; + // --- build feeder gateway server (optional) let gateway_server = if let Some(gw_config) = &config.gateway { @@ -367,6 +397,8 @@ where pool, backend, rpc_server, + #[cfg(feature = "grpc")] + grpc_server, gateway_server, block_producer, metrics_server, @@ -547,6 +579,21 @@ where None => None, }; + // --- start the gRPC server (if configured) + + #[cfg(feature = "grpc")] + let grpc_handle = if let Some(server) = &self.grpc_server { + let config = self + .config() + .grpc + .as_ref() + .expect("qed; config must exist if grpc server is configured"); + + Some(server.start(config.socket_addr()).await?) + } else { + None + }; + // --- start the gas oracle worker task if let Some(worker) = self.backend.gas_oracle.run_worker() { @@ -564,6 +611,8 @@ where node: self, rpc: rpc_handle, gateway: gateway_handle, + #[cfg(feature = "grpc")] + grpc: grpc_handle, metrics: metrics_handle, }) } @@ -611,6 +660,9 @@ where rpc: RpcServerHandle, /// Handle to the gateway server (if enabled). gateway: Option, + /// Handle to the gRPC server (if enabled). + #[cfg(feature = "grpc")] + grpc: Option, /// Handle to the metrics server (if enabled). metrics: Option, } @@ -641,6 +693,12 @@ where self.metrics.as_ref() } + /// Returns a reference to the gRPC server handle (if enabled). + #[cfg(feature = "grpc")] + pub fn grpc(&self) -> Option<&GrpcServerHandle> { + self.grpc.as_ref() + } + /// Stops the node. /// /// This will instruct the node to stop and wait until it has actually stop. @@ -653,6 +711,12 @@ where handle.stop()?; } + // Stop gRPC server if it's running + #[cfg(feature = "grpc")] + if let Some(handle) = self.grpc { + handle.stop()?; + } + // Stop metrics server if it's running if let Some(mut handle) = self.metrics { handle.stop()?; @@ -663,7 +727,7 @@ where } /// Returns a future which resolves only when the node has stopped. - pub fn stopped(&self) -> NodeStoppedFuture<'_, P> { + pub fn stopped(&self) -> NodeStoppedFuture<'_> { NodeStoppedFuture::new(self) } } diff --git a/crates/rpc/rpc-server/src/starknet/mod.rs b/crates/rpc/rpc-server/src/starknet/mod.rs index cd8bc9068..d237c6768 100644 --- a/crates/rpc/rpc-server/src/starknet/mod.rs +++ b/crates/rpc/rpc-server/src/starknet/mod.rs @@ -6,6 +6,7 @@ use std::sync::Arc; use katana_chain_spec::ChainSpec; use katana_core::utils::get_current_timestamp; +use katana_executor::{ExecutionResult, ResultAndStates}; use katana_gas_price_oracle::GasPriceOracle; use katana_pool::TransactionPool; use katana_primitives::block::{BlockHashOrNumber, BlockIdOrTag, FinalityStatus, GasPrices}; @@ -13,14 +14,18 @@ use katana_primitives::class::{ClassHash, CompiledClass}; use katana_primitives::contract::{ContractAddress, Nonce, StorageKey, StorageValue}; use katana_primitives::env::BlockEnv; use katana_primitives::event::MaybeForkedContinuationToken; -use katana_primitives::transaction::{ExecutableTxWithHash, TxHash, TxNumber}; +use katana_primitives::execution::TypedTransactionExecutionInfo; +use katana_primitives::transaction::{ExecutableTx, ExecutableTxWithHash, TxHash, TxNumber}; use katana_primitives::Felt; -use katana_provider::api::block::{BlockHashProvider, BlockIdReader, BlockNumberProvider}; +use katana_provider::api::block::{ + BlockHashProvider, BlockIdReader, BlockNumberProvider, BlockProvider, +}; use katana_provider::api::contract::ContractClassProvider; use katana_provider::api::env::BlockEnvProvider; use katana_provider::api::state::{StateFactoryProvider, StateProvider, StateRootProvider}; use katana_provider::api::transaction::{ - ReceiptProvider, TransactionProvider, TransactionStatusProvider, TransactionsProviderExt, + ReceiptProvider, TransactionProvider, TransactionStatusProvider, TransactionTraceProvider, + TransactionsProviderExt, }; use katana_provider::api::ProviderError; use katana_provider::{ProviderFactory, ProviderRO}; @@ -44,7 +49,11 @@ use katana_rpc_types::trie::{ ClassesProof, ContractLeafData, ContractStorageKeys, ContractStorageProofs, ContractsProof, GetStorageProofResponse, GlobalRoots, Nodes, }; -use katana_rpc_types::{FeeEstimate, TxStatus}; +use katana_rpc_types::{ + to_rpc_fee_estimate, BroadcastedTx, BroadcastedTxWithChainId, CallResponse, + ConfirmedBlockIdOrTag, FeeEstimate, FunctionCall, SimulatedTransactions, SimulationFlag, + TxStatus, TxTrace, TxTraceWithHash, +}; use katana_rpc_types_builder::{BlockBuilder, ReceiptBuilder}; use katana_tasks::{Result as TaskResult, TaskSpawner}; @@ -171,6 +180,11 @@ where pub fn config(&self) -> &StarknetApiConfig { &self.inner.config } + + /// Returns the chain ID. + pub fn chain_id(&self) -> Felt { + self.inner.chain_spec.id().id() + } } impl StarknetApi @@ -338,11 +352,14 @@ where env.ok_or(StarknetApiError::BlockNotFound) } - fn block_hash_and_number(&self) -> StarknetApiResult { - let provider = self.storage().provider(); - let hash = provider.latest_hash()?; - let number = provider.latest_number()?; - Ok(BlockHashAndNumberResponse::new(hash, number)) + pub async fn block_hash_and_number(&self) -> StarknetApiResult { + self.on_io_blocking_task(move |this| { + let provider = this.storage().provider(); + let hash = provider.latest_hash()?; + let number = provider.latest_number()?; + Ok(BlockHashAndNumberResponse::new(hash, number)) + }) + .await? } pub async fn class_at_hash( @@ -415,28 +432,57 @@ where .await? } - pub fn storage_at( + pub async fn call_contract( + &self, + request: FunctionCall, + block_id: BlockIdOrTag, + ) -> StarknetApiResult { + self.on_io_blocking_task(move |this| { + // get the state and block env at the specified block for function call execution + let state = this.state(&block_id)?; + let env = this.block_env_at(&block_id)?; + let cfg_env = this.inner.config.versioned_constant_overrides.as_ref(); + let max_call_gas = this.inner.config.max_call_gas.unwrap_or(1_000_000_000); + + let result = self::blockifier::call( + this.inner.chain_spec.as_ref(), + state, + env, + cfg_env, + request, + max_call_gas, + )?; + + Ok(CallResponse { result }) + }) + .await? + } + + pub async fn storage_at( &self, contract_address: ContractAddress, storage_key: StorageKey, block_id: BlockIdOrTag, ) -> StarknetApiResult { - let state = self.state(&block_id)?; + self.on_io_blocking_task(move |this| { + let state = this.state(&block_id)?; - // Check that contract exist by checking the class hash of the contract, - // unless its address 0x1 or 0x2 which are special system contracts and does not - // have a class. - // - // See https://docs.starknet.io/architecture-and-concepts/network-architecture/starknet-state/#address_0x1. - if contract_address != ContractAddress::ONE - && contract_address != ContractAddress::TWO - && state.class_hash_of_contract(contract_address)?.is_none() - { - return Err(StarknetApiError::ContractNotFound); - } + // Check that contract exist by checking the class hash of the contract, + // unless its address 0x1 or 0x2 which are special system contracts and does not + // have a class. + // + // See https://docs.starknet.io/architecture-and-concepts/network-architecture/starknet-state/#address_0x1. + if contract_address != ContractAddress::ONE + && contract_address != ContractAddress::TWO + && state.class_hash_of_contract(contract_address)?.is_none() + { + return Err(StarknetApiError::ContractNotFound); + } - let value = state.storage(contract_address, storage_key)?; - Ok(value.unwrap_or_default()) + let value = state.storage(contract_address, storage_key)?; + Ok(value.unwrap_or_default()) + }) + .await? } pub async fn block_tx_count(&self, block_id: BlockIdOrTag) -> StarknetApiResult { @@ -473,7 +519,7 @@ where } } - async fn latest_block_number(&self) -> StarknetApiResult { + pub async fn latest_block_number(&self) -> StarknetApiResult { self.on_io_blocking_task(move |this| { let block_number = this.storage().provider().latest_number()?; Ok(BlockNumberResponse { block_number }) @@ -504,7 +550,7 @@ where .await? } - async fn transaction_by_block_id_and_index( + pub async fn transaction_by_block_id_and_index( &self, block_id: BlockIdOrTag, index: u64, @@ -538,7 +584,7 @@ where } } - async fn transaction(&self, hash: TxHash) -> StarknetApiResult { + pub async fn transaction(&self, hash: TxHash) -> StarknetApiResult { let tx = self .on_io_blocking_task(move |this| { if let pending_tx @ Some(..) = @@ -564,7 +610,7 @@ where } } - async fn receipt(&self, hash: Felt) -> StarknetApiResult { + pub async fn receipt(&self, hash: Felt) -> StarknetApiResult { let receipt = self .on_io_blocking_task(move |this| { if let pending_receipt @ Some(..) = @@ -585,7 +631,7 @@ where } } - async fn transaction_status(&self, hash: TxHash) -> StarknetApiResult { + pub async fn transaction_status(&self, hash: TxHash) -> StarknetApiResult { let status = self .on_io_blocking_task(move |this| { let provider = this.storage().provider(); @@ -670,7 +716,7 @@ where } } - async fn block_with_receipts( + pub async fn block_with_receipts( &self, block_id: BlockIdOrTag, ) -> StarknetApiResult { @@ -783,7 +829,10 @@ where } } - async fn events(&self, filter: EventFilterWithPage) -> StarknetApiResult { + pub async fn events( + &self, + filter: EventFilterWithPage, + ) -> StarknetApiResult { let EventFilterWithPage { event_filter, result_page_request } = filter; let ResultPageRequest { continuation_token, chunk_size } = result_page_request; @@ -974,7 +1023,7 @@ where Ok(id) } - async fn get_proofs( + pub async fn get_proofs( &self, block_id: BlockIdOrTag, class_hashes: Option>, @@ -1064,6 +1113,143 @@ where }) .await? } + + pub async fn block_traces( + &self, + block_id: ConfirmedBlockIdOrTag, + ) -> Result, StarknetApiError> { + self.on_io_blocking_task(move |this| { + use StarknetApiError::BlockNotFound; + + let provider = &this.storage().provider(); + + let block_id: BlockHashOrNumber = match block_id { + ConfirmedBlockIdOrTag::L1Accepted => { + unimplemented!("l1 accepted block id") + } + ConfirmedBlockIdOrTag::Latest => provider.latest_number()?.into(), + ConfirmedBlockIdOrTag::Number(num) => num.into(), + ConfirmedBlockIdOrTag::Hash(hash) => hash.into(), + }; + + let indices = provider.block_body_indices(block_id)?.ok_or(BlockNotFound)?; + let tx_hashes = provider.transaction_hashes_in_range(indices.into())?; + + let traces = + provider.transaction_executions_by_block(block_id)?.ok_or(BlockNotFound)?; + let traces = traces.into_iter().map(TxTrace::from); + + let result = tx_hashes + .into_iter() + .zip(traces) + .map(|(h, r)| TxTraceWithHash { transaction_hash: h, trace_root: r }) + .collect::>(); + + Ok(result) + }) + .await? + } + + pub async fn trace(&self, tx_hash: TxHash) -> Result { + self.on_io_blocking_task(move |this| { + // Check in the pending block first + if let Some(pending_trace) = + this.inner.pending_block_provider.get_pending_trace(tx_hash)? + { + Ok(pending_trace) + } else { + // If not found in pending block, fallback to the provider + let trace = this + .storage() + .provider() + .transaction_execution(tx_hash)? + .ok_or(StarknetApiError::TxnHashNotFound)?; + + Ok(TxTrace::from(trace)) + } + }) + .await? + } +} + +impl StarknetApi +where + Pool: TransactionPool + Send + Sync + 'static, + PoolTx: From, + Pending: PendingBlockProvider, + PF: ProviderFactory, + ::Provider: ProviderRO, +{ + pub async fn simulate_txs( + &self, + block_id: BlockIdOrTag, + transactions: Vec, + simulation_flags: Vec, + ) -> Result, StarknetApiError> { + self.on_cpu_blocking_task(move |this| async move { + let chain = this.inner.chain_spec.id(); + + let executables = transactions + .into_iter() + .map(|tx| { + let is_query = tx.is_query(); + let tx = ExecutableTx::from(BroadcastedTxWithChainId { tx, chain }); + ExecutableTxWithHash::new_query(tx, is_query) + }) + .collect::>(); + + // If the node is run with transaction validation disabled, then we should not validate + // even if the `SKIP_VALIDATE` flag is not set. + let should_validate = !simulation_flags.contains(&SimulationFlag::SkipValidate) + && this.inner.config.simulation_flags.account_validation(); + + // If the node is run with fee charge disabled, then we should disable charing fees even + // if the `SKIP_FEE_CHARGE` flag is not set. + let should_charge_fee = !simulation_flags.contains(&SimulationFlag::SkipFeeCharge) + && this.inner.config.simulation_flags.fee(); + + let flags = katana_executor::ExecutionFlags::new() + .with_account_validation(should_validate) + .with_fee(should_charge_fee) + .with_nonce_check(false); + + // get the state and block env at the specified block for execution + let state = this.state(&block_id)?; + let env = this.block_env_at(&block_id)?; + + // use the blockifier utils function + let chain_spec = this.inner.chain_spec.as_ref(); + let overrides = this.inner.config.versioned_constant_overrides.as_ref(); + let results = + self::blockifier::simulate(chain_spec, state, env, overrides, executables, flags); + + let mut simulated = Vec::with_capacity(results.len()); + for (i, ResultAndStates { result, .. }) in results.into_iter().enumerate() { + match result { + ExecutionResult::Success { trace, receipt } => { + let trace = TypedTransactionExecutionInfo::new(receipt.r#type(), trace); + + let transaction_trace = TxTrace::from(trace); + let fee_estimation = + to_rpc_fee_estimate(receipt.resources_used(), receipt.fee()); + let value = SimulatedTransactions { transaction_trace, fee_estimation }; + + simulated.push(value) + } + + ExecutionResult::Failed { error } => { + return Err(StarknetApiError::transaction_execution_error( + i as u64, + error.to_string(), + )); + } + } + } + + Ok(simulated) + }) + .await? + } } ///////////////////////////////////////////////////// diff --git a/crates/rpc/rpc-server/src/starknet/read.rs b/crates/rpc/rpc-server/src/starknet/read.rs index 00d2f1ff4..eda02b919 100644 --- a/crates/rpc/rpc-server/src/starknet/read.rs +++ b/crates/rpc/rpc-server/src/starknet/read.rs @@ -81,7 +81,7 @@ where } async fn block_hash_and_number(&self) -> RpcResult { - self.on_io_blocking_task(move |this| Ok(this.block_hash_and_number()?)).await? + Ok(self.block_hash_and_number().await?) } async fn get_block_with_tx_hashes( @@ -146,24 +146,7 @@ where } async fn call(&self, request: FunctionCall, block_id: BlockIdOrTag) -> RpcResult { - self.on_io_blocking_task(move |this| { - // get the state and block env at the specified block for function call execution - let state = this.state(&block_id)?; - let env = this.block_env_at(&block_id)?; - let cfg_env = this.inner.config.versioned_constant_overrides.as_ref(); - let max_call_gas = this.inner.config.max_call_gas.unwrap_or(1_000_000_000); - - let result = super::blockifier::call( - this.inner.chain_spec.as_ref(), - state, - env, - cfg_env, - request, - max_call_gas, - )?; - Ok(CallResponse { result }) - }) - .await? + Ok(self.call_contract(request, block_id).await?) } async fn get_storage_at( @@ -172,11 +155,7 @@ where key: StorageKey, block_id: BlockIdOrTag, ) -> RpcResult { - self.on_io_blocking_task(move |this| { - let value = this.storage_at(contract_address, key, block_id)?; - Ok(value) - }) - .await? + Ok(self.storage_at(contract_address, key, block_id).await?) } async fn estimate_fee( diff --git a/crates/rpc/rpc-server/src/starknet/trace.rs b/crates/rpc/rpc-server/src/starknet/trace.rs index e764d13bf..9658492c7 100644 --- a/crates/rpc/rpc-server/src/starknet/trace.rs +++ b/crates/rpc/rpc-server/src/starknet/trace.rs @@ -1,147 +1,18 @@ use jsonrpsee::core::{async_trait, RpcResult}; -use katana_executor::{ExecutionResult, ResultAndStates}; use katana_pool::TransactionPool; -use katana_primitives::block::{BlockHashOrNumber, BlockIdOrTag, ConfirmedBlockIdOrTag}; -use katana_primitives::execution::TypedTransactionExecutionInfo; -use katana_primitives::transaction::{ExecutableTx, ExecutableTxWithHash, TxHash}; -use katana_provider::api::block::{BlockNumberProvider, BlockProvider}; -use katana_provider::api::transaction::{TransactionTraceProvider, TransactionsProviderExt}; +use katana_primitives::block::{BlockIdOrTag, ConfirmedBlockIdOrTag}; +use katana_primitives::transaction::TxHash; use katana_provider::{ProviderFactory, ProviderRO}; -use katana_rpc_api::error::starknet::StarknetApiError; use katana_rpc_api::starknet::StarknetTraceApiServer; use katana_rpc_types::broadcasted::BroadcastedTx; use katana_rpc_types::trace::{ - to_rpc_fee_estimate, SimulatedTransactions, SimulatedTransactionsResponse, - TraceBlockTransactionsResponse, TxTrace, TxTraceWithHash, + SimulatedTransactionsResponse, TraceBlockTransactionsResponse, TxTrace, }; use katana_rpc_types::{BroadcastedTxWithChainId, SimulationFlag}; use super::StarknetApi; use crate::starknet::pending::PendingBlockProvider; -impl StarknetApi -where - Pool: TransactionPool + Send + Sync + 'static, - PoolTx: From, - Pending: PendingBlockProvider, - PF: ProviderFactory, - ::Provider: ProviderRO, -{ - fn simulate_txs( - &self, - block_id: BlockIdOrTag, - transactions: Vec, - simulation_flags: Vec, - ) -> Result, StarknetApiError> { - let chain = self.inner.chain_spec.id(); - - let executables = transactions - .into_iter() - .map(|tx| { - let is_query = tx.is_query(); - let tx = ExecutableTx::from(BroadcastedTxWithChainId { tx, chain }); - ExecutableTxWithHash::new_query(tx, is_query) - }) - .collect::>(); - - // If the node is run with transaction validation disabled, then we should not validate - // even if the `SKIP_VALIDATE` flag is not set. - let should_validate = !simulation_flags.contains(&SimulationFlag::SkipValidate) - && self.inner.config.simulation_flags.account_validation(); - - // If the node is run with fee charge disabled, then we should disable charing fees even - // if the `SKIP_FEE_CHARGE` flag is not set. - let should_charge_fee = !simulation_flags.contains(&SimulationFlag::SkipFeeCharge) - && self.inner.config.simulation_flags.fee(); - - let flags = katana_executor::ExecutionFlags::new() - .with_account_validation(should_validate) - .with_fee(should_charge_fee) - .with_nonce_check(false); - - // get the state and block env at the specified block for execution - let state = self.state(&block_id)?; - let env = self.block_env_at(&block_id)?; - - // use the blockifier utils function - let chain_spec = self.inner.chain_spec.as_ref(); - let overrides = self.inner.config.versioned_constant_overrides.as_ref(); - let results = - super::blockifier::simulate(chain_spec, state, env, overrides, executables, flags); - - let mut simulated = Vec::with_capacity(results.len()); - for (i, ResultAndStates { result, .. }) in results.into_iter().enumerate() { - match result { - ExecutionResult::Success { trace, receipt } => { - let trace = TypedTransactionExecutionInfo::new(receipt.r#type(), trace); - - let transaction_trace = TxTrace::from(trace); - let fee_estimation = - to_rpc_fee_estimate(receipt.resources_used(), receipt.fee()); - let value = SimulatedTransactions { transaction_trace, fee_estimation }; - - simulated.push(value) - } - - ExecutionResult::Failed { error } => { - return Err(StarknetApiError::transaction_execution_error( - i as u64, - error.to_string(), - )); - } - } - } - - Ok(simulated) - } - - fn block_traces( - &self, - block_id: ConfirmedBlockIdOrTag, - ) -> Result, StarknetApiError> { - use StarknetApiError::BlockNotFound; - - let provider = &self.storage().provider(); - - let block_id: BlockHashOrNumber = match block_id { - ConfirmedBlockIdOrTag::L1Accepted => { - unimplemented!("l1 accepted block id") - } - ConfirmedBlockIdOrTag::Latest => provider.latest_number()?.into(), - ConfirmedBlockIdOrTag::Number(num) => num.into(), - ConfirmedBlockIdOrTag::Hash(hash) => hash.into(), - }; - - let indices = provider.block_body_indices(block_id)?.ok_or(BlockNotFound)?; - let tx_hashes = provider.transaction_hashes_in_range(indices.into())?; - - let traces = provider.transaction_executions_by_block(block_id)?.ok_or(BlockNotFound)?; - let traces = traces.into_iter().map(TxTrace::from); - - let result = tx_hashes - .into_iter() - .zip(traces) - .map(|(h, r)| TxTraceWithHash { transaction_hash: h, trace_root: r }) - .collect::>(); - - Ok(result) - } - - fn trace(&self, tx_hash: TxHash) -> Result { - use StarknetApiError::TxnHashNotFound; - - // Check in the pending block first - if let Some(pending_trace) = self.inner.pending_block_provider.get_pending_trace(tx_hash)? { - Ok(pending_trace) - } else { - // If not found in pending block, fallback to the provider - let trace = - self.storage().provider().transaction_execution(tx_hash)?.ok_or(TxnHashNotFound)?; - Ok(TxTrace::from(trace)) - } - } -} - #[async_trait] impl StarknetTraceApiServer for StarknetApi where @@ -152,7 +23,7 @@ where ::Provider: ProviderRO, { async fn trace_transaction(&self, transaction_hash: TxHash) -> RpcResult { - self.on_io_blocking_task(move |this| Ok(this.trace(transaction_hash)?)).await? + Ok(self.trace(transaction_hash).await?) } async fn simulate_transactions( @@ -161,21 +32,15 @@ where transactions: Vec, simulation_flags: Vec, ) -> RpcResult { - self.on_cpu_blocking_task(move |this| async move { - let transactions = this.simulate_txs(block_id, transactions, simulation_flags)?; - Ok(SimulatedTransactionsResponse { transactions }) - }) - .await? + let transactions = self.simulate_txs(block_id, transactions, simulation_flags).await?; + Ok(SimulatedTransactionsResponse { transactions }) } async fn trace_block_transactions( &self, block_id: ConfirmedBlockIdOrTag, ) -> RpcResult { - self.on_io_blocking_task(move |this| { - let traces = this.block_traces(block_id)?; - Ok(TraceBlockTransactionsResponse { traces }) - }) - .await? + let traces = self.block_traces(block_id).await?; + Ok(TraceBlockTransactionsResponse { traces }) } } diff --git a/crates/rpc/rpc-server/src/starknet/write.rs b/crates/rpc/rpc-server/src/starknet/write.rs index 79bc64a98..e065557ad 100644 --- a/crates/rpc/rpc-server/src/starknet/write.rs +++ b/crates/rpc/rpc-server/src/starknet/write.rs @@ -20,7 +20,7 @@ where Pending: PendingBlockProvider, PF: ProviderFactory, { - async fn add_invoke_transaction_impl( + pub async fn add_invoke_tx( &self, tx: BroadcastedInvokeTx, ) -> Result { @@ -38,7 +38,7 @@ where .await? } - async fn add_declare_transaction_impl( + pub async fn add_declare_tx( &self, tx: BroadcastedDeclareTx, ) -> Result { @@ -58,7 +58,7 @@ where .await? } - async fn add_deploy_account_transaction_impl( + pub async fn add_deploy_account_tx( &self, tx: BroadcastedDeployAccountTx, ) -> Result { @@ -91,20 +91,20 @@ where &self, invoke_transaction: BroadcastedInvokeTx, ) -> RpcResult { - Ok(self.add_invoke_transaction_impl(invoke_transaction).await?) + Ok(self.add_invoke_tx(invoke_transaction).await?) } async fn add_declare_transaction( &self, declare_transaction: BroadcastedDeclareTx, ) -> RpcResult { - Ok(self.add_declare_transaction_impl(declare_transaction).await?) + Ok(self.add_declare_tx(declare_transaction).await?) } async fn add_deploy_account_transaction( &self, deploy_account_transaction: BroadcastedDeployAccountTx, ) -> RpcResult { - Ok(self.add_deploy_account_transaction_impl(deploy_account_transaction).await?) + Ok(self.add_deploy_account_tx(deploy_account_transaction).await?) } }