openzeppelin_relayer/domain/transaction/stellar/
utils.rs

1//! Utility functions for Stellar transaction domain logic.
2use crate::models::OperationSpec;
3use crate::models::RelayerError;
4use crate::services::StellarProviderTrait;
5use log::info;
6
7/// Returns true if any operation needs simulation (contract invocation, creation, or wasm upload).
8pub fn needs_simulation(operations: &[OperationSpec]) -> bool {
9    operations.iter().any(|op| {
10        matches!(
11            op,
12            OperationSpec::InvokeContract { .. }
13                | OperationSpec::CreateContract { .. }
14                | OperationSpec::UploadWasm { .. }
15        )
16    })
17}
18
19pub fn next_sequence_u64(seq_num: i64) -> Result<u64, RelayerError> {
20    let next_i64 = seq_num
21        .checked_add(1)
22        .ok_or_else(|| RelayerError::ProviderError("sequence overflow".into()))?;
23    u64::try_from(next_i64)
24        .map_err(|_| RelayerError::ProviderError("sequence overflows u64".into()))
25}
26
27pub fn i64_from_u64(value: u64) -> Result<i64, RelayerError> {
28    i64::try_from(value).map_err(|_| RelayerError::ProviderError("u64→i64 overflow".into()))
29}
30
31/// Detects if an error is due to a bad sequence number.
32/// Returns true if the error message contains indicators of sequence number mismatch.
33pub fn is_bad_sequence_error(error_msg: &str) -> bool {
34    let error_lower = error_msg.to_lowercase();
35    error_lower.contains("txbadseq")
36}
37
38/// Fetches the current sequence number from the blockchain and calculates the next usable sequence.
39/// This is a shared helper that can be used by both stellar_relayer and stellar_transaction.
40///
41/// # Returns
42/// The next usable sequence number (on-chain sequence + 1)
43pub async fn fetch_next_sequence_from_chain<P>(
44    provider: &P,
45    relayer_address: &str,
46) -> Result<u64, String>
47where
48    P: StellarProviderTrait,
49{
50    info!(
51        "Fetching sequence from chain for address: {}",
52        relayer_address
53    );
54
55    // Fetch account info from chain
56    let account = provider
57        .get_account(relayer_address)
58        .await
59        .map_err(|e| format!("Failed to fetch account from chain: {}", e))?;
60
61    let on_chain_seq = account.seq_num.0; // Extract the i64 value
62    let next_usable = next_sequence_u64(on_chain_seq)
63        .map_err(|e| format!("Failed to calculate next sequence: {}", e))?;
64
65    info!(
66        "Fetched sequence from chain: on-chain={}, next usable={}",
67        on_chain_seq, next_usable
68    );
69    Ok(next_usable)
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75    use crate::models::AssetSpec;
76    use crate::models::{AuthSpec, ContractSource, WasmSource};
77
78    const TEST_PK: &str = "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF";
79
80    fn payment_op(destination: &str) -> OperationSpec {
81        OperationSpec::Payment {
82            destination: destination.to_string(),
83            amount: 100,
84            asset: AssetSpec::Native,
85        }
86    }
87
88    #[test]
89    fn returns_false_for_only_payment_ops() {
90        let ops = vec![payment_op(TEST_PK)];
91        assert!(!needs_simulation(&ops));
92    }
93
94    #[test]
95    fn returns_true_for_invoke_contract_ops() {
96        let ops = vec![OperationSpec::InvokeContract {
97            contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
98                .to_string(),
99            function_name: "transfer".to_string(),
100            args: vec![],
101            auth: None,
102        }];
103        assert!(needs_simulation(&ops));
104    }
105
106    #[test]
107    fn returns_true_for_upload_wasm_ops() {
108        let ops = vec![OperationSpec::UploadWasm {
109            wasm: WasmSource::Hex {
110                hex: "deadbeef".to_string(),
111            },
112            auth: None,
113        }];
114        assert!(needs_simulation(&ops));
115    }
116
117    #[test]
118    fn returns_true_for_create_contract_ops() {
119        let ops = vec![OperationSpec::CreateContract {
120            source: ContractSource::Address {
121                address: TEST_PK.to_string(),
122            },
123            wasm_hash: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
124                .to_string(),
125            salt: None,
126            constructor_args: None,
127            auth: None,
128        }];
129        assert!(needs_simulation(&ops));
130    }
131
132    #[test]
133    fn returns_true_for_single_invoke_host_function() {
134        let ops = vec![OperationSpec::InvokeContract {
135            contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
136                .to_string(),
137            function_name: "transfer".to_string(),
138            args: vec![],
139            auth: Some(AuthSpec::SourceAccount),
140        }];
141        assert!(needs_simulation(&ops));
142    }
143
144    #[test]
145    fn returns_false_for_multiple_payment_ops() {
146        let ops = vec![payment_op(TEST_PK), payment_op(TEST_PK)];
147        assert!(!needs_simulation(&ops));
148    }
149
150    mod next_sequence_u64_tests {
151        use super::*;
152
153        #[test]
154        fn test_increment() {
155            assert_eq!(next_sequence_u64(0).unwrap(), 1);
156
157            assert_eq!(next_sequence_u64(12345).unwrap(), 12346);
158        }
159
160        #[test]
161        fn test_error_path_overflow_i64_max() {
162            let result = next_sequence_u64(i64::MAX);
163            assert!(result.is_err());
164            match result.unwrap_err() {
165                RelayerError::ProviderError(msg) => assert_eq!(msg, "sequence overflow"),
166                _ => panic!("Unexpected error type"),
167            }
168        }
169    }
170
171    mod i64_from_u64_tests {
172        use super::*;
173
174        #[test]
175        fn test_happy_path_conversion() {
176            assert_eq!(i64_from_u64(0).unwrap(), 0);
177            assert_eq!(i64_from_u64(12345).unwrap(), 12345);
178            assert_eq!(i64_from_u64(i64::MAX as u64).unwrap(), i64::MAX);
179        }
180
181        #[test]
182        fn test_error_path_overflow_u64_max() {
183            let result = i64_from_u64(u64::MAX);
184            assert!(result.is_err());
185            match result.unwrap_err() {
186                RelayerError::ProviderError(msg) => assert_eq!(msg, "u64→i64 overflow"),
187                _ => panic!("Unexpected error type"),
188            }
189        }
190
191        #[test]
192        fn test_edge_case_just_above_i64_max() {
193            // Smallest u64 value that will overflow i64
194            let value = (i64::MAX as u64) + 1;
195            let result = i64_from_u64(value);
196            assert!(result.is_err());
197            match result.unwrap_err() {
198                RelayerError::ProviderError(msg) => assert_eq!(msg, "u64→i64 overflow"),
199                _ => panic!("Unexpected error type"),
200            }
201        }
202    }
203
204    mod is_bad_sequence_error_tests {
205        use super::*;
206
207        #[test]
208        fn test_detects_txbadseq() {
209            assert!(is_bad_sequence_error(
210                "Failed to send transaction: transaction submission failed: TxBadSeq"
211            ));
212            assert!(is_bad_sequence_error("Error: TxBadSeq"));
213            assert!(is_bad_sequence_error("txbadseq"));
214            assert!(is_bad_sequence_error("TXBADSEQ"));
215        }
216
217        #[test]
218        fn test_returns_false_for_other_errors() {
219            assert!(!is_bad_sequence_error("network timeout"));
220            assert!(!is_bad_sequence_error("insufficient balance"));
221            assert!(!is_bad_sequence_error("tx_insufficient_fee"));
222            assert!(!is_bad_sequence_error("bad_auth"));
223            assert!(!is_bad_sequence_error(""));
224        }
225    }
226}