starknet_rust_providers/sequencer/models/
transaction.rs

1use serde::{de::Visitor, Deserialize, Serialize};
2use serde_with::serde_as;
3use starknet_rust_core::{
4    serde::unsigned_field_element::{UfeHex, UfePendingBlockHash},
5    types::Felt,
6};
7
8use super::{
9    serde_impls::{u128_hex, u64_hex, u64_hex_opt},
10    transaction_receipt::{TransactionExecutionStatus, TransactionFinalityStatus},
11    TransactionStatus,
12};
13
14#[derive(Debug, Clone, Deserialize)]
15#[serde(tag = "type", rename_all = "SCREAMING_SNAKE_CASE")]
16#[cfg_attr(feature = "no_unknown_fields", serde(deny_unknown_fields))]
17pub enum TransactionType {
18    Declare(DeclareTransaction),
19    Deploy(DeployTransaction),
20    DeployAccount(DeployAccountTransaction),
21    InvokeFunction(InvokeFunctionTransaction),
22    L1Handler(L1HandlerTransaction),
23}
24
25#[serde_as]
26#[derive(Debug, Deserialize)]
27#[cfg_attr(feature = "no_unknown_fields", serde(deny_unknown_fields))]
28pub struct TransactionStatusInfo {
29    #[serde(default)]
30    #[serde_as(as = "UfePendingBlockHash")]
31    pub block_hash: Option<Felt>,
32    #[serde(alias = "tx_status")]
33    pub status: TransactionStatus,
34    // This field is actually always present since v0.12.1, but we're keeping it optional until
35    // mainnet is upgraded.
36    #[serde(default)]
37    pub finality_status: Option<TransactionFinalityStatus>,
38    #[serde(default)]
39    #[serde(alias = "tx_revert_reason")]
40    pub transaction_revert_reason: Option<String>,
41    #[serde(default)]
42    pub execution_status: Option<TransactionExecutionStatus>,
43    #[serde(alias = "tx_failure_reason")]
44    pub transaction_failure_reason: Option<TransactionFailureReason>,
45}
46#[derive(Debug, Deserialize)]
47#[cfg_attr(feature = "no_unknown_fields", serde(deny_unknown_fields))]
48pub struct TransactionFailureReason {
49    pub code: String,
50    pub error_message: Option<String>,
51}
52
53#[serde_as]
54#[derive(Debug, Deserialize)]
55#[cfg_attr(feature = "no_unknown_fields", serde(deny_unknown_fields))]
56pub struct TransactionInfo {
57    #[serde(default)]
58    #[serde_as(as = "UfePendingBlockHash")]
59    pub block_hash: Option<Felt>,
60    pub block_number: Option<u64>,
61    pub status: TransactionStatus,
62    // This field is actually always present since v0.12.1, but we're keeping it optional until
63    // mainnet is upgraded.
64    #[serde(default)]
65    pub finality_status: Option<TransactionFinalityStatus>,
66    #[serde(default)]
67    pub revert_error: Option<String>,
68    #[serde(default)]
69    pub execution_status: Option<TransactionExecutionStatus>,
70    #[serde(rename(deserialize = "transaction"))]
71    pub r#type: Option<TransactionType>,
72    pub transaction_failure_reason: Option<TransactionFailureReason>,
73    pub transaction_index: Option<u64>,
74}
75
76#[derive(Debug, Deserialize, PartialEq, Eq)]
77#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
78#[cfg_attr(feature = "no_unknown_fields", serde(deny_unknown_fields))]
79pub enum EntryPointType {
80    External,
81    L1Handler,
82    Constructor,
83}
84
85#[serde_as]
86#[derive(Debug, Clone, Deserialize)]
87#[cfg_attr(feature = "no_unknown_fields", serde(deny_unknown_fields))]
88pub struct DeclareTransaction {
89    #[serde_as(as = "UfeHex")]
90    pub class_hash: Felt,
91    #[serde_as(as = "Option<UfeHex>")]
92    pub compiled_class_hash: Option<Felt>,
93    #[serde_as(as = "UfeHex")]
94    pub sender_address: Felt,
95    #[serde_as(as = "UfeHex")]
96    pub nonce: Felt,
97    #[serde(default)]
98    #[serde_as(as = "Option<UfeHex>")]
99    pub max_fee: Option<Felt>,
100    #[serde_as(as = "UfeHex")]
101    pub version: Felt,
102    #[serde_as(as = "UfeHex")]
103    pub transaction_hash: Felt,
104    #[serde_as(deserialize_as = "Vec<UfeHex>")]
105    pub signature: Vec<Felt>,
106    pub nonce_data_availability_mode: Option<DataAvailabilityMode>,
107    pub fee_data_availability_mode: Option<DataAvailabilityMode>,
108    pub resource_bounds: Option<ResourceBoundsMapping>,
109    #[serde(default, with = "u64_hex_opt")]
110    pub tip: Option<u64>,
111    #[serde_as(as = "Option<Vec<UfeHex>>")]
112    pub paymaster_data: Option<Vec<Felt>>,
113    #[serde_as(deserialize_as = "Option<Vec<UfeHex>>")]
114    pub account_deployment_data: Option<Vec<Felt>>,
115}
116
117#[serde_as]
118#[derive(Debug, Clone, Deserialize)]
119#[cfg_attr(feature = "no_unknown_fields", serde(deny_unknown_fields))]
120pub struct DeployTransaction {
121    #[serde_as(deserialize_as = "Vec<UfeHex>")]
122    pub constructor_calldata: Vec<Felt>,
123    #[serde_as(as = "UfeHex")]
124    pub contract_address: Felt,
125    #[serde_as(as = "UfeHex")]
126    pub contract_address_salt: Felt,
127    #[serde_as(as = "UfeHex")]
128    pub class_hash: Felt,
129    #[serde_as(as = "UfeHex")]
130    pub transaction_hash: Felt,
131    #[serde_as(as = "UfeHex")]
132    pub version: Felt,
133}
134
135#[serde_as]
136#[derive(Debug, Clone, Deserialize)]
137#[cfg_attr(feature = "no_unknown_fields", serde(deny_unknown_fields))]
138pub struct DeployAccountTransaction {
139    #[serde_as(deserialize_as = "Vec<UfeHex>")]
140    pub constructor_calldata: Vec<Felt>,
141    #[serde(default)]
142    #[serde_as(as = "Option<UfeHex>")]
143    pub contract_address: Option<Felt>,
144    #[serde_as(as = "UfeHex")]
145    pub contract_address_salt: Felt,
146    #[serde_as(as = "UfeHex")]
147    pub class_hash: Felt,
148    #[serde_as(as = "UfeHex")]
149    pub transaction_hash: Felt,
150    #[serde_as(as = "UfeHex")]
151    pub nonce: Felt,
152    #[serde_as(as = "UfeHex")]
153    pub version: Felt,
154    #[serde_as(deserialize_as = "Vec<UfeHex>")]
155    pub signature: Vec<Felt>,
156    #[serde(default)]
157    #[serde_as(as = "Option<UfeHex>")]
158    pub max_fee: Option<Felt>,
159    pub nonce_data_availability_mode: Option<DataAvailabilityMode>,
160    pub fee_data_availability_mode: Option<DataAvailabilityMode>,
161    pub resource_bounds: Option<ResourceBoundsMapping>,
162    #[serde(default, with = "u64_hex_opt")]
163    pub tip: Option<u64>,
164    #[serde_as(as = "Option<Vec<UfeHex>>")]
165    pub paymaster_data: Option<Vec<Felt>>,
166    #[serde(default)]
167    #[serde_as(as = "Option<UfeHex>")]
168    pub sender_address: Option<Felt>,
169}
170
171#[serde_as]
172#[derive(Debug, Clone, Deserialize)]
173#[cfg_attr(feature = "no_unknown_fields", serde(deny_unknown_fields))]
174pub struct InvokeFunctionTransaction {
175    #[serde_as(as = "UfeHex")]
176    // Need this alias because older blocks still use `contract_address`
177    #[serde(alias = "contract_address")]
178    pub sender_address: Felt,
179    #[serde_as(as = "Option<UfeHex>")]
180    pub entry_point_selector: Option<Felt>,
181    #[serde_as(deserialize_as = "Vec<UfeHex>")]
182    pub calldata: Vec<Felt>,
183    #[serde_as(deserialize_as = "Vec<UfeHex>")]
184    pub signature: Vec<Felt>,
185    #[serde_as(as = "UfeHex")]
186    pub transaction_hash: Felt,
187    #[serde(default)]
188    #[serde_as(as = "Option<UfeHex>")]
189    pub max_fee: Option<Felt>,
190    #[serde_as(as = "Option<UfeHex>")]
191    pub nonce: Option<Felt>,
192    pub nonce_data_availability_mode: Option<DataAvailabilityMode>,
193    pub fee_data_availability_mode: Option<DataAvailabilityMode>,
194    pub resource_bounds: Option<ResourceBoundsMapping>,
195    #[serde(default, with = "u64_hex_opt")]
196    pub tip: Option<u64>,
197    #[serde_as(as = "Option<Vec<UfeHex>>")]
198    pub paymaster_data: Option<Vec<Felt>>,
199    #[serde_as(deserialize_as = "Option<Vec<UfeHex>>")]
200    pub account_deployment_data: Option<Vec<Felt>>,
201    #[serde_as(as = "UfeHex")]
202    pub version: Felt,
203}
204
205#[serde_as]
206#[derive(Debug, Clone, Deserialize)]
207#[cfg_attr(feature = "no_unknown_fields", serde(deny_unknown_fields))]
208pub struct L1HandlerTransaction {
209    #[serde_as(as = "UfeHex")]
210    pub contract_address: Felt,
211    #[serde_as(as = "UfeHex")]
212    pub entry_point_selector: Felt,
213    #[serde_as(deserialize_as = "Vec<UfeHex>")]
214    pub calldata: Vec<Felt>,
215    #[serde_as(as = "UfeHex")]
216    pub transaction_hash: Felt,
217    #[serde_as(as = "Option<UfeHex>")]
218    pub nonce: Option<Felt>,
219    #[serde_as(as = "UfeHex")]
220    pub version: Felt,
221}
222
223#[derive(Debug, Clone, Serialize, Deserialize)]
224#[cfg_attr(feature = "no_unknown_fields", serde(deny_unknown_fields))]
225#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
226pub struct ResourceBoundsMapping {
227    #[serde(default)]
228    pub l1_data_gas: ResourceBounds,
229    pub l1_gas: ResourceBounds,
230    pub l2_gas: ResourceBounds,
231}
232
233#[derive(Debug, Default, Clone, Serialize, Deserialize)]
234#[cfg_attr(feature = "no_unknown_fields", serde(deny_unknown_fields))]
235pub struct ResourceBounds {
236    #[serde(with = "u64_hex")]
237    pub max_amount: u64,
238    #[serde(with = "u128_hex")]
239    pub max_price_per_unit: u128,
240}
241
242#[derive(Debug, Clone, Copy)]
243pub enum DataAvailabilityMode {
244    L1,
245    L2,
246}
247
248struct DataAvailabilityModeVisitor;
249
250impl TransactionType {
251    pub const fn transaction_hash(&self) -> Felt {
252        match self {
253            Self::Declare(inner) => inner.transaction_hash,
254            Self::Deploy(inner) => inner.transaction_hash,
255            Self::DeployAccount(inner) => inner.transaction_hash,
256            Self::InvokeFunction(inner) => inner.transaction_hash,
257            Self::L1Handler(inner) => inner.transaction_hash,
258        }
259    }
260}
261
262impl Serialize for DataAvailabilityMode {
263    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
264    where
265        S: serde::Serializer,
266    {
267        serializer.serialize_u32(match self {
268            Self::L1 => 0,
269            Self::L2 => 1,
270        })
271    }
272}
273
274impl<'de> Deserialize<'de> for DataAvailabilityMode {
275    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
276    where
277        D: serde::Deserializer<'de>,
278    {
279        deserializer.deserialize_any(DataAvailabilityModeVisitor)
280    }
281}
282
283impl Visitor<'_> for DataAvailabilityModeVisitor {
284    type Value = DataAvailabilityMode;
285
286    fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
287        write!(formatter, "integer")
288    }
289
290    fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
291    where
292        E: serde::de::Error,
293    {
294        match v {
295            0 => Ok(DataAvailabilityMode::L1),
296            1 => Ok(DataAvailabilityMode::L2),
297            _ => Err(serde::de::Error::invalid_value(
298                serde::de::Unexpected::Unsigned(v),
299                &"0 or 1",
300            )),
301        }
302    }
303}
304
305#[cfg(test)]
306mod tests {
307    use super::*;
308
309    #[test]
310    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
311    fn test_deser_full_invoke_transaction() {
312        let raw =
313            include_str!("../../../test-data/raw_gateway_responses/get_transaction/1_invoke.txt");
314        let tx: TransactionInfo = serde_json::from_str(raw).unwrap();
315
316        assert_eq!(tx.block_number, Some(5));
317        if let TransactionType::InvokeFunction(invoke) = tx.r#type.unwrap() {
318            assert_eq!(invoke.signature.len(), 2);
319        } else {
320            panic!("Did not deserialize TransactionType::InvokeFunction properly")
321        }
322    }
323
324    #[test]
325    #[ignore = "transaction with the same criteria not found in alpha-sepolia yet"]
326    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
327    fn test_deser_full_deploy_transaction() {
328        let raw =
329            include_str!("../../../test-data/raw_gateway_responses/get_transaction/2_deploy.txt");
330        let tx: TransactionInfo = serde_json::from_str(raw).unwrap();
331
332        assert_eq!(tx.block_number, Some(100));
333        if let TransactionType::Deploy(deploy) = tx.r#type.unwrap() {
334            assert_eq!(deploy.constructor_calldata.len(), 2);
335        } else {
336            panic!("Did not deserialize TransactionType::Deploy properly");
337        }
338    }
339
340    #[test]
341    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
342    fn test_deser_not_received() {
343        let raw = include_str!(
344            "../../../test-data/raw_gateway_responses/get_transaction/3_not_received.txt"
345        );
346        let tx: TransactionInfo = serde_json::from_str(raw).unwrap();
347
348        assert_eq!(tx.block_number, None);
349        assert!(tx.status.is_not_received());
350    }
351
352    #[test]
353    #[ignore = "transaction with the same criteria not found in alpha-sepolia yet"]
354    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
355    fn test_deser_failure() {
356        let raw =
357            include_str!("../../../test-data/raw_gateway_responses/get_transaction/4_failure.txt");
358        let tx: TransactionInfo = serde_json::from_str(raw).unwrap();
359
360        assert!(tx.transaction_failure_reason.is_some());
361        let failure_reason = tx.transaction_failure_reason.unwrap();
362        assert_eq!(failure_reason.code, "TRANSACTION_FAILED");
363    }
364
365    #[test]
366    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
367    fn test_deser_declare_v1_transaction() {
368        let raw = include_str!(
369            "../../../test-data/raw_gateway_responses/get_transaction/5_declare_v1.txt"
370        );
371        let tx: TransactionInfo = serde_json::from_str(raw).unwrap();
372
373        match tx.r#type.unwrap() {
374            TransactionType::Declare(_) => {}
375            _ => panic!("Did not deserialize TransactionType::Declare properly"),
376        }
377    }
378
379    #[test]
380    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
381    fn test_deser_declare_v2_transaction() {
382        let raw = include_str!(
383            "../../../test-data/raw_gateway_responses/get_transaction/6_declare_v2.txt"
384        );
385        let tx: TransactionInfo = serde_json::from_str(raw).unwrap();
386
387        match tx.r#type.unwrap() {
388            TransactionType::Declare(_) => {}
389            _ => panic!("Did not deserialize TransactionType::Declare properly"),
390        }
391    }
392
393    #[test]
394    #[ignore = "transaction with the same criteria not found in alpha-sepolia yet"]
395    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
396    fn test_deser_reverted() {
397        let raw =
398            include_str!("../../../test-data/raw_gateway_responses/get_transaction/7_reverted.txt");
399        let tx: TransactionInfo = serde_json::from_str(raw).unwrap();
400
401        match tx.execution_status.unwrap() {
402            TransactionExecutionStatus::Reverted => {}
403            _ => panic!("Unexpected execution status"),
404        }
405    }
406
407    #[test]
408    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
409    fn test_deser_invoke_v3_transaction() {
410        let raw = include_str!(
411            "../../../test-data/raw_gateway_responses/get_transaction/8_invoke_v3.txt"
412        );
413        let tx: TransactionInfo = serde_json::from_str(raw).unwrap();
414
415        match tx.r#type.unwrap() {
416            TransactionType::InvokeFunction(tx) => {
417                assert_eq!(tx.version, Felt::THREE);
418            }
419            _ => panic!("Did not deserialize TransactionType::InvokeFunction properly"),
420        }
421    }
422
423    #[test]
424    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
425    fn test_deser_declare_v3_transaction() {
426        let raw = include_str!(
427            "../../../test-data/raw_gateway_responses/get_transaction/9_declare_v3.txt"
428        );
429        let tx: TransactionInfo = serde_json::from_str(raw).unwrap();
430
431        match tx.r#type.unwrap() {
432            TransactionType::Declare(tx) => {
433                assert_eq!(tx.version, Felt::THREE);
434            }
435            _ => panic!("Did not deserialize TransactionType::Declare properly"),
436        }
437    }
438
439    #[test]
440    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
441    fn test_deser_deploy_account_v3_transaction() {
442        let raw = include_str!(
443            "../../../test-data/raw_gateway_responses/get_transaction/10_deploy_account_v3.txt"
444        );
445        let tx: TransactionInfo = serde_json::from_str(raw).unwrap();
446
447        match tx.r#type.unwrap() {
448            TransactionType::DeployAccount(tx) => {
449                assert_eq!(tx.version, Felt::THREE);
450            }
451            _ => panic!("Did not deserialize TransactionType::DeployAccount properly"),
452        }
453    }
454
455    #[test]
456    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
457    fn test_deser_brief_accepted() {
458        let raw = include_str!(
459            "../../../test-data/raw_gateway_responses/get_transaction_status/1_accepted.txt"
460        );
461
462        let tx: TransactionStatusInfo = serde_json::from_str(raw).unwrap();
463
464        assert!(tx.status.is_accepted_on_l1());
465        assert_eq!(
466            tx.block_hash,
467            Some(
468                Felt::from_hex("0x13b390a0b2c48f907cda28c73a12aa31b96d51bc1be004ba5f71174d8d70e4f")
469                    .unwrap()
470            )
471        );
472    }
473
474    #[test]
475    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
476    fn test_deser_brief_not_received() {
477        let raw = include_str!(
478            "../../../test-data/raw_gateway_responses/get_transaction_status/2_not_received.txt"
479        );
480
481        let tx: TransactionStatusInfo = serde_json::from_str(raw).unwrap();
482
483        assert!(tx.status.is_not_received());
484        assert!(tx.block_hash.is_none());
485    }
486
487    #[test]
488    #[ignore = "transaction with the same criteria not found in alpha-sepolia yet"]
489    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
490    fn test_deser_brief_failure() {
491        let raw = include_str!(
492            "../../../test-data/raw_gateway_responses/get_transaction_status/3_failure.txt"
493        );
494
495        let tx: TransactionStatusInfo = serde_json::from_str(raw).unwrap();
496
497        assert!(tx.status.is_rejected());
498        assert!(tx.block_hash.is_none());
499        assert!(tx.transaction_failure_reason.is_some());
500    }
501
502    #[test]
503    #[ignore = "transaction with the same criteria not found in alpha-sepolia yet"]
504    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
505    fn test_deser_brief_reverted() {
506        let raw = include_str!(
507            "../../../test-data/raw_gateway_responses/get_transaction_status/4_reverted.txt"
508        );
509
510        let tx: TransactionStatusInfo = serde_json::from_str(raw).unwrap();
511
512        assert_eq!(tx.status, TransactionStatus::Reverted);
513        assert!(tx.block_hash.is_some());
514        assert!(tx.transaction_failure_reason.is_none());
515        assert!(tx.transaction_revert_reason.is_some());
516    }
517}