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 }
16
17#[derive(Debug)]
18pub enum ParseResult {
19 AccountSetOptions(Option<AccountEntry>),
20 InvokeFunction(Option<ScVal>),
21 Deploy(Option<ContractId>),
22 }
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 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 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 Ok(ParseResult::InvokeFunction(None))
73 }
74 ParserType::Deploy => {
75 self.check_tx_success(&response.result)?;
76
77 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 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 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 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 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, 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 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 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}