diff --git a/Cargo.lock b/Cargo.lock index b044c95..b3dda28 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -121,10 +121,13 @@ dependencies = [ "anyhow", "clap", "console", + "dialoguer", "gutenberg", + "hex", "pretty_assertions", "serde", "serde_yaml", + "thiserror", "tokio", ] @@ -200,6 +203,18 @@ dependencies = [ "syn", ] +[[package]] +name = "dialoguer" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af3c796f3b0b408d9fd581611b47fa850821fcb84aa640b83a3c1a5be2d691f2" +dependencies = [ + "console", + "shell-words", + "tempfile", + "zeroize", +] + [[package]] name = "diff" version = "0.1.13" @@ -248,6 +263,15 @@ dependencies = [ "libc", ] +[[package]] +name = "fastrand" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" +dependencies = [ + "instant", +] + [[package]] name = "getrandom" version = "0.2.8" @@ -320,6 +344,12 @@ dependencies = [ "libc", ] +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "indexmap" version = "1.9.2" @@ -539,6 +569,15 @@ dependencies = [ "bitflags", ] +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + [[package]] name = "rustix" version = "0.36.6" @@ -610,6 +649,12 @@ dependencies = [ "unsafe-libyaml", ] +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + [[package]] name = "smallvec" version = "1.10.0" @@ -639,6 +684,20 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +dependencies = [ + "cfg-if", + "fastrand", + "libc", + "redox_syscall", + "remove_dir_all", + "winapi", +] + [[package]] name = "termcolor" version = "1.1.3" @@ -918,3 +977,9 @@ name = "yansi" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" + +[[package]] +name = "zeroize" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f" diff --git a/crates/byte_cli/Cargo.toml b/crates/byte_cli/Cargo.toml index 93b6751..40be027 100644 --- a/crates/byte_cli/Cargo.toml +++ b/crates/byte_cli/Cargo.toml @@ -5,13 +5,15 @@ edition = "2021" [dependencies] gutenberg = { path = "../gutenberg" } +dialoguer = "0.10" serde = { version = "1.0", features = ["derive"] } serde_yaml = "0.9" -# thiserror = "1.0" +thiserror = "1.0" tokio = {version = "1.24", features = ["macros", "rt-multi-thread"]} clap = {version = "4.0", features = ["derive"]} anyhow = "1.0" console = "0.15.4" +hex = "0.4" [dev-dependencies] pretty_assertions = "1.3.0" diff --git a/crates/byte_cli/src/endpoints.rs b/crates/byte_cli/src/endpoints.rs new file mode 100644 index 0000000..cbcdd9f --- /dev/null +++ b/crates/byte_cli/src/endpoints.rs @@ -0,0 +1,3 @@ +pub mod init_config; + +pub use init_config::*; diff --git a/crates/byte_cli/src/endpoints/init_config.rs b/crates/byte_cli/src/endpoints/init_config.rs new file mode 100644 index 0000000..d113b4d --- /dev/null +++ b/crates/byte_cli/src/endpoints/init_config.rs @@ -0,0 +1,332 @@ +use crate::prelude::*; +use anyhow::Result; +use console::{style, Style}; +use dialoguer::theme::ColorfulTheme; +use dialoguer::{Confirm, Input, MultiSelect, Select}; +use gutenberg::{ + models::nft, + schema, + types::{Listing, Market, Royalties}, +}; +use hex; + +const TAG_OPTIONS: [&str; 11] = [ + "Art", + "ProfilePicture", + "Collectible", + "GameAsset", + "TokenisedAsset", + "Ticker", + "DomainName", + "Music", + "Video", + "Ticket", + "License", +]; + +const FIELD_OPTIONS: [&str; 3] = ["display", "url", "attributes"]; +const BEHAVIOUR_OPTIONS: [&str; 2] = ["composable", "loose"]; +const SUPPLY_OPTIONS: [&str; 2] = ["Unlimited", "Limited"]; +const MINTING_OPTIONS: [&str; 3] = ["Launchpad", "Direct", "Airdrop"]; +const ROYALTY_OPTIONS: [&str; 3] = ["Proportional", "Constant", "None"]; +const MARKET_OPTIONS: [&str; 2] = ["FixedPrice", "DutchAuction"]; + +pub fn get_dialoguer_theme() -> ColorfulTheme { + ColorfulTheme { + prompt_style: Style::new(), + checked_item_prefix: style("✔".to_string()).green().force_styling(true), + unchecked_item_prefix: style("✔".to_string()) + .black() + .force_styling(true), + ..Default::default() + } +} + +pub fn map_indices(indices: Vec, arr: &[&str]) -> Vec { + let vec: Vec = indices + .iter() + .map(|index| arr[*index].to_string()) + .collect(); + vec +} + +pub fn init_collection_config() { + let mut schema = schema::Schema::new(); + let theme = get_dialoguer_theme(); + + let string_validator = |_input: &String| -> Result<(), String> { Ok(()) }; + + let address_validator = |input: &String| -> Result<(), CliError> { + let hexa_str = if &input[..2] == "0x" { + &input[2..] + } else { + &input + }; + + let hexa = + hex::decode(hexa_str).map_err(|_| CliError::InvalidAddress)?; + + if hexa.len() != 20 { + Err(CliError::InvalidAddress) + } else { + Ok(()) + } + }; + + let number_validator = |input: &String| -> Result<(), String> { + if input.parse::().is_err() { + Err(format!("Couldn't parse input of '{}' to a number.", input)) + } else { + Ok(()) + } + }; + + let name = Input::with_theme(&theme) + .with_prompt("What is the name of the Collection?") + .validate_with(string_validator) + .interact() + .unwrap(); + + schema.collection.set_name(name); + + let description = Input::with_theme(&theme) + .with_prompt("What is the description of the Collection?") + .validate_with(string_validator) + .interact() + .unwrap(); + + schema.collection.set_description(description); + + let symbol = Input::with_theme(&theme) + .with_prompt("What is the symbol of the Collection?") + .validate_with(string_validator) + .interact() + .unwrap(); + + schema.collection.set_symbol(symbol); + + let has_tags = Confirm::with_theme(&theme) + .with_prompt("Do you want to add Tags to your Collection?") + .interact() + .unwrap(); + + if has_tags { + let tag_indices = MultiSelect::with_theme(&theme) + .with_prompt("Which tags do you want to add? (use [SPACEBAR] to select options you want and hit [ENTER] when done)") + .items(&TAG_OPTIONS) + .interact() + .unwrap(); + + let tags = map_indices(tag_indices, &TAG_OPTIONS); + + schema.collection.set_tags(&tags).unwrap(); + } + + let has_url = Confirm::with_theme(&theme) + .with_prompt("Do you want to add a URL to your Collection Website?") + .interact() + .unwrap(); + + if has_url { + let url = Input::with_theme(&theme) + .with_prompt("What is the URL of the Collection Website?") + .validate_with(string_validator) + .interact() + .unwrap(); + + schema.collection.set_url(url); + }; + + let nft_field_indices = MultiSelect::with_theme(&theme) + .with_prompt("Which NFT fields do you want the NFTs to have? (use [SPACEBAR] to select options you want and hit [ENTER] when done)") + .items(&FIELD_OPTIONS) + .interact() + .unwrap(); + + let mut nft_fields = map_indices(nft_field_indices, &FIELD_OPTIONS); + + // Since the creator has already mentioned that the Collection has Tags + if has_tags { + nft_fields.push("tags".to_string()); + }; + + schema.nft.fields = nft::Fields::new_from(nft_fields).unwrap(); + + let nft_behaviour_indices = MultiSelect::with_theme(&theme) + .with_prompt("Which NFT behaviours do you want the NFTs to have? (use [SPACEBAR] to select options you want and hit [ENTER] when done)") + .items(&BEHAVIOUR_OPTIONS) + .interact() + .unwrap(); + + let nft_behaviours = map_indices(nft_behaviour_indices, &BEHAVIOUR_OPTIONS); + + schema.nft.behaviours = nft::Behaviours::new_from(nft_behaviours).unwrap(); + + let supply_index = Select::with_theme(&theme) + .with_prompt("Which Supply Policy do you want your Collection to have?") + .items(&SUPPLY_OPTIONS) + .interact() + .unwrap(); + + let supply_policy = SUPPLY_OPTIONS[supply_index]; + + let mut limit = Option::None; + + if supply_policy == "Limited" { + limit = Some( + Input::with_theme(&theme) + .with_prompt("What is the supply limit of the Collection?") + .validate_with(number_validator) + .interact() + .unwrap() + .parse::() + .expect("Failed to parse String into u64 - This error should not occur has input has been already validated.") + ); + } + + schema.nft.supply_policy = + nft::SupplyPolicy::new_from(supply_policy, limit).unwrap(); + + let royalty_index = Select::with_theme(&theme) + .with_prompt( + "Which Royalty Policy do you want your Collection to have?", + ) + .items(&ROYALTY_OPTIONS) + .interact() + .unwrap(); + + let royalty_policy = ROYALTY_OPTIONS[royalty_index]; + + let mut fee = Option::None; + + if royalty_policy == "Proportional" { + fee = Some( + Input::with_theme(&theme) + .with_prompt("What is the royalty fee in Basis Points?") + .validate_with(number_validator) + .interact() + .unwrap() + .parse::() + .expect("Failed to parse String into u64 - This error should not occur has input has been already validated.") + ); + } + if royalty_policy == "Constant" { + fee = Some( + Input::with_theme(&theme) + .with_prompt("What is the constant royalty commission?") + .validate_with(number_validator) + .interact() + .unwrap() + .parse::() + .expect("Failed to parse String into u64 - This error should not occur has input has been already validated.") + ); + } + + schema.royalties = Royalties::new_from(royalty_policy, fee).unwrap(); + + let mint_strategy_indices = MultiSelect::with_theme(&theme) + .with_prompt("Which minting strategies do you plan using? (use [SPACEBAR] to select options you want and hit [ENTER] when done)") + .items(&MINTING_OPTIONS) + .interact() + .unwrap(); + + let mint_strategies = map_indices(mint_strategy_indices, &MINTING_OPTIONS); + + let contains_launchpad = mint_strategies.contains(&"Launchpad".to_owned()); + + schema.nft.mint_strategy = + nft::MintStrategy::new_from(mint_strategies).unwrap(); + + if contains_launchpad { + let admin_address = Input::with_theme(&theme) + .with_prompt("What is the address of the Listing administrator?") + .validate_with(address_validator) + .interact() + .unwrap(); + + let receiver_address = Input::with_theme(&theme) + .with_prompt("What is the address that receives the sale proceeds?") + .validate_with(address_validator) + .interact() + .unwrap(); + + let listings: u64 = Input::with_theme(&theme) + .with_prompt( + // TODO: The meaning of this questions may be ambiguous + // from the perspective of the creator + "How many Primary Market Listings do you plan on having?", + ) + .validate_with(number_validator) + .interact() + .unwrap() + .parse::() + .expect("Failed to parse String into u64 - This error should not occur has input has been already validated."); + + for i in 0..listings { + let prompt = format!( + "What is the market primitive to use for the sale nº {}", + i + 1 + ); + + let market_index = Select::with_theme(&theme) + .with_prompt(prompt) + .items(&MARKET_OPTIONS) + .interact() + .unwrap(); + + let is_whitelisted = Confirm::with_theme(&theme) + .with_prompt("What is it a whitelisted sale?") + .interact() + .unwrap(); + + let market: Market; + + match MARKET_OPTIONS[market_index] { + "FixedPrice" => { + let price = Input::with_theme(&theme) + .with_prompt("What is the fixed price of the sale?") + .validate_with(number_validator) + .interact() + .unwrap() + .parse::() + .expect("Failed to parse String into u64 - This error should not occur has input has been already validated."); + + market = Market::FixedPrice { + token: "sui::sui::SUI".to_string(), + price, + is_whitelisted, + }; + } + "DutchAuction" => { + let reserve_price = Input::with_theme(&theme) + .with_prompt( + "What is the reserve price of the auction?", + ) + .validate_with(number_validator) + .interact() + .unwrap() + .parse::() + .expect("Failed to parse String into u64 - This error should not occur has input has been already validated."); + + market = Market::DutchAuction { + token: "sui::sui::SUI".to_string(), + reserve_price, + is_whitelisted, + }; + } + _ => { + eprintln!("TODO: This error handling"); + std::process::exit(2); + } + } + + let listing = Listing::new( + admin_address.as_str(), + receiver_address.as_str(), + market, + ); + + schema.add_listing(listing); + } + } +} diff --git a/crates/byte_cli/src/err.rs b/crates/byte_cli/src/err.rs new file mode 100644 index 0000000..e0c9290 --- /dev/null +++ b/crates/byte_cli/src/err.rs @@ -0,0 +1,13 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum CliError { + #[error("An IO error has occured")] + IoError(#[from] std::io::Error), + // #[error("The tag provided is not supported")] + // UnsupportedTag, + #[error("The address provided is invalid")] + InvalidAddress, + #[error("The market type provided is invalid")] + InvalidMarket, +} diff --git a/crates/byte_cli/src/lib.rs b/crates/byte_cli/src/lib.rs index 438d7e0..cdfaf29 100644 --- a/crates/byte_cli/src/lib.rs +++ b/crates/byte_cli/src/lib.rs @@ -1,3 +1,5 @@ pub mod cli; pub mod consts; +pub mod endpoints; +pub mod err; pub mod prelude; diff --git a/crates/byte_cli/src/main.rs b/crates/byte_cli/src/main.rs index 698b238..9abcfe2 100644 --- a/crates/byte_cli/src/main.rs +++ b/crates/byte_cli/src/main.rs @@ -1,12 +1,14 @@ pub mod cli; pub mod consts; +pub mod endpoints; +pub mod err; pub mod prelude; +use crate::endpoints::init_config; use crate::prelude::*; use anyhow::Result; use clap::Parser; use console::style; -use gutenberg; #[tokio::main] async fn main() { @@ -29,7 +31,9 @@ async fn run() -> Result<()> { let cli = Cli::parse(); match cli.command { - Commands::InitCollectionConfig {} => {} + Commands::InitCollectionConfig {} => { + init_config::init_collection_config() + } Commands::InitUploadConfig { assets_dir: _ } => {} Commands::InitConfig { assets_dir: _ } => {} Commands::DeployAssets { assets_dir: _ } => {} diff --git a/crates/byte_cli/src/prelude.rs b/crates/byte_cli/src/prelude.rs index ff55808..5eabdef 100644 --- a/crates/byte_cli/src/prelude.rs +++ b/crates/byte_cli/src/prelude.rs @@ -1,2 +1,4 @@ pub use crate::cli::{Cli, Commands}; pub use crate::consts; +pub use crate::endpoints::init_config; +pub use crate::err::{self, CliError};