diff --git a/Cargo.lock b/Cargo.lock index eb012eee036..178a0c4c97c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2032,6 +2032,7 @@ dependencies = [ "async-trait", "chrono", "clap 4.5.4", + "colored", "devault", "forc", "forc-pkg", diff --git a/forc-pkg/src/manifest/mod.rs b/forc-pkg/src/manifest/mod.rs index e6d94f1079f..c319df6f313 100644 --- a/forc-pkg/src/manifest/mod.rs +++ b/forc-pkg/src/manifest/mod.rs @@ -184,6 +184,7 @@ pub struct PackageManifest { pub build_target: Option>, build_profile: Option>, pub contract_dependencies: Option>, + pub proxy: Option, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] @@ -242,6 +243,17 @@ pub struct DependencyDetails { pub(crate) ipfs: Option, } +/// Describes the details around proxy contract. +#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub struct Proxy { + pub enabled: bool, + /// Points to the proxy contract to be updated with the new contract id. + /// If there is a value for this field, forc will try to update the proxy contract's storage + /// field such that it points to current contract's deployed instance. + pub address: Option, +} + impl DependencyDetails { /// Checks if dependency details reserved for a specific dependency type used without the main /// detail for that type. @@ -619,6 +631,11 @@ impl PackageManifest { .and_then(|patches| patches.get(patch_name)) } + /// Retrieve the proxy table for the package. + pub fn proxy(&self) -> Option<&Proxy> { + self.proxy.as_ref() + } + /// Check for the `core` and `std` packages under `[dependencies]`. If both are missing, add /// `std` implicitly. /// diff --git a/forc-pkg/src/pkg.rs b/forc-pkg/src/pkg.rs index 2c24049acb5..7e14f20a322 100644 --- a/forc-pkg/src/pkg.rs +++ b/forc-pkg/src/pkg.rs @@ -282,7 +282,7 @@ pub struct MinifyOpts { type ContractIdConst = String; /// The set of options provided to the `build` functions. -#[derive(Default)] +#[derive(Default, Clone)] pub struct BuildOpts { pub pkg: PkgOpts, pub print: PrintOpts, @@ -315,6 +315,7 @@ pub struct BuildOpts { } /// The set of options to filter type of projects to build in a workspace. +#[derive(Clone)] pub struct MemberFilter { pub build_contracts: bool, pub build_scripts: bool, @@ -2126,6 +2127,9 @@ pub fn build_with_options(build_options: &BuildOpts) -> Result { .as_ref() .map_or_else(|| current_dir, PathBuf::from); + let building = ansi_term::Colour::Green.bold().paint("Building"); + info!(" {} {}", building, path.display()); + let build_plan = BuildPlan::from_build_opts(build_options)?; let graph = build_plan.graph(); let manifest_map = build_plan.manifest_map(); diff --git a/forc-plugins/forc-client/Cargo.toml b/forc-plugins/forc-client/Cargo.toml index 521c6e03f13..aa10f4ba07c 100644 --- a/forc-plugins/forc-client/Cargo.toml +++ b/forc-plugins/forc-client/Cargo.toml @@ -13,6 +13,7 @@ anyhow = "1" async-trait = "0.1.58" chrono = { version = "0.4", default-features = false, features = ["std"] } clap = { version = "4.5.4", features = ["derive", "env"] } +colored = "2.0.0" devault = "0.1" forc = { version = "0.60.0", path = "../../forc" } forc-pkg = { version = "0.60.0", path = "../../forc-pkg" } diff --git a/forc-plugins/forc-client/src/op/deploy.rs b/forc-plugins/forc-client/src/op/deploy.rs index e0270fa5245..4d0729b54b0 100644 --- a/forc-plugins/forc-client/src/op/deploy.rs +++ b/forc-plugins/forc-client/src/op/deploy.rs @@ -3,29 +3,35 @@ use crate::{ util::{ gas::get_estimated_max_fee, node_url::get_node_url, - pkg::built_pkgs, - tx::{TransactionBuilderExt, WalletSelectionMode, TX_SUBMIT_TIMEOUT_MS}, + pkg::{built_pkgs, create_proxy_contract}, + tx::{ + check_and_create_wallet_at_default_path, first_user_account, TransactionBuilderExt, + WalletSelectionMode, TX_SUBMIT_TIMEOUT_MS, + }, }, }; use anyhow::{bail, Context, Result}; +use colored::Colorize; use forc_pkg::manifest::GenericManifestFile; use forc_pkg::{self as pkg, PackageManifestFile}; use forc_tracing::println_warning; use forc_util::default_output_directory; +use forc_wallet::utils::default_wallet_path; use fuel_core_client::client::types::TransactionStatus; use fuel_core_client::client::FuelClient; use fuel_crypto::fuel_types::ChainId; use fuel_tx::{Output, Salt, TransactionBuilder}; use fuel_vm::prelude::*; use fuels_accounts::provider::Provider; +use fuels_core::types::bech32::Bech32Address; use futures::FutureExt; -use pkg::{manifest::build_profile::ExperimentalFlags, BuildProfile, BuiltPackage}; +use pkg::{manifest::build_profile::ExperimentalFlags, BuildOpts, BuildProfile, BuiltPackage}; use serde::{Deserialize, Serialize}; -use std::time::Duration; use std::{ collections::BTreeMap, path::{Path, PathBuf}, }; +use std::{sync::Arc, time::Duration}; use sway_core::language::parsed::TreeType; use sway_core::BuildTarget; use tracing::info; @@ -113,6 +119,27 @@ fn validate_and_parse_salts<'a>( Ok(contract_salt_map) } +/// Build a proxy contract owned by the deployer. +/// First creates the contract project at the current dir. The source code for the proxy contract is updated +/// with 'owner_adr'. +pub fn build_proxy_contract( + owner_addr: &str, + impl_contract_id: &str, + pkg_name: &str, + build_opts: &BuildOpts, +) -> Result> { + let proxy_contract_dir = create_proxy_contract(owner_addr, impl_contract_id, pkg_name)?; + let mut build_opts = build_opts.clone(); + let proxy_contract_dir_str = format!("{}", proxy_contract_dir.clone().display()); + build_opts.pkg.path = Some(proxy_contract_dir_str); + let built_pkgs = built_pkgs(&proxy_contract_dir, &build_opts)?; + let built_pkg = built_pkgs + .first() + .cloned() + .ok_or_else(|| anyhow::anyhow!("could not get proxy contract"))?; + Ok(built_pkg) +} + /// Builds and deploys contract(s). If the given path corresponds to a workspace, all deployable members /// will be built and deployed. /// @@ -176,6 +203,7 @@ pub async fn deploy(command: cmd::Deploy) -> Result> { None }; + let mut owner_account_address: Option = None; for pkg in built_pkgs { if pkg .descriptor @@ -197,8 +225,44 @@ pub async fn deploy(command: cmd::Deploy) -> Result> { bail!("Both `--salt` and `--default-salt` were specified: must choose one") } }; + println!( + " {} contract: {}", + "Deploying".bold().green(), + &pkg.descriptor.name + ); let contract_id = deploy_pkg(&command, &pkg.descriptor.manifest_file, &pkg, salt).await?; + let proxy = &pkg.descriptor.manifest_file.proxy(); + if let Some(proxy) = proxy { + if proxy.enabled { + println!(" {} proxy contract", "Creating".bold().green()); + let user_addr = if let Some(owner_address) = &owner_account_address { + anyhow::Ok(owner_address.clone()) + } else { + // Check if the wallet exists and if not create it at the default path. + let default_path = default_wallet_path(); + check_and_create_wallet_at_default_path(&default_path)?; + let account = first_user_account(&default_wallet_path())?; + owner_account_address = Some(account.clone()); + Ok(account) + }?; + let user_addr_hex: fuels_core::types::Address = user_addr.into(); + let user_addr = format!("0x{}", user_addr_hex); + let pkg_name = pkg.descriptor.manifest_file.project_name(); + let contract_addr = format!("0x{}", contract_id.id); + let proxy_contract = + build_proxy_contract(&user_addr, &contract_addr, pkg_name, &build_opts)?; + println!(" {} proxy contract", "Deploying".bold().green()); + deploy_pkg( + &command, + &pkg.descriptor.manifest_file, + &proxy_contract, + salt, + ) + .await?; + } + } + contract_ids.push(contract_id); } } diff --git a/forc-plugins/forc-client/src/util/pkg.rs b/forc-plugins/forc-client/src/util/pkg.rs index f7b6384cb74..4310b1a39cb 100644 --- a/forc-plugins/forc-client/src/util/pkg.rs +++ b/forc-plugins/forc-client/src/util/pkg.rs @@ -1,9 +1,119 @@ use anyhow::Result; use forc_pkg::manifest::GenericManifestFile; use forc_pkg::{self as pkg, manifest::ManifestFile, BuildOpts, BuildPlan}; +use forc_util::user_forc_directory; use pkg::{build_with_options, BuiltPackage}; +use std::io::Write; +use std::path::PathBuf; use std::{collections::HashMap, path::Path, sync::Arc}; +use sway_utils::{MAIN_ENTRY, MANIFEST_FILE_NAME, SRC_DIR}; +/// The name of the folder that forc generated proxy contract project will reside at. +pub const PROXY_CONTRACT_FOLDER_NAME: &str = ".generated_proxy_contracts"; +/// Forc.toml for the default proxy contract that 'generate_proxy_contract_src()' returns. +pub const PROXY_CONTRACT_FORC_TOML: &str = r#" +[project] +authors = ["Fuel Labs "] +entry = "main.sw" +license = "Apache-2.0" +name = "proxy_contract" + +[dependencies] +standards = { git = "https://github.com/FuelLabs/sway-standards/" } +"#; + +/// Generates source code for proxy contract owner set to the given 'addr'. +pub(crate) fn generate_proxy_contract_src(addr: &str, impl_contract_id: &str) -> String { + format!( + r#" +contract; + +use std::execution::run_external; +use standards::src5::{{AccessError, SRC5, State}}; +use standards::src14::SRC14; + +/// The owner of this contract at deployment. +const INITIAL_OWNER: Identity = Identity::Address(Address::from({addr})); + +// use sha256("storage_SRC14") as base to avoid collisions +#[namespace(SRC14)] +storage {{ + // target is at sha256("storage_SRC14_0") + target: ContractId = ContractId::from({impl_contract_id}), + owner: State = State::Initialized(INITIAL_OWNER), +}} + +impl SRC5 for Contract {{ + #[storage(read)] + fn owner() -> State {{ + storage.owner.read() + }} +}} + +impl SRC14 for Contract {{ + #[storage(write)] + fn set_proxy_target(new_target: ContractId) {{ + only_owner(); + storage.target.write(new_target); + }} +}} + +#[fallback] +#[storage(read)] +fn fallback() {{ + // pass through any other method call to the target + run_external(storage.target.read()) +}} + +#[storage(read)] +fn only_owner() {{ + require( + storage + .owner + .read() == State::Initialized(msg_sender().unwrap()), + AccessError::NotOwner, + ); +}} +"# + ) +} + +/// Creates a proxy contract project at the given path, adds a forc.toml and source file. +pub(crate) fn create_proxy_contract( + addr: &str, + impl_contract_id: &str, + pkg_name: &str, +) -> Result { + // Create the proxy contract folder. + let proxy_contract_dir = user_forc_directory() + .join(PROXY_CONTRACT_FOLDER_NAME) + .join(pkg_name); + std::fs::create_dir_all(&proxy_contract_dir)?; + + // Create the Forc.toml + let mut f = std::fs::OpenOptions::new() + .read(true) + .write(true) + .create(true) + .truncate(true) + .open(proxy_contract_dir.join(MANIFEST_FILE_NAME))?; + write!(f, "{}", PROXY_CONTRACT_FORC_TOML)?; + + // Create the src folder + std::fs::create_dir_all(proxy_contract_dir.join(SRC_DIR))?; + + // Create main.sw + let mut f = std::fs::OpenOptions::new() + .read(true) + .write(true) + .create(true) + .truncate(true) + .open(proxy_contract_dir.join(SRC_DIR).join(MAIN_ENTRY))?; + + let contract_str = generate_proxy_contract_src(addr, impl_contract_id); + write!(f, "{}", contract_str)?; + Ok(proxy_contract_dir) +} pub(crate) fn built_pkgs(path: &Path, build_opts: &BuildOpts) -> Result>> { let manifest_file = ManifestFile::from_dir(path)?; let lock_path = manifest_file.lock_path()?; diff --git a/forc-plugins/forc-client/src/util/tx.rs b/forc-plugins/forc-client/src/util/tx.rs index 84441b79933..4eec15b697a 100644 --- a/forc-plugins/forc-client/src/util/tx.rs +++ b/forc-plugins/forc-client/src/util/tx.rs @@ -1,4 +1,4 @@ -use std::{io::Write, str::FromStr}; +use std::{collections::BTreeMap, io::Write, path::Path, str::FromStr}; use anyhow::{Error, Result}; use async_trait::async_trait; @@ -66,6 +66,57 @@ fn ask_user_yes_no_question(question: &str) -> Result { let ans = ans.trim(); Ok(ans == "y" || ans == "Y") } + +fn collect_user_accounts( + wallet_path: &Path, + password: &str, +) -> Result> { + let verification = AccountVerification::Yes(password.to_string()); + let accounts = collect_accounts_with_verification(wallet_path, verification).map_err(|e| { + if e.to_string().contains("Mac Mismatch") { + anyhow::anyhow!("Failed to access forc-wallet vault. Please check your password") + } else { + e + } + })?; + Ok(accounts) +} + +pub(crate) fn first_user_account(wallet_path: &Path) -> Result { + let prompt = format!( + "\nPlease provide the password of your encrypted wallet vault at {wallet_path:?}: " + ); + let password = rpassword::prompt_password(prompt)?; + let accounts = collect_user_accounts(wallet_path, &password)?; + + let account = accounts + .get(&0) + .ok_or_else(|| anyhow::anyhow!("No account derived for this wallet"))? + .clone(); + Ok(account) +} + +pub(crate) fn check_and_create_wallet_at_default_path(wallet_path: &Path) -> Result<()> { + if !wallet_path.exists() { + let question = format!("Could not find a wallet at {wallet_path:?}, would you like to create a new one? [y/N]: "); + let accepted = ask_user_yes_no_question(&question)?; + let new_options = New { + force: false, + cache_accounts: None, + }; + if accepted { + new_wallet_cli(wallet_path, new_options)?; + println!("Wallet created successfully."); + // Derive first account for the fresh wallet we created. + new_at_index_cli(wallet_path, 0)?; + println!("Account derived successfully."); + } else { + anyhow::bail!("Refused to create a new wallet. If you don't want to use forc-wallet, you can sign this transaction manually with --manual-signing flag.") + } + } + Ok(()) +} + /// Collect and return balances of each account in the accounts map. async fn collect_account_balances( accounts_map: &AccountsMap, @@ -174,41 +225,15 @@ impl TransactionBuilderExt for Tran let params = chain_info.consensus_parameters; let signing_key = match (wallet_mode, signing_key, default_sign) { (WalletSelectionMode::ForcWallet, None, false) => { + let wallet_path = default_wallet_path(); + check_and_create_wallet_at_default_path(&wallet_path)?; // TODO: This is a very simple TUI, we should consider adding a nice TUI // capabilities for selections and answer collection. - let wallet_path = default_wallet_path(); - if !wallet_path.exists() { - let question = format!("Could not find a wallet at {wallet_path:?}, would you like to create a new one? [y/N]: "); - let accepted = ask_user_yes_no_question(&question)?; - let new_options = New { - force: false, - cache_accounts: None, - }; - if accepted { - new_wallet_cli(&wallet_path, new_options)?; - println!("Wallet created successfully."); - // Derive first account for the fresh wallet we created. - new_at_index_cli(&wallet_path, 0)?; - println!("Account derived successfully."); - } else { - anyhow::bail!("Refused to create a new wallet. If you don't want to use forc-wallet, you can sign this transaction manually with --manual-signing flag.") - } - } let prompt = format!( "\nPlease provide the password of your encrypted wallet vault at {wallet_path:?}: " ); let password = rpassword::prompt_password(prompt)?; - let verification = AccountVerification::Yes(password.clone()); - let accounts = collect_accounts_with_verification(&wallet_path, verification) - .map_err(|e| { - if e.to_string().contains("Mac Mismatch") { - anyhow::anyhow!( - "Failed to access forc-wallet vault. Please check your password" - ) - } else { - e - } - })?; + let accounts = collect_user_accounts(&wallet_path, &password)?; let account_balances = collect_account_balances(&accounts, &provider).await?; let total_balance = account_balances