openzeppelin_relayer/domain/transaction/stellar/
utils.rs1use crate::models::OperationSpec;
3use crate::models::RelayerError;
4use crate::services::StellarProviderTrait;
5use soroban_rs::xdr;
6use tracing::info;
7
8pub fn needs_simulation(operations: &[OperationSpec]) -> bool {
10 operations.iter().any(|op| {
11 matches!(
12 op,
13 OperationSpec::InvokeContract { .. }
14 | OperationSpec::CreateContract { .. }
15 | OperationSpec::UploadWasm { .. }
16 )
17 })
18}
19
20pub fn next_sequence_u64(seq_num: i64) -> Result<u64, RelayerError> {
21 let next_i64 = seq_num
22 .checked_add(1)
23 .ok_or_else(|| RelayerError::ProviderError("sequence overflow".into()))?;
24 u64::try_from(next_i64)
25 .map_err(|_| RelayerError::ProviderError("sequence overflows u64".into()))
26}
27
28pub fn i64_from_u64(value: u64) -> Result<i64, RelayerError> {
29 i64::try_from(value).map_err(|_| RelayerError::ProviderError("u64→i64 overflow".into()))
30}
31
32pub fn is_bad_sequence_error(error_msg: &str) -> bool {
35 let error_lower = error_msg.to_lowercase();
36 error_lower.contains("txbadseq")
37}
38
39pub async fn fetch_next_sequence_from_chain<P>(
45 provider: &P,
46 relayer_address: &str,
47) -> Result<u64, String>
48where
49 P: StellarProviderTrait,
50{
51 info!(
52 "Fetching sequence from chain for address: {}",
53 relayer_address
54 );
55
56 let account = provider
58 .get_account(relayer_address)
59 .await
60 .map_err(|e| format!("Failed to fetch account from chain: {}", e))?;
61
62 let on_chain_seq = account.seq_num.0; let next_usable = next_sequence_u64(on_chain_seq)
64 .map_err(|e| format!("Failed to calculate next sequence: {}", e))?;
65
66 info!(
67 "Fetched sequence from chain: on-chain={}, next usable={}",
68 on_chain_seq, next_usable
69 );
70 Ok(next_usable)
71}
72
73pub fn convert_v0_to_v1_transaction(v0_tx: &xdr::TransactionV0) -> xdr::Transaction {
76 xdr::Transaction {
77 source_account: xdr::MuxedAccount::Ed25519(v0_tx.source_account_ed25519.clone()),
78 fee: v0_tx.fee,
79 seq_num: v0_tx.seq_num.clone(),
80 cond: match v0_tx.time_bounds.clone() {
81 Some(tb) => xdr::Preconditions::Time(tb),
82 None => xdr::Preconditions::None,
83 },
84 memo: v0_tx.memo.clone(),
85 operations: v0_tx.operations.clone(),
86 ext: xdr::TransactionExt::V0,
87 }
88}
89
90pub fn create_signature_payload(
92 envelope: &xdr::TransactionEnvelope,
93 network_id: &xdr::Hash,
94) -> Result<xdr::TransactionSignaturePayload, RelayerError> {
95 let tagged_transaction = match envelope {
96 xdr::TransactionEnvelope::TxV0(e) => {
97 let v1_tx = convert_v0_to_v1_transaction(&e.tx);
99 xdr::TransactionSignaturePayloadTaggedTransaction::Tx(v1_tx)
100 }
101 xdr::TransactionEnvelope::Tx(e) => {
102 xdr::TransactionSignaturePayloadTaggedTransaction::Tx(e.tx.clone())
103 }
104 xdr::TransactionEnvelope::TxFeeBump(e) => {
105 xdr::TransactionSignaturePayloadTaggedTransaction::TxFeeBump(e.tx.clone())
106 }
107 };
108
109 Ok(xdr::TransactionSignaturePayload {
110 network_id: network_id.clone(),
111 tagged_transaction,
112 })
113}
114
115pub fn create_transaction_signature_payload(
117 transaction: &xdr::Transaction,
118 network_id: &xdr::Hash,
119) -> xdr::TransactionSignaturePayload {
120 xdr::TransactionSignaturePayload {
121 network_id: network_id.clone(),
122 tagged_transaction: xdr::TransactionSignaturePayloadTaggedTransaction::Tx(
123 transaction.clone(),
124 ),
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131 use crate::models::AssetSpec;
132 use crate::models::{AuthSpec, ContractSource, WasmSource};
133
134 const TEST_PK: &str = "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF";
135
136 fn payment_op(destination: &str) -> OperationSpec {
137 OperationSpec::Payment {
138 destination: destination.to_string(),
139 amount: 100,
140 asset: AssetSpec::Native,
141 }
142 }
143
144 #[test]
145 fn returns_false_for_only_payment_ops() {
146 let ops = vec![payment_op(TEST_PK)];
147 assert!(!needs_simulation(&ops));
148 }
149
150 #[test]
151 fn returns_true_for_invoke_contract_ops() {
152 let ops = vec![OperationSpec::InvokeContract {
153 contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
154 .to_string(),
155 function_name: "transfer".to_string(),
156 args: vec![],
157 auth: None,
158 }];
159 assert!(needs_simulation(&ops));
160 }
161
162 #[test]
163 fn returns_true_for_upload_wasm_ops() {
164 let ops = vec![OperationSpec::UploadWasm {
165 wasm: WasmSource::Hex {
166 hex: "deadbeef".to_string(),
167 },
168 auth: None,
169 }];
170 assert!(needs_simulation(&ops));
171 }
172
173 #[test]
174 fn returns_true_for_create_contract_ops() {
175 let ops = vec![OperationSpec::CreateContract {
176 source: ContractSource::Address {
177 address: TEST_PK.to_string(),
178 },
179 wasm_hash: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
180 .to_string(),
181 salt: None,
182 constructor_args: None,
183 auth: None,
184 }];
185 assert!(needs_simulation(&ops));
186 }
187
188 #[test]
189 fn returns_true_for_single_invoke_host_function() {
190 let ops = vec![OperationSpec::InvokeContract {
191 contract_address: "CA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJUWDA"
192 .to_string(),
193 function_name: "transfer".to_string(),
194 args: vec![],
195 auth: Some(AuthSpec::SourceAccount),
196 }];
197 assert!(needs_simulation(&ops));
198 }
199
200 #[test]
201 fn returns_false_for_multiple_payment_ops() {
202 let ops = vec![payment_op(TEST_PK), payment_op(TEST_PK)];
203 assert!(!needs_simulation(&ops));
204 }
205
206 mod next_sequence_u64_tests {
207 use super::*;
208
209 #[test]
210 fn test_increment() {
211 assert_eq!(next_sequence_u64(0).unwrap(), 1);
212
213 assert_eq!(next_sequence_u64(12345).unwrap(), 12346);
214 }
215
216 #[test]
217 fn test_error_path_overflow_i64_max() {
218 let result = next_sequence_u64(i64::MAX);
219 assert!(result.is_err());
220 match result.unwrap_err() {
221 RelayerError::ProviderError(msg) => assert_eq!(msg, "sequence overflow"),
222 _ => panic!("Unexpected error type"),
223 }
224 }
225 }
226
227 mod i64_from_u64_tests {
228 use super::*;
229
230 #[test]
231 fn test_happy_path_conversion() {
232 assert_eq!(i64_from_u64(0).unwrap(), 0);
233 assert_eq!(i64_from_u64(12345).unwrap(), 12345);
234 assert_eq!(i64_from_u64(i64::MAX as u64).unwrap(), i64::MAX);
235 }
236
237 #[test]
238 fn test_error_path_overflow_u64_max() {
239 let result = i64_from_u64(u64::MAX);
240 assert!(result.is_err());
241 match result.unwrap_err() {
242 RelayerError::ProviderError(msg) => assert_eq!(msg, "u64→i64 overflow"),
243 _ => panic!("Unexpected error type"),
244 }
245 }
246
247 #[test]
248 fn test_edge_case_just_above_i64_max() {
249 let value = (i64::MAX as u64) + 1;
251 let result = i64_from_u64(value);
252 assert!(result.is_err());
253 match result.unwrap_err() {
254 RelayerError::ProviderError(msg) => assert_eq!(msg, "u64→i64 overflow"),
255 _ => panic!("Unexpected error type"),
256 }
257 }
258 }
259
260 mod is_bad_sequence_error_tests {
261 use super::*;
262
263 #[test]
264 fn test_detects_txbadseq() {
265 assert!(is_bad_sequence_error(
266 "Failed to send transaction: transaction submission failed: TxBadSeq"
267 ));
268 assert!(is_bad_sequence_error("Error: TxBadSeq"));
269 assert!(is_bad_sequence_error("txbadseq"));
270 assert!(is_bad_sequence_error("TXBADSEQ"));
271 }
272
273 #[test]
274 fn test_returns_false_for_other_errors() {
275 assert!(!is_bad_sequence_error("network timeout"));
276 assert!(!is_bad_sequence_error("insufficient balance"));
277 assert!(!is_bad_sequence_error("tx_insufficient_fee"));
278 assert!(!is_bad_sequence_error("bad_auth"));
279 assert!(!is_bad_sequence_error(""));
280 }
281 }
282
283 #[test]
284 fn test_create_signature_payload_functions() {
285 use xdr::{
286 Hash, SequenceNumber, TransactionEnvelope, TransactionV0, TransactionV0Envelope,
287 Uint256,
288 };
289
290 let transaction = xdr::Transaction {
292 source_account: xdr::MuxedAccount::Ed25519(Uint256([1u8; 32])),
293 fee: 100,
294 seq_num: SequenceNumber(123),
295 cond: xdr::Preconditions::None,
296 memo: xdr::Memo::None,
297 operations: vec![].try_into().unwrap(),
298 ext: xdr::TransactionExt::V0,
299 };
300 let network_id = Hash([2u8; 32]);
301
302 let payload = create_transaction_signature_payload(&transaction, &network_id);
303 assert_eq!(payload.network_id, network_id);
304
305 let v0_tx = TransactionV0 {
307 source_account_ed25519: Uint256([1u8; 32]),
308 fee: 100,
309 seq_num: SequenceNumber(123),
310 time_bounds: None,
311 memo: xdr::Memo::None,
312 operations: vec![].try_into().unwrap(),
313 ext: xdr::TransactionV0Ext::V0,
314 };
315 let v0_envelope = TransactionEnvelope::TxV0(TransactionV0Envelope {
316 tx: v0_tx,
317 signatures: vec![].try_into().unwrap(),
318 });
319
320 let v0_payload = create_signature_payload(&v0_envelope, &network_id).unwrap();
321 assert_eq!(v0_payload.network_id, network_id);
322 }
323
324 mod convert_v0_to_v1_transaction_tests {
325 use super::*;
326 use xdr::{SequenceNumber, TransactionV0, Uint256};
327
328 #[test]
329 fn test_convert_v0_to_v1_transaction() {
330 let v0_tx = TransactionV0 {
332 source_account_ed25519: Uint256([1u8; 32]),
333 fee: 100,
334 seq_num: SequenceNumber(123),
335 time_bounds: None,
336 memo: xdr::Memo::None,
337 operations: vec![].try_into().unwrap(),
338 ext: xdr::TransactionV0Ext::V0,
339 };
340
341 let v1_tx = convert_v0_to_v1_transaction(&v0_tx);
343
344 assert_eq!(v1_tx.fee, v0_tx.fee);
346 assert_eq!(v1_tx.seq_num, v0_tx.seq_num);
347 assert_eq!(v1_tx.memo, v0_tx.memo);
348 assert_eq!(v1_tx.operations, v0_tx.operations);
349 assert!(matches!(v1_tx.ext, xdr::TransactionExt::V0));
350 assert!(matches!(v1_tx.cond, xdr::Preconditions::None));
351
352 match v1_tx.source_account {
354 xdr::MuxedAccount::Ed25519(addr) => {
355 assert_eq!(addr, v0_tx.source_account_ed25519);
356 }
357 _ => panic!("Expected Ed25519 muxed account"),
358 }
359 }
360
361 #[test]
362 fn test_convert_v0_to_v1_transaction_with_time_bounds() {
363 let time_bounds = xdr::TimeBounds {
365 min_time: xdr::TimePoint(100),
366 max_time: xdr::TimePoint(200),
367 };
368
369 let v0_tx = TransactionV0 {
370 source_account_ed25519: Uint256([2u8; 32]),
371 fee: 200,
372 seq_num: SequenceNumber(456),
373 time_bounds: Some(time_bounds.clone()),
374 memo: xdr::Memo::Text("test".try_into().unwrap()),
375 operations: vec![].try_into().unwrap(),
376 ext: xdr::TransactionV0Ext::V0,
377 };
378
379 let v1_tx = convert_v0_to_v1_transaction(&v0_tx);
381
382 match v1_tx.cond {
384 xdr::Preconditions::Time(tb) => {
385 assert_eq!(tb, time_bounds);
386 }
387 _ => panic!("Expected Time preconditions"),
388 }
389 }
390 }
391}