starknet_providers/sequencer/
mod.rs

1use crate::provider::ProviderError;
2
3use log::trace;
4use reqwest::{Client, Error as ReqwestError, StatusCode};
5use serde::{de::DeserializeOwned, Deserialize, Serialize};
6use serde_json::Error as SerdeJsonError;
7use serde_with::serde_as;
8use starknet_core::{
9    chain_id,
10    serde::unsigned_field_element::UfeHex,
11    types::{contract::CompiledClass, Felt, StarknetError},
12};
13use url::Url;
14
15// Sequencer specific model types. Not exposed by design to discourage sequencer usage.
16#[allow(unused)]
17pub mod models;
18use models::{conversions::ConversionError, *};
19
20// Allows sequencer gateway to be used as if it's jsonrpc.
21mod provider;
22
23#[derive(Debug, Clone)]
24pub struct SequencerGatewayProvider {
25    client: Client,
26    gateway_url: Url,
27    feeder_gateway_url: Url,
28    chain_id: Felt,
29    headers: Vec<(String, String)>,
30}
31
32#[derive(Debug, thiserror::Error)]
33pub enum GatewayClientError {
34    /// Network related error
35    #[error(transparent)]
36    Network(#[from] ReqwestError),
37    /// JSON serialization/deserialization error
38    #[error(transparent)]
39    Serde(SerdeJsonError),
40    /// Sequencer error responses not parsable into [`StarknetError`]
41    #[error(transparent)]
42    SequencerError(SequencerError),
43    /// Method is not supported (only when using as [`Provider`](crate::Provider))
44    #[error("method not supported")]
45    MethodNotSupported,
46    /// Model conversion error (only when using as [`Provider`](crate::Provider))
47    #[error("unable to convert gateway models to jsonrpc types")]
48    ModelConversionError,
49    /// Simulating multiple transactions is not supported (only when using as
50    /// [`Provider`](crate::Provider))
51    #[error("simulating multiple transactions not supported")]
52    BulkSimulationNotSupported,
53    /// At least one of the simulation flags is not supported (only when using as
54    /// [`Provider`](crate::Provider))
55    #[error("unsupported simulation flag")]
56    UnsupportedSimulationFlag,
57}
58
59#[derive(Debug, thiserror::Error, Deserialize)]
60#[cfg_attr(feature = "no_unknown_fields", serde(deny_unknown_fields))]
61#[error("{message} ({code:?})")]
62pub struct SequencerError {
63    pub code: ErrorCode,
64    pub message: String,
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
68#[cfg_attr(feature = "no_unknown_fields", serde(deny_unknown_fields))]
69pub enum ErrorCode {
70    #[serde(rename = "StarknetErrorCode.BLOCK_NOT_FOUND")]
71    BlockNotFound,
72    #[serde(rename = "StarknetErrorCode.ENTRY_POINT_NOT_FOUND_IN_CONTRACT")]
73    EntryPointNotFoundInContract,
74    #[serde(rename = "StarknetErrorCode.INVALID_PROGRAM")]
75    InvalidProgram,
76    #[serde(rename = "StarknetErrorCode.TRANSACTION_FAILED")]
77    TransactionFailed,
78    #[serde(rename = "StarknetErrorCode.TRANSACTION_NOT_FOUND")]
79    TransactionNotFound,
80    #[serde(rename = "StarknetErrorCode.UNINITIALIZED_CONTRACT")]
81    UninitializedContract,
82    #[serde(rename = "StarkErrorCode.MALFORMED_REQUEST")]
83    MalformedRequest,
84    #[serde(rename = "StarknetErrorCode.UNDECLARED_CLASS")]
85    UndeclaredClass,
86    #[serde(rename = "StarknetErrorCode.INVALID_TRANSACTION_NONCE")]
87    InvalidTransactionNonce,
88    #[serde(rename = "StarknetErrorCode.VALIDATE_FAILURE")]
89    ValidateFailure,
90    #[serde(rename = "StarknetErrorCode.CLASS_ALREADY_DECLARED")]
91    ClassAlreadyDeclared,
92    #[serde(rename = "StarknetErrorCode.COMPILATION_FAILED")]
93    CompilationFailed,
94    #[serde(rename = "StarknetErrorCode.INVALID_COMPILED_CLASS_HASH")]
95    InvalidCompiledClassHash,
96    #[serde(rename = "StarknetErrorCode.DUPLICATED_TRANSACTION")]
97    DuplicatedTransaction,
98    #[serde(rename = "StarknetErrorCode.INVALID_CONTRACT_CLASS")]
99    InvalidContractClass,
100    #[serde(rename = "StarknetErrorCode.DEPRECATED_ENDPOINT")]
101    DeprecatedEndpoint,
102}
103
104impl SequencerGatewayProvider {
105    pub fn new(
106        gateway_url: impl Into<Url>,
107        feeder_gateway_url: impl Into<Url>,
108        chain_id: Felt,
109    ) -> Self {
110        Self::new_with_client(gateway_url, feeder_gateway_url, chain_id, Client::new())
111    }
112
113    pub fn new_with_client(
114        gateway_url: impl Into<Url>,
115        feeder_gateway_url: impl Into<Url>,
116        chain_id: Felt,
117        client: Client,
118    ) -> Self {
119        Self {
120            client,
121            gateway_url: gateway_url.into(),
122            feeder_gateway_url: feeder_gateway_url.into(),
123            chain_id,
124            headers: vec![],
125        }
126    }
127
128    pub fn starknet_alpha_mainnet() -> Self {
129        Self::new(
130            Url::parse("https://alpha-mainnet.starknet.io/gateway").unwrap(),
131            Url::parse("https://alpha-mainnet.starknet.io/feeder_gateway").unwrap(),
132            chain_id::MAINNET,
133        )
134    }
135
136    pub fn starknet_alpha_sepolia() -> Self {
137        Self::new(
138            Url::parse("https://alpha-sepolia.starknet.io/gateway").unwrap(),
139            Url::parse("https://alpha-sepolia.starknet.io/feeder_gateway").unwrap(),
140            chain_id::SEPOLIA,
141        )
142    }
143
144    /// Consumes the current [`SequencerGatewayProvider`] instance and returns a new one with the
145    /// header appended. Same as calling [`add_header`](fn.add_header).
146    pub fn with_header(self, name: String, value: String) -> Self {
147        let mut headers = self.headers;
148        headers.push((name, value));
149
150        Self {
151            client: self.client,
152            gateway_url: self.gateway_url,
153            feeder_gateway_url: self.feeder_gateway_url,
154            chain_id: self.chain_id,
155            headers,
156        }
157    }
158
159    /// Adds a custom HTTP header to be sent for requests to the sequencer.
160    pub fn add_header(&mut self, name: String, value: String) {
161        self.headers.push((name, value))
162    }
163}
164
165enum GatewayResponse<D> {
166    Data(D),
167    SequencerError(SequencerError),
168}
169
170// Work Felt deserialization
171#[serde_as]
172#[derive(Deserialize)]
173#[serde(untagged)]
174enum RawFieldElementResponse {
175    Data(#[serde_as(as = "UfeHex")] Felt),
176    SequencerError(SequencerError),
177}
178
179impl SequencerGatewayProvider {
180    fn extend_gateway_url(&self, segment: &str) -> Url {
181        let mut url = self.gateway_url.clone();
182        extend_url(&mut url, segment);
183        url
184    }
185
186    fn extend_feeder_gateway_url(&self, segment: &str) -> Url {
187        let mut url = self.feeder_gateway_url.clone();
188        extend_url(&mut url, segment);
189        url
190    }
191
192    async fn send_get_request<T>(&self, url: Url) -> Result<T, ProviderError>
193    where
194        T: DeserializeOwned,
195    {
196        trace!("Sending GET request to sequencer API ({})", url);
197
198        let mut request = self.client.get(url);
199        for (name, value) in &self.headers {
200            request = request.header(name, value);
201        }
202
203        let res = request.send().await.map_err(GatewayClientError::Network)?;
204        if res.status() == StatusCode::TOO_MANY_REQUESTS {
205            Err(ProviderError::RateLimited)
206        } else {
207            let body = res.text().await.map_err(GatewayClientError::Network)?;
208
209            trace!("Response from sequencer API: {}", body);
210
211            Ok(serde_json::from_str(&body).map_err(GatewayClientError::Serde)?)
212        }
213    }
214
215    async fn send_post_request<Q, S>(&self, url: Url, body: &Q) -> Result<S, ProviderError>
216    where
217        Q: Serialize,
218        S: DeserializeOwned,
219    {
220        let request_body = serde_json::to_string(body).map_err(GatewayClientError::Serde)?;
221
222        trace!(
223            "Sending POST request to sequencer API ({}): {}",
224            url,
225            request_body
226        );
227
228        let mut request = self
229            .client
230            .post(url)
231            .header("Content-Type", "application/json")
232            .body(request_body);
233        for (name, value) in &self.headers {
234            request = request.header(name, value);
235        }
236
237        let res = request.send().await.map_err(GatewayClientError::Network)?;
238        if res.status() == StatusCode::TOO_MANY_REQUESTS {
239            Err(ProviderError::RateLimited)
240        } else {
241            let body = res.text().await.map_err(GatewayClientError::Network)?;
242
243            trace!("Response from sequencer API: {}", body);
244
245            Ok(serde_json::from_str(&body).map_err(GatewayClientError::Serde)?)
246        }
247    }
248}
249
250impl SequencerGatewayProvider {
251    #[deprecated(
252        note = "Sequencer-specific functions are deprecated. Use it via the Provider trait instead."
253    )]
254    pub async fn add_transaction(
255        &self,
256        tx: TransactionRequest,
257    ) -> Result<AddTransactionResult, ProviderError> {
258        let request_url = self.extend_gateway_url("add_transaction");
259
260        self.send_post_request::<_, GatewayResponse<_>>(request_url, &tx)
261            .await?
262            .into()
263    }
264
265    #[deprecated(
266        note = "Sequencer-specific functions are deprecated. Use it via the Provider trait instead."
267    )]
268    pub async fn get_contract_addresses(&self) -> Result<ContractAddresses, ProviderError> {
269        let request_url = self.extend_feeder_gateway_url("get_contract_addresses");
270
271        self.send_get_request::<GatewayResponse<_>>(request_url)
272            .await?
273            .into()
274    }
275
276    #[deprecated(
277        note = "Sequencer-specific functions are deprecated. Use it via the Provider trait instead."
278    )]
279    pub async fn get_block(&self, block_identifier: BlockId) -> Result<Block, ProviderError> {
280        let mut request_url = self.extend_feeder_gateway_url("get_block");
281        append_block_id(&mut request_url, block_identifier);
282
283        self.send_get_request::<GatewayResponse<_>>(request_url)
284            .await?
285            .into()
286    }
287
288    #[deprecated(
289        note = "Sequencer-specific functions are deprecated. Use it via the Provider trait instead."
290    )]
291    pub async fn get_state_update(
292        &self,
293        block_identifier: BlockId,
294    ) -> Result<StateUpdate, ProviderError> {
295        let mut request_url = self.extend_feeder_gateway_url("get_state_update");
296        append_block_id(&mut request_url, block_identifier);
297
298        self.send_get_request::<GatewayResponse<_>>(request_url)
299            .await?
300            .into()
301    }
302
303    #[deprecated(
304        note = "Sequencer-specific functions are deprecated. Use it via the Provider trait instead."
305    )]
306    pub async fn get_state_update_with_block(
307        &self,
308        block_identifier: BlockId,
309    ) -> Result<StateUpdateWithBlock, ProviderError> {
310        let mut request_url = self.extend_feeder_gateway_url("get_state_update");
311        append_block_id(&mut request_url, block_identifier);
312        request_url
313            .query_pairs_mut()
314            .append_pair("includeBlock", "true");
315
316        self.send_get_request::<GatewayResponse<_>>(request_url)
317            .await?
318            .into()
319    }
320
321    #[deprecated(
322        note = "Sequencer-specific functions are deprecated. Use it via the Provider trait instead."
323    )]
324    pub async fn get_compiled_class_by_class_hash(
325        &self,
326        class_hash: Felt,
327        block_identifier: BlockId,
328    ) -> Result<CompiledClass, ProviderError> {
329        let mut request_url = self.extend_feeder_gateway_url("get_compiled_class_by_class_hash");
330        request_url
331            .query_pairs_mut()
332            .append_pair("classHash", &format!("{class_hash:#x}"));
333        append_block_id(&mut request_url, block_identifier);
334
335        self.send_get_request::<GatewayResponse<_>>(request_url)
336            .await?
337            .into()
338    }
339
340    #[deprecated(
341        note = "Sequencer-specific functions are deprecated. Use it via the Provider trait instead."
342    )]
343    pub async fn get_class_by_hash(
344        &self,
345        class_hash: Felt,
346        block_identifier: BlockId,
347    ) -> Result<DeployedClass, ProviderError> {
348        let mut request_url = self.extend_feeder_gateway_url("get_class_by_hash");
349        request_url
350            .query_pairs_mut()
351            .append_pair("classHash", &format!("{class_hash:#x}"));
352        append_block_id(&mut request_url, block_identifier);
353
354        self.send_get_request::<GatewayResponse<_>>(request_url)
355            .await?
356            .into()
357    }
358
359    #[deprecated(
360        note = "Sequencer-specific functions are deprecated. Use it via the Provider trait instead."
361    )]
362    pub async fn get_transaction_status(
363        &self,
364        transaction_hash: Felt,
365    ) -> Result<TransactionStatusInfo, ProviderError> {
366        let mut request_url = self.extend_feeder_gateway_url("get_transaction_status");
367        request_url
368            .query_pairs_mut()
369            .append_pair("transactionHash", &format!("{transaction_hash:#x}"));
370
371        self.send_get_request::<GatewayResponse<_>>(request_url)
372            .await?
373            .into()
374    }
375
376    #[deprecated(
377        note = "Sequencer-specific functions are deprecated. Use it via the Provider trait instead."
378    )]
379    pub async fn get_transaction(
380        &self,
381        transaction_hash: Felt,
382    ) -> Result<TransactionInfo, ProviderError> {
383        let mut request_url = self.extend_feeder_gateway_url("get_transaction");
384        request_url
385            .query_pairs_mut()
386            .append_pair("transactionHash", &format!("{transaction_hash:#x}"));
387
388        self.send_get_request::<GatewayResponse<_>>(request_url)
389            .await?
390            .into()
391    }
392
393    #[deprecated(
394        note = "Sequencer-specific functions are deprecated. Use it via the Provider trait instead."
395    )]
396    pub async fn get_block_hash_by_id(&self, block_number: u64) -> Result<Felt, ProviderError> {
397        let mut request_url = self.extend_feeder_gateway_url("get_block_hash_by_id");
398        request_url
399            .query_pairs_mut()
400            .append_pair("blockId", &block_number.to_string());
401
402        self.send_get_request::<RawFieldElementResponse>(request_url)
403            .await?
404            .into()
405    }
406
407    #[deprecated(
408        note = "Sequencer-specific functions are deprecated. Use it via the Provider trait instead."
409    )]
410    pub async fn get_block_id_by_hash(&self, block_hash: Felt) -> Result<u64, ProviderError> {
411        let mut request_url = self.extend_feeder_gateway_url("get_block_id_by_hash");
412        request_url
413            .query_pairs_mut()
414            .append_pair("blockHash", &format!("{block_hash:#x}"));
415
416        self.send_get_request::<GatewayResponse<_>>(request_url)
417            .await?
418            .into()
419    }
420
421    #[deprecated(
422        note = "Sequencer-specific functions are deprecated. Use it via the Provider trait instead."
423    )]
424    pub async fn get_last_batch_id(&self) -> Result<u64, ProviderError> {
425        let request_url = self.extend_feeder_gateway_url("get_last_batch_id");
426
427        self.send_get_request::<GatewayResponse<_>>(request_url)
428            .await?
429            .into()
430    }
431
432    #[deprecated(
433        note = "Sequencer-specific functions are deprecated. Use it via the Provider trait instead."
434    )]
435    pub async fn get_l1_blockchain_id(&self) -> Result<u64, ProviderError> {
436        let request_url = self.extend_feeder_gateway_url("get_l1_blockchain_id");
437
438        self.send_get_request::<GatewayResponse<_>>(request_url)
439            .await?
440            .into()
441    }
442}
443
444impl From<SequencerError> for ProviderError {
445    fn from(value: SequencerError) -> Self {
446        let matching_code = match value.code {
447            ErrorCode::BlockNotFound => Some(StarknetError::BlockNotFound),
448            ErrorCode::EntryPointNotFoundInContract
449            | ErrorCode::InvalidContractClass
450            | ErrorCode::DeprecatedEndpoint
451            | ErrorCode::MalformedRequest
452            | ErrorCode::InvalidProgram => None,
453            ErrorCode::TransactionFailed | ErrorCode::ValidateFailure => {
454                Some(StarknetError::ValidationFailure(value.message.clone()))
455            }
456            ErrorCode::TransactionNotFound | ErrorCode::UninitializedContract => {
457                Some(StarknetError::ContractNotFound)
458            }
459            ErrorCode::UndeclaredClass => Some(StarknetError::ClassHashNotFound),
460            ErrorCode::InvalidTransactionNonce => Some(StarknetError::InvalidTransactionNonce(
461                value.message.clone(),
462            )),
463            ErrorCode::ClassAlreadyDeclared => Some(StarknetError::ClassAlreadyDeclared),
464            ErrorCode::CompilationFailed => {
465                Some(StarknetError::CompilationFailed(value.message.clone()))
466            }
467            ErrorCode::InvalidCompiledClassHash => Some(StarknetError::CompiledClassHashMismatch),
468            ErrorCode::DuplicatedTransaction => Some(StarknetError::DuplicateTx),
469        };
470
471        match matching_code {
472            Some(code) => Self::StarknetError(code),
473            None => GatewayClientError::SequencerError(value).into(),
474        }
475    }
476}
477
478impl From<ConversionError> for ProviderError {
479    fn from(_value: ConversionError) -> Self {
480        Self::Other(Box::new(GatewayClientError::ModelConversionError))
481    }
482}
483
484impl<D> From<GatewayResponse<D>> for Result<D, ProviderError> {
485    fn from(value: GatewayResponse<D>) -> Self {
486        match value {
487            GatewayResponse::Data(data) => Ok(data),
488            GatewayResponse::SequencerError(err) => Err(err.into()),
489        }
490    }
491}
492
493impl From<RawFieldElementResponse> for Result<Felt, ProviderError> {
494    fn from(value: RawFieldElementResponse) -> Self {
495        match value {
496            RawFieldElementResponse::Data(data) => Ok(data),
497            RawFieldElementResponse::SequencerError(err) => Err(err.into()),
498        }
499    }
500}
501
502// We need to manually implement this because `raw_value` doesn't work with `untagged`:
503//   https://github.com/serde-rs/serde/issues/1183
504impl<'de, T> Deserialize<'de> for GatewayResponse<T>
505where
506    T: DeserializeOwned,
507{
508    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
509    where
510        D: serde::Deserializer<'de>,
511    {
512        let temp_value = serde_json::Value::deserialize(deserializer)?;
513        if let Ok(value) = T::deserialize(&temp_value) {
514            return Ok(Self::Data(value));
515        }
516        if let Ok(value) = SequencerError::deserialize(&temp_value) {
517            return Ok(Self::SequencerError(value));
518        }
519        Err(serde::de::Error::custom(
520            "data did not match any variant of enum GatewayResponse",
521        ))
522    }
523}
524
525fn extend_url(url: &mut Url, segment: &str) {
526    url.path_segments_mut()
527        .expect("Invalid base URL")
528        .extend(&[segment]);
529}
530
531fn append_block_id(url: &mut Url, block_identifier: BlockId) {
532    match block_identifier {
533        BlockId::Hash(block_hash) => {
534            url.query_pairs_mut()
535                .append_pair("blockHash", &format!("{block_hash:#x}"));
536        }
537        BlockId::Number(block_number) => {
538            url.query_pairs_mut()
539                .append_pair("blockNumber", &block_number.to_string());
540        }
541        BlockId::Pending => {
542            url.query_pairs_mut().append_pair("blockNumber", "pending");
543        }
544        BlockId::Latest => (), // latest block is implicit
545    };
546}
547
548#[cfg(test)]
549mod tests {
550    use super::*;
551
552    #[test]
553    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
554    fn test_get_class_by_hash_deser_success() {
555        for raw in [
556            include_str!("../../test-data/raw_gateway_responses/get_class_by_hash/1_cairo_0.txt"),
557            include_str!("../../test-data/raw_gateway_responses/get_class_by_hash/3_cairo_1.txt"),
558        ] {
559            serde_json::from_str::<GatewayResponse<DeployedClass>>(raw).unwrap();
560        }
561    }
562
563    #[test]
564    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
565    fn test_get_class_by_hash_deser_not_declared() {
566        match serde_json::from_str::<GatewayResponse<DeployedClass>>(include_str!(
567            "../../test-data/raw_gateway_responses/get_class_by_hash/2_not_declared.txt"
568        ))
569        .unwrap()
570        {
571            GatewayResponse::SequencerError(err) => {
572                assert_eq!(err.code, ErrorCode::UndeclaredClass);
573            }
574            _ => panic!("Unexpected result"),
575        }
576    }
577
578    #[test]
579    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
580    fn test_error_deser_invalid_contract_class() {
581        let error: SequencerError = serde_json::from_str(include_str!(
582            "../../test-data/serde/sequencer_error_invalid_contract_class.json"
583        ))
584        .unwrap();
585
586        assert_eq!(error.code, ErrorCode::InvalidContractClass);
587    }
588}