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