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(hash)) => Some(ContractId(hash.0)),
151            _ => None,
152        }
153    }
154}
155
156#[cfg(test)]
157mod tests {
158    use crate::error::SorobanHelperError;
159    use crate::mock::transaction::{
160        create_contract_id_val, mock_transaction_response_with_account_entry,
161        mock_transaction_response_with_return_value,
162    };
163    use crate::parser::{ParseResult, Parser, ParserType};
164    use stellar_rpc_client::GetTransactionResponse;
165    use stellar_xdr::curr::{
166        AccountEntry, InvokeHostFunctionResult, OperationResult, OperationResultTr, ScVal,
167        TransactionResult, TransactionResultExt, TransactionResultResult,
168    };
169
170    #[test]
171    fn test_new_parser() {
172        let parser = Parser::new(ParserType::InvokeFunction);
173        assert!(matches!(parser.parser_type, ParserType::InvokeFunction));
174    }
175
176    #[test]
177    fn test_extract_operation_result_success() {
178        let hash_data = [42u8; 32];
179        let hash = stellar_xdr::curr::Hash(hash_data);
180
181        let parser = Parser::new(ParserType::InvokeFunction);
182        let result = parser
183            .extract_operation_result(&OperationResult::OpInner(
184                OperationResultTr::InvokeHostFunction(InvokeHostFunctionResult::Success(
185                    hash.clone(),
186                )),
187            ))
188            .unwrap();
189
190        assert!(matches!(result, ScVal::Symbol(_)));
191        if let ScVal::Symbol(symbol) = result {
192            assert_eq!(symbol.0.as_slice(), hash_data);
193        }
194    }
195    #[test]
196    fn test_extract_operation_result_non_invoke_function() {
197        let parser = Parser::new(ParserType::InvokeFunction);
198
199        // non-InvokeHostFunction operation result
200        let op_result = OperationResult::OpInner(OperationResultTr::CreateAccount(
201            stellar_xdr::curr::CreateAccountResult::Success,
202        ));
203
204        let extracted = parser.extract_operation_result(&op_result);
205        assert!(extracted.is_none());
206    }
207
208    #[test]
209    fn test_extract_operation_result_non_success() {
210        let parser = Parser::new(ParserType::InvokeFunction);
211
212        // Failed InvokeHostFunction result
213        let op_result = OperationResult::OpInner(OperationResultTr::InvokeHostFunction(
214            InvokeHostFunctionResult::ResourceLimitExceeded,
215        ));
216
217        let extracted = parser.extract_operation_result(&op_result);
218        assert!(extracted.is_none());
219    }
220
221    #[test]
222    fn test_deploy_parser() {
223        let parser = Parser::new(ParserType::Deploy);
224
225        let contract_val = create_contract_id_val();
226        let res = mock_transaction_response_with_return_value(contract_val.clone());
227
228        let result = parser.parse(&res.response);
229        assert!(matches!(result, Ok(ParseResult::Deploy(Some(_)))));
230    }
231
232    #[test]
233    fn test_invoke_function_parser() {
234        let parser = Parser::new(ParserType::InvokeFunction);
235
236        let return_val = ScVal::I32(42);
237        let res = mock_transaction_response_with_return_value(return_val.clone());
238
239        let result = parser.parse(&res.response);
240        assert!(matches!(result, Ok(ParseResult::InvokeFunction(Some(_)))));
241        if let Ok(ParseResult::InvokeFunction(Some(value))) = result {
242            assert_eq!(value, return_val);
243        }
244    }
245
246    #[test]
247    fn test_account_set_options_parser() {
248        let parser = Parser::new(ParserType::AccountSetOptions);
249
250        // Create a mock account entry
251        let account_entry = AccountEntry {
252            account_id: stellar_xdr::curr::AccountId(
253                stellar_xdr::curr::PublicKey::PublicKeyTypeEd25519(stellar_xdr::curr::Uint256(
254                    [0; 32],
255                )),
256            ),
257            balance: 1000,
258            seq_num: 123.into(),
259            num_sub_entries: 0,
260            inflation_dest: None,
261            flags: 0,
262            home_domain: stellar_xdr::curr::String32(vec![].try_into().unwrap()),
263            thresholds: stellar_xdr::curr::Thresholds([0, 0, 0, 0]),
264            signers: stellar_xdr::curr::VecM::default(),
265            ext: stellar_xdr::curr::AccountEntryExt::V0,
266        };
267        let response = mock_transaction_response_with_account_entry(account_entry.clone());
268
269        let result = parser.parse(&response);
270        assert!(matches!(
271            result,
272            Ok(ParseResult::AccountSetOptions(Some(_)))
273        ));
274        if let Ok(ParseResult::AccountSetOptions(Some(acct))) = result {
275            assert_eq!(acct.balance, 1000);
276        }
277    }
278
279    #[test]
280    fn test_no_transaction_result() {
281        let response = GetTransactionResponse {
282            status: "SUCCESS".to_string(),
283            envelope: None,
284            result: None, // This is what we're testing - no result
285            result_meta: None,
286        };
287
288        let parser = Parser::new(ParserType::InvokeFunction);
289        let result = parser.parse(&response);
290        assert!(matches!(
291            result,
292            Err(SorobanHelperError::TransactionFailed(_))
293        ));
294        if let Err(SorobanHelperError::TransactionFailed(msg)) = result {
295            assert!(msg.contains("No transaction result available"));
296        }
297    }
298
299    #[test]
300    fn test_invoke_function_fallback_to_operation_result() {
301        let parser = Parser::new(ParserType::InvokeFunction);
302
303        // Create a transaction with no metadata but with operation results
304        // We simulate a successful transaction but with no result_meta
305        let response = GetTransactionResponse {
306            status: "SUCCESS".to_string(),
307            envelope: None,
308            result_meta: None,
309            result: Some(TransactionResult {
310                fee_charged: 100,
311                result: TransactionResultResult::TxSuccess(vec![].try_into().unwrap()),
312                ext: TransactionResultExt::V0,
313            }),
314        };
315
316        // Test the fallback code path where an operation result is checked
317        // but not found (empty operations)
318        let result = parser.parse(&response);
319        assert!(matches!(result, Ok(ParseResult::InvokeFunction(None))));
320    }
321
322    #[test]
323    fn test_extract_contract_id() {
324        let parser = Parser::new(ParserType::Deploy);
325
326        let sc_val = create_contract_id_val();
327
328        let result = parser.extract_contract_id(&sc_val);
329        assert!(result.is_some());
330
331        let non_contract_val = ScVal::Bool(true);
332        assert!(parser.extract_contract_id(&non_contract_val).is_none());
333    }
334
335    #[test]
336    fn test_deploy_parser_fallback() {
337        let parser = Parser::new(ParserType::Deploy);
338
339        let non_contract_val = ScVal::Bool(true);
340        let res = mock_transaction_response_with_return_value(non_contract_val);
341
342        let result = parser.parse(&res.response);
343        assert!(matches!(result, Ok(ParseResult::Deploy(None))));
344
345        let response_no_meta = GetTransactionResponse {
346            status: "SUCCESS".to_string(),
347            envelope: None,
348            result: Some(TransactionResult {
349                fee_charged: 100,
350                result: TransactionResultResult::TxSuccess(vec![].try_into().unwrap()),
351                ext: TransactionResultExt::V0,
352            }),
353            result_meta: None,
354        };
355
356        let result = parser.parse(&response_no_meta);
357        assert!(matches!(result, Ok(ParseResult::Deploy(None))));
358    }
359}