soroban_rs/
parser.rs

1use crate::error::SorobanHelperError;
2use stellar_rpc_client::GetTransactionResponse;
3use stellar_strkey::Contract as ContractId;
4use stellar_xdr::curr::{
5    AccountEntry, LedgerEntryChange, LedgerEntryData, OperationResult, ScAddress, ScVal,
6    TransactionMeta, TransactionResultResult,
7};
8
9#[derive(Debug)]
10pub enum ParserType {
11    AccountSetOptions,
12    InvokeFunction,
13    Deploy,
14    // Add more parser types as needed
15}
16
17#[derive(Debug)]
18pub enum ParseResult {
19    AccountSetOptions(Option<AccountEntry>),
20    InvokeFunction(Option<ScVal>),
21    Deploy(Option<ContractId>),
22    // Add more result types as needed
23}
24
25pub struct Parser {
26    parser_type: ParserType,
27}
28
29impl Parser {
30    pub fn new(parser_type: ParserType) -> Self {
31        Self { parser_type }
32    }
33
34    pub fn parse(
35        &self,
36        response: &GetTransactionResponse,
37    ) -> Result<ParseResult, SorobanHelperError> {
38        match self.parser_type {
39            ParserType::AccountSetOptions => {
40                self.check_tx_success(&response.result)?;
41
42                // Extract account entry from transaction metadata
43                let result = response
44                    .result_meta
45                    .as_ref()
46                    .and_then(|meta| self.extract_account_entry(meta));
47
48                Ok(ParseResult::AccountSetOptions(result))
49            }
50            ParserType::InvokeFunction => {
51                let op_results = self.check_tx_success(&response.result)?;
52
53                // Try to extract return value from transaction metadata first
54                let result_from_meta = response
55                    .result_meta
56                    .as_ref()
57                    .and_then(|meta| self.extract_return_value(meta))
58                    .map(|value| ParseResult::InvokeFunction(Some(value)));
59                if let Some(result) = result_from_meta {
60                    return Ok(result);
61                }
62
63                let result_from_op_results = op_results
64                    .first()
65                    .and_then(|op| self.extract_operation_result(op))
66                    .map(|value| ParseResult::InvokeFunction(Some(value)));
67                if let Some(result) = result_from_op_results {
68                    return Ok(result);
69                }
70
71                // If we couldn't extract a valid result but transaction succeeded
72                Ok(ParseResult::InvokeFunction(None))
73            }
74            ParserType::Deploy => {
75                self.check_tx_success(&response.result)?;
76
77                // Extract contract hash from transaction metadata
78                let result = response
79                    .result_meta
80                    .as_ref()
81                    .and_then(|meta| self.extract_return_value(meta))
82                    .and_then(|val| self.extract_contract_id(&val))
83                    .map(|contract_id| ParseResult::Deploy(Some(contract_id)));
84
85                if let Some(result) = result {
86                    return Ok(result);
87                }
88
89                // If we couldn't extract a valid result but transaction succeeded
90                Ok(ParseResult::Deploy(None))
91            }
92        }
93    }
94
95    fn check_tx_success<'a>(
96        &self,
97        tx_result: &'a Option<stellar_xdr::curr::TransactionResult>,
98    ) -> Result<&'a [OperationResult], SorobanHelperError> {
99        let tx_result = tx_result.as_ref().ok_or_else(|| {
100            SorobanHelperError::TransactionFailed("No transaction result available".to_string())
101        })?;
102
103        match &tx_result.result {
104            TransactionResultResult::TxSuccess(results) => Ok(results.as_slice()),
105            _ => Err(SorobanHelperError::TransactionFailed(format!(
106                "Transaction failed: {:?}",
107                tx_result.result
108            ))),
109        }
110    }
111
112    fn extract_account_entry(&self, meta: &TransactionMeta) -> Option<AccountEntry> {
113        match meta {
114            TransactionMeta::V3(v3) => v3.operations.last().and_then(|op| {
115                op.changes.0.iter().rev().find_map(|change| match change {
116                    LedgerEntryChange::Updated(entry) => {
117                        if let LedgerEntryData::Account(account) = &entry.data {
118                            Some(account.clone())
119                        } else {
120                            None
121                        }
122                    }
123                    _ => None,
124                })
125            }),
126            _ => None,
127        }
128    }
129
130    fn extract_return_value(&self, meta: &TransactionMeta) -> Option<ScVal> {
131        match meta {
132            TransactionMeta::V3(v3) => v3.soroban_meta.as_ref().map(|sm| sm.return_value.clone()),
133            _ => None,
134        }
135    }
136
137    fn extract_operation_result(&self, op_result: &OperationResult) -> Option<ScVal> {
138        match op_result {
139            OperationResult::OpInner(stellar_xdr::curr::OperationResultTr::InvokeHostFunction(
140                stellar_xdr::curr::InvokeHostFunctionResult::Success(value),
141            )) => Some(ScVal::Symbol(stellar_xdr::curr::ScSymbol(
142                value.0.to_vec().try_into().unwrap_or_default(),
143            ))),
144            _ => None,
145        }
146    }
147
148    fn extract_contract_id(&self, val: &ScVal) -> Option<ContractId> {
149        match val {
150            ScVal::Address(ScAddress::Contract(stellar_xdr::curr::ContractId(hash))) => {
151                Some(ContractId(hash.0))
152            }
153            _ => None,
154        }
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use crate::error::SorobanHelperError;
161    use crate::mock::transaction::{
162        create_contract_id_val, mock_transaction_response_with_account_entry,
163        mock_transaction_response_with_return_value,
164    };
165    use crate::parser::{ParseResult, Parser, ParserType};
166    use stellar_rpc_client::GetTransactionResponse;
167    use stellar_xdr::curr::{
168        AccountEntry, InvokeHostFunctionResult, OperationResult, OperationResultTr, ScVal,
169        TransactionResult, TransactionResultExt, TransactionResultResult,
170    };
171
172    #[test]
173    fn test_new_parser() {
174        let parser = Parser::new(ParserType::InvokeFunction);
175        assert!(matches!(parser.parser_type, ParserType::InvokeFunction));
176    }
177
178    #[test]
179    fn test_extract_operation_result_success() {
180        let hash_data = [42u8; 32];
181        let hash = stellar_xdr::curr::Hash(hash_data);
182
183        let parser = Parser::new(ParserType::InvokeFunction);
184        let result = parser
185            .extract_operation_result(&OperationResult::OpInner(
186                OperationResultTr::InvokeHostFunction(InvokeHostFunctionResult::Success(
187                    hash.clone(),
188                )),
189            ))
190            .unwrap();
191
192        assert!(matches!(result, ScVal::Symbol(_)));
193        if let ScVal::Symbol(symbol) = result {
194            assert_eq!(symbol.0.as_slice(), hash_data);
195        }
196    }
197    #[test]
198    fn test_extract_operation_result_non_invoke_function() {
199        let parser = Parser::new(ParserType::InvokeFunction);
200
201        // non-InvokeHostFunction operation result
202        let op_result = OperationResult::OpInner(OperationResultTr::CreateAccount(
203            stellar_xdr::curr::CreateAccountResult::Success,
204        ));
205
206        let extracted = parser.extract_operation_result(&op_result);
207        assert!(extracted.is_none());
208    }
209
210    #[test]
211    fn test_extract_operation_result_non_success() {
212        let parser = Parser::new(ParserType::InvokeFunction);
213
214        // Failed InvokeHostFunction result
215        let op_result = OperationResult::OpInner(OperationResultTr::InvokeHostFunction(
216            InvokeHostFunctionResult::ResourceLimitExceeded,
217        ));
218
219        let extracted = parser.extract_operation_result(&op_result);
220        assert!(extracted.is_none());
221    }
222
223    #[test]
224    fn test_deploy_parser() {
225        let parser = Parser::new(ParserType::Deploy);
226
227        let contract_val = create_contract_id_val();
228        let res = mock_transaction_response_with_return_value(contract_val.clone());
229
230        let result = parser.parse(&res.response);
231        assert!(matches!(result, Ok(ParseResult::Deploy(Some(_)))));
232    }
233
234    #[test]
235    fn test_invoke_function_parser() {
236        let parser = Parser::new(ParserType::InvokeFunction);
237
238        let return_val = ScVal::I32(42);
239        let res = mock_transaction_response_with_return_value(return_val.clone());
240
241        let result = parser.parse(&res.response);
242        assert!(matches!(result, Ok(ParseResult::InvokeFunction(Some(_)))));
243        if let Ok(ParseResult::InvokeFunction(Some(value))) = result {
244            assert_eq!(value, return_val);
245        }
246    }
247
248    #[test]
249    fn test_account_set_options_parser() {
250        let parser = Parser::new(ParserType::AccountSetOptions);
251
252        // Create a mock account entry
253        let account_entry = AccountEntry {
254            account_id: stellar_xdr::curr::AccountId(
255                stellar_xdr::curr::PublicKey::PublicKeyTypeEd25519(stellar_xdr::curr::Uint256(
256                    [0; 32],
257                )),
258            ),
259            balance: 1000,
260            seq_num: 123.into(),
261            num_sub_entries: 0,
262            inflation_dest: None,
263            flags: 0,
264            home_domain: stellar_xdr::curr::String32(vec![].try_into().unwrap()),
265            thresholds: stellar_xdr::curr::Thresholds([0, 0, 0, 0]),
266            signers: stellar_xdr::curr::VecM::default(),
267            ext: stellar_xdr::curr::AccountEntryExt::V0,
268        };
269        let response = mock_transaction_response_with_account_entry(account_entry.clone());
270
271        let result = parser.parse(&response);
272        assert!(matches!(
273            result,
274            Ok(ParseResult::AccountSetOptions(Some(_)))
275        ));
276        if let Ok(ParseResult::AccountSetOptions(Some(acct))) = result {
277            assert_eq!(acct.balance, 1000);
278        }
279    }
280
281    #[test]
282    fn test_no_transaction_result() {
283        let response = GetTransactionResponse {
284            status: "SUCCESS".to_string(),
285            envelope: None,
286            result: None, // This is what we're testing - no result
287            result_meta: None,
288            ledger: None,
289            events: stellar_rpc_client::GetTransactionEvents {
290                contract_events: vec![],
291                diagnostic_events: vec![],
292                transaction_events: vec![],
293            },
294        };
295
296        let parser = Parser::new(ParserType::InvokeFunction);
297        let result = parser.parse(&response);
298        assert!(matches!(
299            result,
300            Err(SorobanHelperError::TransactionFailed(_))
301        ));
302        if let Err(SorobanHelperError::TransactionFailed(msg)) = result {
303            assert!(msg.contains("No transaction result available"));
304        }
305    }
306
307    #[test]
308    fn test_invoke_function_fallback_to_operation_result() {
309        let parser = Parser::new(ParserType::InvokeFunction);
310
311        // Create a transaction with no metadata but with operation results
312        // We simulate a successful transaction but with no result_meta
313        let response = GetTransactionResponse {
314            status: "SUCCESS".to_string(),
315            envelope: None,
316            result_meta: None,
317            ledger: None,
318            result: Some(TransactionResult {
319                fee_charged: 100,
320                result: TransactionResultResult::TxSuccess(vec![].try_into().unwrap()),
321                ext: TransactionResultExt::V0,
322            }),
323            events: stellar_rpc_client::GetTransactionEvents {
324                contract_events: vec![],
325                diagnostic_events: vec![],
326                transaction_events: vec![],
327            },
328        };
329
330        // Test the fallback code path where an operation result is checked
331        // but not found (empty operations)
332        let result = parser.parse(&response);
333        assert!(matches!(result, Ok(ParseResult::InvokeFunction(None))));
334    }
335
336    #[test]
337    fn test_extract_contract_id() {
338        let parser = Parser::new(ParserType::Deploy);
339
340        let sc_val = create_contract_id_val();
341
342        let result = parser.extract_contract_id(&sc_val);
343        assert!(result.is_some());
344
345        let non_contract_val = ScVal::Bool(true);
346        assert!(parser.extract_contract_id(&non_contract_val).is_none());
347    }
348
349    #[test]
350    fn test_deploy_parser_fallback() {
351        let parser = Parser::new(ParserType::Deploy);
352
353        let non_contract_val = ScVal::Bool(true);
354        let res = mock_transaction_response_with_return_value(non_contract_val);
355
356        let result = parser.parse(&res.response);
357        assert!(matches!(result, Ok(ParseResult::Deploy(None))));
358
359        let response_no_meta = GetTransactionResponse {
360            status: "SUCCESS".to_string(),
361            envelope: None,
362            ledger: None,
363            result: Some(TransactionResult {
364                fee_charged: 100,
365                result: TransactionResultResult::TxSuccess(vec![].try_into().unwrap()),
366                ext: TransactionResultExt::V0,
367            }),
368            result_meta: None,
369            events: stellar_rpc_client::GetTransactionEvents {
370                contract_events: vec![],
371                diagnostic_events: vec![],
372                transaction_events: vec![],
373            },
374        };
375
376        let result = parser.parse(&response_no_meta);
377        assert!(matches!(result, Ok(ParseResult::Deploy(None))));
378    }
379}