From aec37f2a570fd0eb10b022a6aca2cee359563cb7 Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Sat, 21 Mar 2026 23:09:15 -0500 Subject: [PATCH 1/7] feat(sync): add gRPC-based block downloader for full node sync Add a third sync source alongside the existing feeder gateway and JSON-RPC downloaders, enabling block synchronization via Katana's gRPC endpoint. Co-Authored-By: Claude Opus 4.6 (1M context) --- Cargo.lock | 2 + crates/cli/src/full.rs | 2 + crates/cli/src/options.rs | 16 +- crates/grpc/src/handlers/starknet.rs | 105 ++- crates/node/full/src/lib.rs | 72 ++- crates/sync/stage/Cargo.toml | 2 + crates/sync/stage/src/blocks/downloader.rs | 713 ++++++++++++++++++++- crates/sync/stage/src/blocks/mod.rs | 1 + 8 files changed, 862 insertions(+), 51 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0dffdc521..981b21c06 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6926,6 +6926,7 @@ dependencies = [ "katana-db", "katana-gateway-client", "katana-gateway-types", + "katana-grpc", "katana-messaging", "katana-pool", "katana-primitives", @@ -6942,6 +6943,7 @@ dependencies = [ "starknet-types-core 0.2.3", "thiserror 1.0.69", "tokio", + "tonic 0.11.0", "tracing", "url", ] diff --git a/crates/cli/src/full.rs b/crates/cli/src/full.rs index 8bab752a6..1d4fe0530 100644 --- a/crates/cli/src/full.rs +++ b/crates/cli/src/full.rs @@ -135,6 +135,8 @@ impl FullNodeArgs { fn sync_source(&self) -> Option { if let Some(ref url) = self.sync.rpc { Some(SyncSource::JsonRpc(url.clone())) + } else if let Some(ref url) = self.sync.grpc { + Some(SyncSource::Grpc(url.clone())) } else { self.sync.gateway.clone().map(SyncSource::Gateway) } diff --git a/crates/cli/src/options.rs b/crates/cli/src/options.rs index c3fc3f5c8..17d93288e 100644 --- a/crates/cli/src/options.rs +++ b/crates/cli/src/options.rs @@ -993,7 +993,7 @@ pub struct SyncOptions { /// feeder gateway. #[arg(long = "sync.gateway")] #[arg(value_name = "URL")] - #[arg(conflicts_with = "rpc")] + #[arg(conflicts_with_all = ["rpc", "grpc"])] pub gateway: Option, /// JSON-RPC endpoint URL to use as the block download source instead of @@ -1004,9 +1004,20 @@ pub struct SyncOptions { /// This is mainly intended for development and testing purposes. #[arg(long = "sync.rpc")] #[arg(value_name = "URL")] - #[arg(conflicts_with = "gateway")] + #[arg(conflicts_with_all = ["gateway", "grpc"])] pub rpc: Option, + /// gRPC endpoint URL to use as the block download source (e.g., + /// `http://localhost:5051`). When set, blocks are fetched via + /// gRPC from another Katana node's gRPC server. + /// + /// This is a Katana-specific endpoint and only works when syncing + /// from another Katana node with `--grpc` enabled. + #[arg(long = "sync.grpc")] + #[arg(value_name = "URL")] + #[arg(conflicts_with_all = ["gateway", "rpc"])] + pub grpc: Option, + /// Maximum number of blocks to process per pipeline iteration before /// advancing to the next chunk. #[arg(long = "sync.chunk-size")] @@ -1030,6 +1041,7 @@ impl Default for SyncOptions { tip: None, gateway: None, rpc: None, + grpc: None, chunk_size: katana_full_node::DEFAULT_SYNC_CHUNK_SIZE, download_batch_size: katana_full_node::DEFAULT_DOWNLOAD_BATCH_SIZE, } diff --git a/crates/grpc/src/handlers/starknet.rs b/crates/grpc/src/handlers/starknet.rs index d3bb7b96c..727310014 100644 --- a/crates/grpc/src/handlers/starknet.rs +++ b/crates/grpc/src/handlers/starknet.rs @@ -243,18 +243,10 @@ where .try_into()?; let class = self.api.class_at_hash(block_id, class_hash).await.into_grpc_result()?; + let json = serde_json::to_string(&class) + .map_err(|e| Status::internal(format!("failed to serialize class: {e}")))?; - // 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(), - }, - )), - })) + Ok(Response::new(class_to_proto_response::(class, json))) } async fn get_class_hash_at( @@ -287,18 +279,10 @@ where let class = self.api.class_at_address(block_id, contract_address).await.into_grpc_result()?; + let json = serde_json::to_string(&class) + .map_err(|e| Status::internal(format!("failed to serialize class: {e}")))?; - // 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(), - }, - )), - })) + Ok(Response::new(class_to_proto_response::(class, json))) } async fn get_block_transaction_count( @@ -588,3 +572,80 @@ fn execution_result_to_string(exec: &katana_rpc_types::ExecutionResult) -> Strin katana_rpc_types::ExecutionResult::Reverted { .. } => "REVERTED".to_string(), } } + +/// Helper trait for building class responses from either `GetClassResponse` or +/// `GetClassAtResponse`, since both have the same shape. +trait ClassProtoResponse: Sized { + fn from_sierra(json: String) -> Self; + fn from_legacy(json: String) -> Self; +} + +impl ClassProtoResponse for GetClassResponse { + fn from_sierra(json: String) -> Self { + GetClassResponse { + result: Some(crate::protos::starknet::get_class_response::Result::ContractClass( + crate::protos::types::ContractClass { + sierra_program: Vec::new(), + contract_class_version: String::new(), + entry_points_by_type: None, + abi: json, + }, + )), + } + } + + fn from_legacy(json: String) -> Self { + GetClassResponse { + result: Some( + crate::protos::starknet::get_class_response::Result::DeprecatedContractClass( + crate::protos::types::DeprecatedContractClass { + program: String::new(), + entry_points_by_type: None, + abi: json, + }, + ), + ), + } + } +} + +impl ClassProtoResponse for GetClassAtResponse { + fn from_sierra(json: String) -> Self { + 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: json, + }, + )), + } + } + + fn from_legacy(json: String) -> Self { + GetClassAtResponse { + result: Some( + crate::protos::starknet::get_class_at_response::Result::DeprecatedContractClass( + crate::protos::types::DeprecatedContractClass { + program: String::new(), + entry_points_by_type: None, + abi: json, + }, + ), + ), + } + } +} + +/// Converts a `Class` into the appropriate proto response, using the JSON +/// serialization of the class as the payload. +fn class_to_proto_response( + class: katana_rpc_types::Class, + json: String, +) -> R { + match class { + katana_rpc_types::Class::Sierra(_) => R::from_sierra(json), + katana_rpc_types::Class::Legacy(_) => R::from_legacy(json), + } +} diff --git a/crates/node/full/src/lib.rs b/crates/node/full/src/lib.rs index 8b6a88337..b65742bdb 100644 --- a/crates/node/full/src/lib.rs +++ b/crates/node/full/src/lib.rs @@ -33,7 +33,7 @@ use katana_rpc_api::starknet::{StarknetApiServer, StarknetTraceApiServer, Starkn use katana_rpc_server::cors::Cors; use katana_rpc_server::starknet::{StarknetApi, StarknetApiConfig}; use katana_rpc_server::{RpcServer, RpcServerHandle}; -use katana_stage::blocks::{BatchBlockDownloader, JsonRpcBlockDownloader}; +use katana_stage::blocks::{BatchBlockDownloader, GrpcBlockDownloader, JsonRpcBlockDownloader}; use katana_stage::classes::{GatewayClassDownloader, JsonRpcClassDownloader}; use katana_stage::{Blocks, Classes, IndexHistory, StateTrie}; use katana_tasks::TaskManager; @@ -111,6 +111,8 @@ pub enum SyncSource { Gateway(Url), /// JSON-RPC endpoint URL. JsonRpc(Url), + /// gRPC endpoint URL. + Grpc(Url), } #[derive(Debug)] @@ -198,31 +200,49 @@ impl Node { Network::Sepolia => katana_primitives::chain::ChainId::SEPOLIA, }; - if let Some(SyncSource::JsonRpc(ref rpc_url)) = config.sync.source { - let rpc_client = katana_starknet::rpc::Client::new(rpc_url.clone()); - let block_downloader = - JsonRpcBlockDownloader::new_json_rpc(rpc_client.clone(), batch_size); - pipeline.add_stage(Blocks::new( - storage_provider.clone(), - block_downloader, - chain_id, - task_spawner.clone(), - )); - - let class_downloader = JsonRpcClassDownloader::new(rpc_client, batch_size); - pipeline.add_stage(Classes::new(storage_provider.clone(), class_downloader)); - } else { - let block_downloader = - BatchBlockDownloader::new_gateway(gateway_client.clone(), batch_size); - pipeline.add_stage(Blocks::new( - storage_provider.clone(), - block_downloader, - chain_id, - task_spawner.clone(), - )); - - let class_downloader = GatewayClassDownloader::new(gateway_client.clone(), batch_size); - pipeline.add_stage(Classes::new(storage_provider.clone(), class_downloader)); + match config.sync.source { + Some(SyncSource::JsonRpc(ref rpc_url)) => { + let rpc_client = katana_starknet::rpc::Client::new(rpc_url.clone()); + let block_downloader = + JsonRpcBlockDownloader::new_json_rpc(rpc_client.clone(), batch_size); + pipeline.add_stage(Blocks::new( + storage_provider.clone(), + block_downloader, + chain_id, + task_spawner.clone(), + )); + + let class_downloader = JsonRpcClassDownloader::new(rpc_client, batch_size); + pipeline.add_stage(Classes::new(storage_provider.clone(), class_downloader)); + } + Some(SyncSource::Grpc(ref grpc_url)) => { + let block_downloader = GrpcBlockDownloader::new(grpc_url.to_string(), batch_size); + pipeline.add_stage(Blocks::new( + storage_provider.clone(), + block_downloader, + chain_id, + task_spawner.clone(), + )); + + // gRPC class downloader not yet implemented; fall back to gateway + let class_downloader = + GatewayClassDownloader::new(gateway_client.clone(), batch_size); + pipeline.add_stage(Classes::new(storage_provider.clone(), class_downloader)); + } + _ => { + let block_downloader = + BatchBlockDownloader::new_gateway(gateway_client.clone(), batch_size); + pipeline.add_stage(Blocks::new( + storage_provider.clone(), + block_downloader, + chain_id, + task_spawner.clone(), + )); + + let class_downloader = + GatewayClassDownloader::new(gateway_client.clone(), batch_size); + pipeline.add_stage(Classes::new(storage_provider.clone(), class_downloader)); + } } pipeline.add_stage(IndexHistory::new(storage_provider.clone(), task_spawner.clone())); if config.trie.compute { diff --git a/crates/sync/stage/Cargo.toml b/crates/sync/stage/Cargo.toml index 677652d88..639a77c23 100644 --- a/crates/sync/stage/Cargo.toml +++ b/crates/sync/stage/Cargo.toml @@ -9,6 +9,7 @@ version.workspace = true katana-core.workspace = true katana-db.workspace = true katana-gateway-client.workspace = true +katana-grpc.workspace = true katana-gateway-types.workspace = true katana-messaging.workspace = true katana-pool.workspace = true @@ -22,6 +23,7 @@ katana-trie.workspace = true anyhow.workspace = true backon.workspace = true starknet-types-core.workspace = true +tonic.workspace = true futures.workspace = true num-traits.workspace = true rayon.workspace = true diff --git a/crates/sync/stage/src/blocks/downloader.rs b/crates/sync/stage/src/blocks/downloader.rs index aaf95e8a7..37ae9eef8 100644 --- a/crates/sync/stage/src/blocks/downloader.rs +++ b/crates/sync/stage/src/blocks/downloader.rs @@ -3,7 +3,7 @@ //! This module defines the [`BlockDownloader`] trait, which provides a stage-specific //! interface for downloading block data. The trait is designed to be flexible and can //! be implemented in various ways depending on the download strategy and data source -//! (e.g., gateway-based, JSON-RPC-based, P2P-based, or custom implementations). +//! (e.g., gateway-based, JSON-RPC-based, gRPC-based, P2P-based, or custom implementations). //! //! [`BatchBlockDownloader`] is one such implementation that leverages the generic //! [`BatchDownloader`](crate::downloader::BatchDownloader) utility for concurrent @@ -501,3 +501,714 @@ pub mod json_rpc { } } } + +pub mod grpc { + use std::collections::BTreeMap; + use std::sync::Arc; + + use katana_grpc::proto::{ + BlockId as ProtoBlockId, GetBlockRequest, GetBlockWithReceiptsResponse, + GetStateUpdateResponse, + }; + use katana_grpc::{ClientError, GrpcClient}; + use katana_primitives::block::{ + BlockNumber, FinalityStatus, GasPrices, Header, SealedBlock, SealedBlockWithStatus, + }; + use katana_primitives::contract::ContractAddress; + use katana_primitives::da::L1DataAvailabilityMode; + use katana_primitives::fee::{FeeInfo, PriceUnit}; + use katana_primitives::receipt::{ + DeclareTxReceipt, DeployAccountTxReceipt, DeployTxReceipt, Event, ExecutionResources, + GasUsed, InvokeTxReceipt, L1HandlerTxReceipt, MessageToL1, Receipt, + }; + use katana_primitives::state::{StateUpdates, StateUpdatesWithClasses}; + use katana_primitives::transaction::{Tx, TxWithHash}; + use katana_primitives::Felt; + use num_traits::ToPrimitive; + use tokio::sync::OnceCell; + use tonic::Code; + use tracing::error; + + use super::{BlockData, BlockDownloader}; + + /// Type alias for the gRPC block downloader. + pub type GrpcBlockDownloader = GrpcBatchBlockDownloader; + + #[derive(Debug, thiserror::Error)] + pub enum Error { + #[error(transparent)] + Grpc(#[from] tonic::Status), + + #[error(transparent)] + Transport(#[from] ClientError), + + #[error(transparent)] + Other(#[from] anyhow::Error), + } + + /// A [`BlockDownloader`] that fetches blocks via gRPC. + /// + /// Uses lazy connection — the gRPC client connects on first download. + /// The client is cloned per download call (tonic Channel is cheap to clone). + #[derive(Debug)] + pub struct GrpcBatchBlockDownloader { + endpoint: String, + batch_size: usize, + client: Arc>, + } + + impl GrpcBatchBlockDownloader { + pub fn new(endpoint: String, batch_size: usize) -> Self { + Self { endpoint, batch_size, client: Arc::new(OnceCell::new()) } + } + + async fn get_or_connect(&self) -> Result { + let client = self + .client + .get_or_try_init(|| async { GrpcClient::connect(&self.endpoint).await }) + .await?; + Ok(client.clone()) + } + } + + impl BlockDownloader for GrpcBatchBlockDownloader { + type Error = Error; + + async fn download_blocks( + &self, + from: BlockNumber, + to: BlockNumber, + ) -> Result, Self::Error> { + let client = self.get_or_connect().await?; + let downloader = GrpcDownloader { client }; + let batch = crate::downloader::BatchDownloader::new(downloader, self.batch_size); + + let block_keys = (from..=to).collect::>(); + let results = batch.download(block_keys).await?; + Ok(results) + } + } + + /// Internal downloader that fetches a single block via gRPC. + #[derive(Debug)] + struct GrpcDownloader { + client: GrpcClient, + } + + impl crate::downloader::Downloader for GrpcDownloader { + type Key = BlockNumber; + type Value = BlockData; + type Error = Error; + + #[allow(clippy::manual_async_fn)] + fn download( + &self, + key: &Self::Key, + ) -> impl std::future::Future< + Output = crate::downloader::DownloaderResult, + > { + let block_num = *key; + let client = self.client.clone(); + async move { + let block_id = Some(ProtoBlockId { + identifier: Some(katana_grpc::proto::block_id::Identifier::Number(block_num)), + }); + + let block_request = GetBlockRequest { block_id: block_id.clone() }; + let state_request = GetBlockRequest { block_id }; + + // Clone the client for each concurrent call since GrpcClient + // methods take &mut self. + let mut block_client = client.clone(); + let mut state_client = client; + + let result = tokio::try_join!( + async { + block_client + .get_block_with_receipts(tonic::Request::new(block_request)) + .await + .inspect_err(|e| { + error!( + block = %block_num, + error = %e, + "Error downloading block via gRPC." + ) + }) + .map_err(Error::from) + }, + async { + state_client + .get_state_update(tonic::Request::new(state_request)) + .await + .inspect_err(|e| { + error!( + block = %block_num, + error = %e, + "Error downloading state update via gRPC." + ) + }) + .map_err(Error::from) + }, + ); + + match result { + Ok((block_resp, state_resp)) => { + match BlockData::from_grpc(block_resp.into_inner(), state_resp.into_inner()) + { + Ok(data) => crate::downloader::DownloaderResult::Ok(data), + Err(e) => crate::downloader::DownloaderResult::Err(Error::from(e)), + } + } + Err(err) => { + if is_retryable(&err) { + crate::downloader::DownloaderResult::Retry(err) + } else { + crate::downloader::DownloaderResult::Err(err) + } + } + } + } + } + } + + fn is_retryable(err: &Error) -> bool { + match err { + Error::Grpc(status) => matches!( + status.code(), + Code::Unavailable | Code::ResourceExhausted | Code::DeadlineExceeded + ), + Error::Transport(_) => true, + Error::Other(_) => false, + } + } + + // ========================================================================= + // Proto → BlockData conversion + // ========================================================================= + + fn felt_from_proto(f: Option) -> Felt { + f.map(|f| Felt::from_bytes_be_slice(&f.value)).unwrap_or_default() + } + + fn to_gas_prices(price: Option) -> GasPrices { + let price = price.unwrap_or_default(); + let eth = felt_from_proto(price.price_in_wei).to_u128().unwrap_or(1); + let strk = felt_from_proto(price.price_in_fri).to_u128().unwrap_or(1); + let eth = if eth == 0 { 1 } else { eth }; + let strk = if strk == 0 { 1 } else { strk }; + unsafe { GasPrices::new_unchecked(eth, strk) } + } + + fn l1_da_mode_from_proto(mode: i32) -> L1DataAvailabilityMode { + use katana_grpc::proto::L1DataAvailabilityMode as ProtoMode; + match ProtoMode::try_from(mode) { + Ok(ProtoMode::Calldata) => L1DataAvailabilityMode::Calldata, + _ => L1DataAvailabilityMode::Blob, + } + } + + fn finality_status_from_proto(status: i32) -> FinalityStatus { + use katana_grpc::proto::FinalityStatus as ProtoStatus; + match ProtoStatus::try_from(status) { + Ok(ProtoStatus::AcceptedOnL1) => FinalityStatus::AcceptedOnL1, + _ => FinalityStatus::AcceptedOnL2, + } + } + + fn events_from_proto(events: Vec) -> Vec { + events + .into_iter() + .map(|e| Event { + from_address: ContractAddress::new(felt_from_proto(e.from_address)), + keys: e.keys.into_iter().map(|f| Felt::from_bytes_be_slice(&f.value)).collect(), + data: e.data.into_iter().map(|f| Felt::from_bytes_be_slice(&f.value)).collect(), + }) + .collect() + } + + fn messages_from_proto(messages: Vec) -> Vec { + messages + .into_iter() + .map(|m| MessageToL1 { + from_address: ContractAddress::new(felt_from_proto(m.from_address)), + to_address: felt_from_proto(m.to_address), + payload: m + .payload + .into_iter() + .map(|f| Felt::from_bytes_be_slice(&f.value)) + .collect(), + }) + .collect() + } + + fn execution_resources_from_proto( + res: Option, + ) -> ExecutionResources { + let res = res.unwrap_or_default(); + ExecutionResources { + total_gas_consumed: GasUsed { + l1_gas: res.l1_gas, + l1_data_gas: res.l1_data_gas, + l2_gas: res.l2_gas, + }, + ..Default::default() + } + } + + fn tx_from_proto( + proto_tx: katana_grpc::proto::Transaction, + tx_hash: Felt, + ) -> anyhow::Result { + use katana_grpc::proto::transaction::Transaction as ProtoTxVariant; + use katana_primitives::transaction::{ + DeclareTx, DeclareTxV0, DeclareTxV1, DeclareTxV2, DeclareTxV3, DeployAccountTx, + DeployAccountTxV1, DeployAccountTxV3, DeployTx, InvokeTx, InvokeTxV0, InvokeTxV1, + InvokeTxV3, L1HandlerTx, + }; + + let variant = + proto_tx.transaction.ok_or_else(|| anyhow::anyhow!("missing transaction variant"))?; + + let felts = |v: Vec| -> Vec { + v.into_iter().map(|f| Felt::from_bytes_be_slice(&f.value)).collect() + }; + + let tx = match variant { + ProtoTxVariant::InvokeV1(v) => { + let version = parse_version(&v.version); + if version == Felt::ZERO { + Tx::Invoke(InvokeTx::V0(InvokeTxV0 { + max_fee: felt_from_proto(v.max_fee).to_u128().unwrap_or(0), + signature: felts(v.signature), + contract_address: ContractAddress::new(felt_from_proto( + v.sender_address.clone(), + )), + entry_point_selector: Felt::ZERO, + calldata: felts(v.calldata), + })) + } else { + Tx::Invoke(InvokeTx::V1(InvokeTxV1 { + chain_id: Default::default(), + max_fee: felt_from_proto(v.max_fee).to_u128().unwrap_or(0), + signature: felts(v.signature), + nonce: felt_from_proto(v.nonce), + sender_address: ContractAddress::new(felt_from_proto(v.sender_address)), + calldata: felts(v.calldata), + })) + } + } + + ProtoTxVariant::InvokeV3(v) => { + let resource_bounds = v + .resource_bounds + .as_ref() + .map(resource_bounds_from_proto) + .unwrap_or_else(default_resource_bounds); + Tx::Invoke(InvokeTx::V3(InvokeTxV3 { + chain_id: Default::default(), + signature: felts(v.signature), + nonce: felt_from_proto(v.nonce), + sender_address: ContractAddress::new(felt_from_proto(v.sender_address)), + calldata: felts(v.calldata), + resource_bounds, + tip: felt_from_proto(v.tip).to_u64().unwrap_or(0), + paymaster_data: felts(v.paymaster_data), + account_deployment_data: felts(v.account_deployment_data), + nonce_data_availability_mode: da_mode_from_string( + &v.nonce_data_availability_mode, + ), + fee_data_availability_mode: da_mode_from_string(&v.fee_data_availability_mode), + })) + } + + ProtoTxVariant::DeclareV1(v) => { + let version = parse_version(&v.version); + if version == Felt::ZERO { + Tx::Declare(DeclareTx::V0(DeclareTxV0 { + chain_id: Default::default(), + max_fee: felt_from_proto(v.max_fee).to_u128().unwrap_or(0), + signature: felts(v.signature), + sender_address: ContractAddress::new(felt_from_proto(v.sender_address)), + class_hash: felt_from_proto(v.class_hash), + })) + } else { + Tx::Declare(DeclareTx::V1(DeclareTxV1 { + chain_id: Default::default(), + max_fee: felt_from_proto(v.max_fee).to_u128().unwrap_or(0), + signature: felts(v.signature), + nonce: felt_from_proto(v.nonce), + sender_address: ContractAddress::new(felt_from_proto(v.sender_address)), + class_hash: felt_from_proto(v.class_hash), + })) + } + } + + ProtoTxVariant::DeclareV2(v) => Tx::Declare(DeclareTx::V2(DeclareTxV2 { + chain_id: Default::default(), + max_fee: felt_from_proto(v.max_fee).to_u128().unwrap_or(0), + signature: felts(v.signature), + nonce: felt_from_proto(v.nonce), + sender_address: ContractAddress::new(felt_from_proto(v.sender_address)), + compiled_class_hash: felt_from_proto(v.compiled_class_hash), + class_hash: Felt::ZERO, + })), + + ProtoTxVariant::DeclareV3(v) => { + let resource_bounds = v + .resource_bounds + .as_ref() + .map(resource_bounds_from_proto) + .unwrap_or_else(default_resource_bounds); + Tx::Declare(DeclareTx::V3(DeclareTxV3 { + chain_id: Default::default(), + signature: felts(v.signature), + nonce: felt_from_proto(v.nonce), + sender_address: ContractAddress::new(felt_from_proto(v.sender_address)), + compiled_class_hash: felt_from_proto(v.compiled_class_hash), + class_hash: felt_from_proto(v.class_hash), + resource_bounds, + tip: felt_from_proto(v.tip).to_u64().unwrap_or(0), + paymaster_data: felts(v.paymaster_data), + account_deployment_data: felts(v.account_deployment_data), + nonce_data_availability_mode: da_mode_from_string( + &v.nonce_data_availability_mode, + ), + fee_data_availability_mode: da_mode_from_string(&v.fee_data_availability_mode), + })) + } + + ProtoTxVariant::DeployAccount(v) => { + Tx::DeployAccount(DeployAccountTx::V1(DeployAccountTxV1 { + chain_id: Default::default(), + max_fee: felt_from_proto(v.max_fee).to_u128().unwrap_or(0), + signature: felts(v.signature), + nonce: felt_from_proto(v.nonce), + class_hash: felt_from_proto(v.class_hash), + contract_address: Default::default(), + contract_address_salt: felt_from_proto(v.contract_address_salt), + constructor_calldata: felts(v.constructor_calldata), + })) + } + + ProtoTxVariant::DeployAccountV3(v) => { + let resource_bounds = v + .resource_bounds + .as_ref() + .map(resource_bounds_from_proto) + .unwrap_or_else(default_resource_bounds); + Tx::DeployAccount(DeployAccountTx::V3(DeployAccountTxV3 { + chain_id: Default::default(), + signature: felts(v.signature), + nonce: felt_from_proto(v.nonce), + class_hash: felt_from_proto(v.class_hash), + contract_address: Default::default(), + contract_address_salt: felt_from_proto(v.contract_address_salt), + constructor_calldata: felts(v.constructor_calldata), + resource_bounds, + tip: felt_from_proto(v.tip).to_u64().unwrap_or(0), + paymaster_data: felts(v.paymaster_data), + nonce_data_availability_mode: da_mode_from_string( + &v.nonce_data_availability_mode, + ), + fee_data_availability_mode: da_mode_from_string(&v.fee_data_availability_mode), + })) + } + + ProtoTxVariant::L1Handler(v) => Tx::L1Handler(L1HandlerTx { + chain_id: Default::default(), + version: parse_version(&v.version), + nonce: felt_from_proto(v.nonce), + contract_address: ContractAddress::new(felt_from_proto(v.contract_address)), + entry_point_selector: felt_from_proto(v.entry_point_selector), + calldata: felts(v.calldata), + message_hash: Default::default(), + paid_fee_on_l1: 0, + }), + + ProtoTxVariant::Deploy(v) => Tx::Deploy(DeployTx { + version: parse_version(&v.version), + class_hash: felt_from_proto(v.class_hash), + contract_address_salt: felt_from_proto(v.contract_address_salt), + constructor_calldata: felts(v.constructor_calldata), + contract_address: Felt::ZERO, + }), + }; + + Ok(TxWithHash { hash: tx_hash, transaction: tx }) + } + + fn parse_version(version: &str) -> Felt { + Felt::from_hex(version).unwrap_or_default() + } + + fn da_mode_from_string(s: &str) -> katana_primitives::da::DataAvailabilityMode { + match s.to_uppercase().as_str() { + "L1" => katana_primitives::da::DataAvailabilityMode::L1, + _ => katana_primitives::da::DataAvailabilityMode::L2, + } + } + + fn default_resource_bounds() -> katana_primitives::fee::ResourceBoundsMapping { + use katana_primitives::fee::L1GasResourceBoundsMapping; + katana_primitives::fee::ResourceBoundsMapping::L1Gas(L1GasResourceBoundsMapping::default()) + } + + fn resource_bounds_from_proto( + rbm: &katana_grpc::proto::ResourceBoundsMapping, + ) -> katana_primitives::fee::ResourceBoundsMapping { + use katana_primitives::fee::{ + AllResourceBoundsMapping, L1GasResourceBoundsMapping, + ResourceBounds as PrimResourceBounds, ResourceBoundsMapping, + }; + + let rb_from = |rb: &katana_grpc::proto::ResourceBounds| -> PrimResourceBounds { + PrimResourceBounds { + max_amount: felt_from_proto(rb.max_amount.clone()).to_u64().unwrap_or(0), + max_price_per_unit: felt_from_proto(rb.max_price_per_unit.clone()) + .to_u128() + .unwrap_or(0), + } + }; + + let l1_gas = rbm.l1_gas.as_ref().map(rb_from).unwrap_or_default(); + let l2_gas = rbm.l2_gas.as_ref().map(rb_from).unwrap_or_default(); + + if let Some(ref l1_data_gas_proto) = rbm.l1_data_gas { + let l1_data_gas = rb_from(l1_data_gas_proto); + ResourceBoundsMapping::All(AllResourceBoundsMapping { l1_gas, l2_gas, l1_data_gas }) + } else { + ResourceBoundsMapping::L1Gas(L1GasResourceBoundsMapping { l1_gas, l2_gas }) + } + } + + impl BlockData { + /// Converts gRPC proto responses into [`BlockData`]. + pub fn from_grpc( + block_resp: GetBlockWithReceiptsResponse, + state_resp: GetStateUpdateResponse, + ) -> anyhow::Result { + use katana_grpc::proto::get_block_with_receipts_response::Result as BlockResult; + use katana_grpc::proto::get_state_update_response::Result as StateResult; + + let block_result = block_resp + .result + .ok_or_else(|| anyhow::anyhow!("missing block result in gRPC response"))?; + + let (status, header, tx_with_receipts) = match block_result { + BlockResult::Block(b) => { + let status = finality_status_from_proto(b.status); + (status, b.header, b.transactions) + } + BlockResult::PendingBlock(_) => { + anyhow::bail!("pre-confirmed blocks are not supported for syncing"); + } + }; + + let header = + header.ok_or_else(|| anyhow::anyhow!("missing block header in gRPC response"))?; + + let mut transactions = Vec::with_capacity(tx_with_receipts.len()); + let mut receipts = Vec::with_capacity(tx_with_receipts.len()); + + for twr in tx_with_receipts { + let proto_receipt = + twr.receipt.ok_or_else(|| anyhow::anyhow!("missing receipt"))?; + let proto_tx = + twr.transaction.ok_or_else(|| anyhow::anyhow!("missing transaction"))?; + + let tx_hash = felt_from_proto(proto_receipt.transaction_hash.clone()); + let tx = tx_from_proto(proto_tx, tx_hash)?; + + let fee_amount = proto_receipt + .actual_fee + .as_ref() + .map(|f| felt_from_proto(f.amount.clone()).to_u128().unwrap_or(0)) + .unwrap_or(0); + + let fee_unit = proto_receipt + .actual_fee + .as_ref() + .map(|f| match f.unit.to_uppercase().as_str() { + "FRI" => PriceUnit::Fri, + _ => PriceUnit::Wei, + }) + .unwrap_or(PriceUnit::Wei); + + let fee = FeeInfo { unit: fee_unit, overall_fee: fee_amount, ..Default::default() }; + let events = events_from_proto(proto_receipt.events); + let messages_sent = messages_from_proto(proto_receipt.messages_sent); + let execution_resources = + execution_resources_from_proto(proto_receipt.execution_resources); + let revert_error = if proto_receipt.revert_reason.is_empty() { + None + } else { + Some(proto_receipt.revert_reason) + }; + + let receipt = match &tx.transaction { + Tx::Invoke(_) => Receipt::Invoke(InvokeTxReceipt { + fee, + events, + revert_error, + messages_sent, + execution_resources, + }), + Tx::Declare(_) => Receipt::Declare(DeclareTxReceipt { + fee, + events, + revert_error, + messages_sent, + execution_resources, + }), + Tx::L1Handler(_) => Receipt::L1Handler(L1HandlerTxReceipt { + fee, + events, + messages_sent, + revert_error, + message_hash: Default::default(), + execution_resources, + }), + Tx::DeployAccount(da_tx) => Receipt::DeployAccount(DeployAccountTxReceipt { + fee, + events, + revert_error, + messages_sent, + contract_address: da_tx.contract_address(), + execution_resources, + }), + Tx::Deploy(d_tx) => Receipt::Deploy(DeployTxReceipt { + fee, + events, + revert_error, + messages_sent, + contract_address: d_tx.contract_address.into(), + execution_resources, + }), + }; + + transactions.push(tx); + receipts.push(receipt); + } + + let transaction_count = transactions.len() as u32; + let events_count = receipts.iter().map(|r| r.events().len() as u32).sum::(); + + let block = SealedBlock { + body: transactions, + hash: felt_from_proto(header.block_hash), + header: Header { + transaction_count, + events_count, + timestamp: header.timestamp, + l1_da_mode: l1_da_mode_from_proto(header.l1_da_mode), + parent_hash: felt_from_proto(header.parent_hash), + number: header.block_number, + state_root: felt_from_proto(header.new_root), + sequencer_address: ContractAddress::new(felt_from_proto( + header.sequencer_address, + )), + l1_gas_prices: to_gas_prices(header.l1_gas_price), + l2_gas_prices: to_gas_prices(header.l2_gas_price), + l1_data_gas_prices: to_gas_prices(header.l1_data_gas_price), + starknet_version: header.starknet_version.try_into().unwrap_or_default(), + // Commitments will be computed by `compute_missing_commitments` + events_commitment: Felt::ZERO, + receipts_commitment: Felt::ZERO, + transactions_commitment: Felt::ZERO, + state_diff_length: Default::default(), + state_diff_commitment: Default::default(), + }, + }; + + // Parse state update + let state_result = state_resp + .result + .ok_or_else(|| anyhow::anyhow!("missing state update result in gRPC response"))?; + + let state_diff = match state_result { + StateResult::StateUpdate(update) => update.state_diff, + StateResult::PendingStateUpdate(update) => update.state_diff, + }; + + let state_diff = + state_diff.ok_or_else(|| anyhow::anyhow!("missing state_diff in gRPC response"))?; + + let state_updates = state_diff_from_proto(state_diff); + let state_updates = StateUpdatesWithClasses { state_updates, ..Default::default() }; + + Ok(BlockData { + block: SealedBlockWithStatus { block, status }, + receipts, + state_updates, + }) + } + } + + fn state_diff_from_proto(diff: katana_grpc::proto::StateDiff) -> StateUpdates { + let storage_updates = diff + .storage_diffs + .into_iter() + .map(|sd| { + let address = ContractAddress::new(felt_from_proto(sd.address)); + let entries: BTreeMap = sd + .storage_entries + .into_iter() + .map(|se| (felt_from_proto(se.key), felt_from_proto(se.value))) + .collect(); + (address, entries) + }) + .collect(); + + let nonce_updates = diff + .nonces + .into_iter() + .map(|n| { + ( + ContractAddress::new(felt_from_proto(n.contract_address)), + felt_from_proto(n.nonce), + ) + }) + .collect(); + + let deployed_contracts = diff + .deployed_contracts + .into_iter() + .map(|dc| { + (ContractAddress::new(felt_from_proto(dc.address)), felt_from_proto(dc.class_hash)) + }) + .collect(); + + let declared_classes = diff + .declared_classes + .into_iter() + .map(|dc| (felt_from_proto(dc.class_hash), felt_from_proto(dc.compiled_class_hash))) + .collect(); + + let deprecated_declared_classes = diff + .deprecated_declared_classes + .into_iter() + .map(|f| Felt::from_bytes_be_slice(&f.value)) + .collect(); + + let replaced_classes = diff + .replaced_classes + .into_iter() + .map(|rc| { + ( + ContractAddress::new(felt_from_proto(rc.contract_address)), + felt_from_proto(rc.class_hash), + ) + }) + .collect(); + + StateUpdates { + nonce_updates, + storage_updates, + deployed_contracts, + declared_classes, + deprecated_declared_classes, + replaced_classes, + migrated_compiled_classes: Default::default(), + } + } +} diff --git a/crates/sync/stage/src/blocks/mod.rs b/crates/sync/stage/src/blocks/mod.rs index d22d0d46a..e8125fcff 100644 --- a/crates/sync/stage/src/blocks/mod.rs +++ b/crates/sync/stage/src/blocks/mod.rs @@ -16,6 +16,7 @@ use crate::{ mod downloader; pub mod hash; +pub use downloader::grpc::GrpcBlockDownloader; pub use downloader::json_rpc::JsonRpcBlockDownloader; pub use downloader::{BatchBlockDownloader, BlockData, BlockDownloader}; From b9eb6aff2926c634651215ea377be754fbe3e120 Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Sat, 21 Mar 2026 23:25:28 -0500 Subject: [PATCH 2/7] feat(sync): add gRPC class downloader Implement GrpcClassDownloader for the Classes stage, completing gRPC sync support for both blocks and classes. The class data is deserialized from the JSON payload in the gRPC response's abi field. Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/node/full/src/lib.rs | 6 +- crates/sync/stage/Cargo.toml | 2 +- crates/sync/stage/src/classes/downloader.rs | 150 ++++++++++++++++++++ crates/sync/stage/src/classes/mod.rs | 1 + 4 files changed, 154 insertions(+), 5 deletions(-) diff --git a/crates/node/full/src/lib.rs b/crates/node/full/src/lib.rs index b65742bdb..505cc0f3d 100644 --- a/crates/node/full/src/lib.rs +++ b/crates/node/full/src/lib.rs @@ -34,7 +34,7 @@ use katana_rpc_server::cors::Cors; use katana_rpc_server::starknet::{StarknetApi, StarknetApiConfig}; use katana_rpc_server::{RpcServer, RpcServerHandle}; use katana_stage::blocks::{BatchBlockDownloader, GrpcBlockDownloader, JsonRpcBlockDownloader}; -use katana_stage::classes::{GatewayClassDownloader, JsonRpcClassDownloader}; +use katana_stage::classes::{GatewayClassDownloader, GrpcClassDownloader, JsonRpcClassDownloader}; use katana_stage::{Blocks, Classes, IndexHistory, StateTrie}; use katana_tasks::TaskManager; use tracing::{error, info}; @@ -224,9 +224,7 @@ impl Node { task_spawner.clone(), )); - // gRPC class downloader not yet implemented; fall back to gateway - let class_downloader = - GatewayClassDownloader::new(gateway_client.clone(), batch_size); + let class_downloader = GrpcClassDownloader::new(grpc_url.to_string(), batch_size); pipeline.add_stage(Classes::new(storage_provider.clone(), class_downloader)); } _ => { diff --git a/crates/sync/stage/Cargo.toml b/crates/sync/stage/Cargo.toml index 639a77c23..20d8c2739 100644 --- a/crates/sync/stage/Cargo.toml +++ b/crates/sync/stage/Cargo.toml @@ -27,6 +27,7 @@ tonic.workspace = true futures.workspace = true num-traits.workspace = true rayon.workspace = true +serde_json.workspace = true starknet.workspace = true thiserror.workspace = true tokio.workspace = true @@ -36,5 +37,4 @@ tracing.workspace = true katana-provider = { workspace = true, features = [ "test-utils" ] } katana-trie.workspace = true rstest.workspace = true -serde_json.workspace = true url.workspace = true diff --git a/crates/sync/stage/src/classes/downloader.rs b/crates/sync/stage/src/classes/downloader.rs index 4aebad0a0..c51f9a5cc 100644 --- a/crates/sync/stage/src/classes/downloader.rs +++ b/crates/sync/stage/src/classes/downloader.rs @@ -56,6 +56,156 @@ pub mod gateway { } } +pub mod grpc { + use std::future::Future; + use std::sync::Arc; + + use katana_grpc::{ClientError, GrpcClient}; + use katana_rpc_types::Class; + use tokio::sync::OnceCell; + use tonic::Code; + use tracing::error; + + use super::super::{ClassDownloadKey, ClassDownloader}; + use crate::downloader::{BatchDownloader, Downloader, DownloaderResult}; + + #[derive(Debug, thiserror::Error)] + pub enum Error { + #[error(transparent)] + Grpc(#[from] tonic::Status), + + #[error(transparent)] + Transport(#[from] ClientError), + + #[error(transparent)] + Other(#[from] anyhow::Error), + } + + /// A [`ClassDownloader`] that fetches classes via gRPC. + /// + /// Uses lazy connection and clones the client per download (tonic Channel is + /// cheap to clone). + #[derive(Debug)] + pub struct GrpcClassDownloader { + endpoint: String, + batch_size: usize, + client: Arc>, + } + + impl GrpcClassDownloader { + pub fn new(endpoint: String, batch_size: usize) -> Self { + Self { endpoint, batch_size, client: Arc::new(OnceCell::new()) } + } + + async fn get_or_connect(&self) -> Result { + let client = self + .client + .get_or_try_init(|| async { GrpcClient::connect(&self.endpoint).await }) + .await?; + Ok(client.clone()) + } + } + + impl ClassDownloader for GrpcClassDownloader { + type Error = Error; + + async fn download_classes( + &self, + keys: Vec, + ) -> Result, Self::Error> { + let client = self.get_or_connect().await?; + let downloader = GrpcDownloaderInner { client }; + let batch = BatchDownloader::new(downloader, self.batch_size); + batch.download(keys).await + } + } + + #[derive(Debug)] + struct GrpcDownloaderInner { + client: GrpcClient, + } + + impl Downloader for GrpcDownloaderInner { + type Key = ClassDownloadKey; + type Value = Class; + type Error = Error; + + #[allow(clippy::manual_async_fn)] + fn download( + &self, + key: &Self::Key, + ) -> impl Future> { + let mut client = self.client.clone(); + let block = key.block; + let class_hash = key.class_hash; + + async move { + let block_id = Some(katana_grpc::proto::BlockId { + identifier: Some(katana_grpc::proto::block_id::Identifier::Number(block)), + }); + + let request = katana_grpc::proto::GetClassRequest { + block_id, + class_hash: Some(class_hash.into()), + }; + + match client.get_class(tonic::Request::new(request)).await.inspect_err(|e| { + error!( + block = %block, + class_hash = %format!("{:#x}", class_hash), + error = %e, + "Error downloading class via gRPC." + ); + }) { + Ok(resp) => match class_from_proto(resp.into_inner()) { + Ok(class) => DownloaderResult::Ok(class), + Err(e) => DownloaderResult::Err(Error::from(e)), + }, + Err(status) => { + let err = Error::from(status); + if is_retryable(&err) { + DownloaderResult::Retry(err) + } else { + DownloaderResult::Err(err) + } + } + } + } + } + } + + fn is_retryable(err: &Error) -> bool { + match err { + Error::Grpc(status) => matches!( + status.code(), + Code::Unavailable | Code::ResourceExhausted | Code::DeadlineExceeded + ), + Error::Transport(_) => true, + Error::Other(_) => false, + } + } + + /// Deserializes a `Class` from the gRPC response. + /// + /// The server serializes the full class as JSON into the `abi` field of the + /// proto message (because the class types are not compatible with non-human- + /// readable serialization formats). + fn class_from_proto(resp: katana_grpc::proto::GetClassResponse) -> anyhow::Result { + use katana_grpc::proto::get_class_response::Result as ClassResult; + + let result = + resp.result.ok_or_else(|| anyhow::anyhow!("missing class result in gRPC response"))?; + + let json = match &result { + ClassResult::ContractClass(c) => &c.abi, + ClassResult::DeprecatedContractClass(c) => &c.abi, + }; + + let class: Class = serde_json::from_str(json)?; + Ok(class) + } +} + pub mod json_rpc { use std::future::Future; diff --git a/crates/sync/stage/src/classes/mod.rs b/crates/sync/stage/src/classes/mod.rs index bed169320..7f4841fae 100644 --- a/crates/sync/stage/src/classes/mod.rs +++ b/crates/sync/stage/src/classes/mod.rs @@ -21,6 +21,7 @@ use super::{ mod downloader; pub use downloader::gateway::GatewayClassDownloader; +pub use downloader::grpc::GrpcClassDownloader; pub use downloader::json_rpc::JsonRpcClassDownloader; /// Trait for downloading contract class artifacts. From a8865a6c46d6c7eca2abd21e8eff70ab09ee1bf0 Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Sat, 21 Mar 2026 23:51:50 -0500 Subject: [PATCH 3/7] refactor(grpc): inline class response construction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove ClassProtoResponse trait abstraction — both call sites construct their response directly. Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/grpc/src/handlers/starknet.rs | 125 ++++++++++----------------- 1 file changed, 46 insertions(+), 79 deletions(-) diff --git a/crates/grpc/src/handlers/starknet.rs b/crates/grpc/src/handlers/starknet.rs index 727310014..c3064c50c 100644 --- a/crates/grpc/src/handlers/starknet.rs +++ b/crates/grpc/src/handlers/starknet.rs @@ -246,7 +246,29 @@ where let json = serde_json::to_string(&class) .map_err(|e| Status::internal(format!("failed to serialize class: {e}")))?; - Ok(Response::new(class_to_proto_response::(class, json))) + let result = match class { + katana_rpc_types::Class::Sierra(_) => { + crate::protos::starknet::get_class_response::Result::ContractClass( + crate::protos::types::ContractClass { + sierra_program: Vec::new(), + contract_class_version: String::new(), + entry_points_by_type: None, + abi: json, + }, + ) + } + katana_rpc_types::Class::Legacy(_) => { + crate::protos::starknet::get_class_response::Result::DeprecatedContractClass( + crate::protos::types::DeprecatedContractClass { + program: String::new(), + entry_points_by_type: None, + abi: json, + }, + ) + } + }; + + Ok(Response::new(GetClassResponse { result: Some(result) })) } async fn get_class_hash_at( @@ -282,7 +304,29 @@ where let json = serde_json::to_string(&class) .map_err(|e| Status::internal(format!("failed to serialize class: {e}")))?; - Ok(Response::new(class_to_proto_response::(class, json))) + let result = match class { + katana_rpc_types::Class::Sierra(_) => { + 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: json, + }, + ) + } + katana_rpc_types::Class::Legacy(_) => { + crate::protos::starknet::get_class_at_response::Result::DeprecatedContractClass( + crate::protos::types::DeprecatedContractClass { + program: String::new(), + entry_points_by_type: None, + abi: json, + }, + ) + } + }; + + Ok(Response::new(GetClassAtResponse { result: Some(result) })) } async fn get_block_transaction_count( @@ -572,80 +616,3 @@ fn execution_result_to_string(exec: &katana_rpc_types::ExecutionResult) -> Strin katana_rpc_types::ExecutionResult::Reverted { .. } => "REVERTED".to_string(), } } - -/// Helper trait for building class responses from either `GetClassResponse` or -/// `GetClassAtResponse`, since both have the same shape. -trait ClassProtoResponse: Sized { - fn from_sierra(json: String) -> Self; - fn from_legacy(json: String) -> Self; -} - -impl ClassProtoResponse for GetClassResponse { - fn from_sierra(json: String) -> Self { - GetClassResponse { - result: Some(crate::protos::starknet::get_class_response::Result::ContractClass( - crate::protos::types::ContractClass { - sierra_program: Vec::new(), - contract_class_version: String::new(), - entry_points_by_type: None, - abi: json, - }, - )), - } - } - - fn from_legacy(json: String) -> Self { - GetClassResponse { - result: Some( - crate::protos::starknet::get_class_response::Result::DeprecatedContractClass( - crate::protos::types::DeprecatedContractClass { - program: String::new(), - entry_points_by_type: None, - abi: json, - }, - ), - ), - } - } -} - -impl ClassProtoResponse for GetClassAtResponse { - fn from_sierra(json: String) -> Self { - 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: json, - }, - )), - } - } - - fn from_legacy(json: String) -> Self { - GetClassAtResponse { - result: Some( - crate::protos::starknet::get_class_at_response::Result::DeprecatedContractClass( - crate::protos::types::DeprecatedContractClass { - program: String::new(), - entry_points_by_type: None, - abi: json, - }, - ), - ), - } - } -} - -/// Converts a `Class` into the appropriate proto response, using the JSON -/// serialization of the class as the payload. -fn class_to_proto_response( - class: katana_rpc_types::Class, - json: String, -) -> R { - match class { - katana_rpc_types::Class::Sierra(_) => R::from_sierra(json), - katana_rpc_types::Class::Legacy(_) => R::from_legacy(json), - } -} From 8d1fbbd7410c9ab5bca845d1074177183e54b064 Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Sat, 21 Mar 2026 23:57:51 -0500 Subject: [PATCH 4/7] refactor(grpc): simplify class response proto to raw bytes oneof Replace the structured ContractClass/DeprecatedContractClass proto messages with raw bytes variants in the GetClass and GetClassAt responses. The oneof still distinguishes Sierra vs Legacy, but each variant is just the JSON-serialized class bytes. Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/grpc/proto/starknet.proto | 12 ++++--- crates/grpc/src/handlers/starknet.rs | 36 ++++----------------- crates/sync/stage/src/classes/downloader.rs | 11 ++++--- 3 files changed, 20 insertions(+), 39 deletions(-) diff --git a/crates/grpc/proto/starknet.proto b/crates/grpc/proto/starknet.proto index 2ce026b17..58c619b2d 100644 --- a/crates/grpc/proto/starknet.proto +++ b/crates/grpc/proto/starknet.proto @@ -195,9 +195,11 @@ message GetClassRequest { } message GetClassResponse { + // JSON-serialized class bytes. The class types are not compatible with + // non-human-readable serialization, so we use JSON here. oneof result { - types.DeprecatedContractClass deprecated_contract_class = 1; - types.ContractClass contract_class = 2; + bytes contract_class = 1; + bytes deprecated_contract_class = 2; } } @@ -216,9 +218,11 @@ message GetClassAtRequest { } message GetClassAtResponse { + // JSON-serialized class bytes. The class types are not compatible with + // non-human-readable serialization, so we use JSON here. oneof result { - types.DeprecatedContractClass deprecated_contract_class = 1; - types.ContractClass contract_class = 2; + bytes contract_class = 1; + bytes deprecated_contract_class = 2; } } diff --git a/crates/grpc/src/handlers/starknet.rs b/crates/grpc/src/handlers/starknet.rs index c3064c50c..9a5f1c192 100644 --- a/crates/grpc/src/handlers/starknet.rs +++ b/crates/grpc/src/handlers/starknet.rs @@ -243,28 +243,15 @@ where .try_into()?; let class = self.api.class_at_hash(block_id, class_hash).await.into_grpc_result()?; - let json = serde_json::to_string(&class) + let json = serde_json::to_vec(&class) .map_err(|e| Status::internal(format!("failed to serialize class: {e}")))?; let result = match class { katana_rpc_types::Class::Sierra(_) => { - crate::protos::starknet::get_class_response::Result::ContractClass( - crate::protos::types::ContractClass { - sierra_program: Vec::new(), - contract_class_version: String::new(), - entry_points_by_type: None, - abi: json, - }, - ) + crate::protos::starknet::get_class_response::Result::ContractClass(json) } katana_rpc_types::Class::Legacy(_) => { - crate::protos::starknet::get_class_response::Result::DeprecatedContractClass( - crate::protos::types::DeprecatedContractClass { - program: String::new(), - entry_points_by_type: None, - abi: json, - }, - ) + crate::protos::starknet::get_class_response::Result::DeprecatedContractClass(json) } }; @@ -301,27 +288,16 @@ where let class = self.api.class_at_address(block_id, contract_address).await.into_grpc_result()?; - let json = serde_json::to_string(&class) + let json = serde_json::to_vec(&class) .map_err(|e| Status::internal(format!("failed to serialize class: {e}")))?; let result = match class { katana_rpc_types::Class::Sierra(_) => { - 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: json, - }, - ) + crate::protos::starknet::get_class_at_response::Result::ContractClass(json) } katana_rpc_types::Class::Legacy(_) => { crate::protos::starknet::get_class_at_response::Result::DeprecatedContractClass( - crate::protos::types::DeprecatedContractClass { - program: String::new(), - entry_points_by_type: None, - abi: json, - }, + json, ) } }; diff --git a/crates/sync/stage/src/classes/downloader.rs b/crates/sync/stage/src/classes/downloader.rs index c51f9a5cc..706e3d264 100644 --- a/crates/sync/stage/src/classes/downloader.rs +++ b/crates/sync/stage/src/classes/downloader.rs @@ -187,8 +187,8 @@ pub mod grpc { /// Deserializes a `Class` from the gRPC response. /// - /// The server serializes the full class as JSON into the `abi` field of the - /// proto message (because the class types are not compatible with non-human- + /// The server serializes the full class as JSON bytes into the oneof + /// variant (because the class types are not compatible with non-human- /// readable serialization formats). fn class_from_proto(resp: katana_grpc::proto::GetClassResponse) -> anyhow::Result { use katana_grpc::proto::get_class_response::Result as ClassResult; @@ -197,11 +197,12 @@ pub mod grpc { resp.result.ok_or_else(|| anyhow::anyhow!("missing class result in gRPC response"))?; let json = match &result { - ClassResult::ContractClass(c) => &c.abi, - ClassResult::DeprecatedContractClass(c) => &c.abi, + ClassResult::ContractClass(bytes) | ClassResult::DeprecatedContractClass(bytes) => { + bytes + } }; - let class: Class = serde_json::from_str(json)?; + let class: Class = serde_json::from_slice(json)?; Ok(class) } } From 8b215a219adb8e043e497a7d41cd84323f9e69c4 Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Sun, 22 Mar 2026 12:50:59 -0500 Subject: [PATCH 5/7] fmt --- Cargo.lock | 2 + crates/grpc/Cargo.toml | 2 + crates/grpc/tests/starknet.rs | 119 +++++++++++++++------------------- 3 files changed, 58 insertions(+), 65 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 981b21c06..264ecfd29 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6479,6 +6479,7 @@ dependencies = [ "katana-rpc-api", "katana-rpc-server", "katana-rpc-types", + "katana-starknet", "katana-utils", "num-bigint", "prost 0.12.6", @@ -6491,6 +6492,7 @@ dependencies = [ "tonic-reflection", "tower-service", "tracing", + "url", ] [[package]] diff --git a/crates/grpc/Cargo.toml b/crates/grpc/Cargo.toml index 23717a83d..4afc39de4 100644 --- a/crates/grpc/Cargo.toml +++ b/crates/grpc/Cargo.toml @@ -42,6 +42,8 @@ num-bigint.workspace = true tower-service.workspace = true [dev-dependencies] +katana-starknet.workspace = true +url.workspace = true tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } hex = "0.4" katana-utils = { workspace = true, features = ["node"] } diff --git a/crates/grpc/tests/starknet.rs b/crates/grpc/tests/starknet.rs index 4b78f1c90..90582b2b3 100644 --- a/crates/grpc/tests/starknet.rs +++ b/crates/grpc/tests/starknet.rs @@ -5,14 +5,18 @@ use katana_grpc::proto::{ SpecVersionRequest, SyncingRequest, }; use katana_grpc::GrpcClient; -use katana_primitives::Felt; +use katana_primitives::block::BlockIdOrTag; +use katana_primitives::transaction::TxHash; +use katana_primitives::{ContractAddress, Felt}; +use katana_rpc_types::{ + Class, EventFilter, GetBlockWithTxHashesResponse, MaybePreConfirmedBlock, StateUpdate, + SyncingResponse, +}; +use katana_starknet::rpc::Client as StarknetClient; use katana_utils::node::TestNode; -use starknet::core::types::{BlockId, BlockTag as StarknetBlockTag, EventFilter}; -use starknet::providers::jsonrpc::HttpTransport; -use starknet::providers::{JsonRpcClient, Provider, Url}; use tonic::Request; -async fn setup() -> (TestNode, GrpcClient, JsonRpcClient) { +async fn setup() -> (TestNode, GrpcClient, StarknetClient) { let node = TestNode::new_with_spawn_and_move_db().await; let grpc_addr = *node.grpc_addr().expect("grpc not enabled"); @@ -20,17 +24,15 @@ async fn setup() -> (TestNode, GrpcClient, JsonRpcClient) { .await .expect("failed to connect to gRPC server"); - let rpc_addr = *node.rpc_addr(); - let url = Url::parse(&format!("http://{rpc_addr}")).expect("failed to parse url"); - let rpc = JsonRpcClient::new(HttpTransport::new(url)); + let rpc_client = node.starknet_rpc_client(); - (node, grpc, rpc) + (node, grpc, rpc_client) } -fn genesis_address(node: &TestNode) -> Felt { +fn genesis_address(node: &TestNode) -> ContractAddress { let (address, _) = node.backend().chain_spec.genesis().accounts().next().expect("must have genesis account"); - (*address).into() + *address } fn felt_to_proto(felt: Felt) -> katana_grpc::proto::Felt { @@ -76,7 +78,7 @@ async fn test_chain_id() { async fn test_block_number() { let (_node, mut grpc, rpc) = setup().await; - let rpc_block_number = rpc.block_number().await.expect("rpc block_number failed"); + let rpc_block_number = rpc.block_number().await.expect("rpc block_number failed").block_number; let grpc_block_number = grpc .block_number(Request::new(BlockNumberRequest {})) @@ -110,7 +112,7 @@ async fn test_block_hash_and_number() { async fn test_get_block_with_txs() { let (_node, mut grpc, rpc) = setup().await; - let rpc_block = rpc.get_block_with_txs(BlockId::Number(0)).await.expect("rpc failed"); + let rpc_block = rpc.get_block_with_txs(BlockIdOrTag::Number(0)).await.expect("rpc failed"); let grpc_result = grpc .get_block_with_txs(Request::new(GetBlockRequest { block_id: grpc_block_id_number(0) })) @@ -119,7 +121,7 @@ async fn test_get_block_with_txs() { .into_inner(); let rpc_block = match rpc_block { - starknet::core::types::MaybePreConfirmedBlockWithTxs::Block(b) => b, + MaybePreConfirmedBlock::Confirmed(b) => b, _ => panic!("Expected confirmed block from rpc"), }; @@ -141,7 +143,8 @@ async fn test_get_block_with_txs() { async fn test_get_block_with_tx_hashes() { let (_node, mut grpc, rpc) = setup().await; - let rpc_block = rpc.get_block_with_tx_hashes(BlockId::Number(0)).await.expect("rpc failed"); + let rpc_block = + rpc.get_block_with_tx_hashes(BlockIdOrTag::Number(0)).await.expect("rpc failed"); let grpc_result = grpc .get_block_with_tx_hashes(Request::new(GetBlockRequest { @@ -152,7 +155,7 @@ async fn test_get_block_with_tx_hashes() { .into_inner(); let rpc_block = match rpc_block { - starknet::core::types::MaybePreConfirmedBlockWithTxHashes::Block(b) => b, + GetBlockWithTxHashesResponse::Block(b) => b, _ => panic!("Expected confirmed block from rpc"), }; @@ -180,8 +183,7 @@ async fn test_get_block_with_tx_hashes() { async fn test_get_block_with_txs_latest() { let (_node, mut grpc, rpc) = setup().await; - let rpc_block = - rpc.get_block_with_txs(BlockId::Tag(StarknetBlockTag::Latest)).await.expect("rpc failed"); + let rpc_block = rpc.get_block_with_txs(BlockIdOrTag::Latest).await.expect("rpc failed"); let grpc_result = grpc .get_block_with_txs(Request::new(GetBlockRequest { block_id: grpc_block_id_latest() })) @@ -190,7 +192,7 @@ async fn test_get_block_with_txs_latest() { .into_inner(); let rpc_block = match rpc_block { - starknet::core::types::MaybePreConfirmedBlockWithTxs::Block(b) => b, + MaybePreConfirmedBlock::Confirmed(b) => b, _ => panic!("Expected confirmed block from rpc"), }; @@ -209,25 +211,20 @@ async fn test_get_class_at() { let (node, mut grpc, rpc) = setup().await; let address = genesis_address(&node); - let rpc_class = rpc - .get_class_at(BlockId::Tag(StarknetBlockTag::Latest), address) - .await - .expect("rpc get_class_at failed"); + let rpc_class = + rpc.get_class_at(BlockIdOrTag::Latest, address).await.expect("rpc get_class_at failed"); let grpc_result = grpc .get_class_at(Request::new(GetClassAtRequest { block_id: grpc_block_id_latest(), - contract_address: Some(felt_to_proto(address)), + contract_address: Some(address.into()), })) .await .expect("grpc get_class_at failed") .into_inner(); // Verify both return a Sierra class (not Legacy) - assert!( - matches!(rpc_class, starknet::core::types::ContractClass::Sierra(_)), - "Expected Sierra class from rpc" - ); + assert!(matches!(rpc_class, Class::Sierra(_)), "Expected Sierra class from rpc"); assert!( matches!( grpc_result.result, @@ -243,14 +240,14 @@ async fn test_get_class_hash_at() { let address = genesis_address(&node); let rpc_class_hash = rpc - .get_class_hash_at(BlockId::Tag(StarknetBlockTag::Latest), address) + .get_class_hash_at(BlockIdOrTag::Latest, address) .await .expect("rpc get_class_hash_at failed"); let grpc_result = grpc .get_class_hash_at(Request::new(GetClassHashAtRequest { block_id: grpc_block_id_latest(), - contract_address: Some(felt_to_proto(address)), + contract_address: Some(address.into()), })) .await .expect("grpc get_class_hash_at failed") @@ -266,15 +263,15 @@ async fn test_get_storage_at() { let address = genesis_address(&node); let rpc_value = rpc - .get_storage_at(address, Felt::ZERO, BlockId::Tag(StarknetBlockTag::Latest)) + .get_storage_at(address, Felt::ZERO, BlockIdOrTag::Latest) .await .expect("rpc get_storage_at failed"); let grpc_result = grpc .get_storage_at(Request::new(GetStorageAtRequest { block_id: grpc_block_id_latest(), - contract_address: Some(felt_to_proto(address)), - key: Some(felt_to_proto(Felt::ZERO)), + contract_address: Some(address.into()), + key: Some(Felt::ZERO.into()), })) .await .expect("grpc get_storage_at failed") @@ -289,15 +286,13 @@ async fn test_get_nonce() { let (node, mut grpc, rpc) = setup().await; let address = genesis_address(&node); - let rpc_nonce = rpc - .get_nonce(BlockId::Tag(StarknetBlockTag::Latest), address) - .await - .expect("rpc get_nonce failed"); + let rpc_nonce = + rpc.get_nonce(BlockIdOrTag::Latest, address).await.expect("rpc get_nonce failed"); let grpc_result = grpc .get_nonce(Request::new(GetNonceRequest { block_id: grpc_block_id_latest(), - contract_address: Some(felt_to_proto(address)), + contract_address: Some(address.into()), })) .await .expect("grpc get_nonce failed") @@ -337,7 +332,7 @@ async fn test_syncing() { .into_inner(); assert!( - matches!(rpc_syncing, starknet::core::types::SyncStatusType::NotSyncing), + matches!(rpc_syncing, SyncingResponse::NotSyncing), "Expected rpc to report not syncing" ); assert!( @@ -354,7 +349,7 @@ async fn test_get_block_transaction_count() { let (_node, mut grpc, rpc) = setup().await; let rpc_count = rpc - .get_block_transaction_count(BlockId::Number(0)) + .get_block_transaction_count(BlockIdOrTag::Number(0)) .await .expect("rpc get_block_transaction_count failed"); @@ -375,7 +370,7 @@ async fn test_get_state_update() { let (_node, mut grpc, rpc) = setup().await; let rpc_state = - rpc.get_state_update(BlockId::Number(0)).await.expect("rpc get_state_update failed"); + rpc.get_state_update(BlockIdOrTag::Number(0)).await.expect("rpc get_state_update failed"); let grpc_result = grpc .get_state_update(Request::new(GetBlockRequest { block_id: grpc_block_id_number(0) })) @@ -384,7 +379,7 @@ async fn test_get_state_update() { .into_inner(); let rpc_state = match rpc_state { - starknet::core::types::MaybePreConfirmedStateUpdate::Update(s) => s, + StateUpdate::Confirmed(s) => s, _ => panic!("Expected confirmed state update from rpc"), }; @@ -414,8 +409,8 @@ async fn test_get_events() { let rpc_events = rpc .get_events( EventFilter { - from_block: Some(BlockId::Number(0)), - to_block: Some(BlockId::Tag(StarknetBlockTag::Latest)), + from_block: Some(BlockIdOrTag::Number(0)), + to_block: Some(BlockIdOrTag::Latest), address: None, keys: None, }, @@ -446,14 +441,13 @@ async fn test_get_events() { let rpc_event = &rpc_events.events[0]; let grpc_event = &grpc_events.events[0]; - assert_eq!( - proto_to_felt(grpc_event.from_address.as_ref().expect("grpc missing from_address")), - rpc_event.from_address - ); - assert_eq!( - proto_to_felt(grpc_event.transaction_hash.as_ref().expect("grpc missing tx_hash")), - rpc_event.transaction_hash - ); + let grpc_event_address: ContractAddress = + grpc_event.from_address.clone().unwrap().try_into().unwrap(); + let grpc_event_tx_hash: ContractAddress = + grpc_event.transaction_hash.clone().unwrap().try_into().unwrap(); + + assert_eq!(grpc_event_address, rpc_event.from_address); + assert_eq!(grpc_event_tx_hash, rpc_event.transaction_hash); assert_eq!(grpc_event.keys.len(), rpc_event.keys.len()); assert_eq!(grpc_event.data.len(), rpc_event.data.len()); } @@ -464,10 +458,10 @@ async fn test_get_transaction_by_hash() { // Get a transaction hash from block 1 let rpc_block = - rpc.get_block_with_tx_hashes(BlockId::Number(1)).await.expect("rpc get_block failed"); + rpc.get_block_with_tx_hashes(BlockIdOrTag::Number(1)).await.expect("rpc get_block failed"); let tx_hash = match rpc_block { - starknet::core::types::MaybePreConfirmedBlockWithTxHashes::Block(b) => { + GetBlockWithTxHashesResponse::Block(b) => { *b.transactions.first().expect("no transactions in block 1") } _ => panic!("Expected confirmed block"), @@ -487,7 +481,7 @@ async fn test_get_transaction_by_hash() { .into_inner(); // Verify RPC returned the correct hash - assert_eq!(*rpc_tx.transaction_hash(), tx_hash); + assert_eq!(rpc_tx.transaction_hash, tx_hash); // Verify gRPC returned a valid transaction let grpc_tx = grpc_result.transaction.expect("grpc missing transaction"); @@ -500,10 +494,10 @@ async fn test_get_transaction_receipt() { // Get a transaction hash from block 1 let rpc_block = - rpc.get_block_with_tx_hashes(BlockId::Number(1)).await.expect("rpc get_block failed"); + rpc.get_block_with_tx_hashes(BlockIdOrTag::Number(1)).await.expect("rpc get_block failed"); let tx_hash = match rpc_block { - starknet::core::types::MaybePreConfirmedBlockWithTxHashes::Block(b) => { + GetBlockWithTxHashesResponse::Block(b) => { *b.transactions.first().expect("no transactions in block 1") } _ => panic!("Expected confirmed block"), @@ -523,14 +517,9 @@ async fn test_get_transaction_receipt() { .into_inner(); let grpc_receipt = grpc_result.receipt.expect("grpc missing receipt"); + let grpc_receipt_tx_hash: TxHash = grpc_receipt.transaction_hash.unwrap().try_into().unwrap(); - // Compare transaction hash - assert_eq!(*rpc_receipt.receipt.transaction_hash(), tx_hash); - assert_eq!( - proto_to_felt(&grpc_receipt.transaction_hash.expect("grpc missing tx_hash")), - tx_hash - ); - - // Compare block number + assert_eq!(rpc_receipt.transaction_hash, tx_hash); + assert_eq!(grpc_receipt_tx_hash, tx_hash); assert_eq!(grpc_receipt.block_number, rpc_receipt.block.block_number()); } From 56434f5ff111ad8c662f56d09ed9956c3373d516 Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Sun, 22 Mar 2026 13:01:38 -0500 Subject: [PATCH 6/7] wip --- crates/grpc/proto/starknet.proto | 6 +++--- crates/grpc/src/handlers/starknet.rs | 13 +++++-------- crates/sync/stage/src/classes/downloader.rs | 11 ++++------- 3 files changed, 12 insertions(+), 18 deletions(-) diff --git a/crates/grpc/proto/starknet.proto b/crates/grpc/proto/starknet.proto index 58c619b2d..7ebe2b6f4 100644 --- a/crates/grpc/proto/starknet.proto +++ b/crates/grpc/proto/starknet.proto @@ -197,9 +197,9 @@ message GetClassRequest { message GetClassResponse { // JSON-serialized class bytes. The class types are not compatible with // non-human-readable serialization, so we use JSON here. - oneof result { - bytes contract_class = 1; - bytes deprecated_contract_class = 2; + oneof Class { + bytes sierra = 1; + bytes legacy = 2; } } diff --git a/crates/grpc/src/handlers/starknet.rs b/crates/grpc/src/handlers/starknet.rs index 9a5f1c192..7e2d01187 100644 --- a/crates/grpc/src/handlers/starknet.rs +++ b/crates/grpc/src/handlers/starknet.rs @@ -13,6 +13,7 @@ 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::get_class_response; use crate::protos::starknet::starknet_server::Starknet; use crate::protos::starknet::starknet_trace_server::StarknetTrace; use crate::protos::starknet::starknet_write_server::StarknetWrite; @@ -246,16 +247,12 @@ where let json = serde_json::to_vec(&class) .map_err(|e| Status::internal(format!("failed to serialize class: {e}")))?; - let result = match class { - katana_rpc_types::Class::Sierra(_) => { - crate::protos::starknet::get_class_response::Result::ContractClass(json) - } - katana_rpc_types::Class::Legacy(_) => { - crate::protos::starknet::get_class_response::Result::DeprecatedContractClass(json) - } + let serialized = match class { + katana_rpc_types::Class::Sierra(_) => get_class_response::Class::Sierra(json), + katana_rpc_types::Class::Legacy(_) => get_class_response::Class::Legacy(json), }; - Ok(Response::new(GetClassResponse { result: Some(result) })) + Ok(Response::new(GetClassResponse { class: Some(serialized) })) } async fn get_class_hash_at( diff --git a/crates/sync/stage/src/classes/downloader.rs b/crates/sync/stage/src/classes/downloader.rs index 706e3d264..4ecf52a34 100644 --- a/crates/sync/stage/src/classes/downloader.rs +++ b/crates/sync/stage/src/classes/downloader.rs @@ -191,19 +191,16 @@ pub mod grpc { /// variant (because the class types are not compatible with non-human- /// readable serialization formats). fn class_from_proto(resp: katana_grpc::proto::GetClassResponse) -> anyhow::Result { - use katana_grpc::proto::get_class_response::Result as ClassResult; + use katana_grpc::proto::get_class_response::Class as ClassResult; let result = - resp.result.ok_or_else(|| anyhow::anyhow!("missing class result in gRPC response"))?; + resp.class.ok_or_else(|| anyhow::anyhow!("missing class result in gRPC response"))?; let json = match &result { - ClassResult::ContractClass(bytes) | ClassResult::DeprecatedContractClass(bytes) => { - bytes - } + ClassResult::Sierra(bytes) | ClassResult::Legacy(bytes) => bytes, }; - let class: Class = serde_json::from_slice(json)?; - Ok(class) + Ok(serde_json::from_slice(json)?) } } From aaff16fbec1468d824a508a20c1c7934a41bd628 Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Sun, 22 Mar 2026 13:09:33 -0500 Subject: [PATCH 7/7] fmt --- crates/grpc/src/handlers/starknet.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/grpc/src/handlers/starknet.rs b/crates/grpc/src/handlers/starknet.rs index 7e2d01187..0c5e8cc7a 100644 --- a/crates/grpc/src/handlers/starknet.rs +++ b/crates/grpc/src/handlers/starknet.rs @@ -13,12 +13,11 @@ 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::get_class_response; 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, + get_class_response, AddDeclareTransactionRequest, AddDeclareTransactionResponse, AddDeployAccountTransactionRequest, AddDeployAccountTransactionResponse, AddInvokeTransactionRequest, AddInvokeTransactionResponse, BlockHashAndNumberRequest, BlockHashAndNumberResponse, BlockNumberRequest, BlockNumberResponse, CallRequest, CallResponse,