diff --git a/crates/katana/feeder-gateway/src/client.rs b/crates/katana/feeder-gateway/src/client.rs index 78653e6fe0..1ada9be517 100644 --- a/crates/katana/feeder-gateway/src/client.rs +++ b/crates/katana/feeder-gateway/src/client.rs @@ -1,12 +1,16 @@ use katana_primitives::block::{BlockIdOrTag, BlockTag}; use katana_primitives::class::CasmContractClass; use katana_primitives::Felt; +use reqwest::header::{HeaderMap, HeaderValue}; use reqwest::{Client, StatusCode}; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use url::Url; -use crate::types::{ContractClass, StateUpdate, StateUpdateWithBlock}; +use crate::types::{Block, ContractClass, StateUpdate, StateUpdateWithBlock}; + +/// HTTP request header for the feeder gateway API key. This allow bypassing the rate limiting. +const X_THROTTLING_BYPASS: &str = "X-Throttling-Bypass"; #[derive(Debug, thiserror::Error)] pub enum Error { @@ -16,15 +20,22 @@ pub enum Error { #[error(transparent)] Sequencer(SequencerError), - #[error("Request rate limited")] + #[error("failed to parse header value '{value}'")] + InvalidHeaderValue { value: String }, + + #[error("request rate limited")] RateLimited, } /// Client for interacting with the Starknet's feeder gateway. #[derive(Debug, Clone)] pub struct SequencerGateway { + /// The feeder gateway base URL. base_url: Url, - client: Client, + /// The HTTP client used to send the requests. + http_client: Client, + /// The API key used to bypass the rate limiting of the feeder gateway. + api_key: Option, } impl SequencerGateway { @@ -44,8 +55,19 @@ impl SequencerGateway { /// Creates a new gateway client at the given base URL. pub fn new(base_url: Url) -> Self { + let api_key = None; let client = Client::new(); - Self { client, base_url } + Self { http_client: client, base_url, api_key } + } + + /// Sets the API key. + pub fn with_api_key(mut self, api_key: String) -> Self { + self.api_key = Some(api_key); + self + } + + pub async fn get_block(&self, block_id: BlockIdOrTag) -> Result { + self.feeder_gateway("get_block").with_block_id(block_id).send().await } pub async fn get_state_update(&self, block_id: BlockIdOrTag) -> Result { @@ -90,7 +112,7 @@ impl SequencerGateway { fn feeder_gateway(&self, method: &str) -> RequestBuilder<'_> { let mut url = self.base_url.clone(); url.path_segments_mut().expect("invalid base url").extend(["feeder_gateway", method]); - RequestBuilder { client: &self.client, url } + RequestBuilder { gateway_client: self, url } } } @@ -103,7 +125,7 @@ enum Response { #[derive(Debug, Clone)] struct RequestBuilder<'a> { - client: &'a Client, + gateway_client: &'a SequencerGateway, url: Url, } @@ -124,7 +146,17 @@ impl<'a> RequestBuilder<'a> { } async fn send(self) -> Result { - let response = self.client.get(self.url).send().await?; + let mut headers = HeaderMap::new(); + + if let Some(key) = self.gateway_client.api_key.as_ref() { + let value = HeaderValue::from_str(key) + .map_err(|_| Error::InvalidHeaderValue { value: key.to_string() })?; + headers.insert(X_THROTTLING_BYPASS, value); + } + + let response = + self.gateway_client.http_client.get(self.url).headers(headers).send().await?; + if response.status() == StatusCode::TOO_MANY_REQUESTS { Err(Error::RateLimited) } else { @@ -186,9 +218,9 @@ mod tests { #[test] fn request_block_id() { - let client = Client::new(); let base_url = Url::parse("https://example.com/").unwrap(); - let req = RequestBuilder { client: &client, url: base_url }; + let client = SequencerGateway::new(base_url); + let req = client.feeder_gateway("test"); // Test pending block let pending_url = req.clone().with_block_id(BlockIdOrTag::Tag(BlockTag::Pending)).url; @@ -210,9 +242,9 @@ mod tests { #[test] fn multiple_query_params() { - let client = Client::new(); let base_url = Url::parse("https://example.com/").unwrap(); - let req = RequestBuilder { client: &client, url: base_url }; + let client = SequencerGateway::new(base_url); + let req = client.feeder_gateway("test"); let url = req .add_query_param("param1", "value1") @@ -229,9 +261,9 @@ mod tests { #[test] #[ignore] fn request_block_id_overwrite() { - let client = Client::new(); let base_url = Url::parse("https://example.com/").unwrap(); - let req = RequestBuilder { client: &client, url: base_url }; + let client = SequencerGateway::new(base_url); + let req = client.feeder_gateway("test"); let url = req .clone() diff --git a/crates/katana/feeder-gateway/src/types/mod.rs b/crates/katana/feeder-gateway/src/types/mod.rs index d236ae3a67..24e5cb18f4 100644 --- a/crates/katana/feeder-gateway/src/types/mod.rs +++ b/crates/katana/feeder-gateway/src/types/mod.rs @@ -16,6 +16,7 @@ use starknet::core::types::ResourcePrice; use starknet::providers::sequencer::models::BlockStatus; mod receipt; +mod serde_utils; mod transaction; pub use receipt::*; diff --git a/crates/katana/feeder-gateway/src/types/serde_utils.rs b/crates/katana/feeder-gateway/src/types/serde_utils.rs new file mode 100644 index 0000000000..5741f64992 --- /dev/null +++ b/crates/katana/feeder-gateway/src/types/serde_utils.rs @@ -0,0 +1,106 @@ +use serde::de::Visitor; +use serde::Deserialize; + +pub fn deserialize_u64<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + struct U64HexVisitor; + + impl<'de> Visitor<'de> for U64HexVisitor { + type Value = u64; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "0x-prefix hex string or decimal number") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + if let Some(hex) = v.strip_prefix("0x") { + u64::from_str_radix(hex, 16).map_err(serde::de::Error::custom) + } else { + v.parse::().map_err(serde::de::Error::custom) + } + } + } + + deserializer.deserialize_any(U64HexVisitor) +} + +pub fn deserialize_u128<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + struct U128HexVisitor; + + impl<'de> Visitor<'de> for U128HexVisitor { + type Value = u128; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "0x-prefix hex string or decimal number") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + if let Some(hex) = v.strip_prefix("0x") { + u128::from_str_radix(hex, 16).map_err(serde::de::Error::custom) + } else { + v.parse::().map_err(serde::de::Error::custom) + } + } + } + + deserializer.deserialize_any(U128HexVisitor) +} + +pub fn deserialize_optional_u64<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + #[derive(Deserialize)] + #[serde(untagged)] + enum StringOrNum { + String(String), + Number(u64), + } + + match Option::::deserialize(deserializer)? { + None => Ok(None), + Some(StringOrNum::Number(n)) => Ok(Some(n)), + Some(StringOrNum::String(s)) => { + if let Some(hex) = s.strip_prefix("0x") { + u64::from_str_radix(hex, 16).map(Some).map_err(serde::de::Error::custom) + } else { + s.parse().map(Some).map_err(serde::de::Error::custom) + } + } + } +} + +pub fn deserialize_optional_u128<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + #[derive(Deserialize)] + #[serde(untagged)] + enum StringOrNum { + String(String), + Number(u128), + } + + match Option::::deserialize(deserializer)? { + None => Ok(None), + Some(StringOrNum::Number(n)) => Ok(Some(n)), + Some(StringOrNum::String(s)) => { + if let Some(hex) = s.strip_prefix("0x") { + u128::from_str_radix(hex, 16).map(Some).map_err(serde::de::Error::custom) + } else { + s.parse().map(Some).map_err(serde::de::Error::custom) + } + } + } +} diff --git a/crates/katana/feeder-gateway/src/types/transaction.rs b/crates/katana/feeder-gateway/src/types/transaction.rs index 9425d3c10a..a081b22c66 100644 --- a/crates/katana/feeder-gateway/src/types/transaction.rs +++ b/crates/katana/feeder-gateway/src/types/transaction.rs @@ -1,6 +1,5 @@ use katana_primitives::class::{ClassHash, CompiledClassHash}; use katana_primitives::contract::Nonce; -use katana_primitives::fee::ResourceBoundsMapping; use katana_primitives::transaction::{ DeclareTx, DeclareTxV1, DeclareTxV2, DeclareTxV3, DeployAccountTx, DeployAccountTxV1, DeployAccountTxV3, DeployTx, InvokeTx, InvokeTxV0, InvokeTxV1, InvokeTxV3, L1HandlerTx, Tx, @@ -9,6 +8,10 @@ use katana_primitives::transaction::{ use katana_primitives::{ContractAddress, Felt}; use serde::Deserialize; +use super::serde_utils::{ + deserialize_optional_u128, deserialize_optional_u64, deserialize_u128, deserialize_u64, +}; + #[derive(Debug, Deserialize)] pub struct ConfirmedTransaction { #[serde(rename = "transaction_hash")] @@ -53,6 +56,23 @@ impl<'de> Deserialize<'de> for DataAvailabilityMode { } } +// Same reason as `DataAvailabilityMode` above, this struct is also defined because the serde +// implementation of its primitive counterpart is different. +#[derive(Debug, Deserialize)] +pub struct ResourceBounds { + #[serde(deserialize_with = "deserialize_u64")] + pub max_amount: u64, + #[serde(deserialize_with = "deserialize_u128")] + pub max_price_per_unit: u128, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub struct ResourceBoundsMapping { + pub l1_gas: ResourceBounds, + pub l2_gas: ResourceBounds, +} + #[derive(Debug, Deserialize)] pub struct RawInvokeTx { // Alias for v0 transaction @@ -113,7 +133,7 @@ pub struct RawDeployAccountTx { pub nonce: Nonce, pub signature: Vec, pub class_hash: ClassHash, - pub contract_address: ContractAddress, + pub contract_address: Option, pub contract_address_salt: Felt, pub constructor_calldata: Vec, #[serde(default)] @@ -169,54 +189,6 @@ pub enum TxTryFromError { MissingCompiledClassHash, } -fn deserialize_optional_u128<'de, D>(deserializer: D) -> Result, D::Error> -where - D: serde::Deserializer<'de>, -{ - #[derive(Deserialize)] - #[serde(untagged)] - enum StringOrNum { - String(String), - Number(u128), - } - - match Option::::deserialize(deserializer)? { - None => Ok(None), - Some(StringOrNum::Number(n)) => Ok(Some(n)), - Some(StringOrNum::String(s)) => { - if let Some(hex) = s.strip_prefix("0x") { - u128::from_str_radix(hex, 16).map(Some).map_err(serde::de::Error::custom) - } else { - s.parse().map(Some).map_err(serde::de::Error::custom) - } - } - } -} - -fn deserialize_optional_u64<'de, D>(deserializer: D) -> Result, D::Error> -where - D: serde::Deserializer<'de>, -{ - #[derive(Deserialize)] - #[serde(untagged)] - enum StringOrNum { - String(String), - Number(u64), - } - - match Option::::deserialize(deserializer)? { - None => Ok(None), - Some(StringOrNum::Number(n)) => Ok(Some(n)), - Some(StringOrNum::String(s)) => { - if let Some(hex) = s.strip_prefix("0x") { - u64::from_str_radix(hex, 16).map(Some).map_err(serde::de::Error::custom) - } else { - s.parse().map(Some).map_err(serde::de::Error::custom) - } - } - } -} - // -- Conversion to Katana primitive types. impl TryFrom for TxWithHash { @@ -282,7 +254,7 @@ impl TryFrom for InvokeTx { calldata: value.calldata, signature: value.signature, sender_address: value.sender_address, - resource_bounds, + resource_bounds: resource_bounds.into(), account_deployment_data, fee_data_availability_mode: fee_data_availability_mode.into(), nonce_data_availability_mode: nonce_data_availability_mode.into(), @@ -341,7 +313,7 @@ impl TryFrom for DeclareTx { signature: value.signature, class_hash: value.class_hash, compiled_class_hash, - resource_bounds, + resource_bounds: resource_bounds.into(), tip, paymaster_data, account_deployment_data, @@ -364,7 +336,7 @@ impl TryFrom for DeployAccountTx { nonce: value.nonce, signature: value.signature, class_hash: value.class_hash, - contract_address: value.contract_address, + contract_address: value.contract_address.unwrap_or_default(), contract_address_salt: value.contract_address_salt, constructor_calldata: value.constructor_calldata, max_fee: value.max_fee.ok_or(TxTryFromError::MissingMaxFee)?, @@ -385,10 +357,10 @@ impl TryFrom for DeployAccountTx { nonce: value.nonce, signature: value.signature, class_hash: value.class_hash, - contract_address: value.contract_address, + contract_address: value.contract_address.unwrap_or_default(), contract_address_salt: value.contract_address_salt, constructor_calldata: value.constructor_calldata, - resource_bounds, + resource_bounds: resource_bounds.into(), tip, paymaster_data, nonce_data_availability_mode: nonce_data_availability_mode.into(), @@ -408,3 +380,18 @@ impl From for katana_primitives::da::DataAvailabilityMode } } } + +impl From for katana_primitives::fee::ResourceBoundsMapping { + fn from(bounds: ResourceBoundsMapping) -> Self { + Self { + l1_gas: katana_primitives::fee::ResourceBounds { + max_amount: bounds.l1_gas.max_amount, + max_price_per_unit: bounds.l1_gas.max_price_per_unit, + }, + l2_gas: katana_primitives::fee::ResourceBounds { + max_amount: bounds.l2_gas.max_amount, + max_price_per_unit: bounds.l2_gas.max_price_per_unit, + }, + } + } +}