From e960fe976a2dc9ae6445bc85d4d5dc3c720ae734 Mon Sep 17 00:00:00 2001 From: Dmitry Zakharov Date: Mon, 24 Jun 2024 13:50:29 +0400 Subject: [PATCH] Dz/fuel contract import 5 (#43) * Prompt to get a single fuel contract * A semi-working contract import for Fuel * Fix contract import templates for Fuel * Vendor Fuel abi * Fix evm indexer * Fix rescript_types tests * Human readable names for Fuel logs * Support setting only one log_id per event * Events selection * Add shared_prompts mod * Prompt multiple contracts for Fuel * Refactor prompt_to_continue_adding to reuse for multiple ecosystems * Fix event_name index * Add first non-etherscan contract verification * Remove unused block * Remove Addresses import from Test file --------- Co-authored-by: Jason --- codegenerator/Cargo.lock | 35 +- codegenerator/cli/Cargo.toml | 2 + codegenerator/cli/CommandLineHelp.md | 34 + .../cli/src/cli_args/clap_definitions.rs | 6 +- codegenerator/cli/src/cli_args/init_config.rs | 284 +++++++- .../cli_args/interactive_init/evm_prompts.rs | 491 ++++--------- .../cli_args/interactive_init/fuel_prompts.rs | 144 +++- .../cli/src/cli_args/interactive_init/mod.rs | 40 +- .../interactive_init/shared_prompts.rs | 202 ++++++ .../cli/src/config_parsing/chain_helpers.rs | 19 +- .../contract_import/converters.rs | 229 +----- .../contract_import/etherscan_helpers.rs | 6 +- .../cli/src/config_parsing/human_config.rs | 5 +- codegenerator/cli/src/executor/init.rs | 91 ++- codegenerator/cli/src/fuel/abi.rs | 423 +++++++++++ codegenerator/cli/src/fuel/mod.rs | 1 + .../contract_import_templates.rs | 95 ++- codegenerator/cli/src/lib.rs | 1 + codegenerator/cli/src/rescript_types.rs | 655 ++++++++++++++++++ .../dynamic/codegen/src/TestHelpers.res.hbs | 2 - .../javascript/src/EventHandlers.js.hbs | 5 +- .../javascript/test/Test.js.hbs | 33 +- .../rescript/src/EventHandlers.res.hbs | 2 +- .../rescript/test/Test.res.hbs | 27 +- .../typescript/src/EventHandlers.ts.hbs | 2 +- .../typescript/test/Test.ts.hbs | 35 +- 26 files changed, 2132 insertions(+), 737 deletions(-) create mode 100644 codegenerator/cli/src/cli_args/interactive_init/shared_prompts.rs create mode 100644 codegenerator/cli/src/fuel/abi.rs create mode 100644 codegenerator/cli/src/rescript_types.rs diff --git a/codegenerator/Cargo.lock b/codegenerator/Cargo.lock index cc07dc2ac..2550d4551 100644 --- a/codegenerator/Cargo.lock +++ b/codegenerator/Cargo.lock @@ -968,11 +968,12 @@ dependencies = [ "colored", "dialoguer", "ethers", + "fuel-abi-types", "graphql-parser", "handlebars", "include_dir", "inquire", - "itertools", + "itertools 0.11.0", "open", "openssl", "paste", @@ -1461,6 +1462,23 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" +[[package]] +name = "fuel-abi-types" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0e7e87f94417ff1a5d60e496906033c58bfe5367546621f131fe8cdabaa2671" +dependencies = [ + "itertools 0.10.5", + "lazy_static", + "proc-macro2", + "quote", + "regex", + "serde", + "serde_json", + "syn 2.0.38", + "thiserror", +] + [[package]] name = "funty" version = "2.0.0" @@ -2022,6 +2040,15 @@ dependencies = [ "once_cell", ] +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.11.0" @@ -2101,7 +2128,7 @@ dependencies = [ "ascii-canvas", "bit-set", "ena", - "itertools", + "itertools 0.11.0", "lalrpop-util", "petgraph", "regex", @@ -3621,7 +3648,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cb9fa2fa2fa6837be8a2495486ff92e3ffe68a99b6eeba288e139efdd842457" dependencies = [ - "itertools", + "itertools 0.11.0", "lalrpop", "lalrpop-util", "phf", @@ -3660,7 +3687,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b7b278788e7be4d0d29c0f39497a0eef3fba6bbc8e70d8bf7fde46edeaa9e85" dependencies = [ - "itertools", + "itertools 0.11.0", "nom", "unicode_categories", ] diff --git a/codegenerator/cli/Cargo.toml b/codegenerator/cli/Cargo.toml index 84d594365..c30baba40 100644 --- a/codegenerator/cli/Cargo.toml +++ b/codegenerator/cli/Cargo.toml @@ -41,6 +41,8 @@ sqlx = { version = "0.7.2", features = [ "postgres", ] } thiserror = "1.0.50" +fuel-abi-types = "0.5.2" + [dev-dependencies] tempdir = "0.3" paste = "1.0.12" diff --git a/codegenerator/cli/CommandLineHelp.md b/codegenerator/cli/CommandLineHelp.md index c4aca746c..fbaf5b311 100644 --- a/codegenerator/cli/CommandLineHelp.md +++ b/codegenerator/cli/CommandLineHelp.md @@ -12,6 +12,8 @@ This document contains the help content for the `envio` command-line program. * [`envio init contract-import local`↴](#envio-init-contract-import-local) * [`envio init fuel`↴](#envio-init-fuel) * [`envio init fuel template`↴](#envio-init-fuel-template) +* [`envio init fuel contract-import`↴](#envio-init-fuel-contract-import) +* [`envio init fuel contract-import local`↴](#envio-init-fuel-contract-import-local) * [`envio dev`↴](#envio-dev) * [`envio stop`↴](#envio-stop) * [`envio codegen`↴](#envio-codegen) @@ -145,6 +147,7 @@ Initialization option for creating Fuel indexer ###### **Subcommands:** * `template` — Initialize Fuel indexer from an example template +* `contract-import` — Initialize Fuel indexer by importing config from a contract for a given chain @@ -163,6 +166,37 @@ Initialize Fuel indexer from an example template +## `envio init fuel contract-import` + +Initialize Fuel indexer by importing config from a contract for a given chain + +**Usage:** `envio init fuel contract-import [OPTIONS] [COMMAND]` + +###### **Subcommands:** + +* `local` — Initialize from a local json ABI file + +###### **Options:** + +* `-c`, `--contract-address ` — Contract address to generate the config from +* `--single-contract` — If selected, prompt will not ask for additional contracts/addresses/networks +* `--all-events` — If selected, prompt will not ask to confirm selection of events on a contract + + + +## `envio init fuel contract-import local` + +Initialize from a local json ABI file + +**Usage:** `envio init fuel contract-import local [OPTIONS]` + +###### **Options:** + +* `-a`, `--abi-file ` — The path to a json abi file +* `--contract-name ` — The name of the contract + + + ## `envio dev` Development commands for starting, stopping, and restarting the indexer with automatic codegen for any changed files diff --git a/codegenerator/cli/src/cli_args/clap_definitions.rs b/codegenerator/cli/src/cli_args/clap_definitions.rs index 1ffc5b984..9ba247b87 100644 --- a/codegenerator/cli/src/cli_args/clap_definitions.rs +++ b/codegenerator/cli/src/cli_args/clap_definitions.rs @@ -254,9 +254,9 @@ pub mod fuel { pub enum InitFlow { ///Initialize Fuel indexer from an example template Template(TemplateArgs), - // ///Initialize Fuel indexer by importing config from a contract for a given chain - // #[strum(serialize = "Contract Import")] - // ContractImport(ContractImportArgs), + ///Initialize Fuel indexer by importing config from a contract for a given chain + #[strum(serialize = "Contract Import")] + ContractImport(ContractImportArgs), } #[derive(Args, Debug, Default, Clone)] diff --git a/codegenerator/cli/src/cli_args/init_config.rs b/codegenerator/cli/src/cli_args/init_config.rs index efc89aa32..0f67c3c67 100644 --- a/codegenerator/cli/src/cli_args/init_config.rs +++ b/codegenerator/cli/src/cli_args/init_config.rs @@ -3,11 +3,28 @@ use serde::{Deserialize, Serialize}; use strum::{Display, EnumIter, EnumString}; pub mod evm { + use std::collections::HashMap; + + use anyhow::{Context, Result}; use clap::ValueEnum; + use itertools::Itertools; use serde::{Deserialize, Serialize}; use strum::{Display, EnumIter, EnumString}; - use crate::config_parsing::contract_import::converters::AutoConfigSelection; + use crate::{ + config_parsing::{ + contract_import::converters::{NetworkKind, SelectedContract}, + human_config::{ + evm::{ + ContractConfig, EventConfig, HumanConfig, Network, RpcConfig, SyncSourceConfig, + }, + GlobalContract, NetworkContract, + }, + }, + utils::unique_hashmap, + }; + + use super::InitConfig; #[derive(Clone, Debug, ValueEnum, Serialize, Deserialize, EnumIter, EnumString, Display)] pub enum Template { @@ -15,11 +32,139 @@ pub mod evm { Erc20, } + ///A an object that holds all the values a user can select during + ///the auto config generation. Values can come from etherscan or + ///abis etc. + #[derive(Clone, Debug)] + pub struct ContractImportSelection { + pub selected_contracts: Vec, + } + + ///Converts the selection object into a human config + type ContractName = String; + impl ContractImportSelection { + pub fn to_human_config(self: &Self, init_config: &InitConfig) -> Result { + let mut networks_map: HashMap = HashMap::new(); + let mut global_contracts: HashMap> = + HashMap::new(); + + for selected_contract in self.selected_contracts.clone() { + let is_multi_chain_contract = selected_contract.networks.len() > 1; + + let events: Vec = selected_contract + .events + .into_iter() + .map(|event| EventConfig { + event: EventConfig::event_string_from_abi_event(&event), + required_entities: None, + is_async: None, + }) + .collect(); + + let handler = init_config.language.get_event_handler_directory(); + + let config = if is_multi_chain_contract { + //Add the contract to global contract config and return none for local contract + //config + let global_contract = GlobalContract { + name: selected_contract.name.clone(), + config: ContractConfig { + abi_file_path: None, + handler, + events, + }, + }; + + unique_hashmap::try_insert( + &mut global_contracts, + selected_contract.name.clone(), + global_contract, + ) + .context(format!( + "Unexpected, failed to add global contract {}. Contract should have unique \ + names", + selected_contract.name + ))?; + None + } else { + //Return some for local contract config + Some(ContractConfig { + abi_file_path: None, + handler, + events, + }) + }; + + for selected_network in &selected_contract.networks { + let address = selected_network + .addresses + .iter() + .map(|a| a.to_string()) + .collect::>() + .into(); + + let network = networks_map + .entry(selected_network.network.get_network_id()) + .or_insert({ + let sync_source = match &selected_network.network { + NetworkKind::Supported(_) => None, + NetworkKind::Unsupported(_, url) => { + Some(SyncSourceConfig::RpcConfig(RpcConfig { + url: url.clone(), + unstable__sync_config: None, + })) + } + }; + + Network { + id: selected_network.network.get_network_id(), + sync_source, + start_block: 0, + end_block: None, + confirmed_block_threshold: None, + contracts: Vec::new(), + } + }); + + let contract = NetworkContract { + name: selected_contract.name.clone(), + address, + config: config.clone(), + }; + + network.contracts.push(contract); + } + } + + let contracts = match global_contracts + .into_values() + .sorted_by_key(|v| v.name.clone()) + .collect::>() + { + values if values.is_empty() => None, + values => Some(values), + }; + + Ok(HumanConfig { + name: init_config.name.clone(), + description: None, + ecosystem: None, + schema: None, + contracts, + networks: networks_map.into_values().sorted_by_key(|v| v.id).collect(), + unordered_multichain_mode: None, + event_decoder: None, + rollback_on_reorg: None, + save_full_history: None, + }) + } + } + #[derive(Clone, Debug, Display)] pub enum InitFlow { Template(Template), SubgraphID(String), - ContractImport(AutoConfigSelection), + ContractImport(ContractImportSelection), } } @@ -28,28 +173,140 @@ pub mod fuel { use serde::{Deserialize, Serialize}; use strum::{Display, EnumIter, EnumString}; + use crate::{ + config_parsing::human_config::{ + self, + fuel::{ContractConfig, EcosystemTag, EventConfig, HumanConfig, Network}, + NetworkContract, + }, + fuel::{ + abi::{Abi, FuelLog}, + address::Address, + }, + }; + + use super::InitConfig; + #[derive(Clone, Debug, ValueEnum, Serialize, Deserialize, EnumIter, EnumString, Display)] pub enum Template { Greeter, } #[derive(Clone, Debug)] - pub struct EventSelection { + pub struct SelectedContract { pub name: String, - pub log_id: Option>, + pub addresses: Vec
, + pub abi: Abi, + pub selected_logs: Vec, + } + + impl SelectedContract { + pub fn get_vendored_abi_file_path(self: &Self) -> String { + format!("abis/{}-abi.json", self.name.to_lowercase()) + } } #[derive(Clone, Debug)] pub struct ContractImportSelection { - pub name: String, - pub abi_file_path: String, - pub events: Vec, + pub contracts: Vec, + } + + impl ContractImportSelection { + pub fn to_human_config(self: &Self, init_config: &InitConfig) -> HumanConfig { + HumanConfig { + name: init_config.name.clone(), + description: None, + ecosystem: EcosystemTag::Fuel, + schema: None, + contracts: None, + networks: vec![Network { + id: 0, + start_block: 0, + end_block: None, + contracts: self + .contracts + .iter() + .map(|selected_contract| NetworkContract { + name: selected_contract.name.clone(), + address: selected_contract + .addresses + .iter() + .map(|a| a.to_string()) + .collect::>() + .into(), + config: Some(ContractConfig { + abi_file_path: selected_contract.get_vendored_abi_file_path(), + handler: init_config.language.get_event_handler_directory(), + events: selected_contract + .selected_logs + .iter() + .map(|selected_log| EventConfig { + name: selected_log.event_name.clone(), + log_id: selected_log.id.clone().into(), + }) + .collect(), + }), + }) + .collect(), + }], + } + } + + pub fn to_evm_human_config( + self: &Self, + init_config: &InitConfig, + ) -> human_config::evm::HumanConfig { + human_config::evm::HumanConfig { + name: init_config.name.clone(), + description: None, + ecosystem: None, + schema: None, + unordered_multichain_mode: None, + event_decoder: None, + rollback_on_reorg: None, + save_full_history: None, + contracts: None, + networks: vec![human_config::evm::Network { + id: 1, + start_block: 0, + sync_source: None, + confirmed_block_threshold: None, + end_block: None, + contracts: self + .contracts + .iter() + .map(|selected_contract| NetworkContract { + name: selected_contract.name.clone(), + address: selected_contract + .addresses + .iter() + .map(|a| a.to_string()) + .collect::>() + .into(), + config: Some(human_config::evm::ContractConfig { + abi_file_path: None, + handler: init_config.language.get_event_handler_directory(), + events: selected_contract + .selected_logs + .iter() + .map(|selected_log| human_config::evm::EventConfig { + event: format!("{}()", selected_log.event_name), + is_async: None, + required_entities: None, + }) + .collect(), + }), + }) + .collect(), + }], + } + } } #[derive(Clone, Debug, Display)] pub enum InitFlow { Template(Template), - ContractImport(Vec), + ContractImport(ContractImportSelection), } } @@ -72,6 +329,17 @@ pub enum Language { ReScript, } +impl Language { + // Logic to get the event handler directory based on the language + pub fn get_event_handler_directory(self: &Self) -> String { + match self { + Language::ReScript => "./src/EventHandlers.bs.js".to_string(), + Language::TypeScript => "src/EventHandlers.ts".to_string(), + Language::JavaScript => "./src/EventHandlers.js".to_string(), + } + } +} + #[derive(Clone, Debug)] pub struct InitConfig { pub name: String, diff --git a/codegenerator/cli/src/cli_args/interactive_init/evm_prompts.rs b/codegenerator/cli/src/cli_args/interactive_init/evm_prompts.rs index 5ca76687a..4ab9601a2 100644 --- a/codegenerator/cli/src/cli_args/interactive_init/evm_prompts.rs +++ b/codegenerator/cli/src/cli_args/interactive_init/evm_prompts.rs @@ -2,316 +2,54 @@ use super::{ clap_definitions::evm::{ ContractImportArgs, ExplorerImportArgs, LocalImportArgs, LocalOrExplorerImport, }, - inquire_helpers::FilePathCompleter, - validation::{ - contains_no_whitespace_validator, first_char_is_alphabet_validator, - is_only_alpha_numeric_characters_validator, UniqueValueValidator, + shared_prompts::{ + prompt_abi_file_path, prompt_contract_address, prompt_contract_name, + prompt_events_selection, prompt_to_continue_adding, Contract, SelectItem, }, + validation::UniqueValueValidator, }; use crate::{ cli_args::interactive_init::validation::filter_duplicate_events, config_parsing::{ chain_helpers::{HypersyncNetwork, Network, NetworkWithExplorer}, - contract_import::converters::{ - self, AutoConfigError, AutoConfigSelection, ContractImportNetworkSelection, - ContractImportSelection, - }, + contract_import::converters::{self, ContractImportNetworkSelection, SelectedContract}, human_config::evm::EventConfig, }, evm::address::Address, + init_config::evm::{ContractImportSelection, InitFlow}, }; use anyhow::{anyhow, Context, Result}; -use async_recursion::async_recursion; -use inquire::{validator::Validation, CustomType, CustomUserError, MultiSelect, Select, Text}; -use std::{env, fmt::Display, path::PathBuf, str::FromStr}; +use inquire::{validator::Validation, CustomType, Select, Text}; +use std::{env, path::PathBuf, str::FromStr}; use strum::IntoEnumIterator; -use strum_macros::EnumIter; - -///Returns the prompter which can call .prompt() to action, or add validators/other -///properties -fn contract_address_prompter() -> CustomType<'static, Address> { - CustomType::
::new("What is the address of the contract?") - .with_help_message("Use the proxy address if your abi is a proxy implementation") - .with_error_message( - "Please input a valid contract address (should be a hexadecimal starting with (0x))", - ) -} - -///Immediately calls the prompter -fn contract_address_prompt() -> Result
{ - contract_address_prompter() - .prompt() - .context("Prompting user for contract address") -} - -///Used a wrapper to implement own Display (Display formats to a string of the -///human readable event signature) -struct DisplayEventWrapper(ethers::abi::Event); - -impl Display for DisplayEventWrapper { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", EventConfig::event_string_from_abi_event(&self.0)) - } -} - -///Convert to and from ethers Event -impl From for DisplayEventWrapper { - fn from(value: ethers::abi::Event) -> Self { - Self(value) - } -} - -///Convert to and from ethers Event -impl From for ethers::abi::Event { - fn from(value: DisplayEventWrapper) -> Self { - value.0 - } -} - -///Takes a vec of Events and sets up a multi selecet prompt -///with all selected by default. Whatever is selected in the prompt -///is returned -fn prompt_for_event_selection(events: Vec) -> Result> { - //Wrap events with Display wrapper - let wrapped_events: Vec<_> = events - .into_iter() - .map(|event| DisplayEventWrapper::from(event)) - .collect(); - - //Collect all the indexes of the vector in another vector which will be used - //to preselect all events - let all_indexes_of_events = wrapped_events - .iter() - .enumerate() - .map(|(i, _)| i) - .collect::>(); - - //Prompt for selection with all events selected by default - let selected_wrapped_events = - MultiSelect::new("Which events would you like to index?", wrapped_events) - .with_default(&all_indexes_of_events) - .prompt()?; - - //Unwrap the selected events and return - let selected_events = selected_wrapped_events - .into_iter() - .map(|w_event| w_event.into()) - .collect(); - - Ok(selected_events) -} - -///Represents the choice a user makes for adding values to -///their auto config selection -#[derive(strum_macros::Display, EnumIter, Default, PartialEq)] -enum AddNewContractOption { - #[default] - #[strum(serialize = "I'm finished")] - Finished, - #[strum(serialize = "Add a new address for same contract on same network")] - AddAddress, - #[strum(serialize = "Add a new network for same contract")] - AddNetwork, - #[strum(serialize = "Add a new contract (with a different ABI)")] - AddContract, -} - -impl ContractImportNetworkSelection { - ///Recursively asks to add an address to ContractImportNetworkSelection - fn prompt_add_contract_address_to_network_selection( - self, - current_contract_name: &str, - //Used in the case where we want to preselect add address - preselected_add_new_contract_option: Option, - ) -> Result<(Self, AddNewContractOption)> { - let selected_option = match preselected_add_new_contract_option { - Some(preselected) => preselected, - None => { - let options = AddNewContractOption::iter().collect::>(); - let help_message = format!( - "Current contract: {}, on network: {}", - current_contract_name, self.network - ); - Select::new("Would you like to add another contract?", options) - .with_starting_cursor(0) - .with_help_message(&help_message) - .prompt() - .context("Failed prompting for add contract")? - } - }; - - if selected_option == AddNewContractOption::AddAddress { - let address = contract_address_prompter() - .with_validator(UniqueValueValidator::new(self.addresses.clone())) - .prompt() - .context("Failed prompting user for new address")?; - let updated_selection = self.add_address(address); - - updated_selection - .prompt_add_contract_address_to_network_selection(current_contract_name, None) - } else { - Ok((self, selected_option)) - } - } -} - -impl ContractImportSelection { - //Recursively asks to add networks with addresses to ContractImportNetworkSelection - fn prompt_add_network_to_contract_import_selection( - self, - add_new_contract_option: AddNewContractOption, - ) -> Result<(Self, AddNewContractOption)> { - if add_new_contract_option == AddNewContractOption::AddNetwork { - //In a new network case, no RPC url could be - //derived from CLI flags - const NO_RPC_URL: Option = None; - - //Select a new network (not from the list of existing network ids already added) - let selected_network = prompt_for_network_id(&NO_RPC_URL, self.get_network_ids()) - .context("Failed selecting network")?; - - //Instantiate a network_selection without any contract addresses - let network_selection = - ContractImportNetworkSelection::new_without_addresses(selected_network); - //Populate contract addresses with prompt - let (network_selection, add_new_contract_option) = network_selection - .prompt_add_contract_address_to_network_selection( - &self.name, - Some(AddNewContractOption::AddAddress), - ) - .context("Failed adding new contract address")?; - - //Add the network to the contract selection - let contract_selection = self.add_network(network_selection); - - //Reprompt to add more or exit - contract_selection - .prompt_add_network_to_contract_import_selection(add_new_contract_option) - } else { - //Exit if the user does not want to add more networks - Ok((self, add_new_contract_option)) - } - } -} -impl AutoConfigSelection { - ///Recursively prompts to import a new contract or exits - #[async_recursion] - async fn prompt_for_add_contract_import_selection( - self, - add_new_contract_option: AddNewContractOption, - ) -> Result { - if add_new_contract_option == AddNewContractOption::AddContract { - //Import a new contract - let (contract_import_selection, add_new_contract_option) = - ContractImportArgs::default() - .get_contract_import_selection() - .await - .context("Failed getting new contract import selection")?; - - //Add contract to AutoConfigSelection, method will handle duplicate names - //and prompting for new names - let auto_config_selection = self - .add_contract_with_prompt(contract_import_selection) - .context("Failed adding contract import selection to AutoConfigSelection")?; - - auto_config_selection - .prompt_for_add_contract_import_selection(add_new_contract_option) - .await - } else { - Ok(self) - } - } - - ///Calls add_contract but handles case where these is a name collision and prompts for a new - ///name - fn add_contract_with_prompt( - self, - contract_import_selection: ContractImportSelection, - ) -> Result { - self.add_contract(contract_import_selection) - .or_else(|e| match e { - AutoConfigError::ContractNameExists(mut contract, auto_config_selection) => { - let prompt_text = format!( - "Contract with name {} already exists in your project. Please provide an \ - alternative name: ", - contract.name - ); - contract.name = Text::new(&prompt_text) - .prompt() - .context("Failed prompting for new Contract name")?; - auto_config_selection.add_contract_with_prompt(contract) - } +fn prompt_abi_events_selection(events: Vec) -> Result> { + prompt_events_selection( + events + .into_iter() + .map(|abi_event| SelectItem { + display: EventConfig::event_string_from_abi_event(&abi_event), + item: abi_event, }) - } + .collect(), + ) + .context("Failed selecting ABI events") } impl ContractImportArgs { - ///Constructs AutoConfigSelection vial cli args and prompts - pub async fn get_auto_config_selection(&self) -> Result { - let (contract_import_selection, add_new_contract_option) = self - .get_contract_import_selection() - .await - .context("Failed getting ContractImportSelection")?; - - let auto_config_selection = AutoConfigSelection::new(contract_import_selection); - - let auto_config_selection = if !self.single_contract { - auto_config_selection - .prompt_for_add_contract_import_selection(add_new_contract_option) - .await - .context("Failed adding contracts to AutoConfigSelection")? - } else { - auto_config_selection - }; - - Ok(auto_config_selection) - } - - ///Constructs ContractImportSelection via cli args and prompts - async fn get_contract_import_selection( - &self, - ) -> Result<(ContractImportSelection, AddNewContractOption)> { - //Construct ContractImportSelection via explorer or local import - let (contract_import_selection, add_new_contract_option) = - match &self.get_local_or_explorer()? { - LocalOrExplorerImport::Explorer(explorer_import_args) => self - .get_contract_import_selection_from_explore_import_args(explorer_import_args) - .await - .context("Failed getting ContractImportSelection from explorer")?, - LocalOrExplorerImport::Local(local_import_args) => self - .get_contract_import_selection_from_local_import_args(local_import_args) - .await - .context("Failed getting ContractImportSelection from local")?, - }; - - //If --single-contract flag was not passed in, prompt to ask the user - //if they would like to add networks to their contract selection - let (contract_import_selection, add_new_contract_option) = if !self.single_contract { - contract_import_selection - .prompt_add_network_to_contract_import_selection(add_new_contract_option) - .context("Failed adding networks to ContractImportSelection")? - } else { - (contract_import_selection, AddNewContractOption::Finished) - }; - - Ok((contract_import_selection, add_new_contract_option)) - } - - //Constructs ContractImportSelection via local prompt. Uses abis and manual + //Constructs SelectedContract via local prompt. Uses abis and manual //network/contract config async fn get_contract_import_selection_from_local_import_args( &self, local_import_args: &LocalImportArgs, - ) -> Result<(ContractImportSelection, AddNewContractOption)> { + ) -> Result { let parsed_abi = local_import_args - .get_parsed_abi() + .get_abi() .context("Failed getting parsed abi")?; let mut abi_events: Vec = parsed_abi.events().cloned().collect(); if !self.all_events { - abi_events = - prompt_for_event_selection(abi_events).context("Failed selecting events")?; + abi_events = prompt_abi_events_selection(abi_events)?; } let network = local_import_args @@ -328,28 +66,19 @@ impl ContractImportArgs { let network_selection = ContractImportNetworkSelection::new(network, address); - //If the flag for --single-contract was not added, continue to prompt for adding - //addresses to the given network for this contract - let (network_selection, add_new_contract_option) = if !self.single_contract { - network_selection - .prompt_add_contract_address_to_network_selection(&contract_name, None) - .context("Failed prompting for more contract addresses on network")? - } else { - (network_selection, AddNewContractOption::Finished) - }; - - let contract_selection = - ContractImportSelection::new(contract_name, network_selection, abi_events); - - Ok((contract_selection, add_new_contract_option)) + Ok(SelectedContract::new( + contract_name, + network_selection, + abi_events, + )) } - ///Constructs ContractImportSelection via block explorer requests. + ///Constructs SelectedContract via block explorer requests. async fn get_contract_import_selection_from_explore_import_args( &self, explorer_import_args: &ExplorerImportArgs, - ) -> Result<(ContractImportSelection, AddNewContractOption)> { - let network_with_explorer = explorer_import_args + ) -> Result { + let network_with_explorer: NetworkWithExplorer = explorer_import_args .get_network_with_explorer() .context("Failed getting NetworkWithExporer")?; @@ -357,21 +86,18 @@ impl ContractImportArgs { .get_contract_address() .context("Failed getting contract address")?; - let contract_selection_from_etherscan = ContractImportSelection::from_etherscan( - &network_with_explorer, - chosen_contract_address, - ) - .await - .context("Failed getting ContractImportSelection from explorer")?; + let contract_selection_from_etherscan = + SelectedContract::from_etherscan(&network_with_explorer, chosen_contract_address) + .await + .context("Failed getting SelectedContract from explorer")?; - let ContractImportSelection { + let SelectedContract { name, networks, events, } = if !self.all_events { - let events = prompt_for_event_selection(contract_selection_from_etherscan.events) - .context("Failed selecting events")?; - ContractImportSelection { + let events = prompt_abi_events_selection(contract_selection_from_etherscan.events)?; + SelectedContract { events, ..contract_selection_from_etherscan } @@ -379,23 +105,11 @@ impl ContractImportArgs { contract_selection_from_etherscan }; - let last_network_selection = networks.last().cloned().ok_or_else(|| { - anyhow!("Expected a network seletion to be constructed with ContractImportSelection") + let network_selection = networks.last().cloned().ok_or_else(|| { + anyhow!("Expected a network seletion to be constructed with SelectedContract") })?; - //If the flag for --single-contract was not added, continue to prompt for adding - //addresses to the given network for this contract - let (network_selection, add_new_contract_option) = if !self.single_contract { - last_network_selection - .prompt_add_contract_address_to_network_selection(&name, None) - .context("Failed prompting for more contract addresses on network")? - } else { - (last_network_selection, AddNewContractOption::Finished) - }; - - let contract_selection = ContractImportSelection::new(name, network_selection, events); - - Ok((contract_selection, add_new_contract_option)) + Ok(SelectedContract::new(name, network_selection, events)) } ///Takes either the address passed in by cli flag or prompts @@ -403,13 +117,13 @@ impl ContractImportArgs { fn get_contract_address(&self) -> Result
{ match &self.contract_address { Some(c) => Ok(c.clone()), - None => contract_address_prompt(), + None => prompt_contract_address(None), } } ///Takes either the "local" or "explorer" subcommand from the cli args ///or prompts for a choice from the user - fn get_local_or_explorer(&self) -> Result { + fn get_local_or_explorer_import(&self) -> Result { match &self.local_or_explorer { Some(v) => Ok(v.clone()), None => { @@ -565,37 +279,23 @@ impl LocalImportArgs { Ok(abi) } - fn is_abi_file_validator(abi_file_path: &str) -> Result { - let maybe_parsed_abi = Self::parse_contract_abi(PathBuf::from(abi_file_path)); - - match maybe_parsed_abi { - Ok(_) => Ok(Validation::Valid), - Err(e) => Ok(Validation::Invalid(e.into())), - } - } - ///Internal function to get the abi path from the cli args or prompt for ///a file path to the abi fn get_abi_path_string(&self) -> Result { match &self.abi_file { - Some(p) => Ok(p.to_owned()), - None => { - let abi_path = Text::new("What is the path to your json abi file?") - //Auto completes path for user with tab/selection - .with_autocomplete(FilePathCompleter::default()) - //Tries to parse the abi to ensure its valid and doesn't - //crash the prompt if not. Simply asks for a valid abi - .with_validator(Self::is_abi_file_validator) - .prompt() - .context("Failed during prompt for abi file path")?; - - Ok(abi_path) - } + Some(p) => Ok(p.clone()), + None => prompt_abi_file_path(|path| { + let maybe_parsed_abi = Self::parse_contract_abi(PathBuf::from(path)); + match maybe_parsed_abi { + Ok(_) => Validation::Valid, + Err(e) => Validation::Invalid(e.into()), + } + }), } } ///Get the file path for the abi and parse it into an abi - fn get_parsed_abi(&self) -> Result { + fn get_abi(&self) -> Result { let abi_path_string = self.get_abi_path_string()?; let mut parsed_abi = Self::parse_contract_abi(PathBuf::from(abi_path_string)) @@ -622,12 +322,89 @@ impl LocalImportArgs { fn get_contract_name(&self) -> Result { match &self.contract_name { Some(n) => Ok(n.clone()), - None => Text::new("What is the name of this contract?") - .with_validator(contains_no_whitespace_validator) - .with_validator(is_only_alpha_numeric_characters_validator) - .with_validator(first_char_is_alphabet_validator) - .prompt() - .context("Failed during contract name prompt"), + None => prompt_contract_name(), } } } + +impl Contract for SelectedContract { + fn get_network_name(&self) -> Result { + self.get_last_network_name() + } + + fn get_name(&self) -> String { + self.name.clone() + } + + fn add_address(&mut self) -> Result<()> { + let network = self.get_last_network_mut()?; + let address = prompt_contract_address(Some(&network.addresses)) + .context("Failed prompting user for new address")?; + network.addresses.push(address); + Ok(()) + } + + fn add_network(&mut self) -> Result<()> { + //In a new network case, no RPC url could be + //derived from CLI flags + const NO_RPC_URL: Option = None; + + //Select a new network (not from the list of existing network ids already added) + let selected_network = prompt_for_network_id(&NO_RPC_URL, self.get_network_ids()) + .context("Failed selecting network")?; + + //Instantiate a network_selection without any contract addresses + let network_selection = + ContractImportNetworkSelection::new_without_addresses(selected_network); + + //Add the network to the contract selection + self.networks.push(network_selection); + + //Populate contract addresses with prompt + self.add_address()?; + + Ok(()) + } +} + +///Constructs SelectedContract via cli args and prompts +async fn get_contract_import_selection(args: ContractImportArgs) -> Result { + //Construct SelectedContract via explorer or local import + match &args.get_local_or_explorer_import()? { + LocalOrExplorerImport::Explorer(explorer_import_args) => args + .get_contract_import_selection_from_explore_import_args(explorer_import_args) + .await + .context("Failed getting SelectedContract from explorer"), + LocalOrExplorerImport::Local(local_import_args) => args + .get_contract_import_selection_from_local_import_args(local_import_args) + .await + .context("Failed getting local contract selection"), + } +} + +//Constructs SelectedContract via local prompt. Uses abis and manual +//network/contract config +async fn prompt_selected_contracts(args: ContractImportArgs) -> Result> { + let should_prompt_to_continue_adding = !args.single_contract.clone(); + let first_contract = get_contract_import_selection(args).await?; + let mut contracts = vec![first_contract]; + + if should_prompt_to_continue_adding { + prompt_to_continue_adding( + &mut contracts, + || get_contract_import_selection(ContractImportArgs::default()), + true, + ) + .await? + } + + Ok(contracts) +} + +pub async fn prompt_contract_import_init_flow(args: ContractImportArgs) -> Result { + Ok(InitFlow::ContractImport(ContractImportSelection { + selected_contracts: prompt_selected_contracts(args) + .await + .context("Failed getting contract selection")?, + })) +} diff --git a/codegenerator/cli/src/cli_args/interactive_init/fuel_prompts.rs b/codegenerator/cli/src/cli_args/interactive_init/fuel_prompts.rs index 06f512497..54699833f 100644 --- a/codegenerator/cli/src/cli_args/interactive_init/fuel_prompts.rs +++ b/codegenerator/cli/src/cli_args/interactive_init/fuel_prompts.rs @@ -1,12 +1,19 @@ use crate::{ - clap_definitions::fuel::{InitFlow as ClapInitFlow, TemplateArgs}, - init_config::fuel::{InitFlow, Template}, + clap_definitions::fuel::{ + ContractImportArgs, InitFlow as ClapInitFlow, LocalImportArgs, LocalOrExplorerImport, + TemplateArgs, + }, + fuel::abi::{Abi, FuelLog}, + init_config::fuel::{ContractImportSelection, InitFlow, SelectedContract, Template}, }; use anyhow::{Context, Result}; -use inquire::Select; +use inquire::{validator::Validation, Select}; use strum::IntoEnumIterator; -use super::prompt_template; +use super::shared_prompts::{ + prompt_abi_file_path, prompt_contract_address, prompt_contract_name, prompt_events_selection, + prompt_template, prompt_to_continue_adding, Contract, SelectItem, +}; pub fn prompt_init_flow_missing(maybe_init_flow: Option) -> Result { let init_flow = match maybe_init_flow { @@ -32,10 +39,125 @@ pub fn prompt_template_init_flow(args: TemplateArgs) -> Result { Ok(InitFlow::Template(chosen_template)) } -// pub fn prompt_contract_import_init_flow(_args: ContractImportArgs) -> Result { -// Ok(InitFlow::ContractImport(vec![ContractImportSelection { -// abi_file_path: "TODO: abi_file_path".to_string(), -// name: "TODO: Name".to_string(), -// events: vec![], -// }])) -// } +///Takes either the "local" or "explorer" subcommand from the cli args +///or prompts for a choice from the user (not supported by Fuel yet) +fn get_local_or_explorer_import(args: &ContractImportArgs) -> LocalOrExplorerImport { + match &args.local_or_explorer { + Some(v) => v.clone(), + None => LocalOrExplorerImport::Local(LocalImportArgs { + abi_file: None, + contract_name: None, + }), + } +} + +///Internal function to get the abi path from the cli args or prompt for +///a file path to the abi +fn get_abi_path_string(local_import_args: &LocalImportArgs) -> Result { + match &local_import_args.abi_file { + Some(p) => Ok(p.clone()), + None => prompt_abi_file_path(|path| { + let maybe_parsed_abi = Abi::parse(&path.to_string()); + match maybe_parsed_abi { + Ok(_) => Validation::Valid, + Err(e) => Validation::Invalid(e.into()), + } + }), + } +} + +///Prompts for a contract name +fn get_contract_name(local_import_args: &LocalImportArgs) -> Result { + match &local_import_args.contract_name { + Some(n) => Ok(n.clone()), + None => prompt_contract_name(), + } +} + +fn prompt_logs_selection(logs: Vec) -> Result> { + prompt_events_selection( + logs.into_iter() + .map(|log| SelectItem { + display: log.event_name.clone(), + item: log, + }) + .collect(), + ) + .context("Failed selecting ABI events") +} + +impl Contract for SelectedContract { + fn get_network_name(&self) -> Result { + Ok("Fuel".to_string()) + } + + fn get_name(&self) -> String { + self.name.clone() + } + + fn add_address(&mut self) -> Result<()> { + let address = prompt_contract_address(Some(&self.addresses))?; + self.addresses.push(address); + Ok(()) + } + + fn add_network(&mut self) -> Result<()> { + todo!("Fuel supports only one network at the moment") + } +} + +//Constructs SelectedContract via local prompt. Uses abis and manual +//network/contract config +async fn get_contract_import_selection(args: ContractImportArgs) -> Result { + let local_or_explorer_import = get_local_or_explorer_import(&args); + let local_import_args = match local_or_explorer_import { + LocalOrExplorerImport::Local(local_import_args) => local_import_args, + }; + + let abi_path_string = + get_abi_path_string(&local_import_args).context("Failed getting Fuel ABI path")?; + let abi = Abi::parse(&abi_path_string).context("Failed parsing Fuel ABI")?; + + let mut selected_logs = abi.get_logs(); + if !args.all_events { + selected_logs = prompt_logs_selection(selected_logs)?; + } + + let name = get_contract_name(&local_import_args).context("Failed getting contract name")?; + + let addresses = vec![prompt_contract_address(None)?]; + + Ok(SelectedContract { + name, + addresses, + abi, + selected_logs, + }) +} + +//Constructs SelectedContract via local prompt. Uses abis and manual +//network/contract config +async fn prompt_selected_contracts(args: ContractImportArgs) -> Result> { + let should_prompt_to_continue_adding = !args.single_contract.clone(); + let first_contract = get_contract_import_selection(args).await?; + let mut contracts = vec![first_contract]; + + if should_prompt_to_continue_adding { + prompt_to_continue_adding( + &mut contracts, + || get_contract_import_selection(ContractImportArgs::default()), + false, + ) + .await? + } + + Ok(contracts) +} + +pub async fn prompt_contract_import_init_flow(args: ContractImportArgs) -> Result { + Ok(InitFlow::ContractImport(ContractImportSelection { + contracts: prompt_selected_contracts(args) + .await + .context("Failed getting contract selection")?, + })) +} diff --git a/codegenerator/cli/src/cli_args/interactive_init/mod.rs b/codegenerator/cli/src/cli_args/interactive_init/mod.rs index a7c6075fc..cee776327 100644 --- a/codegenerator/cli/src/cli_args/interactive_init/mod.rs +++ b/codegenerator/cli/src/cli_args/interactive_init/mod.rs @@ -1,17 +1,21 @@ mod evm_prompts; mod fuel_prompts; mod inquire_helpers; +mod shared_prompts; pub mod validation; -use std::fmt::Display; - use super::{ - clap_definitions::{self, InitArgs, InitFlow, ProjectPaths}, - init_config::{evm, Ecosystem, InitConfig, Language}, + clap_definitions::{self, InitArgs, ProjectPaths}, + init_config::{InitConfig, Language}, +}; +use crate::{ + clap_definitions::InitFlow, + constants::project_paths::DEFAULT_PROJECT_ROOT_PATH, + init_config::{evm, Ecosystem}, }; -use crate::constants::project_paths::DEFAULT_PROJECT_ROOT_PATH; use anyhow::{Context, Result}; use inquire::{Select, Text}; +use shared_prompts::prompt_template; use std::str::FromStr; use strum::{Display, EnumIter, IntoEnumIterator}; use validation::{ @@ -20,17 +24,11 @@ use validation::{ }; #[derive(Clone, Debug, Display, PartialEq, EnumIter)] -pub enum EcosystemOption { +enum EcosystemOption { Evm, Fuel, } -fn prompt_template(options: Vec) -> Result { - Select::new("Which template would you like to use?", options) - .prompt() - .context("Prompting user for template selection") -} - async fn prompt_ecosystem(cli_init_flow: Option) -> Result { let init_flow = match cli_init_flow { Some(v) => v, @@ -65,9 +63,9 @@ async fn prompt_ecosystem(cli_init_flow: Option) -> Result clap_definitions::fuel::InitFlow::Template(args) => Ecosystem::Fuel { init_flow: fuel_prompts::prompt_template_init_flow(args)?, }, - // clap_definitions::fuel::InitFlow::ContractImport(args) => Ecosystem::Fuel { - // init_flow: fuel_prompts::prompt_contract_import_init_flow(args)?, - // }, + clap_definitions::fuel::InitFlow::ContractImport(args) => Ecosystem::Fuel { + init_flow: fuel_prompts::prompt_contract_import_init_flow(args).await?, + }, }, InitFlow::Template(args) => { let chosen_template = match args.template { @@ -93,15 +91,9 @@ async fn prompt_ecosystem(cli_init_flow: Option) -> Result } } - InitFlow::ContractImport(args) => { - let auto_config_selection = args - .get_auto_config_selection() - .await - .context("Failed getting AutoConfigSelection selection")?; - Ecosystem::Evm { - init_flow: evm::InitFlow::ContractImport(auto_config_selection), - } - } + InitFlow::ContractImport(args) => Ecosystem::Evm { + init_flow: evm_prompts::prompt_contract_import_init_flow(args).await?, + }, }; Ok(initialization) diff --git a/codegenerator/cli/src/cli_args/interactive_init/shared_prompts.rs b/codegenerator/cli/src/cli_args/interactive_init/shared_prompts.rs new file mode 100644 index 000000000..fbc9bb4db --- /dev/null +++ b/codegenerator/cli/src/cli_args/interactive_init/shared_prompts.rs @@ -0,0 +1,202 @@ +use std::{fmt::Display, future::Future}; + +use super::{ + inquire_helpers::FilePathCompleter, + validation::{ + contains_no_whitespace_validator, first_char_is_alphabet_validator, + is_only_alpha_numeric_characters_validator, UniqueValueValidator, + }, +}; + +use anyhow::{Context, Result}; +use async_recursion::async_recursion; +use inquire::{validator::Validation, CustomType, MultiSelect, Select, Text}; + +use std::str::FromStr; +use strum::{EnumIter, IntoEnumIterator}; + +pub fn prompt_template(options: Vec) -> Result { + Select::new("Which template would you like to use?", options) + .prompt() + .context("Prompting user for template selection") +} + +pub struct SelectItem { + pub item: T, + pub display: String, +} + +impl Display for SelectItem { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.display) + } +} + +///Takes a vec of Events and sets up a multi selecet prompt +///with all selected by default. Whatever is selected in the prompt +///is returned +pub fn prompt_events_selection(events: Vec>) -> Result> { + //Collect all the indexes of the vector in another vector which will be used + //to preselect all events + let all_indexes_of_events = events + .iter() + .enumerate() + .map(|(i, _)| i) + .collect::>(); + + //Prompt for selection with all events selected by default + let selected_wrapped_events = MultiSelect::new("Which events would you like to index?", events) + .with_default(&all_indexes_of_events) + .prompt()?; + + //Unwrap the selected events and return + let selected_events = selected_wrapped_events + .into_iter() + .map(|w_event| w_event.item) + .collect(); + + Ok(selected_events) +} + +pub fn prompt_abi_file_path( + abi_validator: fn(abi_file_path: &str) -> Validation, +) -> Result { + Text::new("What is the path to your json abi file?") + //Auto completes path for user with tab/selection + .with_autocomplete(FilePathCompleter::default()) + //Tries to parse the abi to ensure its valid and doesn't + //crash the prompt if not. Simply asks for a valid abi + .with_validator(move |path: &str| Ok(abi_validator(path))) + .prompt() + .context("Failed during prompt for abi file path") +} + +pub fn prompt_contract_name() -> Result { + Text::new("What is the name of this contract?") + .with_validator(contains_no_whitespace_validator) + .with_validator(is_only_alpha_numeric_characters_validator) + .with_validator(first_char_is_alphabet_validator) + .prompt() + .context("Failed during contract name prompt") +} + +pub fn prompt_contract_address( + selected: Option<&Vec>, +) -> Result { + let mut prompter = CustomType::::new("What is the address of the contract?") + .with_help_message("Use the proxy address if your abi is a proxy implementation") + .with_error_message( + "Please input a valid contract address (should be a hexadecimal starting with (0x))", + ); + if let Some(selected) = selected { + prompter = prompter.with_validator(UniqueValueValidator::new(selected.clone())) + } + prompter + .prompt() + .context("Failed during contract address prompt") +} + +///Represents the choice a user makes for adding values to +///their auto config selection +#[derive(strum_macros::Display, EnumIter, Default, PartialEq)] +enum AddNewContractOption { + #[default] + #[strum(serialize = "I'm finished")] + Finished, + #[strum(serialize = "Add a new address for same contract on same network")] + AddAddress, + #[strum(serialize = "Add a new network for same contract")] + AddNetwork, + #[strum(serialize = "Add a new contract (with a different ABI)")] + AddContract, +} + +fn prompt_add_new_contract_option( + contract_name: &String, + network: &String, + can_add_network: bool, +) -> Result { + let mut options = AddNewContractOption::iter().collect::>(); + if !can_add_network { + options = options + .into_iter() + .filter(|o| o != &AddNewContractOption::AddNetwork) + .collect(); + } + let help_message = format!( + "Current contract: {}, on network: {}", + contract_name, network + ); + Select::new("Would you like to add another contract?", options) + .with_starting_cursor(0) + .with_help_message(&help_message) + .prompt() + .context("Failed prompting for add contract") +} + +pub trait Contract { + fn get_network_name(&self) -> Result; + fn get_name(&self) -> String; + fn add_address(&mut self) -> Result<()>; + fn add_network(&mut self) -> Result<()>; +} + +#[derive(thiserror::Error, Debug)] +enum AutoConfigError { + #[error("Contract with the name '{}' already selected", .name)] + ContractNameExists { name: String }, +} + +#[async_recursion] +pub async fn prompt_to_continue_adding( + contracts: &mut Vec, + mut add_contract: CF, + can_add_network: bool, +) -> Result<()> +where + T: Contract + Send, + CF: FnMut() -> CFut + Send, + CFut: Future> + Send, +{ + let active_contract = contracts + .last_mut() + .context("Failed to get the last selected contract")?; + let add_new_contract_option = prompt_add_new_contract_option( + &active_contract.get_name(), + &active_contract.get_network_name()?, + can_add_network, + )?; + match add_new_contract_option { + AddNewContractOption::Finished => Ok(()), + AddNewContractOption::AddAddress => { + active_contract.add_address()?; + prompt_to_continue_adding(contracts, add_contract, can_add_network).await + } + AddNewContractOption::AddNetwork => { + active_contract.add_network()?; + prompt_to_continue_adding(contracts, add_contract, can_add_network).await + } + AddNewContractOption::AddContract => { + let contract = add_contract().await?; + let contract_name_lower = contract.get_name().to_lowercase(); + let contract_name_exists = contracts + .iter() + .find(|c| &c.get_name().to_lowercase() == &contract_name_lower) + .is_some(); + + if contract_name_exists { + //TODO: Handle more cases gracefully like: + // - contract + event is exact match, in which case it should just merge networks and + // addresses + // - Contract has some matching addresses to another contract but all different events + // - Contract has some matching events as another contract? + Err(AutoConfigError::ContractNameExists { + name: contract.get_name(), + })? + } else { + contracts.push(contract); + prompt_to_continue_adding(contracts, add_contract, can_add_network).await + } + } + } +} diff --git a/codegenerator/cli/src/config_parsing/chain_helpers.rs b/codegenerator/cli/src/config_parsing/chain_helpers.rs index 73c4bed52..1bffecfc8 100644 --- a/codegenerator/cli/src/config_parsing/chain_helpers.rs +++ b/codegenerator/cli/src/config_parsing/chain_helpers.rs @@ -100,7 +100,7 @@ pub enum Network { Celo = 42220, #[subenum(GraphNetwork)] Fuji = 43113, - #[subenum(HypersyncNetwork, GraphNetwork)] + #[subenum(HypersyncNetwork, GraphNetwork, NetworkWithExplorer)] Avalanche = 43114, #[subenum(GraphNetwork)] CeloAlfajores = 44787, @@ -384,6 +384,23 @@ impl NetworkWithExplorer { NetworkWithExplorer::BlastSepolia => { BlockExplorerApi::custom("blastscan.io", "api-testnet.blastscan.io") } + NetworkWithExplorer::Avalanche => BlockExplorerApi::custom( + "avalanche.routescan.io", + "api.routescan.io/v2/network/mainnet/evm/43114/etherscan", + ), + //// Having issues getting blockscout to work. + // NetworkWithExplorer::Aurora => BlockExplorerApi::custom( + // "explorer.mainnet.aurora.dev", + // "explorer.mainnet.aurora.dev/api", + // /// also tried some variations: explorer.mainnet.aurora.dev/api/v2 + // ), + // NetworkWithExplorer::Lukso => BlockExplorerApi::custom( + // "explorer.execution.mainnet.lukso.network", + // "explorer.execution.mainnet.lukso.network/api", + // /// Also tried some variations: + // blockscout.com/lukso/l14 + // + // ), _ => BlockExplorerApi::DefaultEthers, } } diff --git a/codegenerator/cli/src/config_parsing/contract_import/converters.rs b/codegenerator/cli/src/config_parsing/contract_import/converters.rs index 2fb4e1c30..b0f95b541 100644 --- a/codegenerator/cli/src/config_parsing/contract_import/converters.rs +++ b/codegenerator/cli/src/config_parsing/contract_import/converters.rs @@ -1,94 +1,23 @@ use super::etherscan_helpers::fetch_contract_auto_selection_from_etherscan; use crate::{ - cli_args::init_config::Language, - config_parsing::{ - chain_helpers::{HypersyncNetwork, NetworkWithExplorer}, - human_config::{ - evm::{ContractConfig, EventConfig, HumanConfig, Network, RpcConfig, SyncSourceConfig}, - GlobalContract, NetworkContract, - }, - }, + config_parsing::chain_helpers::{HypersyncNetwork, NetworkWithExplorer}, evm::address::Address, - init_config::InitConfig, - utils::unique_hashmap, }; use anyhow::{Context, Result}; -use itertools::{self, Itertools}; -use std::{ - collections::HashMap, - fmt::{self, Display}, -}; -use thiserror; - -///A an object that holds all the values a user can select during -///the auto config generation. Values can come from etherscan or -///abis etc. -#[derive(Clone, Debug)] -pub struct AutoConfigSelection { - selected_contracts: Vec, -} - -#[derive(thiserror::Error, Debug)] -pub enum AutoConfigError { - #[error("Contract '{}' already exists in AutoConfigSelection", .0.name)] - ContractNameExists(ContractImportSelection, AutoConfigSelection), -} - -impl AutoConfigSelection { - pub fn new(selected_contract: ContractImportSelection) -> Self { - Self { - selected_contracts: vec![selected_contract], - } - } - - pub fn add_contract( - mut self, - contract: ContractImportSelection, - ) -> Result { - let contract_name_lower = contract.name.to_lowercase(); - let contract_name_exists = self - .selected_contracts - .iter() - .find(|c| &c.name.to_lowercase() == &contract_name_lower) - .is_some(); - - if contract_name_exists { - //TODO: Handle more cases gracefully like: - // - contract + event is exact match, in which case it should just merge networks and - // addresses - // - Contract has some matching addresses to another contract but all different events - // - Contract has some matching events as another contract? - Err(AutoConfigError::ContractNameExists(contract, self))? - } else { - self.selected_contracts.push(contract); - Ok(self) - } - } - - pub async fn from_etherscan( - network: &NetworkWithExplorer, - address: Address, - ) -> anyhow::Result { - let selected_contract = fetch_contract_auto_selection_from_etherscan(address, network) - .await - .context("Failed fetching selected contract")?; - - Ok(Self::new(selected_contract)) - } -} +use std::fmt::{self, Display}; ///The hierarchy is based on how you would add items to ///your selection as you go. Ie. Once you have constructed ///the selection of a contract you can add more addresses or ///networks #[derive(Clone, Debug)] -pub struct ContractImportSelection { +pub struct SelectedContract { pub name: String, pub networks: Vec, pub events: Vec, } -impl ContractImportSelection { +impl SelectedContract { pub fn new( name: String, network_selection: ContractImportNetworkSelection, @@ -101,9 +30,18 @@ impl ContractImportSelection { } } - pub fn add_network(mut self, network_selection: ContractImportNetworkSelection) -> Self { - self.networks.push(network_selection); - self + pub fn get_last_network_mut(&mut self) -> Result<&mut ContractImportNetworkSelection> { + self.networks + .last_mut() + .context("Failed to get the last select contract network") + } + + pub fn get_last_network_name(&self) -> Result { + let network_selection = self + .networks + .last() + .context("Failed to get the last select contract network")?; + Ok(network_selection.network.to_string()) } pub async fn from_etherscan( @@ -168,139 +106,4 @@ impl ContractImportNetworkSelection { addresses: vec![], } } - - pub fn add_address(mut self, address: Address) -> Self { - self.addresses.push(address); - - self - } -} - -///Converts the selection object into a human config -type ContractName = String; -impl AutoConfigSelection { - pub fn to_human_config(self: &Self, init_config: &InitConfig) -> Result { - let mut networks_map: HashMap = HashMap::new(); - let mut global_contracts: HashMap> = - HashMap::new(); - - for selected_contract in self.selected_contracts.clone() { - let is_multi_chain_contract = selected_contract.networks.len() > 1; - - let events: Vec = selected_contract - .events - .into_iter() - .map(|event| EventConfig { - event: EventConfig::event_string_from_abi_event(&event), - required_entities: None, - is_async: None, - }) - .collect(); - - let handler = get_event_handler_directory(&init_config.language); - - let config = if is_multi_chain_contract { - //Add the contract to global contract config and return none for local contract - //config - let global_contract = GlobalContract { - name: selected_contract.name.clone(), - config: ContractConfig { - abi_file_path: None, - handler, - events, - }, - }; - - unique_hashmap::try_insert( - &mut global_contracts, - selected_contract.name.clone(), - global_contract, - ) - .context(format!( - "Unexpected, failed to add global contract {}. Contract should have unique \ - names", - selected_contract.name - ))?; - None - } else { - //Return some for local contract config - Some(ContractConfig { - abi_file_path: None, - handler, - events, - }) - }; - - for selected_network in &selected_contract.networks { - let address = selected_network - .addresses - .iter() - .map(|a| a.to_string()) - .collect::>() - .into(); - - let network = networks_map - .entry(selected_network.network.get_network_id()) - .or_insert({ - let sync_source = match &selected_network.network { - NetworkKind::Supported(_) => None, - NetworkKind::Unsupported(_, url) => { - Some(SyncSourceConfig::RpcConfig(RpcConfig { - url: url.clone(), - unstable__sync_config: None, - })) - } - }; - - Network { - id: selected_network.network.get_network_id(), - sync_source, - start_block: 0, - end_block: None, - confirmed_block_threshold: None, - contracts: Vec::new(), - } - }); - - let contract = NetworkContract { - name: selected_contract.name.clone(), - address, - config: config.clone(), - }; - - network.contracts.push(contract); - } - } - - let contracts = match global_contracts - .into_values() - .sorted_by_key(|v| v.name.clone()) - .collect::>() - { - values if values.is_empty() => None, - values => Some(values), - }; - - Ok(HumanConfig { - name: init_config.name.clone(), - description: None, - ecosystem: None, - schema: None, - contracts, - networks: networks_map.into_values().sorted_by_key(|v| v.id).collect(), - unordered_multichain_mode: None, - event_decoder: None, - rollback_on_reorg: None, - save_full_history: None, - }) - } -} - -// Logic to get the event handler directory based on the language -fn get_event_handler_directory(language: &Language) -> String { - match language { - Language::ReScript => "./src/EventHandlers.bs.js".to_string(), - Language::TypeScript => "src/EventHandlers.ts".to_string(), - Language::JavaScript => "./src/EventHandlers.js".to_string(), - } } diff --git a/codegenerator/cli/src/config_parsing/contract_import/etherscan_helpers.rs b/codegenerator/cli/src/config_parsing/contract_import/etherscan_helpers.rs index 7e7c7a283..6c606f12e 100644 --- a/codegenerator/cli/src/config_parsing/contract_import/etherscan_helpers.rs +++ b/codegenerator/cli/src/config_parsing/contract_import/etherscan_helpers.rs @@ -1,4 +1,4 @@ -use super::converters::{self, ContractImportNetworkSelection, ContractImportSelection}; +use super::converters::{self, ContractImportNetworkSelection, SelectedContract}; use crate::{ cli_args::interactive_init::validation::filter_duplicate_events, config_parsing::chain_helpers::{self, NetworkWithExplorer}, @@ -17,7 +17,7 @@ use tokio::time::Duration; pub async fn fetch_contract_auto_selection_from_etherscan( contract_address: Address, network: &NetworkWithExplorer, -) -> Result { +) -> Result { let supported_network: chain_helpers::HypersyncNetwork = chain_helpers::Network::from(network.clone()) .try_into() @@ -39,7 +39,7 @@ pub async fn fetch_contract_auto_selection_from_etherscan( contract_address, ); - Ok(ContractImportSelection::new( + Ok(SelectedContract::new( contract_data.name, network_selection, events, diff --git a/codegenerator/cli/src/config_parsing/human_config.rs b/codegenerator/cli/src/config_parsing/human_config.rs index c30458e68..dc26cf485 100644 --- a/codegenerator/cli/src/config_parsing/human_config.rs +++ b/codegenerator/cli/src/config_parsing/human_config.rs @@ -160,7 +160,6 @@ pub mod evm { pub mod fuel { use super::{GlobalContract, NetworkContract, NetworkId}; - use crate::utils::normalized_list::NormalizedList; use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize, PartialEq)] @@ -203,8 +202,8 @@ pub mod fuel { #[serde(rename_all = "camelCase")] pub struct EventConfig { pub name: String, - #[serde(skip_serializing_if = "NormalizedList::is_empty")] - pub log_id: NormalizedList, + #[serde(skip_serializing_if = "Option::is_none")] + pub log_id: Option, } } diff --git a/codegenerator/cli/src/executor/init.rs b/codegenerator/cli/src/executor/init.rs index 0b9154004..f8f512e50 100644 --- a/codegenerator/cli/src/executor/init.rs +++ b/codegenerator/cli/src/executor/init.rs @@ -6,7 +6,9 @@ use crate::{ }, commands, config_parsing::{ - entity_parsing::Schema, graph_migration::generate_config_from_subgraph_id, human_config, + entity_parsing::Schema, + graph_migration::generate_config_from_subgraph_id, + human_config::{self}, system_config::SystemConfig, }, hbs_templating::{ @@ -102,8 +104,12 @@ pub async fn run_init_args(init_args: InitArgs, project_paths: &ProjectPaths) -> .context("Failed parsing config")?; let auto_schema_handler_template = - contract_import_templates::AutoSchemaHandlerTemplate::try_from(parsed_config) - .context("Failed converting config to auto auto_schema_handler_template")?; + contract_import_templates::AutoSchemaHandlerTemplate::try_from( + parsed_config, + false, + &init_config.language, + ) + .context("Failed converting config to auto auto_schema_handler_template")?; auto_schema_handler_template .generate_subgraph_migration_templates( @@ -114,8 +120,75 @@ pub async fn run_init_args(init_args: InitArgs, project_paths: &ProjectPaths) -> } Ecosystem::Fuel { - init_flow: init_config::fuel::InitFlow::ContractImport(_), - } => todo!("Contract import initialization for Flow is not implemented"), + init_flow: init_config::fuel::InitFlow::ContractImport(contract_import_selection), + } => { + let yaml_config = contract_import_selection.to_human_config(&init_config); + + let serialized_config = + serde_yaml::to_string(&yaml_config).context("Failed serializing config")?; + + // TODO: Allow parsed paths to not depend on a written config.yaml file in file system + file_system::write_file_string_to_system( + serialized_config, + parsed_project_paths.project_root.join("config.yaml"), + ) + .await + .context("Failed writing imported config.yaml")?; + + for selected_contract in &contract_import_selection.contracts { + file_system::write_file_string_to_system( + selected_contract.abi.raw.clone(), + parsed_project_paths + .project_root + .join(selected_contract.get_vendored_abi_file_path()), + ) + .await + .context(format!( + "Failed vendoring ABI file for {} contract", + selected_contract.name + ))?; + } + + // FIXME: This is a hack until system config supports Fuel ecosystem + let evm_yaml_config = contract_import_selection.to_evm_human_config(&init_config); + + //Use an empty schema config to generate auto_schema_handler_template + //After it's been generated, the schema exists and codegen can parse it/use it + let parsed_config = SystemConfig::parse_from_human_cfg_with_schema( + evm_yaml_config, + Schema::empty(), + &parsed_project_paths, + ) + .context("Failed parsing config")?; + + let auto_schema_handler_template = + contract_import_templates::AutoSchemaHandlerTemplate::try_from( + parsed_config, + true, + &init_config.language, + ) + .context("Failed converting config to auto auto_schema_handler_template")?; + + template_dirs + .get_and_extract_blank_template( + &init_config.language, + &parsed_project_paths.project_root, + ) + .context(format!( + "Failed initializing blank template for Contract Import with language {} at \ + path {:?}", + &init_config.language, &parsed_project_paths.project_root, + ))?; + + auto_schema_handler_template + .generate_contract_import_templates( + &init_config.language, + &parsed_project_paths.project_root, + ) + .context( + "Failed generating contract import templates for schema and event handlers.", + )?; + } Ecosystem::Evm { init_flow: init_config::evm::InitFlow::ContractImport(auto_config_selection), @@ -145,8 +218,12 @@ pub async fn run_init_args(init_args: InitArgs, project_paths: &ProjectPaths) -> .context("Failed parsing config")?; let auto_schema_handler_template = - contract_import_templates::AutoSchemaHandlerTemplate::try_from(parsed_config) - .context("Failed converting config to auto auto_schema_handler_template")?; + contract_import_templates::AutoSchemaHandlerTemplate::try_from( + parsed_config, + false, + &init_config.language, + ) + .context("Failed converting config to auto auto_schema_handler_template")?; template_dirs .get_and_extract_blank_template( diff --git a/codegenerator/cli/src/fuel/abi.rs b/codegenerator/cli/src/fuel/abi.rs new file mode 100644 index 000000000..87a5a0f10 --- /dev/null +++ b/codegenerator/cli/src/fuel/abi.rs @@ -0,0 +1,423 @@ +use anyhow::{anyhow, Context, Result}; +use fuel_abi_types::abi::program::ProgramABI; +use itertools::Itertools; +use std::{collections::HashMap, fs, path::PathBuf}; + +use crate::rescript_types::{ + RescriptRecordField, RescriptTypeDecl, RescriptTypeDeclMulti, RescriptTypeExpr, + RescriptTypeIdent, RescriptVariantConstr, +}; + +#[derive(Debug, Clone, PartialEq)] +pub struct FuelType { + pub id: usize, + pub rescript_type_decl: RescriptTypeDecl, + abi_type_field: String, +} + +impl FuelType { + fn get_event_name(self: &Self) -> String { + match self.abi_type_field.as_str() { + "()" => "UnitLog", + "bool" => "BoolLog", + "u8" => "U8Log", + "u16" => "U16Log", + "u32" => "U32Log", + "u64" => "U64Log", + "u128" => "U128Log", + "raw untyped ptr" => "RawUntypedPtrLog", + "b256" => "B256Log", + "address" => "AddressLog", + "Vec" => "VecLog", + type_field if type_field.starts_with("str[") => "StrLog", + "enum Option" => "OptionLog", + type_field if type_field.starts_with("struct ") => type_field + .strip_prefix("struct ") + .unwrap_or_else(|| "StructLog"), + type_field if type_field.starts_with("enum ") => type_field + .strip_prefix("enum ") + .unwrap_or_else(|| "EnumLog"), + type_field if type_field.starts_with("(_,") => "TupleLog", + type_field if type_field.starts_with("[_;") => "ArrayLog", + _ => "UnknownLog", + } + .to_string() + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct FuelLog { + pub id: String, + pub logged_type: FuelType, + pub event_name: String, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Abi { + pub path: String, + pub raw: String, + program: ProgramABI, + logs: HashMap, + types: HashMap, +} + +impl Abi { + fn decode_program(raw: &String) -> Result { + let program: ProgramABI = serde_json::from_str(&raw)?; + Ok(program) + } + + fn decode_types(program: &ProgramABI) -> Result> { + let mut types_map: HashMap = HashMap::new(); + + //eg "generic T" returns "T" for keyword "generic" + fn extract_value_after_keyword(keyword: &str, input: &str) -> Option { + if let Some(start) = input.find(keyword) { + // Calculate the start position of the value after "keyword" + let value_start = start + keyword.len(); + // Extract the value and trim any leading/trailing whitespace + let value = input[value_start..].trim(); + if !value.is_empty() { + return Some(value.to_string()); + } + } + None + } + + fn mk_type_id_name(type_id: &usize) -> String { + format!("type_id_{}", type_id) + } + + let generic_param_name_map = program + .types + .iter() + .filter_map(|type_decl| { + let generic_param_name = + extract_value_after_keyword("generic", &type_decl.type_field)?; + Some((type_decl.type_id, generic_param_name)) + }) + .collect::>(); + + let get_unknown_res_type_ident = |type_field: &str| { + println!("Unhandled type_field \"{}\" in abi", type_field); + RescriptTypeIdent::NamedType("unknown".to_string()) + }; + + let get_unknown_res_type_expr = + |type_field| RescriptTypeExpr::Identifier(get_unknown_res_type_ident(type_field)); + + program + .types + .iter() + .map(|abi_type_decl| { + if abi_type_decl.type_field.starts_with("generic") { + //Generic fields are simple the name of the parameter + //like "T", they should not be declared as types in resript + return Ok(None); + } + let name = mk_type_id_name(&abi_type_decl.type_id); + + let get_first_type_param = || match abi_type_decl + .type_parameters + .clone() + .unwrap_or(vec![]) + .as_slice() + { + [type_id] => generic_param_name_map.get(type_id).cloned().ok_or(anyhow!( + "type_id '{type_id}' should exist in generic_param_name_map" + )), + params => Err(anyhow!( + "Expected single type param but got {}", + params.len() + )), + }; + + let get_components_name_and_type_ident = || { + abi_type_decl + .components + .clone() + .ok_or(anyhow!( + "Expected type_id '{}' components to be 'Some'", + abi_type_decl.type_id + ))? + .iter() + .map(|comp| { + let name = comp.name.clone(); + let type_ident_name = mk_type_id_name(&comp.type_id); + let type_ident = match &comp.type_arguments { + //When there are no type arguments it is a named type or a generic param + None => generic_param_name_map.get(&comp.type_id).cloned().map_or( + //If the type_id is not a defined generic type it is + //a named type + RescriptTypeIdent::NamedType(type_ident_name), + //if the type_id is in the generic_param_name_map + //it is a generic param + |generic_name| RescriptTypeIdent::GenericParam(generic_name), + ), + //When there are type arguments it is a generic type + Some(typ_args) => { + let type_params = typ_args + .iter() + .map(|ta| { + generic_param_name_map.get(&ta.type_id).cloned().map_or( + //If the type_id is not a defined generic type it is + //a named type + RescriptTypeIdent::NamedType(mk_type_id_name( + &ta.type_id, + )), + //if the type_id is in the generic_param_name_map + //it is a generic param + |generic_name| { + RescriptTypeIdent::GenericParam(generic_name) + }, + ) + }) + .collect(); + RescriptTypeIdent::Generic { + name: type_ident_name, + type_params, + } + } + }; + Ok((name, type_ident)) + }) + .collect::>>() + }; + + let type_expr: Result = { + use RescriptTypeIdent::*; + match abi_type_decl.type_field.as_str() { + "()" => Unit.to_ok_expr(), + "bool" => Bool.to_ok_expr(), //Note this is represented as 0 or 1 + //NOTE: its possible when doing rescript int operations you can + //overflow with u32 but its a rare case and likely user will do operation + //int ts/js + "u8" | "u16" | "u32" => Int.to_ok_expr(), + "u64" | "u128" | "u256" | "raw untyped ptr" => BigInt.to_ok_expr(), + "b256" | "address" => String.to_ok_expr(), + type_field if type_field.starts_with("str[") => String.to_ok_expr(), + "struct Vec" => Array(Box::new(GenericParam( + get_first_type_param() + .context("Failed getting param for struct Vec")?, + ))) + .to_ok_expr(), + //TODO: handle nested option since this would need to be flattened to + //single level rescript option. + "enum Option" => Option(Box::new(GenericParam( + get_first_type_param() + .context("Failed getting param for enum Option")?, + ))) + .to_ok_expr(), + type_field if type_field.starts_with("struct ") => { + let record_fields = get_components_name_and_type_ident() + .context(format!( + "Failed getting name and identifier from components for \ + {type_field}", + ))? + .into_iter() + .map(|(name, type_ident)| { + RescriptRecordField::new(name, type_ident) + }) + .collect(); + Ok(RescriptTypeExpr::Record(record_fields)) + } + type_field if type_field.starts_with("enum ") => { + let constructors = get_components_name_and_type_ident() + .context(format!( + "Failed getting name and identifier from components for \ + {type_field}", + ))? + .into_iter() + .map(|(name, type_ident)| { + RescriptVariantConstr::new(name, type_ident) + }) + .collect(); + Ok(RescriptTypeExpr::Variant(constructors)) + } + type_field if type_field.starts_with("(_,") => { + let tuple_types = get_components_name_and_type_ident() + .context(format!( + "Failed getting name and identifier from components for \ + tuple {type_field}", + ))? + .into_iter() + .map(|(_name, type_ident)| type_ident) + .collect(); + + RescriptTypeIdent::Tuple(tuple_types).to_ok_expr() + } + type_field if type_field.starts_with("[_;") => { + //TODO handle fixed array + Ok(get_unknown_res_type_expr(type_field)) + } + type_field => { + //Unknown + Ok(get_unknown_res_type_expr(type_field)) + } + } + }; + + let type_params = abi_type_decl + .type_parameters + .as_ref() + .map_or(Ok(vec![]), |tps| { + tps.iter() + .map(|tp| { + generic_param_name_map + .get(tp) + .ok_or(anyhow!( + "param name for type_id {tp} should exist in \ + generic_param_name_map" + )) + .cloned() + }) + .collect::>>() + }) + .context(format!( + "Failed getting type paramater names for type_id '{}'", + abi_type_decl.type_id + ))?; + + Ok(Some(FuelType { + id: abi_type_decl.type_id, + abi_type_field: abi_type_decl.type_field.clone(), + rescript_type_decl: RescriptTypeDecl::new(name, type_expr?, type_params), + })) + }) + .collect::>>>() + .context("Failed getting type declarations from fuel abi")? + .into_iter() + //Filter out None values since these are declarations we don't want (ie generics) + .filter_map(|x| x) + .for_each(|v| { + types_map.insert(v.id, v); + }); + + Ok(types_map) + } + + fn decode_logs( + program: &ProgramABI, + types: &HashMap, + ) -> Result> { + let mut logs_map: HashMap = HashMap::new(); + let mut names_count: HashMap = HashMap::new(); + + if let Some(logged_types) = &program.logged_types { + for logged_type in logged_types.iter() { + let id = logged_type.log_id.clone(); + let type_id = logged_type.application.type_id; + let logged_type = types.get(&type_id).context("Failed to get logged type")?; + + let event_name = { + // Since Event name doesn't consider the type children, there might be duplications. + // Prevent it by adding a postfix when an even_name appears more than one time + let mut event_name = logged_type.get_event_name(); + let event_name_count = names_count.get(&event_name).unwrap_or(&0); + let event_name_count = event_name_count + 1; + if event_name_count > 1 { + event_name = format!("{event_name}{}", event_name_count) + } + names_count.insert(event_name.clone(), event_name_count); + event_name + }; + + logs_map.insert( + id.clone(), + FuelLog { + id, + event_name: event_name, + logged_type: logged_type.clone(), + }, + ); + } + } else { + Err(anyhow!("ABI doesn't contained defined logged types"))? + } + + Ok(logs_map) + } + + pub fn parse(abi_file_path: &String) -> Result { + let path = PathBuf::from(abi_file_path); + let raw = fs::read_to_string(&path).context(format!( + "Failed to read Fuel ABI file at \"{}\"", + abi_file_path + ))?; + let program = Self::decode_program(&raw).context(format!( + "Failed to decode program of Fuel ABI file at \"{}\"", + abi_file_path + ))?; + let types = Self::decode_types(&program).context(format!( + "Failed to decode types of Fuel ABI file at \"{}\"", + abi_file_path + ))?; + let logs = Self::decode_logs(&program, &types).context(format!( + "Failed to decode logs of Fuel ABI file at \"{}\"", + abi_file_path + ))?; + Ok(Self { + path: abi_file_path.clone(), + raw, + program, + logs, + types, + }) + } + + pub fn get_log(&self, log_id: &String) -> Result { + match self.logs.get(log_id) { + Some(log) => Ok(log.clone()), + None => Err(anyhow!("ABI doesn't contain logged type with id {log_id}")), + } + } + + pub fn get_type_by_struct_name(&self, struct_name: String) -> Result { + let type_name_struct = format!("struct {struct_name}"); + let type_name_enum = format!("enum {struct_name}"); + match self + .program + .types + .iter() + .find(|t| t.type_field == type_name_struct || t.type_field == type_name_enum) + { + Some(t) => self + .types + .get(&t.type_id) + .cloned() + .context(format!("Couldn't find decoded type for id {}", t.type_id)), + None => Err(anyhow!( + "ABI doesn't contain type for the struct {struct_name}" + )), + } + } + + pub fn get_log_ids_by_type(&self, type_id: usize) -> Vec { + self.logs + .values() + .filter_map(|log| { + if log.logged_type.id == type_id { + Some(log.id.clone()) + } else { + None + } + }) + .sorted() + .collect() + } + + pub fn get_logs(&self) -> Vec { + self.logs.values().cloned().collect() + } + + pub fn to_rescript_type_decl_multi(&self) -> Result { + let type_declerations = self + .types + .values() + .sorted_by_key(|t| t.id) + .map(|t| t.rescript_type_decl.clone()) + .collect(); + + Ok(RescriptTypeDeclMulti::new(type_declerations)) + } +} diff --git a/codegenerator/cli/src/fuel/mod.rs b/codegenerator/cli/src/fuel/mod.rs index f74634c7c..0b50ea736 100644 --- a/codegenerator/cli/src/fuel/mod.rs +++ b/codegenerator/cli/src/fuel/mod.rs @@ -1 +1,2 @@ +pub mod abi; pub mod address; diff --git a/codegenerator/cli/src/hbs_templating/contract_import_templates.rs b/codegenerator/cli/src/hbs_templating/contract_import_templates.rs index cd18427c0..69a40e622 100644 --- a/codegenerator/cli/src/hbs_templating/contract_import_templates.rs +++ b/codegenerator/cli/src/hbs_templating/contract_import_templates.rs @@ -187,7 +187,6 @@ mod nested_params { } } -use super::codegen_templates::EventTemplate; use super::hbs_dir_generator::HandleBarsDirGenerator; use crate::{ capitalization::{Capitalize, CapitalizedOptions}, @@ -230,30 +229,18 @@ impl TryInto for AutoSchemaHandlerTemplate { pub struct Contract { name: CapitalizedOptions, imported_events: Vec, - codegen_events: Vec, } impl Contract { fn from_config_contract( contract: &system_config::Contract, - config: &SystemConfig, + is_fuel: bool, + language: &Language, ) -> Result { let imported_events = contract .events .iter() - .map(|event| Event::from_config_event(event)) - .collect::>() - .context(format!( - "Failed getting events for contract {}", - contract.name - ))?; - - let codegen_events = contract - .events - .iter() - .map(|event| { - EventTemplate::from_config_event(event, config, &contract.name.to_string()) - }) + .map(|event| Event::from_config_event(event, &contract, is_fuel, &language)) .collect::>() .context(format!( "Failed getting events for contract {}", @@ -263,7 +250,6 @@ impl Contract { Ok(Contract { name: contract.name.to_capitalized_options(), imported_events, - codegen_events, }) } } @@ -279,22 +265,78 @@ impl TryInto for Contract { #[derive(Serialize)] pub struct Event { name: CapitalizedOptions, + entity_id_from_event_code: String, + create_mock_code: String, params: Vec, } impl Event { - fn from_config_event(e: &system_config::Event) -> Result { - let params = flatten_event_inputs(e.get_event().inputs.clone()) + fn get_entity_id_code(event_var_name: String, is_fuel: bool, language: &Language) -> String { + let to_string_code = match language { + Language::ReScript => "->Belt.Int.toString", + Language::TypeScript => "", + Language::JavaScript => "", + } + .to_string(); + match is_fuel { + true => format!( + "`${{{event_var_name}.transactionId}}_${{{event_var_name}.receiptIndex{}}}`", + to_string_code + ), + false => format!( + "`${{{event_var_name}.transactionHash}}_${{{event_var_name}.logIndex{}}}`", + to_string_code + ), + } + } + + fn get_create_mock_code( + event: &system_config::Event, + contract: &system_config::Contract, + is_fuel: bool, + language: &Language, + ) -> String { + let event_module = format!( + "{}.{}", + contract.name.capitalize(), + event.get_event().name.capitalize() + ); + match is_fuel { + true => { + let data_code = match language { + Language::ReScript => "%raw(`{}`)", + Language::TypeScript => "{}", + Language::JavaScript => "{}", + }; + format!("{event_module}.mock({{data: {data_code} /* It mocks event fields with default values, so you only need to provide data */}})")}, // FIXME: Generate default data + false => format!("{event_module}.createMockEvent({{/* It mocks event fields with default values. You can overwrite them if you need */}})"), + } + } + + fn from_config_event( + event: &system_config::Event, + contract: &system_config::Contract, + is_fuel: bool, + language: &Language, + ) -> Result { + let abi_event = event.get_event(); + let params = flatten_event_inputs(abi_event.inputs.clone()) .into_iter() .map(|input| Param::from_event_param(input)) .collect::>() .context(format!( "Failed getting params for event {}", - e.get_event().name + abi_event.name ))?; Ok(Event { - name: e.get_event().name.to_capitalized_options(), + name: abi_event.name.to_capitalized_options(), + entity_id_from_event_code: Event::get_entity_id_code( + "event".to_string(), + is_fuel, + &language, + ), + create_mock_code: Event::get_create_mock_code(&event, &contract, is_fuel, &language), params, }) } @@ -317,8 +359,9 @@ impl Into for Event { ///Param is used both in the context of an entity and an event for the generating ///schema and handlers. -#[derive(Serialize)] +#[derive(Serialize, Clone)] pub struct Param { + param_name: CapitalizedOptions, ///Event param name + index if its a tuple ie. myTupleParam_0_1 or just myRegularParam entity_key: CapitalizedOptions, ///Just the event param name accessible on the event type @@ -333,6 +376,10 @@ pub struct Param { impl Param { fn from_event_param(flattened_event_param: FlattenedEventParam) -> Result { Ok(Param { + param_name: flattened_event_param + .event_param + .name + .to_capitalized_options(), entity_key: flattened_event_param.get_entity_key(), event_key: flattened_event_param.get_event_param_key(), tuple_param_accessor_indexes: flattened_event_param.accessor_indexes, @@ -353,11 +400,11 @@ impl Into for Param { } impl AutoSchemaHandlerTemplate { - pub fn try_from(config: SystemConfig) -> Result { + pub fn try_from(config: SystemConfig, is_fuel: bool, language: &Language) -> Result { let imported_contracts = config .get_contracts() .iter() - .map(|contract| Contract::from_config_contract(contract, &config)) + .map(|contract| Contract::from_config_contract(contract, is_fuel, &language)) .collect::>()?; Ok(AutoSchemaHandlerTemplate { imported_contracts }) } diff --git a/codegenerator/cli/src/lib.rs b/codegenerator/cli/src/lib.rs index a0baf8bc2..0f4c469ff 100644 --- a/codegenerator/cli/src/lib.rs +++ b/codegenerator/cli/src/lib.rs @@ -11,6 +11,7 @@ mod fuel; mod hbs_templating; mod persisted_state; mod project_paths; +mod rescript_types; mod service_health; mod template_dirs; mod utils; diff --git a/codegenerator/cli/src/rescript_types.rs b/codegenerator/cli/src/rescript_types.rs new file mode 100644 index 000000000..900ecbe2a --- /dev/null +++ b/codegenerator/cli/src/rescript_types.rs @@ -0,0 +1,655 @@ +use crate::capitalization::{Capitalize, CapitalizedOptions}; +use core::fmt; +use itertools::Itertools; +use serde::Serialize; +use std::fmt::Display; + +pub struct RescriptTypeDeclMulti(Vec); + +impl RescriptTypeDeclMulti { + pub fn new(type_declarations: Vec) -> Self { + //TODO: validation + //no duplicates, + //all named types accounted for? (maybe don't want this) + //at least 1 decl + Self(type_declarations) + } + + pub fn to_string(&self) -> String { + match self.0.as_slice() { + [single_decl] => single_decl.to_string(), + multiple_declarations => { + //mutually recursive definitioin + let mut tag_prefix = "".to_string(); + let rec_expr = multiple_declarations + .iter() + .enumerate() + .map(|(i, type_decl)| { + let inner_tag_prefix = type_decl.get_tag_string_if_expr_is_variant(); + let prefix = if i != 0 { + format!("{}and ", inner_tag_prefix) + } else { + //set the top level tag prefix for the first item + tag_prefix = inner_tag_prefix; + "".to_string() + }; + format!("{}{}", prefix, type_decl.to_string_no_type_keyword()) + }) + .join("\n "); + format!("/*Silence warning of label defined in multiple types*/\n@@warning(\"-30\")\n{}type rec {}\n@@warning(\"+30\")", tag_prefix, rec_expr) + } + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct RescriptTypeDecl { + pub name: String, + pub type_expr: RescriptTypeExpr, + pub parameters: Vec, +} + +impl RescriptTypeDecl { + pub fn new(name: String, type_expr: RescriptTypeExpr, parameters: Vec) -> Self { + //TODO: name validation + //validate unique parameters + Self { + name, + type_expr, + parameters, + } + } + + fn get_tag_string_if_expr_is_variant(&self) -> String { + if let RescriptTypeExpr::Variant(_) = self.type_expr { + "@tag(\"case\") ".to_string() + } else { + "".to_string() + } + } + + fn to_string_no_type_keyword(&self) -> String { + let parameters = if self.parameters.is_empty() { + "".to_string() + } else { + // Lowercase generic params because of the issue https://github.com/rescript-lang/rescript-compiler/issues/6759 + let param_names_joined = self + .parameters + .iter() + .map(|p| format!("'{}", p.to_lowercase())) + .join(", "); + format!("<{param_names_joined}>") + }; + format!( + "{}{} = {}", + &self.name, + parameters, + self.type_expr.to_string() + ) + } + + pub fn to_string(&self) -> String { + format!( + "{}type {}", + self.get_tag_string_if_expr_is_variant(), + self.to_string_no_type_keyword(), + ) + } +} + +#[derive(Debug, PartialEq, Clone)] +pub enum RescriptTypeExpr { + Identifier(RescriptTypeIdent), + Record(Vec), + Variant(Vec), +} + +impl RescriptTypeExpr { + pub fn to_string(&self) -> String { + match self { + Self::Identifier(type_ident) => type_ident.to_string(), + Self::Record(params) => { + let params_str = params.iter().map(|p| p.to_string()).join(", "); + format!("{{{params_str}}}") + } + Self::Variant(constructors) => constructors + .iter() + .map(|constr| { + let constr_name = &constr.name; + let constr_payload = constr.payload.to_string(); + format!("| {constr_name}({{payload: {constr_payload}}})") + }) + .join(" "), + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct RescriptRecordField { + pub name: String, + pub as_name: Option, + pub type_ident: RescriptTypeIdent, +} + +impl RescriptRecordField { + pub fn new(name: String, type_ident: RescriptTypeIdent) -> Self { + //TODO: validate name and add as_name if reserved + Self { + name, + as_name: None, + type_ident, + } + } + + fn to_string(&self) -> String { + let as_prefix = self + .as_name + .clone() + .map_or("".to_string(), |s| format!("@as(\"{s}\") ")); + format!("{}{}: {}", as_prefix, self.name, self.type_ident) + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct RescriptVariantConstr { + name: String, + payload: RescriptTypeIdent, //Not supporting records here but tuples are currently part of + //RescriptTypeIdent +} + +impl RescriptVariantConstr { + pub fn new(name: String, payload: RescriptTypeIdent) -> Self { + //TODO: validate uppercase name + Self { name, payload } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub enum RescriptTypeIdent { + Unit, + ID, + Int, + Float, + BigInt, + Address, + String, + Bool, + //Enums defined in the user's schema + SchemaEnum(CapitalizedOptions), + Array(Box), + Option(Box), + //Note: tuple is technically an expression not an identifier + //but it can be inlined and can contain inline tuples in it's parameters + //so it's best suited here for its purpose + Tuple(Vec), + NamedType(String), + GenericParam(String), + Generic { + name: String, + type_params: Vec, + }, +} + +impl RescriptTypeIdent { + //Simply an ergonomic shorthand + pub fn to_expr(self) -> RescriptTypeExpr { + RescriptTypeExpr::Identifier(self) + } + + //Simply an ergonomic shorthand + pub fn to_ok_expr(self) -> anyhow::Result { + Ok(self.to_expr()) + } + + pub fn to_string_decoded_skar(&self) -> String { + match self { + RescriptTypeIdent::Array(inner_type) => format!( + "array>", + inner_type.to_string_decoded_skar() + ), + RescriptTypeIdent::Tuple(inner_types) => { + let inner_types_str = inner_types + .iter() + .map(|inner_type| inner_type.to_string_decoded_skar()) + .collect::>() + .join(", "); + format!( + "HyperSyncClient.Decoder.decodedSolType<({})>", + inner_types_str + ) + } + v => { + format!("HyperSyncClient.Decoder.decodedSolType<{}>", v.to_string()) + } + } + } + + fn to_string(&self) -> String { + match self { + RescriptTypeIdent::Unit => "unit".to_string(), + RescriptTypeIdent::Int => "int".to_string(), + RescriptTypeIdent::Float => "GqlDbCustomTypes.Float.t".to_string(), + RescriptTypeIdent::BigInt => "Ethers.BigInt.t".to_string(), + RescriptTypeIdent::Address => "Ethers.ethAddress".to_string(), + RescriptTypeIdent::String => "string".to_string(), + RescriptTypeIdent::ID => "id".to_string(), + RescriptTypeIdent::Bool => "bool".to_string(), + RescriptTypeIdent::Array(inner_type) => { + format!("array<{}>", inner_type.to_string()) + } + RescriptTypeIdent::Option(inner_type) => { + format!("option<{}>", inner_type.to_string()) + } + RescriptTypeIdent::Tuple(inner_types) => { + let inner_types_str = inner_types + .iter() + .map(|inner_type| inner_type.to_string()) + .collect::>() + .join(", "); + format!("({})", inner_types_str) + } + RescriptTypeIdent::SchemaEnum(enum_name) => { + format!("Enums.{}", &enum_name.uncapitalized) + } + RescriptTypeIdent::NamedType(name) => name.clone(), + // Lowercase generic params because of the issue https://github.com/rescript-lang/rescript-compiler/issues/6759 + RescriptTypeIdent::GenericParam(name) => format!("'{}", name.to_lowercase()), + RescriptTypeIdent::Generic { + name, + type_params: params, + } => { + let params_joined = params + .iter() + .map(|p| p.to_string().uncapitalize()) + .join(", "); + format!("{name}<{params_joined}>") + } + } + } + + pub fn to_rescript_schema(&self) -> String { + match self { + RescriptTypeIdent::Unit => "S.unit".to_string(), + RescriptTypeIdent::Int => "S.int".to_string(), + RescriptTypeIdent::Float => "GqlDbCustomTypes.Float.schema".to_string(), + RescriptTypeIdent::BigInt => "Ethers.BigInt.schema".to_string(), + RescriptTypeIdent::Address => "Ethers.ethAddressSchema".to_string(), + RescriptTypeIdent::String => "S.string".to_string(), + RescriptTypeIdent::ID => "S.string".to_string(), + RescriptTypeIdent::Bool => "S.bool".to_string(), + RescriptTypeIdent::Array(inner_type) => { + format!("S.array({})", inner_type.to_rescript_schema()) + } + RescriptTypeIdent::Option(inner_type) => { + format!("S.null({})", inner_type.to_rescript_schema()) + } + RescriptTypeIdent::Tuple(inner_types) => { + let inner_str = inner_types + .iter() + .enumerate() + .map(|(index, inner_type)| { + format!("s.item({index}, {})", inner_type.to_rescript_schema()) + }) + .collect::>() + .join(", "); + format!("S.tuple((. s) => ({}))", inner_str) + } + RescriptTypeIdent::SchemaEnum(enum_name) => { + format!("Enums.{}Schema", &enum_name.uncapitalized) + } + //TODO: ensure these are defined + RescriptTypeIdent::NamedType(name) | RescriptTypeIdent::GenericParam(name) => { + format!("{name}Schema") + } + RescriptTypeIdent::Generic { + name, + type_params: params, + } => { + let generic_params = params + .iter() + .filter_map(|p| { + if let RescriptTypeIdent::GenericParam(_) = p { + Some(p.to_rescript_schema()) + } else { + None + } + }) + .join(", "); + + let param_schemas_joined = params.iter().map(|p| p.to_rescript_schema()).join(", "); + + let schema_composed = format!("make{name}Schema({param_schemas_joined})"); + if generic_params.is_empty() { + schema_composed + } else { + //if some parameters are generic return a function that takes schemas of those + //parameters + format!("({generic_params}) => {schema_composed}") + } + } + } + } + + pub fn get_default_value_rescript(&self) -> String { + match self { + RescriptTypeIdent::Unit => "()".to_string(), + RescriptTypeIdent::Int => "0".to_string(), + RescriptTypeIdent::Float => "0.0".to_string(), + RescriptTypeIdent::BigInt => "Ethers.BigInt.zero".to_string(), //TODO: Migrate to RescriptCore on ReScript migration + RescriptTypeIdent::Address => "TestHelpers_MockAddresses.defaultAddress".to_string(), + RescriptTypeIdent::String => "\"foo\"".to_string(), + RescriptTypeIdent::ID => "\"my_id\"".to_string(), + RescriptTypeIdent::Bool => "false".to_string(), + RescriptTypeIdent::Array(_) => "[]".to_string(), + RescriptTypeIdent::Option(_) => "None".to_string(), + RescriptTypeIdent::SchemaEnum(enum_name) => { + format!("Enums.{}Default", &enum_name.uncapitalized) + } + RescriptTypeIdent::Tuple(inner_types) => { + let inner_types_str = inner_types + .iter() + .map(|inner_type| inner_type.get_default_value_rescript()) + .collect::>() + .join(", "); + + format!("({})", inner_types_str) + } + //TODO: ensure these are defined + RescriptTypeIdent::NamedType(name) | RescriptTypeIdent::GenericParam(name) => { + format!("{name}Default") + } + RescriptTypeIdent::Generic { + name, + type_params: params, + } => { + let generics_defaults = params + .iter() + .filter_map(|p| { + if let RescriptTypeIdent::GenericParam(_) = p { + Some(p.get_default_value_rescript()) + } else { + None + } + }) + .join(", "); + + let param_defaults_joined = params + .iter() + .map(|p| p.get_default_value_rescript()) + .join(", "); + + let default_composed = format!("make{name}Default({param_defaults_joined})"); + if generics_defaults.is_empty() { + default_composed + } else { + //if some parameters are generic return a function that takes schemas of those + //parameters + format!("({generics_defaults}) => {default_composed}") + } + } + } + } + + pub fn get_default_value_non_rescript(&self) -> String { + match self { + RescriptTypeIdent::Unit => "undefined".to_string(), + RescriptTypeIdent::Int | RescriptTypeIdent::Float => "0".to_string(), + RescriptTypeIdent::BigInt => "0n".to_string(), + RescriptTypeIdent::Address => "Addresses.defaultAddress".to_string(), + RescriptTypeIdent::String => "\"foo\"".to_string(), + RescriptTypeIdent::ID => "\"my_id\"".to_string(), + RescriptTypeIdent::Bool => "false".to_string(), + RescriptTypeIdent::Array(_) => "[]".to_string(), + RescriptTypeIdent::Option(_) => "null".to_string(), + RescriptTypeIdent::SchemaEnum(enum_name) => { + format!("{}Default", &enum_name.uncapitalized) + } + RescriptTypeIdent::Tuple(inner_types) => { + let inner_types_str = inner_types + .iter() + .map(|inner_type| inner_type.get_default_value_non_rescript()) + .join(", "); + format!("[{}]", inner_types_str) + } + //Todo ensure these are defined + RescriptTypeIdent::NamedType(name) | RescriptTypeIdent::GenericParam(name) => { + format!("{name}Default") + } + RescriptTypeIdent::Generic { + name, + type_params: params, + } => { + let generics_defaults = params + .iter() + .filter_map(|p| { + if let RescriptTypeIdent::GenericParam(_) = p { + Some(p.get_default_value_non_rescript()) + } else { + None + } + }) + .join(", "); + + let param_defaults_joined = params + .iter() + .map(|p| p.get_default_value_non_rescript()) + .join(", "); + + let default_composed = format!("make{name}Default({param_defaults_joined})"); + if generics_defaults.is_empty() { + default_composed + } else { + //if some parameters are generic return a function that takes schemas of those + //parameters + format!("({generics_defaults}) => {default_composed}") + } + } + } + } +} + +impl Display for RescriptTypeIdent { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.to_string()) + } +} + +///Implementation of Serialize allows handlebars get a stringified +///version of the string representation of the rescript type +impl Serialize for RescriptTypeIdent { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + // Serialize as display value + self.to_string().serialize(serializer) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn type_decl_to_string_primitive() { + let type_decl = RescriptTypeDecl { + name: "myBool".to_string(), + type_expr: RescriptTypeExpr::Identifier(RescriptTypeIdent::Bool), + parameters: vec![], + }; + + let expected = "type myBool = bool".to_string(); + + assert_eq!(type_decl.to_string(), expected); + } + + #[test] + fn type_decl_to_string_named_ident() { + let type_decl = RescriptTypeDecl { + name: "myAlias".to_string(), + type_expr: RescriptTypeExpr::Identifier(RescriptTypeIdent::NamedType( + "myCustomType".to_string(), + )), + parameters: vec![], + }; + + let expected = "type myAlias = myCustomType".to_string(); + + assert_eq!(type_decl.to_string(), expected); + } + + #[test] + fn type_decl_to_string_record() { + let type_decl = RescriptTypeDecl::new( + "myRecord".to_string(), + RescriptTypeExpr::Record(vec![ + RescriptRecordField { + name: "reservedWord_".to_string(), + as_name: Some("reservedWord".to_string()), + type_ident: RescriptTypeIdent::NamedType("myCustomType".to_string()), + }, + RescriptRecordField::new( + "myOptBool".to_string(), + RescriptTypeIdent::Option(Box::new(RescriptTypeIdent::Bool)), + ), + ]), + vec![], + ); + + let expected = r#"type myRecord = {@as("reservedWord") reservedWord_: myCustomType, myOptBool: option}"#.to_string(); + + assert_eq!(type_decl.to_string(), expected); + } + + #[test] + fn type_decl_multi_to_string() { + let type_decl_1 = RescriptTypeDecl::new( + "myRecord".to_string(), + RescriptTypeExpr::Record(vec![ + RescriptRecordField::new( + "fieldA".to_string(), + RescriptTypeIdent::NamedType("myCustomType".to_string()), + ), + RescriptRecordField::new("fieldB".to_string(), RescriptTypeIdent::Bool), + ]), + vec![], + ); + + let type_decl_2 = RescriptTypeDecl::new( + "myCustomType".to_string(), + RescriptTypeExpr::Identifier(RescriptTypeIdent::Bool), + vec![], + ); + + let type_decl_multi = RescriptTypeDeclMulti::new(vec![type_decl_1, type_decl_2]); + + let expected = "/*Silence warning of label defined in multiple types*/\n@@warning(\"-30\")\ntype rec myRecord = {fieldA: myCustomType, \ + fieldB: bool}\n and myCustomType = bool\n@@warning(\"+30\")" + .to_string(); + + assert_eq!(type_decl_multi.to_string(), expected); + } + + #[test] + fn type_decl_to_string_record_generic() { + let my_custom_type_ident = RescriptTypeIdent::NamedType("myCustomType".to_string()); + let type_decl = RescriptTypeDecl::new( + "myRecord".to_string(), + RescriptTypeExpr::Record(vec![ + RescriptRecordField::new("fieldA".to_string(), my_custom_type_ident.clone()), + RescriptRecordField::new( + "fieldB".to_string(), + RescriptTypeIdent::Generic { + name: "myGenericType".to_string(), + type_params: vec![ + my_custom_type_ident.clone(), + RescriptTypeIdent::GenericParam("a".to_string()), + ], + }, + ), + RescriptRecordField::new( + "fieldC".to_string(), + RescriptTypeIdent::GenericParam("b".to_string()), + ), + ]), + vec!["a".to_string(), "b".to_string()], + ); + + let expected = r#"type myRecord<'a, 'b> = {fieldA: myCustomType, fieldB: myGenericType, fieldC: 'b}"#.to_string(); + + assert_eq!(type_decl.to_string(), expected); + } + + #[test] + fn type_decl_to_string_variant() { + let my_custom_type_ident = RescriptTypeIdent::NamedType("myCustomType".to_string()); + let type_decl = RescriptTypeDecl::new( + "myVariant".to_string(), + RescriptTypeExpr::Variant(vec![ + RescriptVariantConstr::new("ConstrA".to_string(), my_custom_type_ident.clone()), + RescriptVariantConstr::new( + "ConstrB".to_string(), + RescriptTypeIdent::Generic { + name: "myGenericType".to_string(), + type_params: vec![ + my_custom_type_ident.clone(), + RescriptTypeIdent::GenericParam("a".to_string()), + ], + }, + ), + RescriptVariantConstr::new( + "ConstrC".to_string(), + RescriptTypeIdent::GenericParam("b".to_string()), + ), + ]), + vec!["a".to_string(), "b".to_string()], + ); + + let expected = r#"@tag("case") type myVariant<'a, 'b> = | ConstrA({payload: myCustomType}) | ConstrB({payload: myGenericType}) | ConstrC({payload: 'b})"#.to_string(); + + assert_eq!(type_decl.to_string(), expected); + } + + #[test] + fn type_decl_multi_variant_to_string() { + let my_custom_type_ident = RescriptTypeIdent::NamedType("myCustomType".to_string()); + let type_decl_1 = RescriptTypeDecl::new( + "myVariant".to_string(), + RescriptTypeExpr::Variant(vec![ + RescriptVariantConstr::new("ConstrA".to_string(), my_custom_type_ident.clone()), + RescriptVariantConstr::new( + "ConstrB".to_string(), + RescriptTypeIdent::GenericParam("a".to_string()), + ), + ]), + vec!["a".to_string()], + ); + + let type_decl_2 = RescriptTypeDecl::new( + "myCustomType".to_string(), + RescriptTypeExpr::Identifier(RescriptTypeIdent::Bool), + vec![], + ); + + let type_decl_3 = RescriptTypeDecl::new( + "myVariant2".to_string(), + RescriptTypeExpr::Variant(vec![RescriptVariantConstr::new( + "ConstrC".to_string(), + RescriptTypeIdent::Bool, + )]), + vec![], + ); + + let type_decl_multi = + RescriptTypeDeclMulti::new(vec![type_decl_1, type_decl_2, type_decl_3]); + + let expected = "/*Silence warning of label defined in multiple types*/\n@@warning(\"-30\")\n@tag(\"case\") type rec myVariant<'a> = | \ + ConstrA({payload: myCustomType}) | ConstrB({payload: 'a})\n and \ + myCustomType = bool\n @tag(\"case\") and myVariant2 = | \ + ConstrC({payload: bool})\n@@warning(\"+30\")" + .to_string(); + + assert_eq!(type_decl_multi.to_string(), expected); + } +} diff --git a/codegenerator/cli/templates/dynamic/codegen/src/TestHelpers.res.hbs b/codegenerator/cli/templates/dynamic/codegen/src/TestHelpers.res.hbs index b3875ddd6..43019697c 100644 --- a/codegenerator/cli/templates/dynamic/codegen/src/TestHelpers.res.hbs +++ b/codegenerator/cli/templates/dynamic/codegen/src/TestHelpers.res.hbs @@ -333,8 +333,6 @@ module {{contract.name.capitalized}} = { () {{/if}} - - EventFunctions.makeEventMocker(~params, ~mockEventData) } } diff --git a/codegenerator/cli/templates/dynamic/contract_import_templates/javascript/src/EventHandlers.js.hbs b/codegenerator/cli/templates/dynamic/contract_import_templates/javascript/src/EventHandlers.js.hbs index 84d273ded..6bfddfb1f 100644 --- a/codegenerator/cli/templates/dynamic/contract_import_templates/javascript/src/EventHandlers.js.hbs +++ b/codegenerator/cli/templates/dynamic/contract_import_templates/javascript/src/EventHandlers.js.hbs @@ -11,7 +11,7 @@ const { {{contract.name.capitalized}}Contract.{{event.name.capitalized}}.handler(({event, context}) => { const entity = { - id: event.transactionHash + event.logIndex.toString(), + id: {{event.entity_id_from_event_code}}, {{#each event.params as |param|}} {{param.entity_key.uncapitalized }}: event.params.{{param.event_key.uncapitalized}}{{#if @@ -25,8 +25,7 @@ const { {{/each}} }; - context.{{contract.name.capitalized - }}_{{event.name.capitalized}}.set(entity); + context.{{contract.name.capitalized}}_{{event.name.capitalized}}.set(entity); }); {{/each}} {{/each}} diff --git a/codegenerator/cli/templates/dynamic/contract_import_templates/javascript/test/Test.js.hbs b/codegenerator/cli/templates/dynamic/contract_import_templates/javascript/test/Test.js.hbs index 1b65e23df..ea7dfe53e 100644 --- a/codegenerator/cli/templates/dynamic/contract_import_templates/javascript/test/Test.js.hbs +++ b/codegenerator/cli/templates/dynamic/contract_import_templates/javascript/test/Test.js.hbs @@ -2,52 +2,35 @@ {{#with imported_contracts.[0] as | contract |}} const assert = require("assert"); const { TestHelpers } = require("generated"); -const { MockDb, {{contract.name.capitalized}}, Addresses } = TestHelpers; +const { MockDb, {{contract.name.capitalized}} } = TestHelpers; {{/with}} {{#with imported_contracts.[0] as | contract |}} - {{#with contract.codegen_events.[0] as | event |}} + {{#with contract.imported_events.[0] as | event |}} describe("{{contract.name.capitalized}} contract {{event.name.capitalized}} event tests", () => { // Create mock db const mockDb = MockDb.createMockDb(); - // Creating mock {{contract.name.capitalized}} contract {{event.name.capitalized}} event - const mock{{contract.name.capitalized}}{{event.name.capitalized}}Event = {{contract.name.capitalized}}.{{event.name.capitalized}}.createMockEvent({ - {{#each event.params as | param |}} - {{param.param_name.uncapitalized}}: {{param.default_value_non_rescript}}, - {{/each}} - mockEventData: { - chainId: 1, - blockNumber: 0, - blockTimestamp: 0, - blockHash: "0x0000000000000000000000000000000000000000000000000000000000000000", - srcAddress: Addresses.defaultAddress, - transactionHash: "0x0000000000000000000000000000000000000000000000000000000000000000", - transactionIndex: 0, - logIndex: 0, - }, - }); + // Creating mock for {{contract.name.capitalized}} contract {{event.name.capitalized}} event + const event = {{event.create_mock_code}}; // Processing the event const mockDbUpdated = {{contract.name.capitalized}}.{{event.name.capitalized}}.processEvent({ - event: mock{{contract.name.capitalized}}{{event.name.capitalized}}Event, + event, mockDb, }); it("{{contract.name.capitalized}}_{{event.name.capitalized}}Entity is created correctly", () => { // Getting the actual entity from the mock database let actual{{contract.name.capitalized}}{{event.name.capitalized}}Entity = mockDbUpdated.entities.{{contract.name.capitalized}}_{{event.name.capitalized}}.get( - mock{{contract.name.capitalized}}{{event.name.capitalized}}Event.transactionHash + - mock{{contract.name.capitalized}}{{event.name.capitalized}}Event.logIndex.toString() + {{event.entity_id_from_event_code}} ); // Creating the expected entity const expected{{contract.name.capitalized}}{{event.name.capitalized}}Entity = { - id: - mock{{contract.name.capitalized}}{{event.name.capitalized}}Event.transactionHash + - mock{{contract.name.capitalized}}{{event.name.capitalized}}Event.logIndex.toString(), + id: {{event.entity_id_from_event_code}}, {{#each event.params as |param|}} - {{param.param_name.uncapitalized}}: mock{{contract.name.capitalized}}{{event.name.capitalized}}Event.params.{{param.param_name.uncapitalized}}, + {{param.param_name.uncapitalized}}: event.params.{{param.param_name.uncapitalized}}, {{/each}} }; // Asserting that the entity in the mock database is the same as the expected entity diff --git a/codegenerator/cli/templates/dynamic/contract_import_templates/rescript/src/EventHandlers.res.hbs b/codegenerator/cli/templates/dynamic/contract_import_templates/rescript/src/EventHandlers.res.hbs index 0260b8146..d349407d8 100644 --- a/codegenerator/cli/templates/dynamic/contract_import_templates/rescript/src/EventHandlers.res.hbs +++ b/codegenerator/cli/templates/dynamic/contract_import_templates/rescript/src/EventHandlers.res.hbs @@ -10,7 +10,7 @@ Handlers.{{contract.name.capitalized let entity: Types.{{contract.name.uncapitalized }}_{{event.name.capitalized }}Entity = { - id: event.transactionHash ++ event.logIndex->Belt.Int.toString, + id: {{event.entity_id_from_event_code}}, {{#each event.params as |param|}} {{param.entity_key.uncapitalized }}: event.params.{{param.event_key.uncapitalized}} diff --git a/codegenerator/cli/templates/dynamic/contract_import_templates/rescript/test/Test.res.hbs b/codegenerator/cli/templates/dynamic/contract_import_templates/rescript/test/Test.res.hbs index 57a9c51c9..fbcaeb855 100644 --- a/codegenerator/cli/templates/dynamic/contract_import_templates/rescript/test/Test.res.hbs +++ b/codegenerator/cli/templates/dynamic/contract_import_templates/rescript/test/Test.res.hbs @@ -4,31 +4,16 @@ open Belt open TestHelpers {{#with imported_contracts.[0] as | contract |}} - {{#with contract.codegen_events.[0] as | event |}} + {{#with contract.imported_events.[0] as | event |}} describe("{{contract.name.capitalized}} contract {{event.name.capitalized}} event tests", () => { // Create mock db let mockDb = MockDb.createMockDb() - // Creating mock {{contract.name.capitalized}} contract {{event.name.capitalized}} event - let mock{{contract.name.capitalized}}{{event.name.capitalized}}Event = {{contract.name.capitalized}}.{{event.name.capitalized}}.createMockEvent({ - {{#each event.params as | param |}} - {{param.param_name.uncapitalized}}: {{param.default_value_rescript}}, - {{/each}} - mockEventData: { - chainId: 1, - blockNumber: 0, - blockTimestamp: 0, - blockHash: Ethers.Constants.zeroHash, - srcAddress: Addresses.defaultAddress, - transactionHash: Ethers.Constants.zeroHash, - transactionIndex: 0, - logIndex: 0, - }, - }) + let event = {{event.create_mock_code}}; // Processing the event let mockDbUpdated = {{contract.name.capitalized}}.{{event.name.capitalized}}.processEvent({ - event: mock{{contract.name.capitalized}}{{event.name.capitalized}}Event, + event, mockDb, }) @@ -36,14 +21,14 @@ describe("{{contract.name.capitalized}} contract {{event.name.capitalized}} even // Getting the actual entity from the mock database let actual{{contract.name.capitalized}}{{event.name.capitalized}}Entity = mockDbUpdated.entities.{{contract.name.uncapitalized}}_{{event.name.capitalized}}.get( - mock{{contract.name.capitalized}}{{event.name.capitalized}}Event.transactionHash ++ mock{{contract.name.capitalized}}{{event.name.capitalized}}Event.logIndex->Belt.Int.toString, + {{event.entity_id_from_event_code}}, )->Option.getExn // Creating the expected entity let expected{{contract.name.capitalized}}{{event.name.capitalized}}Entity: Types.{{contract.name.uncapitalized}}_{{event.name.capitalized}}Entity = { - id: mock{{contract.name.capitalized}}{{event.name.capitalized}}Event.transactionHash ++ mock{{contract.name.capitalized}}{{event.name.capitalized}}Event.logIndex->Belt.Int.toString, + id: {{event.entity_id_from_event_code}}, {{#each event.params as |param|}} - {{param.param_name.uncapitalized}}: mock{{contract.name.capitalized}}{{event.name.capitalized}}Event.params.{{param.param_name.uncapitalized}}{{#if param.is_eth_address}}->Ethers.ethAddressToString{{/if}}, + {{param.param_name.uncapitalized}}: event.params.{{param.param_name.uncapitalized}}{{#if param.is_eth_address}}->Ethers.ethAddressToString{{/if}}, {{/each}} } //Assert the expected {{contract.name.capitalized}} {{event.name.capitalized}} entity diff --git a/codegenerator/cli/templates/dynamic/contract_import_templates/typescript/src/EventHandlers.ts.hbs b/codegenerator/cli/templates/dynamic/contract_import_templates/typescript/src/EventHandlers.ts.hbs index 6de4a5e9d..e4f80e3a6 100644 --- a/codegenerator/cli/templates/dynamic/contract_import_templates/typescript/src/EventHandlers.ts.hbs +++ b/codegenerator/cli/templates/dynamic/contract_import_templates/typescript/src/EventHandlers.ts.hbs @@ -14,7 +14,7 @@ import { {{contract.name.capitalized}}Contract.{{event.name.capitalized}}.handler(({ event, context }) => { const entity: {{contract.name.capitalized}}_{{event.name.capitalized}}Entity = { - id: event.transactionHash + event.logIndex.toString(), + id: {{event.entity_id_from_event_code}}, {{#each event.params as |param|}} {{param.entity_key.uncapitalized}}: event.params.{{param.event_key.uncapitalized}}{{#if param.tuple_param_accessor_indexes diff --git a/codegenerator/cli/templates/dynamic/contract_import_templates/typescript/test/Test.ts.hbs b/codegenerator/cli/templates/dynamic/contract_import_templates/typescript/test/Test.ts.hbs index 76f8f0a75..2e9b56fc5 100644 --- a/codegenerator/cli/templates/dynamic/contract_import_templates/typescript/test/Test.ts.hbs +++ b/codegenerator/cli/templates/dynamic/contract_import_templates/typescript/test/Test.ts.hbs @@ -1,57 +1,40 @@ {{#with imported_contracts.[0] as | contract |}} - {{#with contract.codegen_events.[0] as | event |}} + {{#with contract.imported_events.[0] as | event |}} import assert from "assert"; import { TestHelpers, {{contract.name.capitalized}}_{{event.name.capitalized}}Entity } from "generated"; -const { MockDb, {{contract.name.capitalized}}, Addresses } = TestHelpers; +const { MockDb, {{contract.name.capitalized}} } = TestHelpers; {{/with}} {{/with}} {{#with imported_contracts.[0] as | contract |}} - {{#with contract.codegen_events.[0] as | event |}} + {{#with contract.imported_events.[0] as | event |}} describe("{{contract.name.capitalized}} contract {{event.name.capitalized}} event tests", () => { // Create mock db const mockDb = MockDb.createMockDb(); - // Creating mock {{contract.name.capitalized}} contract {{event.name.capitalized}} event - const mock{{contract.name.capitalized}}{{event.name.capitalized}}Event = {{contract.name.capitalized}}.{{event.name.capitalized}}.createMockEvent({ - {{#each event.params as | param |}} - {{param.param_name.uncapitalized}}: {{param.default_value_non_rescript}}, - {{/each}} - mockEventData: { - chainId: 1, - blockNumber: 0, - blockTimestamp: 0, - blockHash: "0x0000000000000000000000000000000000000000000000000000000000000000", - srcAddress: Addresses.defaultAddress, - transactionHash: "0x0000000000000000000000000000000000000000000000000000000000000000", - transactionIndex: 0, - logIndex: 0, - }, - }); + // Creating mock for {{contract.name.capitalized}} contract {{event.name.capitalized}} event + const event = {{event.create_mock_code}}; // Processing the event const mockDbUpdated = {{contract.name.capitalized}}.{{event.name.capitalized}}.processEvent({ - event: mock{{contract.name.capitalized}}{{event.name.capitalized}}Event, + event, mockDb, }); it("{{contract.name.capitalized}}_{{event.name.capitalized}}Entity is created correctly", () => { // Getting the actual entity from the mock database let actual{{contract.name.capitalized}}{{event.name.capitalized}}Entity = mockDbUpdated.entities.{{contract.name.capitalized}}_{{event.name.capitalized}}.get( - mock{{contract.name.capitalized}}{{event.name.capitalized}}Event.transactionHash + - mock{{contract.name.capitalized}}{{event.name.capitalized}}Event.logIndex.toString() + {{event.entity_id_from_event_code}} ); // Creating the expected entity const expected{{contract.name.capitalized}}{{event.name.capitalized}}Entity: {{contract.name.capitalized}}_{{event.name.capitalized}}Entity = { - id: - mock{{contract.name.capitalized}}{{event.name.capitalized}}Event.transactionHash + - mock{{contract.name.capitalized}}{{event.name.capitalized}}Event.logIndex.toString(), + id: {{event.entity_id_from_event_code}}, {{#each event.params as |param|}} - {{param.param_name.uncapitalized}}: mock{{contract.name.capitalized}}{{event.name.capitalized}}Event.params.{{param.param_name.uncapitalized}}, + {{param.param_name.uncapitalized}}: event.params.{{param.param_name.uncapitalized}}, {{/each}} }; // Asserting that the entity in the mock database is the same as the expected entity