fuels_programs/calls/
receipt_parser.rs

1use std::collections::VecDeque;
2
3use fuel_tx::{ContractId, Receipt};
4use fuels_core::{
5    codec::{ABIDecoder, DecoderConfig},
6    types::{
7        bech32::Bech32ContractId,
8        errors::{error, Error, Result},
9        param_types::ParamType,
10        Token,
11    },
12};
13
14pub struct ReceiptParser {
15    receipts: VecDeque<Receipt>,
16    decoder: ABIDecoder,
17}
18
19impl ReceiptParser {
20    pub fn new(receipts: &[Receipt], decoder_config: DecoderConfig) -> Self {
21        let relevant_receipts = receipts
22            .iter()
23            .filter(|receipt| matches!(receipt, Receipt::ReturnData { .. } | Receipt::Call { .. }))
24            .cloned()
25            .collect();
26
27        Self {
28            receipts: relevant_receipts,
29            decoder: ABIDecoder::new(decoder_config),
30        }
31    }
32
33    /// Based on receipts returned by a script transaction, the contract ID,
34    /// and the output param, parse the values and return them as Token.
35    pub fn parse_call(
36        &mut self,
37        contract_id: &Bech32ContractId,
38        output_param: &ParamType,
39    ) -> Result<Token> {
40        let data = self
41            .extract_contract_call_data(contract_id.into())
42            .ok_or_else(|| Self::missing_receipts_error(output_param))?;
43
44        self.decoder.decode(output_param, data.as_slice())
45    }
46
47    pub fn parse_script(self, output_param: &ParamType) -> Result<Token> {
48        let data = self
49            .extract_script_data()
50            .ok_or_else(|| Self::missing_receipts_error(output_param))?;
51
52        self.decoder.decode(output_param, data.as_slice())
53    }
54
55    fn missing_receipts_error(output_param: &ParamType) -> Error {
56        error!(
57            Codec,
58            "`ReceiptDecoder`: failed to find matching receipts entry for {output_param:?}"
59        )
60    }
61
62    pub fn extract_contract_call_data(&mut self, target_contract: ContractId) -> Option<Vec<u8>> {
63        // If the script contains nested calls, we need to extract the data of the top-level call
64        let mut nested_calls_stack = vec![];
65
66        while let Some(receipt) = self.receipts.pop_front() {
67            if let Receipt::Call { to, .. } = receipt {
68                nested_calls_stack.push(to);
69            } else if let Receipt::ReturnData {
70                data,
71                id: return_id,
72                ..
73            } = receipt
74            {
75                let call_id = nested_calls_stack.pop();
76
77                // Somethings off if there is a mismatch between the call and return ids
78                debug_assert_eq!(call_id.unwrap(), return_id);
79
80                if nested_calls_stack.is_empty() {
81                    // The top-level call return should match our target contract
82                    debug_assert_eq!(target_contract, return_id);
83
84                    return data.clone();
85                }
86            }
87        }
88
89        None
90    }
91
92    fn extract_script_data(&self) -> Option<Vec<u8>> {
93        self.receipts.iter().find_map(|receipt| match receipt {
94            Receipt::ReturnData {
95                id,
96                data: Some(data),
97                ..
98            } if *id == ContractId::zeroed() => Some(data.clone()),
99            _ => None,
100        })
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use fuel_tx::ScriptExecutionResult;
107    use fuels_core::traits::{Parameterize, Tokenizable};
108
109    use super::*;
110
111    const RECEIPT_DATA: &[u8; 3] = &[8, 8, 3];
112    const DECODED_DATA: &[u8; 3] = &[8, 8, 3];
113
114    fn target_contract() -> ContractId {
115        ContractId::from([1u8; 32])
116    }
117
118    fn get_return_data_receipt(id: ContractId, data: &[u8]) -> Receipt {
119        Receipt::ReturnData {
120            id,
121            ptr: Default::default(),
122            len: Default::default(),
123            digest: Default::default(),
124            data: Some(data.to_vec()),
125            pc: Default::default(),
126            is: Default::default(),
127        }
128    }
129
130    fn get_call_receipt(to: ContractId) -> Receipt {
131        Receipt::Call {
132            id: Default::default(),
133            to,
134            amount: Default::default(),
135            asset_id: Default::default(),
136            gas: Default::default(),
137            param1: Default::default(),
138            param2: Default::default(),
139            pc: Default::default(),
140            is: Default::default(),
141        }
142    }
143
144    fn get_relevant_receipts() -> Vec<Receipt> {
145        let id = target_contract();
146        vec![
147            get_call_receipt(id),
148            get_return_data_receipt(id, RECEIPT_DATA),
149        ]
150    }
151
152    #[tokio::test]
153    async fn receipt_parser_filters_receipts() -> Result<()> {
154        let mut receipts = vec![
155            Receipt::Revert {
156                id: Default::default(),
157                ra: Default::default(),
158                pc: Default::default(),
159                is: Default::default(),
160            },
161            Receipt::Log {
162                id: Default::default(),
163                ra: Default::default(),
164                rb: Default::default(),
165                rc: Default::default(),
166                rd: Default::default(),
167                pc: Default::default(),
168                is: Default::default(),
169            },
170            Receipt::LogData {
171                id: Default::default(),
172                ra: Default::default(),
173                rb: Default::default(),
174                ptr: Default::default(),
175                len: Default::default(),
176                digest: Default::default(),
177                data: Default::default(),
178                pc: Default::default(),
179                is: Default::default(),
180            },
181            Receipt::ScriptResult {
182                result: ScriptExecutionResult::Success,
183                gas_used: Default::default(),
184            },
185        ];
186        let relevant_receipts = get_relevant_receipts();
187        receipts.extend(relevant_receipts.clone());
188
189        let parser = ReceiptParser::new(&receipts, Default::default());
190
191        assert_eq!(parser.receipts, relevant_receipts);
192
193        Ok(())
194    }
195
196    #[tokio::test]
197    async fn receipt_parser_empty_receipts() -> Result<()> {
198        let receipts = [];
199        let output_param = ParamType::U8;
200
201        let error = ReceiptParser::new(&receipts, Default::default())
202            .parse_call(&target_contract().into(), &output_param)
203            .expect_err("should error");
204
205        let expected_error = ReceiptParser::missing_receipts_error(&output_param);
206        assert_eq!(error.to_string(), expected_error.to_string());
207
208        Ok(())
209    }
210
211    #[tokio::test]
212    async fn receipt_parser_extract_return_data() -> Result<()> {
213        let receipts = get_relevant_receipts();
214        let contract_id = target_contract();
215
216        let mut parser = ReceiptParser::new(&receipts, Default::default());
217
218        let token = parser
219            .parse_call(&contract_id.into(), &<[u8; 3]>::param_type())
220            .expect("parsing should succeed");
221
222        assert_eq!(&<[u8; 3]>::from_token(token)?, DECODED_DATA);
223
224        Ok(())
225    }
226
227    #[tokio::test]
228    async fn receipt_parser_extracts_top_level_call_receipts() -> Result<()> {
229        const CORRECT_DATA_1: [u8; 3] = [1, 2, 3];
230        const CORRECT_DATA_2: [u8; 3] = [5, 6, 7];
231
232        let contract_top_lvl = target_contract();
233        let contract_nested = ContractId::from([9u8; 32]);
234
235        let receipts = vec![
236            get_call_receipt(contract_top_lvl),
237            get_call_receipt(contract_nested),
238            get_return_data_receipt(contract_nested, &[9, 9, 9]),
239            get_return_data_receipt(contract_top_lvl, &CORRECT_DATA_1),
240            get_call_receipt(contract_top_lvl),
241            get_call_receipt(contract_nested),
242            get_return_data_receipt(contract_nested, &[7, 7, 7]),
243            get_return_data_receipt(contract_top_lvl, &CORRECT_DATA_2),
244        ];
245
246        let mut parser = ReceiptParser::new(&receipts, Default::default());
247
248        let token_1 = parser
249            .parse_call(&contract_top_lvl.into(), &<[u8; 3]>::param_type())
250            .expect("parsing should succeed");
251        let token_2 = parser
252            .parse_call(&contract_top_lvl.into(), &<[u8; 3]>::param_type())
253            .expect("parsing should succeed");
254
255        assert_eq!(&<[u8; 3]>::from_token(token_1)?, &CORRECT_DATA_1);
256        assert_eq!(&<[u8; 3]>::from_token(token_2)?, &CORRECT_DATA_2);
257
258        Ok(())
259    }
260}