multiversx_sdk/
retrieve_tx_on_network.rs

1use crate::{
2    data::transaction::{ApiLogs, Events, LogData, TransactionOnNetwork},
3    gateway::{GetTxInfo, GetTxProcessStatus},
4    utils::base64_encode,
5};
6use log::info;
7use multiversx_chain_core::{std::Bech32Address, types::ReturnCode};
8
9use crate::gateway::GatewayAsyncService;
10
11const INITIAL_BACKOFF_DELAY: u64 = 1400;
12const MAX_RETRIES: usize = 8;
13const MAX_BACKOFF_DELAY: u64 = 6000;
14const LOG_IDENTIFIER_SIGNAL_ERROR: &str = "signalError";
15
16/// Retrieves a transaction from the network.
17pub async fn retrieve_tx_on_network<GatewayProxy: GatewayAsyncService>(
18    proxy: &GatewayProxy,
19    tx_hash: String,
20) -> (TransactionOnNetwork, ReturnCode) {
21    let mut retries = 0;
22    let mut backoff_delay = INITIAL_BACKOFF_DELAY;
23    let start_time = proxy.now();
24
25    loop {
26        match proxy.request(GetTxProcessStatus::new(&tx_hash)).await {
27            Ok((status, reason)) => {
28                // checks if transaction status is final
29                match status.as_str() {
30                    "success" => {
31                        // retrieve transaction info with results
32                        let transaction_info_with_results = proxy
33                            .request(GetTxInfo::new(&tx_hash).with_results())
34                            .await
35                            .unwrap();
36
37                        log::info!("Transaction retrieved successfully, with status {status}",);
38                        return (transaction_info_with_results, ReturnCode::Success);
39                    }
40                    "fail" => {
41                        let (error_code, reason) = parse_reason(&reason);
42
43                        let mut failed_transaction: TransactionOnNetwork = proxy
44                            .request(GetTxInfo::new(&tx_hash).with_results())
45                            .await
46                            .unwrap();
47
48                        replace_with_error_message(&mut failed_transaction, &reason);
49
50                        log::error!(
51                            "Transaction failed with error code: {} and message: {reason}",
52                            error_code.as_u64()
53                        );
54
55                        return (failed_transaction, error_code);
56                    }
57                    _ => {
58                        continue;
59                    }
60                }
61            }
62            Err(err) => {
63                retries += 1;
64                if retries >= MAX_RETRIES {
65                    info!("Transaction failed, max retries exceeded: {}", err);
66                    println!("Transaction failed, max retries exceeded: {}", err);
67                    break;
68                }
69
70                let backoff_time = backoff_delay.min(MAX_BACKOFF_DELAY);
71                proxy.sleep(backoff_time).await;
72                backoff_delay *= 2; // exponential backoff
73            }
74        }
75    }
76
77    // retries have been exhausted
78    println!(
79            "Fetching transaction failed and retries exhausted, returning default transaction. Total elapsed time: {:?}s",
80            proxy.elapsed_seconds(&start_time)
81        );
82
83    let error_message = ReturnCode::message(ReturnCode::NetworkTimeout);
84    let failed_transaction: TransactionOnNetwork = create_tx_failed(error_message);
85
86    (failed_transaction, ReturnCode::NetworkTimeout)
87}
88
89pub fn parse_reason(reason: &str) -> (ReturnCode, String) {
90    if reason.is_empty() {
91        return (ReturnCode::UserError, "invalid transaction".to_string());
92    }
93
94    let (code, mut message) = find_code_and_message(reason);
95
96    match code {
97        Some(return_code) => {
98            if message.is_empty() {
99                ReturnCode::message(return_code).clone_into(&mut message);
100            }
101
102            (return_code, message)
103        }
104        None => {
105            if message.is_empty() {
106                message = extract_message_from_string_reason(reason);
107            }
108            let return_code = ReturnCode::from_message(&message).unwrap_or(ReturnCode::UserError);
109
110            (return_code, message)
111        }
112    }
113}
114
115pub fn find_code_and_message(reason: &str) -> (Option<ReturnCode>, String) {
116    let mut error_code: Option<ReturnCode> = None;
117    let mut error_message: String = String::new();
118    let parts: Vec<&str> = reason.split('@').filter(|part| !part.is_empty()).collect();
119
120    for part in &parts {
121        if let Ok(code) = u64::from_str_radix(part, 16) {
122            if error_code.is_none() {
123                error_code = ReturnCode::from_u64(code);
124            }
125        } else if let Ok(hex_decode_error_message) = hex::decode(part) {
126            if let Ok(str) = String::from_utf8(hex_decode_error_message.clone()) {
127                error_message = str;
128            }
129        }
130    }
131
132    (error_code, error_message)
133}
134
135pub fn extract_message_from_string_reason(reason: &str) -> String {
136    let contract_error: Vec<&str> = reason.split('[').filter(|part| !part.is_empty()).collect();
137    if contract_error.len() == 3 {
138        let message: Vec<&str> = contract_error[1].split(']').collect();
139        return message[0].to_string();
140    }
141
142    contract_error.last().unwrap_or(&"").split(']').collect()
143}
144
145fn create_tx_failed(error_message: &str) -> TransactionOnNetwork {
146    let mut failed_transaction_info = TransactionOnNetwork::default();
147
148    let log: ApiLogs = ApiLogs {
149        address: Bech32Address::zero_default_hrp(),
150        events: vec![Events {
151            address: Bech32Address::zero_default_hrp(),
152            identifier: LOG_IDENTIFIER_SIGNAL_ERROR.to_string(),
153            topics: vec![String::new(), base64_encode(error_message.as_bytes())],
154            data: LogData::default(),
155        }],
156    };
157
158    failed_transaction_info.logs = Some(log);
159
160    failed_transaction_info
161}
162
163pub fn replace_with_error_message(tx: &mut TransactionOnNetwork, error_message: &str) {
164    if error_message.is_empty() {
165        return;
166    }
167
168    let error_message_encoded = base64_encode(error_message);
169
170    if let Some(event) = find_log(tx) {
171        if event.topics.len() >= 2 && event.topics[1] != error_message_encoded {
172            event.topics[1] = error_message_encoded;
173        }
174    }
175}
176
177fn find_log(tx: &mut TransactionOnNetwork) -> Option<&mut Events> {
178    if let Some(logs) = tx.logs.as_mut() {
179        logs.events
180            .iter_mut()
181            .find(|event| event.identifier == LOG_IDENTIFIER_SIGNAL_ERROR)
182    } else {
183        None
184    }
185}