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#[allow(unused)]
17pub mod models;
18use models::{conversions::ConversionError, *};
19
20mod 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 #[error(transparent)]
36 Network(#[from] ReqwestError),
37 #[error(transparent)]
39 Serde(SerdeJsonError),
40 #[error(transparent)]
42 SequencerError(SequencerError),
43 #[error("method not supported")]
45 MethodNotSupported,
46 #[error("unable to convert gateway models to jsonrpc types")]
48 ModelConversionError,
49 #[error("simulating multiple transactions not supported")]
52 BulkSimulationNotSupported,
53 #[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 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 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#[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
502impl<'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 => (), };
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}