fuels_programs/calls/
receipt_parser.rs

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