openzeppelin_relayer/domain/transaction/stellar/
utils.rs1use crate::models::OperationSpec;
3use crate::models::RelayerError;
4use crate::services::StellarProviderTrait;
5use log::info;
6
7pub 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
31pub fn is_bad_sequence_error(error_msg: &str) -> bool {
34 let error_lower = error_msg.to_lowercase();
35 error_lower.contains("txbadseq")
36}
37
38pub 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 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; 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 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}