fuels_rs/code_gen/
functions_gen.rs

1use crate::abi_encoder::ABIEncoder;
2use crate::code_gen::custom_types_gen::extract_struct_name_from_abi_property;
3use crate::code_gen::docs_gen::expand_doc;
4use crate::errors::Error;
5use crate::json_abi::{parse_param, ABIParser};
6use crate::types::expand_type;
7use crate::utils::{ident, safe_ident};
8use fuels_core::{ParamType, Selector};
9use inflector::Inflector;
10use sway_types::{Function, Property};
11
12use proc_macro2::{Literal, TokenStream};
13use quote::quote;
14use std::collections::HashMap;
15
16/// Functions used by the Abigen to expand functions defined in an ABI spec.
17
18// TODO: Right now we have an "end-to-end" test suite for the Abigen!
19// under `fuels-abigen/tests/harness.rs`. But it would be nice to have
20// tests at the level of this component.
21
22/// Transforms a function defined in [`Function`] into a [`TokenStream`]
23/// that represents that same function signature as a Rust-native function
24/// declaration.
25/// The actual logic inside the function is the function `method_hash` under
26/// [`Contract`], which is responsible for encoding the function selector
27/// and the function parameters that will be used in the actual contract call.
28///
29/// [`Contract`]: crate::contract::Contract
30pub fn expand_function(
31    function: &Function,
32    abi_parser: &ABIParser,
33    custom_enums: &HashMap<String, Property>,
34    custom_structs: &HashMap<String, Property>,
35) -> Result<TokenStream, Error> {
36    let name = safe_ident(&function.name);
37    let fn_signature = abi_parser.build_fn_selector(&function.name, &function.inputs);
38
39    let encoded = ABIEncoder::encode_function_selector(fn_signature?.as_bytes());
40
41    let tokenized_signature = expand_selector(encoded);
42    let tokenized_output = expand_fn_outputs(&function.outputs)?;
43    let result = quote! { ContractCall<#tokenized_output> };
44
45    let (input, arg) = expand_function_arguments(function, custom_enums, custom_structs)?;
46
47    let doc = expand_doc(&format!(
48        "Calls the contract's `{}` (0x{}) function",
49        function.name,
50        hex::encode(encoded)
51    ));
52
53    // Here we turn `ParamType`s into a custom stringified version that's identical
54    // to how we would declare a `ParamType` in Rust code. Which will then
55    // be used to be tokenized and passed onto `method_hash()`.
56    let mut output_params = vec![];
57    for output in &function.outputs {
58        let mut param_type_str: String = "ParamType::".to_owned();
59        let p = parse_param(output).unwrap();
60        param_type_str.push_str(&p.to_string());
61
62        let tok: proc_macro2::TokenStream = param_type_str.parse().unwrap();
63
64        output_params.push(tok);
65    }
66
67    let output_params_token = quote! { &[#( #output_params ),*] };
68
69    Ok(quote! {
70        #doc
71        pub fn #name(&self #input) -> #result {
72            Contract::method_hash(&self.fuel_client, &self.compiled,
73                #tokenized_signature, #output_params_token, #arg).expect("method not found (this should never happen)")
74        }
75    })
76}
77
78fn expand_selector(selector: Selector) -> TokenStream {
79    let bytes = selector.iter().copied().map(Literal::u8_unsuffixed);
80    quote! { [#( #bytes ),*] }
81}
82
83/// Expands the output of a function, i.e. what comes after `->` in a function
84/// signature.
85fn expand_fn_outputs(outputs: &[Property]) -> Result<TokenStream, Error> {
86    match outputs.len() {
87        0 => Ok(quote! { () }),
88        1 => {
89            // If it's a struct as the type of a function's output, use its
90            // tokenized name only. Otherwise, parse and expand.
91            if outputs[0].type_field.contains("struct ") {
92                let tok: proc_macro2::TokenStream =
93                    extract_struct_name_from_abi_property(&outputs[0])
94                        .parse()
95                        .unwrap();
96                Ok(tok)
97            } else {
98                expand_type(&parse_param(&outputs[0])?)
99            }
100        }
101        _ => {
102            let types = outputs
103                .iter()
104                .map(|param| expand_type(&parse_param(param)?))
105                .collect::<Result<Vec<_>, Error>>()?;
106            Ok(quote! { (#( #types ),*) })
107        }
108    }
109}
110
111/// Expands the arguments in a function declaration and the same arguments as input
112/// to a function call. For instance:
113/// 1. The `my_arg: u32` in `pub fn my_func(my_arg: u32) -> ()`
114/// 2. The `my_arg.into_token()` in `another_fn_call(my_arg.into_token())`
115fn expand_function_arguments(
116    fun: &Function,
117    custom_enums: &HashMap<String, Property>,
118    custom_structs: &HashMap<String, Property>,
119) -> Result<(TokenStream, TokenStream), Error> {
120    let mut args = Vec::with_capacity(fun.inputs.len());
121    let mut call_args = Vec::with_capacity(fun.inputs.len());
122
123    // For each [`Property`] in a function input we expand:
124    // 1. The name of the argument;
125    // 2. The type of the argument;
126    for (i, param) in fun.inputs.iter().enumerate() {
127        // This is a (likely) temporary workaround the fact that
128        // Sway ABI functions require gas, coin amount, and color arguments
129        // pre-pending the user-defined function arguments.
130        // Since these values (gas, coin, color) are configured elsewhere when
131        // creating a contract instance in the SDK, it would be noisy to keep them
132        // in the signature of the function that we're expanding here.
133        // It's the difference between being forced to write:
134        // contract_instance.increment_counter($gas, $coin, $color, 42)
135        // versus simply writing:
136        // contract_instance.increment_counter(42)
137        // Note that _any_ significant change in the way the JSON ABI is generated
138        // could affect this function expansion.
139        if param.name == "gas_" || param.name == "amount_" || param.name == "color_" {
140            continue;
141        }
142        // TokenStream representing the name of the argument
143        let name = expand_input_name(i, &param.name);
144
145        let rust_enum_name = custom_enums.get(&param.name);
146        let rust_struct_name = custom_structs.get(&param.name);
147
148        // TokenStream representing the type of the argument
149        let ty = expand_input_param(
150            fun,
151            &param.name,
152            &parse_param(param)?,
153            &rust_enum_name,
154            &rust_struct_name,
155        )?;
156
157        // Add the TokenStream to argument declarations
158        args.push(quote! { #name: #ty });
159
160        // This `name` TokenStream is also added to the call arguments
161        call_args.push(name);
162    }
163
164    // The final TokenStream of the argument declaration in a function declaration
165    let args = quote! { #( , #args )* };
166
167    // The final TokenStream of the arguments being passed in a function call
168    // It'll look like `&[my_arg.into_token(), another_arg.into_token()]`
169    // as the [`Contract`] `method_hash` function expects a slice of Tokens
170    // in order to encode the call.
171    let call_args = match call_args.len() {
172        0 => quote! { () },
173        _ => quote! { &[ #(#call_args.into_token(), )* ] },
174    };
175
176    Ok((args, call_args))
177}
178
179/// Expands a positional identifier string that may be empty.
180///
181/// Note that this expands the parameter name with `safe_ident`, meaning that
182/// identifiers that are reserved keywords get `_` appended to them.
183pub fn expand_input_name(index: usize, name: &str) -> TokenStream {
184    let name_str = match name {
185        "" => format!("p{}", index),
186        n => n.to_snake_case(),
187    };
188    let name = safe_ident(&name_str);
189
190    quote! { #name }
191}
192
193// Expands the type of an argument being passed in a function declaration.
194// I.e.: `pub fn my_func(my_arg: u32) -> ()`, in this case, `u32` is the
195// type, coming in as a `ParamType::U32`.
196fn expand_input_param(
197    fun: &Function,
198    param: &str,
199    kind: &ParamType,
200    rust_enum_name: &Option<&Property>,
201    rust_struct_name: &Option<&Property>,
202) -> Result<TokenStream, Error> {
203    match kind {
204        ParamType::Array(ty, _) => {
205            let ty = expand_input_param(fun, param, ty, rust_enum_name, rust_struct_name)?;
206            Ok(quote! {
207                ::std::vec::Vec<#ty>
208            })
209        }
210        ParamType::Enum(_) => {
211            let ident = ident(
212                &extract_struct_name_from_abi_property(rust_enum_name.unwrap()).to_class_case(),
213            );
214            Ok(quote! { #ident })
215        }
216        ParamType::Struct(_) => {
217            let ident = ident(
218                &extract_struct_name_from_abi_property(rust_struct_name.unwrap()).to_class_case(),
219            );
220            Ok(quote! { #ident })
221        }
222        // Primitive type
223        _ => expand_type(kind),
224    }
225}