multiversx_sc_meta_lib/contract/sc_config/
contract_variant_builder.rs

1use core::panic;
2use multiversx_sc::abi::{ContractAbi, EndpointAbi};
3use std::{
4    collections::{BTreeSet, HashMap, HashSet},
5    fs,
6    path::{Path, PathBuf},
7};
8
9use crate::{
10    ei::parse_check_ei,
11    print_util::print_sc_config_main_deprecated,
12    tools::{self, OpcodeVersion, RustcVersion},
13};
14
15use super::{
16    contract_variant_settings::{parse_allocator, parse_stack_size},
17    proxy_config::ProxyConfig,
18    sc_config_model::SC_CONFIG_FILE_NAMES,
19    ContractVariant, ContractVariantProfile, ContractVariantSerde, ContractVariantSettings,
20    ProxyConfigSerde, ScConfig, ScConfigSerde,
21};
22
23/// Temporary structure, to help create instances of `ContractVariant`. Not publicly exposed.
24struct ContractVariantBuilder {
25    pub contract_id: String,
26    pub explicit_name: String,
27    pub add_unlabelled: bool,
28    pub add_labels: BTreeSet<String>,
29    pub add_endpoints: BTreeSet<String>,
30    pub collected_endpoints: Vec<EndpointAbi>,
31    endpoint_names: HashSet<String>, // help keep endpoints unique
32    pub settings: ContractVariantSettings,
33}
34
35impl Default for ContractVariantBuilder {
36    fn default() -> Self {
37        Self {
38            contract_id: Default::default(),
39            explicit_name: Default::default(),
40            add_unlabelled: true,
41            add_labels: Default::default(),
42            add_endpoints: Default::default(),
43            collected_endpoints: Default::default(),
44            endpoint_names: Default::default(),
45            settings: Default::default(),
46        }
47    }
48}
49
50impl ContractVariantBuilder {
51    fn new(id: String) -> Self {
52        ContractVariantBuilder {
53            contract_id: id.clone(),
54            explicit_name: id,
55            ..Default::default()
56        }
57    }
58
59    fn map_from_config(kvp: (&String, &ContractVariantSerde)) -> (String, ContractVariantBuilder) {
60        let (contract_id, cms) = kvp;
61        let external_view = cms.external_view.unwrap_or_default();
62        let mut collected_endpoints = Vec::new();
63        if external_view {
64            collected_endpoints.push(
65                multiversx_sc::external_view_contract::external_view_contract_constructor_abi(),
66            )
67        }
68        let default = ContractVariantBuilder::default();
69        (
70            contract_id.clone(),
71            ContractVariantBuilder {
72                contract_id: contract_id.clone(),
73                explicit_name: cms.name.clone().unwrap_or(default.explicit_name),
74                add_unlabelled: cms.add_unlabelled.unwrap_or(default.add_unlabelled),
75                add_labels: cms.add_labels.iter().cloned().collect(),
76                add_endpoints: cms.add_endpoints.iter().cloned().collect(),
77                collected_endpoints,
78                settings: ContractVariantSettings {
79                    external_view: cms.external_view.unwrap_or(default.settings.external_view),
80                    panic_message: cms.panic_message.unwrap_or(default.settings.panic_message),
81                    check_ei: parse_check_ei(&cms.ei),
82                    allocator: parse_allocator(&cms.allocator),
83                    stack_size: parse_stack_size(&cms.stack_size),
84                    features: cms.features.clone(),
85                    default_features: cms.default_features,
86                    kill_legacy_callback: cms.kill_legacy_callback,
87                    profile: ContractVariantProfile::from_serde(&cms.profile),
88                    std: cms.std.unwrap_or(default.settings.std),
89                    rustc_version: RustcVersion::from_opt_sc_config_serde(&cms.rustc_version),
90                    rustc_target: cms
91                        .rustc_target
92                        .clone()
93                        .unwrap_or_else(|| tools::build_target::default_target().to_owned()),
94                    opcode_version: cms.opcode_version.as_ref().map_or(
95                        default.settings.opcode_version,
96                        |v| OpcodeVersion::from_settings_str(v).expect("Invalid opcode version in contract variant settings; allowed values are '1' and '2'")
97                    ),
98                    wasm_opt_version: cms.wasm_opt_version.clone(),
99                },
100                ..default
101            },
102        )
103    }
104
105    fn wasm_name(&self) -> &String {
106        if !self.explicit_name.is_empty() {
107            &self.explicit_name
108        } else {
109            &self.contract_id
110        }
111    }
112
113    fn collect_endpoint(&mut self, endpoint_abi: &EndpointAbi) {
114        if !self.endpoint_names.contains(&endpoint_abi.name) {
115            self.endpoint_names.insert(endpoint_abi.name.clone());
116            self.collected_endpoints.push(endpoint_abi.clone());
117        }
118    }
119}
120
121fn process_labels_for_contracts(
122    contract_builders: &mut HashMap<String, ContractVariantBuilder>,
123    labels_for_contracts: &HashMap<String, Vec<String>>,
124) {
125    for (label, targets) in labels_for_contracts {
126        for target in targets {
127            contract_builders
128                .entry(target.clone())
129                .or_insert_with(|| ContractVariantBuilder::new(target.clone()))
130                .add_labels
131                .insert(label.clone());
132        }
133    }
134}
135
136fn endpoint_unlabelled(endpoint_abi: &EndpointAbi) -> bool {
137    endpoint_abi.labels.is_empty()
138}
139
140fn endpoint_matches_labels(endpoint_abi: &EndpointAbi, labels: &BTreeSet<String>) -> bool {
141    endpoint_abi
142        .labels
143        .iter()
144        .any(|endpoint_label| labels.contains(endpoint_label))
145}
146
147fn collect_unlabelled_endpoints(
148    contract_builders: &mut HashMap<String, ContractVariantBuilder>,
149    original_abi: &ContractAbi,
150) {
151    for builder in contract_builders.values_mut() {
152        if builder.add_unlabelled {
153            for endpoint_abi in original_abi.iter_all_exports() {
154                if endpoint_unlabelled(endpoint_abi) {
155                    builder.collect_endpoint(endpoint_abi);
156                }
157            }
158        }
159    }
160}
161
162fn collect_labelled_endpoints(
163    contract_builders: &mut HashMap<String, ContractVariantBuilder>,
164    original_abi: &ContractAbi,
165) {
166    for builder in contract_builders.values_mut() {
167        for endpoint_abi in original_abi.iter_all_exports() {
168            if endpoint_matches_labels(endpoint_abi, &builder.add_labels) {
169                builder.collect_endpoint(endpoint_abi);
170            }
171        }
172    }
173}
174
175fn collect_add_endpoints(
176    contract_builders: &mut HashMap<String, ContractVariantBuilder>,
177    original_abi: &ContractAbi,
178) {
179    for builder in contract_builders.values_mut() {
180        for endpoint_abi in original_abi.iter_all_exports() {
181            if builder.add_endpoints.contains(&endpoint_abi.name) {
182                builder.collect_endpoint(endpoint_abi);
183            }
184        }
185    }
186}
187
188fn build_contract_abi(builder: ContractVariantBuilder, original_abi: &ContractAbi) -> ContractAbi {
189    let mut build_info = original_abi.build_info.clone();
190    build_info.rustc = Some(builder.settings.rustc_version.to_abi());
191    let mut constructors = Vec::new();
192    let mut upgrade_constructors = Vec::new();
193    let mut endpoints = Vec::new();
194    let mut promise_callbacks = Vec::new();
195    for endpoint_abi in builder.collected_endpoints {
196        match endpoint_abi.endpoint_type {
197            multiversx_sc::abi::EndpointTypeAbi::Init => constructors.push(endpoint_abi),
198            multiversx_sc::abi::EndpointTypeAbi::Upgrade => upgrade_constructors.push(endpoint_abi),
199            multiversx_sc::abi::EndpointTypeAbi::Endpoint => endpoints.push(endpoint_abi),
200            multiversx_sc::abi::EndpointTypeAbi::PromisesCallback => {
201                promise_callbacks.push(endpoint_abi)
202            }
203        }
204    }
205    let has_callback = original_abi.has_callback
206        && !builder.settings.external_view
207        && !builder.settings.kill_legacy_callback;
208    ContractAbi {
209        build_info,
210        docs: original_abi.docs.clone(),
211        name: original_abi.name.clone(),
212        constructors,
213        upgrade_constructors,
214        endpoints,
215        promise_callbacks,
216        events: original_abi.events.clone(),
217        has_callback,
218        type_descriptions: original_abi.type_descriptions.clone(),
219        esdt_attributes: original_abi.esdt_attributes.clone(),
220    }
221}
222
223pub(crate) fn default_wasm_crate_name(contract_name: &str) -> String {
224    format!("{contract_name}-wasm")
225}
226
227fn build_contract(builder: ContractVariantBuilder, original_abi: &ContractAbi) -> ContractVariant {
228    let contract_name = builder.wasm_name().clone();
229    let wasm_crate_name = default_wasm_crate_name(&contract_name);
230    ContractVariant {
231        wasm_dir_short_name: false,
232        settings: builder.settings.clone(),
233        contract_id: builder.contract_id.clone(),
234        contract_name,
235        wasm_crate_name,
236        abi: build_contract_abi(builder, original_abi),
237    }
238}
239
240fn set_wasm_dir_short_name(contracts: &mut [ContractVariant]) {
241    if contracts.len() != 1 {
242        return;
243    }
244    if let Some(first_contract) = contracts.first_mut() {
245        first_contract.wasm_dir_short_name = true;
246    }
247}
248
249fn process_contracts(config: &ScConfigSerde, original_abi: &ContractAbi) -> Vec<ContractVariant> {
250    let mut contract_builders: HashMap<String, ContractVariantBuilder> = config
251        .contracts
252        .iter()
253        .map(ContractVariantBuilder::map_from_config)
254        .collect();
255
256    collect_and_process_endpoints(
257        &mut contract_builders,
258        original_abi,
259        &config.labels_for_contracts,
260    );
261
262    let mut contracts: Vec<ContractVariant> = contract_builders
263        .into_values()
264        .map(|builder| build_contract(builder, original_abi))
265        .collect();
266
267    if contracts.is_empty() {
268        contracts.push(ContractVariant::default_from_abi(original_abi));
269    }
270    set_wasm_dir_short_name(&mut contracts);
271
272    contracts
273}
274
275fn process_proxy_contracts(config: &ScConfigSerde, original_abi: &ContractAbi) -> Vec<ProxyConfig> {
276    let mut proxy_contracts = Vec::new();
277
278    proxy_contracts.push(ProxyConfig::output_dir_proxy_config(original_abi.clone()));
279
280    for proxy_config in &config.proxy {
281        let mut contract_builders = HashMap::new();
282
283        match &proxy_config.variant {
284            Some(variant) => {
285                let setting_contract = config
286                    .contracts
287                    .iter()
288                    .find(|setting| setting.0.eq(variant))
289                    .unwrap_or_else(|| panic!("No contact with this name"));
290                let (contract_id, mut contract_builder) =
291                    ContractVariantBuilder::map_from_config(setting_contract);
292                alter_builder_with_proxy_config(proxy_config, &mut contract_builder);
293
294                contract_builders = HashMap::from([(contract_id, contract_builder)]);
295            }
296            None => {
297                let mut contract_builder = ContractVariantBuilder::default();
298                alter_builder_with_proxy_config(proxy_config, &mut contract_builder);
299
300                contract_builders.insert(
301                    proxy_config.path.to_string_lossy().to_string(),
302                    contract_builder,
303                );
304            }
305        }
306
307        collect_and_process_endpoints(
308            &mut contract_builders,
309            original_abi,
310            &config.labels_for_contracts,
311        );
312        if let Some((_, builder)) = contract_builders.into_iter().next() {
313            let contract = build_contract(builder, original_abi);
314
315            proxy_contracts.push(ProxyConfig::new(
316                PathBuf::from(&proxy_config.path),
317                proxy_config.override_import.to_owned(),
318                proxy_config.path_rename.to_owned(),
319                contract.abi,
320            ));
321        }
322    }
323
324    proxy_contracts
325}
326
327impl ScConfig {
328    /// Assembles an `ContractVariantConfig` from a raw config object that was loaded via Serde.
329    ///
330    /// In most cases the config will be loaded from a .toml file, use `load_from_file` for that.
331    pub fn load_from_config(
332        path: &Path,
333        config: &ScConfigSerde,
334        original_abi: &ContractAbi,
335    ) -> Self {
336        if config.settings.main.is_some() {
337            print_sc_config_main_deprecated(path);
338        }
339
340        ScConfig {
341            contracts: process_contracts(config, original_abi),
342            proxy_configs: process_proxy_contracts(config, original_abi),
343        }
344    }
345}
346
347fn alter_builder_with_proxy_config(
348    proxy_config: &ProxyConfigSerde,
349    contract_builder: &mut ContractVariantBuilder,
350) {
351    let default = ContractVariantBuilder::default();
352
353    contract_builder.add_unlabelled = proxy_config
354        .add_unlabelled
355        .unwrap_or(default.add_unlabelled);
356    contract_builder.add_endpoints = proxy_config.add_endpoints.iter().cloned().collect();
357    contract_builder.add_labels = proxy_config.add_labels.iter().cloned().collect();
358}
359
360fn collect_and_process_endpoints(
361    contract_builders: &mut HashMap<String, ContractVariantBuilder>,
362    original_abi: &ContractAbi,
363    labels_for_contracts: &HashMap<String, Vec<String>>,
364) {
365    collect_unlabelled_endpoints(contract_builders, original_abi);
366    collect_labelled_endpoints(contract_builders, original_abi);
367    collect_add_endpoints(contract_builders, original_abi);
368    process_labels_for_contracts(contract_builders, labels_for_contracts);
369}
370
371impl ScConfig {
372    /// Provides the config for the cases where no `multicontract.toml` file is available.
373    ///
374    /// The default configuration contains a single main contract, with all endpoints.
375    pub fn default_config(original_abi: &ContractAbi) -> Self {
376        let default_contract_config_name = original_abi.build_info.contract_crate.name.to_string();
377        let wasm_crate_name = default_wasm_crate_name(&default_contract_config_name);
378        ScConfig {
379            contracts: vec![ContractVariant {
380                wasm_dir_short_name: true,
381                settings: ContractVariantSettings::default(),
382                contract_id: default_contract_config_name.clone(),
383                contract_name: default_contract_config_name,
384                wasm_crate_name,
385                abi: original_abi.clone(),
386            }],
387            proxy_configs: Vec::new(),
388        }
389    }
390
391    /// Loads a contract configuration from file. Will return `None` if the file is not found.
392    pub fn load_from_file<P: AsRef<Path>>(path: P, original_abi: &ContractAbi) -> Option<Self> {
393        match fs::read_to_string(path.as_ref()) {
394            Ok(raw_contents) => {
395                let config_serde: ScConfigSerde = toml::from_str(&raw_contents)
396                    .unwrap_or_else(|error| panic!("error parsing multicontract.toml: {error}"));
397                Some(Self::load_from_config(
398                    path.as_ref(),
399                    &config_serde,
400                    original_abi,
401                ))
402            }
403            Err(_) => None,
404        }
405    }
406
407    /// The standard way of loading a `multicontract.toml` configuration: read the file if present, use the default config otherwise.
408    pub fn load_from_files_or_default<I, P>(paths: I, original_abi: &ContractAbi) -> Self
409    where
410        P: AsRef<Path>,
411        I: Iterator<Item = P>,
412    {
413        for path in paths {
414            if let Some(config) = Self::load_from_file(path.as_ref(), original_abi) {
415                return config;
416            }
417        }
418
419        Self::default_config(original_abi)
420    }
421
422    /// The standard way of loading a `multicontract.toml` configuration: read the file if present, use the default config otherwise.
423    pub fn load_from_crate_or_default<P>(contract_crate_path: P, original_abi: &ContractAbi) -> Self
424    where
425        P: AsRef<Path>,
426    {
427        Self::load_from_files_or_default(
428            SC_CONFIG_FILE_NAMES
429                .iter()
430                .map(|name| PathBuf::from(contract_crate_path.as_ref()).join(name)),
431            original_abi,
432        )
433    }
434}