openzeppelin_relayer/domain/transaction/stellar/
utils.rs

1//! Utility functions for Stellar transaction domain logic.
2use crate::models::OperationSpec;
3use crate::models::RelayerError;
4
5/// Returns true if any operation needs simulation (contract invocation, creation, or wasm upload).
6pub fn needs_simulation(operations: &[OperationSpec]) -> bool {
7    operations.iter().any(|op| {
8        matches!(
9            op,
10            OperationSpec::InvokeContract { .. }
11                | OperationSpec::CreateContract { .. }
12                | OperationSpec::UploadWasm { .. }
13        )
14    })
15}
16
17pub fn next_sequence_u64(seq_num: i64) -> Result<u64, RelayerError> {
18    let next_i64 = seq_num
19        .checked_add(1)
20        .ok_or_else(|| RelayerError::ProviderError("sequence overflow".into()))?;
21    u64::try_from(next_i64)
22        .map_err(|_| RelayerError::ProviderError("sequence overflows u64".into()))
23}
24
25pub fn i64_from_u64(value: u64) -> Result<i64, RelayerError> {
26    i64::try_from(value).map_err(|_| RelayerError::ProviderError("u64→i64 overflow".into()))
27}
28
29#[cfg(test)]
30mod tests {
31    use super::*;
32    use crate::models::AssetSpec;
33    use crate::models::{AuthSpec, ContractSource, WasmSource};
34
35    const TEST_PK: &str = "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF";
36
37    fn payment_op(destination: &str) -> OperationSpec {
38        OperationSpec::Payment {
39            destination: destination.to_string(),
40            amount: 100,
41            asset: AssetSpec::Native,
42        }
43    }
44
45    #[test]
46    fn returns_false_for_only_payment_ops() {
47        let ops = vec![payment_op(TEST_PK)];
48        assert!(!needs_simulation(&ops));
49    }
50
51    #[test]
52    fn returns_true_for_invoke_contract_ops() {
53        let ops = vec![OperationSpec::InvokeContract {
54            contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
55                .to_string(),
56            function_name: "transfer".to_string(),
57            args: vec![],
58            auth: None,
59        }];
60        assert!(needs_simulation(&ops));
61    }
62
63    #[test]
64    fn returns_true_for_upload_wasm_ops() {
65        let ops = vec![OperationSpec::UploadWasm {
66            wasm: WasmSource::Hex {
67                hex: "deadbeef".to_string(),
68            },
69            auth: None,
70        }];
71        assert!(needs_simulation(&ops));
72    }
73
74    #[test]
75    fn returns_true_for_create_contract_ops() {
76        let ops = vec![OperationSpec::CreateContract {
77            source: ContractSource::Address {
78                address: TEST_PK.to_string(),
79            },
80            wasm_hash: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
81                .to_string(),
82            salt: None,
83            constructor_args: None,
84            auth: None,
85        }];
86        assert!(needs_simulation(&ops));
87    }
88
89    #[test]
90    fn returns_true_for_single_invoke_host_function() {
91        let ops = vec![OperationSpec::InvokeContract {
92            contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
93                .to_string(),
94            function_name: "transfer".to_string(),
95            args: vec![],
96            auth: Some(AuthSpec::SourceAccount),
97        }];
98        assert!(needs_simulation(&ops));
99    }
100
101    #[test]
102    fn returns_false_for_multiple_payment_ops() {
103        let ops = vec![payment_op(TEST_PK), payment_op(TEST_PK)];
104        assert!(!needs_simulation(&ops));
105    }
106
107    mod next_sequence_u64_tests {
108        use super::*;
109
110        #[test]
111        fn test_increment() {
112            assert_eq!(next_sequence_u64(0).unwrap(), 1);
113
114            assert_eq!(next_sequence_u64(12345).unwrap(), 12346);
115        }
116
117        #[test]
118        fn test_error_path_overflow_i64_max() {
119            let result = next_sequence_u64(i64::MAX);
120            assert!(result.is_err());
121            match result.unwrap_err() {
122                RelayerError::ProviderError(msg) => assert_eq!(msg, "sequence overflow"),
123                _ => panic!("Unexpected error type"),
124            }
125        }
126    }
127
128    mod i64_from_u64_tests {
129        use super::*;
130
131        #[test]
132        fn test_happy_path_conversion() {
133            assert_eq!(i64_from_u64(0).unwrap(), 0);
134            assert_eq!(i64_from_u64(12345).unwrap(), 12345);
135            assert_eq!(i64_from_u64(i64::MAX as u64).unwrap(), i64::MAX);
136        }
137
138        #[test]
139        fn test_error_path_overflow_u64_max() {
140            let result = i64_from_u64(u64::MAX);
141            assert!(result.is_err());
142            match result.unwrap_err() {
143                RelayerError::ProviderError(msg) => assert_eq!(msg, "u64→i64 overflow"),
144                _ => panic!("Unexpected error type"),
145            }
146        }
147
148        #[test]
149        fn test_edge_case_just_above_i64_max() {
150            // Smallest u64 value that will overflow i64
151            let value = (i64::MAX as u64) + 1;
152            let result = i64_from_u64(value);
153            assert!(result.is_err());
154            match result.unwrap_err() {
155                RelayerError::ProviderError(msg) => assert_eq!(msg, "u64→i64 overflow"),
156                _ => panic!("Unexpected error type"),
157            }
158        }
159    }
160}