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(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 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 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 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, 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 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 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}