From 4b4966af8d378e1b7d9acdc03cbea41a9027a104 Mon Sep 17 00:00:00 2001 From: Kaya Gokalp Date: Tue, 28 May 2024 14:44:01 -0700 Subject: [PATCH 01/15] feat: create and deploy proxy for contracts with proxy enabled --- Cargo.lock | 1 + forc-pkg/src/manifest/mod.rs | 17 ++++ forc-pkg/src/pkg.rs | 6 +- forc-plugins/forc-client/Cargo.toml | 1 + forc-plugins/forc-client/src/op/deploy.rs | 72 ++++++++++++++- forc-plugins/forc-client/src/util/pkg.rs | 107 ++++++++++++++++++++++ forc-plugins/forc-client/src/util/tx.rs | 85 +++++++++++------ 7 files changed, 254 insertions(+), 35 deletions(-) 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..527d1c4477e 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..f5a0463437b 100644 --- a/forc-plugins/forc-client/src/util/pkg.rs +++ b/forc-plugins/forc-client/src/util/pkg.rs @@ -1,9 +1,116 @@ 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}; +/// 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) + .open(proxy_contract_dir.join("Forc.toml"))?; + write!(f, "{}", PROXY_CONTRACT_FORC_TOML)?; + + // Create the src folder + std::fs::create_dir_all(&proxy_contract_dir.join("src"))?; + + // Create main.sw + let mut f = std::fs::OpenOptions::new() + .read(true) + .write(true) + .create(true) + .open(proxy_contract_dir.join("src").join("main.sw"))?; + + 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..140eda3c4aa 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 From 66c038286484a490dfae9a11bf374eabd5a63e6e Mon Sep 17 00:00:00 2001 From: Kaya Gokalp Date: Tue, 28 May 2024 15:08:44 -0700 Subject: [PATCH 02/15] chore: use defaults from constants --- forc-plugins/forc-client/src/util/pkg.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/forc-plugins/forc-client/src/util/pkg.rs b/forc-plugins/forc-client/src/util/pkg.rs index f5a0463437b..58c322f95da 100644 --- a/forc-plugins/forc-client/src/util/pkg.rs +++ b/forc-plugins/forc-client/src/util/pkg.rs @@ -6,6 +6,7 @@ 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"; @@ -94,18 +95,18 @@ pub(crate) fn create_proxy_contract( .read(true) .write(true) .create(true) - .open(proxy_contract_dir.join("Forc.toml"))?; + .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"))?; + 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) - .open(proxy_contract_dir.join("src").join("main.sw"))?; + .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)?; From cdf35bdd6081ce196f7c28c971d79860f680bf9a Mon Sep 17 00:00:00 2001 From: Kaya Gokalp Date: Tue, 28 May 2024 15:11:56 -0700 Subject: [PATCH 03/15] chore: clippy fix --- forc-plugins/forc-client/src/op/deploy.rs | 2 +- forc-plugins/forc-client/src/util/pkg.rs | 4 +++- forc-plugins/forc-client/src/util/tx.rs | 8 ++++---- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/forc-plugins/forc-client/src/op/deploy.rs b/forc-plugins/forc-client/src/op/deploy.rs index 527d1c4477e..4d0729b54b0 100644 --- a/forc-plugins/forc-client/src/op/deploy.rs +++ b/forc-plugins/forc-client/src/op/deploy.rs @@ -251,7 +251,7 @@ pub async fn deploy(command: cmd::Deploy) -> Result> { 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)?; + build_proxy_contract(&user_addr, &contract_addr, pkg_name, &build_opts)?; println!(" {} proxy contract", "Deploying".bold().green()); deploy_pkg( &command, diff --git a/forc-plugins/forc-client/src/util/pkg.rs b/forc-plugins/forc-client/src/util/pkg.rs index 58c322f95da..4310b1a39cb 100644 --- a/forc-plugins/forc-client/src/util/pkg.rs +++ b/forc-plugins/forc-client/src/util/pkg.rs @@ -95,17 +95,19 @@ pub(crate) fn create_proxy_contract( .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))?; + 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); diff --git a/forc-plugins/forc-client/src/util/tx.rs b/forc-plugins/forc-client/src/util/tx.rs index 140eda3c4aa..4eec15b697a 100644 --- a/forc-plugins/forc-client/src/util/tx.rs +++ b/forc-plugins/forc-client/src/util/tx.rs @@ -72,7 +72,7 @@ fn collect_user_accounts( password: &str, ) -> Result> { let verification = AccountVerification::Yes(password.to_string()); - let accounts = collect_accounts_with_verification(&wallet_path, verification).map_err(|e| { + 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 { @@ -87,7 +87,7 @@ pub(crate) fn first_user_account(wallet_path: &Path) -> Result { "\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 accounts = collect_user_accounts(wallet_path, &password)?; let account = accounts .get(&0) @@ -105,10 +105,10 @@ pub(crate) fn check_and_create_wallet_at_default_path(wallet_path: &Path) -> Res cache_accounts: None, }; if accepted { - new_wallet_cli(&wallet_path, new_options)?; + 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)?; + 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.") From d15d8f0d63fb2f8879c4387db286d5925d7d978b Mon Sep 17 00:00:00 2001 From: Kaya Gokalp Date: Tue, 28 May 2024 17:59:53 -0700 Subject: [PATCH 04/15] feat: update proxy addres after new deployment of it --- Cargo.lock | 1 + forc-plugins/forc-client/Cargo.toml | 1 + forc-plugins/forc-client/src/op/deploy.rs | 97 +++++++++++++++-------- forc-plugins/forc-client/src/util/pkg.rs | 25 +++++- 4 files changed, 89 insertions(+), 35 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 178a0c4c97c..61e02171090 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2058,6 +2058,7 @@ dependencies = [ "sway-types", "sway-utils", "tokio", + "toml_edit 0.21.1", "tracing", ] diff --git a/forc-plugins/forc-client/Cargo.toml b/forc-plugins/forc-client/Cargo.toml index aa10f4ba07c..73d22e2bb06 100644 --- a/forc-plugins/forc-client/Cargo.toml +++ b/forc-plugins/forc-client/Cargo.toml @@ -39,6 +39,7 @@ sway-core = { version = "0.60.0", path = "../../sway-core" } sway-types = { version = "0.60.0", path = "../../sway-types" } sway-utils = { version = "0.60.0", path = "../../sway-utils" } tokio = { version = "1.8", features = ["macros", "rt-multi-thread", "process"] } +toml_edit = "0.21.1" tracing = "0.1" [[bin]] diff --git a/forc-plugins/forc-client/src/op/deploy.rs b/forc-plugins/forc-client/src/op/deploy.rs index 4d0729b54b0..f5154b4b6fc 100644 --- a/forc-plugins/forc-client/src/op/deploy.rs +++ b/forc-plugins/forc-client/src/op/deploy.rs @@ -3,7 +3,7 @@ use crate::{ util::{ gas::get_estimated_max_fee, node_url::get_node_url, - pkg::{built_pkgs, create_proxy_contract}, + pkg::{built_pkgs, create_proxy_contract, update_proxy_address}, tx::{ check_and_create_wallet_at_default_path, first_user_account, TransactionBuilderExt, WalletSelectionMode, TX_SUBMIT_TIMEOUT_MS, @@ -139,7 +139,40 @@ pub fn build_proxy_contract( .ok_or_else(|| anyhow::anyhow!("could not get proxy contract"))?; Ok(built_pkg) } - +async fn deploy_new_proxy( + pkg: &BuiltPackage, + owner_account_address: &mut Bech32Address, + impl_contract: &DeployedContract, + build_opts: &BuildOpts, + command: &cmd::Deploy, + salt: Salt, +) -> Result { + println!(" {} proxy contract", "Creating".bold().green()); + let user_addr = if *owner_account_address != Bech32Address::default() { + anyhow::Ok(owner_account_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 = 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{}", impl_contract.id); + let proxy_contract = build_proxy_contract(&user_addr, &contract_addr, pkg_name, &build_opts)?; + println!(" {} proxy contract", "Deploying".bold().green()); + let proxy = deploy_pkg( + &command, + &pkg.descriptor.manifest_file, + &proxy_contract, + salt, + ) + .await?; + Ok(proxy) +} /// Builds and deploys contract(s). If the given path corresponds to a workspace, all deployable members /// will be built and deployed. /// @@ -151,7 +184,7 @@ pub async fn deploy(command: cmd::Deploy) -> Result> { println_warning("--unsigned flag is deprecated, please prefer using --default-signer. Assuming `--default-signer` is passed. This means your transaction will be signed by an account that is funded by fuel-core by default for testing purposes."); } - let mut contract_ids = Vec::new(); + let mut deployed_contracts = Vec::new(); let curr_dir = if let Some(ref path) = command.pkg.path { PathBuf::from(path) } else { @@ -163,7 +196,7 @@ pub async fn deploy(command: cmd::Deploy) -> Result> { if built_pkgs.is_empty() { println_warning("No deployable contracts found in the current directory."); - return Ok(contract_ids); + return Ok(deployed_contracts); } let contract_salt_map = if let Some(salt_input) = &command.salt { @@ -183,7 +216,6 @@ pub async fn deploy(command: cmd::Deploy) -> Result> { // OK to index into salt_input and built_pkgs_with_manifest here, // since both are known to be len 1. - let salt = salt_input[0] .parse::() .map_err(|e| anyhow::anyhow!(e)) @@ -203,7 +235,7 @@ pub async fn deploy(command: cmd::Deploy) -> Result> { None }; - let mut owner_account_address: Option = None; + let mut owner_account_address = Bech32Address::default(); for pkg in built_pkgs { if pkg .descriptor @@ -230,43 +262,42 @@ pub async fn deploy(command: cmd::Deploy) -> Result> { "Deploying".bold().green(), &pkg.descriptor.name ); - let contract_id = + let deployed_contract = 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()) + if let Some(proxy_addr) = &proxy.address { + // Make a call into the contract to update impl contract address to 'deployed_contract'. + + // Create a contract instance for the proxy contract using default proxy contract abi and + // specified address. + todo!() } 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?; + // Deploy a new proxy contract. + let deployed_proxy_contract = deploy_new_proxy( + &pkg, + &mut owner_account_address, + &deployed_contract, + &build_opts, + &command, + salt, + ) + .await?; + + // Update manifest file such that the proxy address field points to the new proxy contract. + update_proxy_address( + &format!("0x{}", deployed_proxy_contract.id), + &pkg.descriptor.manifest_file, + )?; + } } } - contract_ids.push(contract_id); + deployed_contracts.push(deployed_contract); } } - Ok(contract_ids) + Ok(deployed_contracts) } /// Deploy a single pkg given deploy command and the manifest file diff --git a/forc-plugins/forc-client/src/util/pkg.rs b/forc-plugins/forc-client/src/util/pkg.rs index 4310b1a39cb..6a3b69b33a5 100644 --- a/forc-plugins/forc-client/src/util/pkg.rs +++ b/forc-plugins/forc-client/src/util/pkg.rs @@ -2,8 +2,10 @@ 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 fuel_vm::interpreter::diff::AnyDebug; +use pkg::{build_with_options, BuiltPackage, PackageManifestFile}; +use std::fs::File; +use std::io::{Read, Write}; use std::path::PathBuf; use std::{collections::HashMap, path::Path, sync::Arc}; use sway_utils::{MAIN_ENTRY, MANIFEST_FILE_NAME, SRC_DIR}; @@ -78,6 +80,25 @@ fn only_owner() {{ ) } +/// Updates the given package manifest file such that the address field under the proxy table updated to the given value. +/// Updated manifest file is written back to the same location, without thouching anything else such as comments etc. +/// A safety check is done to ensure the proxy table exists before attempting to udpdate the value. +pub(crate) fn update_proxy_address(address: &str, manifest: &PackageManifestFile) -> Result<()> { + let mut toml = String::new(); + let mut file = File::open(manifest.path())?; + file.read_to_string(&mut toml)?; + let mut manifest_toml = toml.parse::()?; + if manifest.proxy().is_some() { + manifest_toml["proxy"]["address"] = toml_edit::value(address); + let mut file = std::fs::OpenOptions::new() + .write(true) + .truncate(true) + .open(manifest.path())?; + file.write_all(manifest_toml.to_string().as_bytes())?; + } + Ok(()) +} + /// Creates a proxy contract project at the given path, adds a forc.toml and source file. pub(crate) fn create_proxy_contract( addr: &str, From 58dfc0e8199f8cd857065684885003749f49be65 Mon Sep 17 00:00:00 2001 From: Kaya Gokalp Date: Tue, 28 May 2024 21:48:17 -0700 Subject: [PATCH 05/15] refactor: remove duplicate password prompts and cleanup secret key selection --- Cargo.lock | 1 + Cargo.toml | 1 + forc-plugins/forc-client/Cargo.toml | 1 + forc-plugins/forc-client/src/cmd/deploy.rs | 3 - forc-plugins/forc-client/src/cmd/run.rs | 3 - forc-plugins/forc-client/src/op/deploy.rs | 54 +++++-- forc-plugins/forc-client/src/op/run/mod.rs | 26 +++- forc-plugins/forc-client/src/util/pkg.rs | 14 +- .../src/util/proxy_contract-abi.json | 146 ++++++++++++++++++ forc-plugins/forc-client/src/util/tx.rs | 95 +++++++----- 10 files changed, 274 insertions(+), 70 deletions(-) create mode 100644 forc-plugins/forc-client/src/util/proxy_contract-abi.json diff --git a/Cargo.lock b/Cargo.lock index 61e02171090..3b4d541389c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2046,6 +2046,7 @@ dependencies = [ "fuel-crypto", "fuel-tx", "fuel-vm", + "fuels", "fuels-accounts", "fuels-core", "futures", diff --git a/Cargo.toml b/Cargo.toml index 4518aa5d62b..453e66ed03c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,7 @@ fuel-vm = "0.49.0" # Dependencies from the `fuels-rs` repository: fuels-core = "0.62.0" fuels-accounts = "0.62.0" +fuels = "0.62.0" # Dependencies from the `forc-wallet` repository: forc-wallet = "0.7.1" diff --git a/forc-plugins/forc-client/Cargo.toml b/forc-plugins/forc-client/Cargo.toml index 73d22e2bb06..7046dfec8ee 100644 --- a/forc-plugins/forc-client/Cargo.toml +++ b/forc-plugins/forc-client/Cargo.toml @@ -27,6 +27,7 @@ fuel-core-types = { workspace = true } fuel-crypto = { workspace = true } fuel-tx = { workspace = true, features = ["test-helpers"] } fuel-vm = { workspace = true } +fuels = { workspace = true } fuels-accounts = { workspace = true } fuels-core = { workspace = true } futures = "0.3" diff --git a/forc-plugins/forc-client/src/cmd/deploy.rs b/forc-plugins/forc-client/src/cmd/deploy.rs index cf1f26e2a2e..abec6c3ed76 100644 --- a/forc-plugins/forc-client/src/cmd/deploy.rs +++ b/forc-plugins/forc-client/src/cmd/deploy.rs @@ -57,9 +57,6 @@ pub struct Command { pub unsigned: bool, /// Set the key to be used for signing. pub signing_key: Option, - /// Sign the deployment transaction manually. - #[clap(long)] - pub manual_signing: bool, /// Override storage slot initialization. /// /// By default, storage slots are initialized with the values defined in the storage block in diff --git a/forc-plugins/forc-client/src/cmd/run.rs b/forc-plugins/forc-client/src/cmd/run.rs index e23d9f6cb43..5875b270adc 100644 --- a/forc-plugins/forc-client/src/cmd/run.rs +++ b/forc-plugins/forc-client/src/cmd/run.rs @@ -52,9 +52,6 @@ pub struct Command { pub unsigned: bool, /// Set the key to be used for signing. pub signing_key: Option, - /// Sign the deployment transaction manually. - #[clap(long)] - pub manual_signing: bool, /// Arguments to pass into main function with forc run. #[clap(long)] pub args: Option>, diff --git a/forc-plugins/forc-client/src/op/deploy.rs b/forc-plugins/forc-client/src/op/deploy.rs index f5154b4b6fc..196b44721e3 100644 --- a/forc-plugins/forc-client/src/op/deploy.rs +++ b/forc-plugins/forc-client/src/op/deploy.rs @@ -5,7 +5,8 @@ use crate::{ node_url::get_node_url, pkg::{built_pkgs, create_proxy_contract, update_proxy_address}, tx::{ - check_and_create_wallet_at_default_path, first_user_account, TransactionBuilderExt, + bech32_from_secret, check_and_create_wallet_at_default_path, first_user_account, + prompt_forc_wallet_password, select_manual_secret_key, TransactionBuilderExt, WalletSelectionMode, TX_SUBMIT_TIMEOUT_MS, }, }, @@ -146,17 +147,30 @@ async fn deploy_new_proxy( build_opts: &BuildOpts, command: &cmd::Deploy, salt: Salt, + wallet_mode: &WalletSelectionMode, ) -> Result { - println!(" {} proxy contract", "Creating".bold().green()); + println!(" {} proxy contract", "Creating".bold().green()); let user_addr = if *owner_account_address != Bech32Address::default() { anyhow::Ok(owner_account_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 = account.clone(); - Ok(account) + match wallet_mode { + WalletSelectionMode::ForcWallet(password) => { + let default_path = default_wallet_path(); + check_and_create_wallet_at_default_path(&default_path)?; + let account = first_user_account(&default_wallet_path(), password)?; + *owner_account_address = account.clone(); + Ok(account) + } + WalletSelectionMode::Manual => { + let secret_key = + select_manual_secret_key(command.default_signer, command.signing_key) + .ok_or_else(|| { + anyhow::anyhow!("couldn't resolve the secret key for manual signing") + })?; + bech32_from_secret(&secret_key) + } + } }?; let user_addr_hex: fuels_core::types::Address = user_addr.into(); let user_addr = format!("0x{}", user_addr_hex); @@ -169,6 +183,7 @@ async fn deploy_new_proxy( &pkg.descriptor.manifest_file, &proxy_contract, salt, + wallet_mode, ) .await?; Ok(proxy) @@ -235,6 +250,13 @@ pub async fn deploy(command: cmd::Deploy) -> Result> { None }; + let wallet_mode = if command.default_signer || command.signing_key.is_some() { + WalletSelectionMode::Manual + } else { + let password = prompt_forc_wallet_password(&default_wallet_path())?; + WalletSelectionMode::ForcWallet(password) + }; + let mut owner_account_address = Bech32Address::default(); for pkg in built_pkgs { if pkg @@ -262,8 +284,14 @@ pub async fn deploy(command: cmd::Deploy) -> Result> { "Deploying".bold().green(), &pkg.descriptor.name ); - let deployed_contract = - deploy_pkg(&command, &pkg.descriptor.manifest_file, &pkg, salt).await?; + let deployed_contract = deploy_pkg( + &command, + &pkg.descriptor.manifest_file, + &pkg, + salt, + &wallet_mode, + ) + .await?; let proxy = &pkg.descriptor.manifest_file.proxy(); if let Some(proxy) = proxy { if proxy.enabled { @@ -282,6 +310,7 @@ pub async fn deploy(command: cmd::Deploy) -> Result> { &build_opts, &command, salt, + &wallet_mode, ) .await?; @@ -306,6 +335,7 @@ pub async fn deploy_pkg( manifest: &PackageManifestFile, compiled: &BuiltPackage, salt: Salt, + wallet_mode: &WalletSelectionMode, ) -> Result { let node_url = get_node_url(&command.node, &manifest.network)?; let client = FuelClient::new(node_url.clone())?; @@ -327,12 +357,6 @@ pub async fn deploy_pkg( let state_root = Contract::initial_state_root(storage_slots.iter()); let contract_id = contract.id(&salt, &root, &state_root); - let wallet_mode = if command.manual_signing { - WalletSelectionMode::Manual - } else { - WalletSelectionMode::ForcWallet - }; - let provider = Provider::connect(node_url.clone()).await?; // We need a tx for estimation without the signature. diff --git a/forc-plugins/forc-client/src/op/run/mod.rs b/forc-plugins/forc-client/src/op/run/mod.rs index 0ea4f073b2a..b7e9356bce4 100644 --- a/forc-plugins/forc-client/src/op/run/mod.rs +++ b/forc-plugins/forc-client/src/op/run/mod.rs @@ -5,13 +5,17 @@ use crate::{ gas::get_script_gas_used, node_url::get_node_url, pkg::built_pkgs, - tx::{TransactionBuilderExt, WalletSelectionMode, TX_SUBMIT_TIMEOUT_MS}, + tx::{ + prompt_forc_wallet_password, TransactionBuilderExt, WalletSelectionMode, + TX_SUBMIT_TIMEOUT_MS, + }, }, }; use anyhow::{anyhow, bail, Context, Result}; use forc_pkg::{self as pkg, fuel_core_not_running, PackageManifestFile}; use forc_tracing::println_warning; use forc_util::tx_utils::format_log_receipts; +use forc_wallet::utils::default_wallet_path; use fuel_core_client::client::FuelClient; use fuel_tx::{ContractId, Transaction, TransactionBuilder}; use fuels_accounts::provider::Provider; @@ -49,6 +53,12 @@ pub async fn run(command: cmd::Run) -> Result> { }; let build_opts = build_opts_from_cmd(&command); let built_pkgs_with_manifest = built_pkgs(&curr_dir, &build_opts)?; + let wallet_mode = if command.default_signer || command.signing_key.is_some() { + WalletSelectionMode::Manual + } else { + let password = prompt_forc_wallet_password(&default_wallet_path())?; + WalletSelectionMode::ForcWallet(password) + }; for built in built_pkgs_with_manifest { if built .descriptor @@ -56,7 +66,13 @@ pub async fn run(command: cmd::Run) -> Result> { .check_program_type(&[TreeType::Script]) .is_ok() { - let pkg_receipts = run_pkg(&command, &built.descriptor.manifest_file, &built).await?; + let pkg_receipts = run_pkg( + &command, + &built.descriptor.manifest_file, + &built, + &wallet_mode, + ) + .await?; receipts.push(pkg_receipts); } } @@ -68,6 +84,7 @@ pub async fn run_pkg( command: &cmd::Run, manifest: &PackageManifestFile, compiled: &BuiltPackage, + wallet_mode: &WalletSelectionMode, ) -> Result { let node_url = get_node_url(&command.node, &manifest.network)?; @@ -101,11 +118,6 @@ pub async fn run_pkg( .map_err(|e| anyhow!("Failed to parse contract id: {}", e)) }) .collect::>>()?; - let wallet_mode = if command.manual_signing { - WalletSelectionMode::Manual - } else { - WalletSelectionMode::ForcWallet - }; let mut tb = TransactionBuilder::script(compiled.bytecode.bytes.clone(), script_data); tb.maturity(command.maturity.maturity.into()) diff --git a/forc-plugins/forc-client/src/util/pkg.rs b/forc-plugins/forc-client/src/util/pkg.rs index 6a3b69b33a5..c1b1f08c746 100644 --- a/forc-plugins/forc-client/src/util/pkg.rs +++ b/forc-plugins/forc-client/src/util/pkg.rs @@ -2,7 +2,7 @@ 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 fuel_vm::interpreter::diff::AnyDebug; +use fuels::prelude::*; use pkg::{build_with_options, BuiltPackage, PackageManifestFile}; use std::fs::File; use std::io::{Read, Write}; @@ -135,6 +135,18 @@ pub(crate) fn create_proxy_contract( write!(f, "{}", contract_str)?; Ok(proxy_contract_dir) } + +pub(crate) fn update_proxy_contract( + provider: &Provider, + proxy_contract_id: ContractId, +) -> Result<()> { + abigen!(Contract( + name = "ProxyContract", + abi = "forc-plugins/forc-client/src/util/proxy_contract-abi.json" + )); + + Ok(()) +} 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/proxy_contract-abi.json b/forc-plugins/forc-client/src/util/proxy_contract-abi.json new file mode 100644 index 00000000000..be94c534664 --- /dev/null +++ b/forc-plugins/forc-client/src/util/proxy_contract-abi.json @@ -0,0 +1,146 @@ +{ + "encoding": "1", + "types": [ + { + "typeId": 0, + "type": "()", + "components": [], + "typeParameters": null + }, + { + "typeId": 1, + "type": "b256", + "components": null, + "typeParameters": null + }, + { + "typeId": 2, + "type": "enum AccessError", + "components": [ + { + "name": "NotOwner", + "type": 0, + "typeArguments": null + } + ], + "typeParameters": null + }, + { + "typeId": 3, + "type": "enum Identity", + "components": [ + { + "name": "Address", + "type": 5, + "typeArguments": null + }, + { + "name": "ContractId", + "type": 6, + "typeArguments": null + } + ], + "typeParameters": null + }, + { + "typeId": 4, + "type": "enum State", + "components": [ + { + "name": "Uninitialized", + "type": 0, + "typeArguments": null + }, + { + "name": "Initialized", + "type": 3, + "typeArguments": null + }, + { + "name": "Revoked", + "type": 0, + "typeArguments": null + } + ], + "typeParameters": null + }, + { + "typeId": 5, + "type": "struct Address", + "components": [ + { + "name": "bits", + "type": 1, + "typeArguments": null + } + ], + "typeParameters": null + }, + { + "typeId": 6, + "type": "struct ContractId", + "components": [ + { + "name": "bits", + "type": 1, + "typeArguments": null + } + ], + "typeParameters": null + } + ], + "functions": [ + { + "inputs": [], + "name": "owner", + "output": { + "name": "", + "type": 4, + "typeArguments": null + }, + "attributes": [ + { + "name": "storage", + "arguments": [ + "read" + ] + } + ] + }, + { + "inputs": [ + { + "name": "new_target", + "type": 6, + "typeArguments": null + } + ], + "name": "set_proxy_target", + "output": { + "name": "", + "type": 0, + "typeArguments": null + }, + "attributes": [ + { + "name": "storage", + "arguments": [ + "write" + ] + } + ] + } + ], + "loggedTypes": [ + { + "logId": "4571204900286667806", + "loggedType": { + "name": "", + "type": 2, + "typeArguments": [] + } + } + ], + "messagesTypes": [], + "configurables": [] +} \ No newline at end of file diff --git a/forc-plugins/forc-client/src/util/tx.rs b/forc-plugins/forc-client/src/util/tx.rs index 4eec15b697a..4e7a6c78311 100644 --- a/forc-plugins/forc-client/src/util/tx.rs +++ b/forc-plugins/forc-client/src/util/tx.rs @@ -34,7 +34,8 @@ pub const DEFAULT_PRIVATE_KEY: &str = #[derive(PartialEq, Eq)] pub enum WalletSelectionMode { - ForcWallet, + /// Holds the password of forc-wallet instance. + ForcWallet(String), Manual, } @@ -82,11 +83,15 @@ fn collect_user_accounts( Ok(accounts) } -pub(crate) fn first_user_account(wallet_path: &Path) -> Result { +pub(crate) fn prompt_forc_wallet_password(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)?; + Ok(password) +} + +pub(crate) fn first_user_account(wallet_path: &Path, password: &str) -> Result { let accounts = collect_user_accounts(wallet_path, &password)?; let account = accounts @@ -95,7 +100,6 @@ pub(crate) fn first_user_account(wallet_path: &Path) -> Result { .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]: "); @@ -117,6 +121,44 @@ pub(crate) fn check_and_create_wallet_at_default_path(wallet_path: &Path) -> Res Ok(()) } +pub(crate) fn secret_key_from_forc_wallet( + wallet_path: &Path, + account_index: usize, + password: &str, +) -> Result { + let secret_key = derive_secret_key(&wallet_path, account_index, &password).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(secret_key) +} + +pub(crate) fn bech32_from_secret(secret_key: &SecretKey) -> Result { + let public_key = PublicKey::from(secret_key); + let hashed = public_key.hash(); + let bech32 = Bech32Address::new(FUEL_BECH32_HRP, hashed); + Ok(bech32) +} + +pub(crate) fn select_manual_secret_key( + default_signer: bool, + signing_key: Option, +) -> Option { + match (default_signer, signing_key) { + // Note: unwrap is safe here as we already know that 'DEFAULT_PRIVATE_KEY' is a valid private key. + (true, None) => Some(SecretKey::from_str(DEFAULT_PRIVATE_KEY).unwrap()), + (true, Some(signing_key)) => { + println_warning("Signing key is provided while requesting to sign with a default signer. Using signing key"); + Some(signing_key) + } + (false, None) => None, + (false, Some(signing_key)) => Some(signing_key), + } +} + /// Collect and return balances of each account in the accounts map. async fn collect_account_balances( accounts_map: &AccountsMap, @@ -146,9 +188,9 @@ pub trait TransactionBuilderExt { async fn finalize_signed( &mut self, client: Provider, - unsigned: bool, + default_signature: bool, signing_key: Option, - wallet_mode: WalletSelectionMode, + wallet_mode: &WalletSelectionMode, ) -> Result; } @@ -219,20 +261,16 @@ impl TransactionBuilderExt for Tran provider: Provider, default_sign: bool, signing_key: Option, - wallet_mode: WalletSelectionMode, + wallet_mode: &WalletSelectionMode, ) -> Result { let chain_info = provider.chain_info().await?; let params = chain_info.consensus_parameters; - let signing_key = match (wallet_mode, signing_key, default_sign) { - (WalletSelectionMode::ForcWallet, None, false) => { + let signing_key = match wallet_mode { + WalletSelectionMode::ForcWallet(password) => { 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 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_balances = collect_account_balances(&accounts, &provider).await?; @@ -271,21 +309,11 @@ impl TransactionBuilderExt for Tran )); } - let secret_key = derive_secret_key(&wallet_path, account_index, &password) - .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 secret_key = + secret_key_from_forc_wallet(&wallet_path, account_index, &password)?; + let bech32 = bech32_from_secret(&secret_key)?; // TODO: Do this via forc-wallet once the functionality is exposed. - let public_key = PublicKey::from(&secret_key); - let hashed = public_key.hash(); - let bech32 = Bech32Address::new(FUEL_BECH32_HRP, hashed); let question = format!( "Do you agree to sign this transaction with {}? [y/N]: ", bech32 @@ -297,22 +325,7 @@ impl TransactionBuilderExt for Tran Some(secret_key) } - (WalletSelectionMode::ForcWallet, Some(key), _) => { - println_warning("Signing key is provided while requesting to sign with forc-wallet or with default signer. Using signing key"); - Some(key) - } - (WalletSelectionMode::Manual, None, false) => None, - (WalletSelectionMode::Manual, Some(key), false) => Some(key), - (_, None, true) => { - // Generate a `SecretKey` to sign this transaction from a default private key used - // by fuel-core. - let secret_key = SecretKey::from_str(DEFAULT_PRIVATE_KEY)?; - Some(secret_key) - } - (WalletSelectionMode::Manual, Some(key), true) => { - println_warning("Signing key is provided while requesting to sign with a default signer. Using signing key"); - Some(key) - } + WalletSelectionMode::Manual => select_manual_secret_key(default_sign, signing_key), }; // Get the address let address = if let Some(key) = signing_key { From d5c730aee20ee36aa8d25042e9cb22781504f869 Mon Sep 17 00:00:00 2001 From: Kaya Gokalp Date: Tue, 28 May 2024 21:58:22 -0700 Subject: [PATCH 06/15] chore: add remove todo comment --- forc-plugins/forc-client/src/util/tx.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/forc-plugins/forc-client/src/util/tx.rs b/forc-plugins/forc-client/src/util/tx.rs index 4e7a6c78311..f786fe4ef47 100644 --- a/forc-plugins/forc-client/src/util/tx.rs +++ b/forc-plugins/forc-client/src/util/tx.rs @@ -331,6 +331,7 @@ impl TransactionBuilderExt for Tran let address = if let Some(key) = signing_key { Address::from(*key.public_key().hash()) } else { + // TODO: Remove this path https://github.com/FuelLabs/sway/issues/6071 Address::from(prompt_address()?) }; From a1e529670cd28b228fa33cf2fbcd3d4006fc812a Mon Sep 17 00:00:00 2001 From: Kaya Gokalp Date: Tue, 28 May 2024 22:55:32 -0700 Subject: [PATCH 07/15] feat: actually update proxy target if there is an address in the forc.toml --- forc-plugins/forc-client/src/op/deploy.rs | 38 ++++- forc-plugins/forc-client/src/util/pkg.rs | 16 +-- forc-plugins/forc-client/src/util/tx.rs | 168 ++++++++++++++-------- 3 files changed, 141 insertions(+), 81 deletions(-) diff --git a/forc-plugins/forc-client/src/op/deploy.rs b/forc-plugins/forc-client/src/op/deploy.rs index 196b44721e3..71514c7b27b 100644 --- a/forc-plugins/forc-client/src/op/deploy.rs +++ b/forc-plugins/forc-client/src/op/deploy.rs @@ -3,11 +3,12 @@ use crate::{ util::{ gas::get_estimated_max_fee, node_url::get_node_url, - pkg::{built_pkgs, create_proxy_contract, update_proxy_address}, + pkg::{built_pkgs, create_proxy_contract, update_proxy_address_in_manifest}, tx::{ bech32_from_secret, check_and_create_wallet_at_default_path, first_user_account, - prompt_forc_wallet_password, select_manual_secret_key, TransactionBuilderExt, - WalletSelectionMode, TX_SUBMIT_TIMEOUT_MS, + prompt_forc_wallet_password, select_manual_secret_key, select_secret_key, + update_proxy_contract_target, TransactionBuilderExt, WalletSelectionMode, + TX_SUBMIT_TIMEOUT_MS, }, }, }; @@ -31,6 +32,7 @@ use serde::{Deserialize, Serialize}; use std::{ collections::BTreeMap, path::{Path, PathBuf}, + str::FromStr, }; use std::{sync::Arc, time::Duration}; use sway_core::language::parsed::TreeType; @@ -279,6 +281,7 @@ pub async fn deploy(command: cmd::Deploy) -> Result> { bail!("Both `--salt` and `--default-salt` were specified: must choose one") } }; + let node_url = get_node_url(&command.node, &pkg.descriptor.manifest_file.network)?; println!( " {} contract: {}", "Deploying".bold().green(), @@ -300,7 +303,31 @@ pub async fn deploy(command: cmd::Deploy) -> Result> { // Create a contract instance for the proxy contract using default proxy contract abi and // specified address. - todo!() + let provider = Provider::connect(node_url.clone()).await?; + // TODO: once https://github.com/FuelLabs/sway/issues/6071 is closed, this will return just a result + // and we won't need to handle the manual prompt based signature case. + let signing_key = select_secret_key( + &wallet_mode, + command.default_signer, + command.signing_key, + &provider, + ) + .await?; + + let signing_key = signing_key.ok_or_else( + + || anyhow::anyhow!("proxy contract deployments are not supported with manual prompt based signing") + )?; + let proxy_contract = + ContractId::from_str(proxy_addr).map_err(|e| anyhow::anyhow!(e))?; + + update_proxy_contract_target( + provider, + signing_key, + proxy_contract, + deployed_contract.id, + ) + .await?; } else { // Deploy a new proxy contract. let deployed_proxy_contract = deploy_new_proxy( @@ -315,7 +342,7 @@ pub async fn deploy(command: cmd::Deploy) -> Result> { .await?; // Update manifest file such that the proxy address field points to the new proxy contract. - update_proxy_address( + update_proxy_address_in_manifest( &format!("0x{}", deployed_proxy_contract.id), &pkg.descriptor.manifest_file, )?; @@ -339,7 +366,6 @@ pub async fn deploy_pkg( ) -> Result { let node_url = get_node_url(&command.node, &manifest.network)?; let client = FuelClient::new(node_url.clone())?; - let bytecode = &compiled.bytecode.bytes; let mut storage_slots = diff --git a/forc-plugins/forc-client/src/util/pkg.rs b/forc-plugins/forc-client/src/util/pkg.rs index c1b1f08c746..f604479d37b 100644 --- a/forc-plugins/forc-client/src/util/pkg.rs +++ b/forc-plugins/forc-client/src/util/pkg.rs @@ -83,7 +83,10 @@ fn only_owner() {{ /// Updates the given package manifest file such that the address field under the proxy table updated to the given value. /// Updated manifest file is written back to the same location, without thouching anything else such as comments etc. /// A safety check is done to ensure the proxy table exists before attempting to udpdate the value. -pub(crate) fn update_proxy_address(address: &str, manifest: &PackageManifestFile) -> Result<()> { +pub(crate) fn update_proxy_address_in_manifest( + address: &str, + manifest: &PackageManifestFile, +) -> Result<()> { let mut toml = String::new(); let mut file = File::open(manifest.path())?; file.read_to_string(&mut toml)?; @@ -136,17 +139,6 @@ pub(crate) fn create_proxy_contract( Ok(proxy_contract_dir) } -pub(crate) fn update_proxy_contract( - provider: &Provider, - proxy_contract_id: ContractId, -) -> Result<()> { - abigen!(Contract( - name = "ProxyContract", - abi = "forc-plugins/forc-client/src/util/proxy_contract-abi.json" - )); - - Ok(()) -} 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 f786fe4ef47..61d56f5b6d4 100644 --- a/forc-plugins/forc-client/src/util/tx.rs +++ b/forc-plugins/forc-client/src/util/tx.rs @@ -2,11 +2,17 @@ use std::{collections::BTreeMap, io::Write, path::Path, str::FromStr}; use anyhow::{Error, Result}; use async_trait::async_trait; +use forc_pkg::manifest::Proxy; use forc_tracing::println_warning; use fuel_crypto::{Message, PublicKey, SecretKey, Signature}; use fuel_tx::{field, Address, Buildable, ContractId, Input, Output, TransactionBuilder, Witness}; -use fuels_accounts::{provider::Provider, wallet::Wallet, ViewOnlyAccount}; +use fuels::macros::abigen; +use fuels_accounts::{ + provider::Provider, + wallet::{Wallet, WalletUnlocked}, + ViewOnlyAccount, +}; use fuels_core::types::{ bech32::{Bech32Address, FUEL_BECH32_HRP}, coin_type::CoinType, @@ -174,6 +180,102 @@ async fn collect_account_balances( .map_err(|e| anyhow::anyhow!("{e}")) } +// TODO: Simplify the function signature once https://github.com/FuelLabs/sway/issues/6071 is closed. +pub(crate) async fn select_secret_key( + wallet_mode: &WalletSelectionMode, + default_sign: bool, + signing_key: Option, + provider: &Provider, +) -> Result> { + let chain_info = provider.chain_info().await?; + let signing_key = match wallet_mode { + WalletSelectionMode::ForcWallet(password) => { + 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 accounts = collect_user_accounts(&wallet_path, &password)?; + let account_balances = collect_account_balances(&accounts, &provider).await?; + + let total_balance = account_balances + .iter() + .flat_map(|account| account.values()) + .sum::(); + if total_balance == 0 { + let first_account = accounts + .get(&0) + .ok_or_else(|| anyhow::anyhow!("No account derived for this wallet"))?; + let target = Target::from_str(&chain_info.name).unwrap_or(Target::testnet()); + let faucet_link = format!("{}/?address={first_account}", target.faucet_url()); + anyhow::bail!("Your wallet does not have any funds to pay for the transaction.\ + \n\nIf you are interacting with a testnet consider using the faucet.\ + \n-> {target} network faucet: {faucet_link}\ + \nIf you are interacting with a local node, consider providing a chainConfig which funds your account.") + } + print_account_balances(&accounts, &account_balances); + + let mut account_index; + loop { + print!("\nPlease provide the index of account to use for signing: "); + std::io::stdout().flush()?; + let mut input_account_index = String::new(); + std::io::stdin().read_line(&mut input_account_index)?; + account_index = input_account_index.trim().parse::()?; + if accounts.contains_key(&account_index) { + break; + } + let options: Vec = accounts.keys().map(|key| key.to_string()).collect(); + println_warning(&format!( + "\"{}\" is not a valid account.\nPlease choose a valid option from {}", + account_index, + options.join(","), + )); + } + + let secret_key = secret_key_from_forc_wallet(&wallet_path, account_index, &password)?; + + let bech32 = bech32_from_secret(&secret_key)?; + // TODO: Do this via forc-wallet once the functionality is exposed. + let question = format!( + "Do you agree to sign this transaction with {}? [y/N]: ", + bech32 + ); + let accepted = ask_user_yes_no_question(&question)?; + if !accepted { + anyhow::bail!("User refused to sign"); + } + + Some(secret_key) + } + WalletSelectionMode::Manual => select_manual_secret_key(default_sign, signing_key), + }; + Ok(signing_key) +} + +pub(crate) async fn update_proxy_contract_target( + provider: Provider, + secret_key: SecretKey, + proxy_contract_id: ContractId, + new_target: ContractId, +) -> Result<()> { + abigen!(Contract( + name = "ProxyContract", + abi = "forc-plugins/forc-client/src/util/proxy_contract-abi.json" + )); + + let wallet = WalletUnlocked::new_from_private_key(secret_key, Some(provider)); + + let proxy_contract = ProxyContract::new(proxy_contract_id, wallet); + + // TODO: what happens if the call fails? Does 'FuelCallResponse' is returned as Err() in that case? + proxy_contract + .methods() + .set_proxy_target(new_target) + .call() + .await?; + Ok(()) +} + #[async_trait] pub trait TransactionBuilderExt { fn add_contract(&mut self, contract_id: ContractId) -> &mut Self; @@ -265,68 +367,8 @@ impl TransactionBuilderExt for Tran ) -> Result { let chain_info = provider.chain_info().await?; let params = chain_info.consensus_parameters; - let signing_key = match wallet_mode { - WalletSelectionMode::ForcWallet(password) => { - 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 accounts = collect_user_accounts(&wallet_path, &password)?; - let account_balances = collect_account_balances(&accounts, &provider).await?; - - let total_balance = account_balances - .iter() - .flat_map(|account| account.values()) - .sum::(); - if total_balance == 0 { - let first_account = accounts - .get(&0) - .ok_or_else(|| anyhow::anyhow!("No account derived for this wallet"))?; - let target = Target::from_str(&chain_info.name).unwrap_or(Target::testnet()); - let faucet_link = format!("{}/?address={first_account}", target.faucet_url()); - anyhow::bail!("Your wallet does not have any funds to pay for the transaction.\ - \n\nIf you are interacting with a testnet consider using the faucet.\ - \n-> {target} network faucet: {faucet_link}\ - \nIf you are interacting with a local node, consider providing a chainConfig which funds your account.") - } - print_account_balances(&accounts, &account_balances); - - let mut account_index; - loop { - print!("\nPlease provide the index of account to use for signing: "); - std::io::stdout().flush()?; - let mut input_account_index = String::new(); - std::io::stdin().read_line(&mut input_account_index)?; - account_index = input_account_index.trim().parse::()?; - if accounts.contains_key(&account_index) { - break; - } - let options: Vec = accounts.keys().map(|key| key.to_string()).collect(); - println_warning(&format!( - "\"{}\" is not a valid account.\nPlease choose a valid option from {}", - account_index, - options.join(","), - )); - } - - let secret_key = - secret_key_from_forc_wallet(&wallet_path, account_index, &password)?; - - let bech32 = bech32_from_secret(&secret_key)?; - // TODO: Do this via forc-wallet once the functionality is exposed. - let question = format!( - "Do you agree to sign this transaction with {}? [y/N]: ", - bech32 - ); - let accepted = ask_user_yes_no_question(&question)?; - if !accepted { - anyhow::bail!("User refused to sign"); - } - - Some(secret_key) - } - WalletSelectionMode::Manual => select_manual_secret_key(default_sign, signing_key), - }; + let signing_key = + select_secret_key(wallet_mode, default_sign, signing_key, &provider).await?; // Get the address let address = if let Some(key) = signing_key { Address::from(*key.public_key().hash()) From ae12944d36fa1fcb00cc5528c6d63cee43b0358e Mon Sep 17 00:00:00 2001 From: Kaya Gokalp Date: Tue, 28 May 2024 22:56:20 -0700 Subject: [PATCH 08/15] clippy fix --- forc-plugins/forc-client/src/op/deploy.rs | 4 ++-- forc-plugins/forc-client/src/util/pkg.rs | 1 - forc-plugins/forc-client/src/util/tx.rs | 11 +++++------ 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/forc-plugins/forc-client/src/op/deploy.rs b/forc-plugins/forc-client/src/op/deploy.rs index 71514c7b27b..6caf2492865 100644 --- a/forc-plugins/forc-client/src/op/deploy.rs +++ b/forc-plugins/forc-client/src/op/deploy.rs @@ -178,10 +178,10 @@ async fn deploy_new_proxy( let user_addr = format!("0x{}", user_addr_hex); let pkg_name = pkg.descriptor.manifest_file.project_name(); let contract_addr = format!("0x{}", impl_contract.id); - let proxy_contract = build_proxy_contract(&user_addr, &contract_addr, pkg_name, &build_opts)?; + let proxy_contract = build_proxy_contract(&user_addr, &contract_addr, pkg_name, build_opts)?; println!(" {} proxy contract", "Deploying".bold().green()); let proxy = deploy_pkg( - &command, + command, &pkg.descriptor.manifest_file, &proxy_contract, salt, diff --git a/forc-plugins/forc-client/src/util/pkg.rs b/forc-plugins/forc-client/src/util/pkg.rs index f604479d37b..2d41dab3471 100644 --- a/forc-plugins/forc-client/src/util/pkg.rs +++ b/forc-plugins/forc-client/src/util/pkg.rs @@ -2,7 +2,6 @@ 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 fuels::prelude::*; use pkg::{build_with_options, BuiltPackage, PackageManifestFile}; use std::fs::File; use std::io::{Read, Write}; diff --git a/forc-plugins/forc-client/src/util/tx.rs b/forc-plugins/forc-client/src/util/tx.rs index 61d56f5b6d4..33b65c6f246 100644 --- a/forc-plugins/forc-client/src/util/tx.rs +++ b/forc-plugins/forc-client/src/util/tx.rs @@ -2,7 +2,6 @@ use std::{collections::BTreeMap, io::Write, path::Path, str::FromStr}; use anyhow::{Error, Result}; use async_trait::async_trait; -use forc_pkg::manifest::Proxy; use forc_tracing::println_warning; use fuel_crypto::{Message, PublicKey, SecretKey, Signature}; @@ -98,7 +97,7 @@ pub(crate) fn prompt_forc_wallet_password(wallet_path: &Path) -> Result } pub(crate) fn first_user_account(wallet_path: &Path, password: &str) -> Result { - let accounts = collect_user_accounts(wallet_path, &password)?; + let accounts = collect_user_accounts(wallet_path, password)?; let account = accounts .get(&0) @@ -132,7 +131,7 @@ pub(crate) fn secret_key_from_forc_wallet( account_index: usize, password: &str, ) -> Result { - let secret_key = derive_secret_key(&wallet_path, account_index, &password).map_err(|e| { + let secret_key = derive_secret_key(wallet_path, account_index, password).map_err(|e| { if e.to_string().contains("Mac Mismatch") { anyhow::anyhow!("Failed to access forc-wallet vault. Please check your password") } else { @@ -194,8 +193,8 @@ pub(crate) async fn select_secret_key( 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 accounts = collect_user_accounts(&wallet_path, &password)?; - let account_balances = collect_account_balances(&accounts, &provider).await?; + let accounts = collect_user_accounts(&wallet_path, password)?; + let account_balances = collect_account_balances(&accounts, provider).await?; let total_balance = account_balances .iter() @@ -232,7 +231,7 @@ pub(crate) async fn select_secret_key( )); } - let secret_key = secret_key_from_forc_wallet(&wallet_path, account_index, &password)?; + let secret_key = secret_key_from_forc_wallet(&wallet_path, account_index, password)?; let bech32 = bech32_from_secret(&secret_key)?; // TODO: Do this via forc-wallet once the functionality is exposed. From 0e7a0be35ae9a17a8b5a71e2f47b571dfadb780a Mon Sep 17 00:00:00 2001 From: Kaya Gokalp Date: Wed, 29 May 2024 13:05:33 -0700 Subject: [PATCH 09/15] test: add proxy contract build test --- forc-plugins/forc-client/src/op/deploy.rs | 24 +--------- forc-plugins/forc-client/src/util/pkg.rs | 57 +++++++++++++++++++++++ 2 files changed, 59 insertions(+), 22 deletions(-) diff --git a/forc-plugins/forc-client/src/op/deploy.rs b/forc-plugins/forc-client/src/op/deploy.rs index 6caf2492865..bb610285565 100644 --- a/forc-plugins/forc-client/src/op/deploy.rs +++ b/forc-plugins/forc-client/src/op/deploy.rs @@ -3,7 +3,7 @@ use crate::{ util::{ gas::get_estimated_max_fee, node_url::get_node_url, - pkg::{built_pkgs, create_proxy_contract, update_proxy_address_in_manifest}, + pkg::{build_proxy_contract, built_pkgs, update_proxy_address_in_manifest}, tx::{ bech32_from_secret, check_and_create_wallet_at_default_path, first_user_account, prompt_forc_wallet_password, select_manual_secret_key, select_secret_key, @@ -29,12 +29,12 @@ use fuels_core::types::bech32::Bech32Address; use futures::FutureExt; use pkg::{manifest::build_profile::ExperimentalFlags, BuildOpts, BuildProfile, BuiltPackage}; use serde::{Deserialize, Serialize}; +use std::time::Duration; use std::{ collections::BTreeMap, path::{Path, PathBuf}, str::FromStr, }; -use std::{sync::Arc, time::Duration}; use sway_core::language::parsed::TreeType; use sway_core::BuildTarget; use tracing::info; @@ -122,26 +122,6 @@ 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) -} async fn deploy_new_proxy( pkg: &BuiltPackage, owner_account_address: &mut Bech32Address, diff --git a/forc-plugins/forc-client/src/util/pkg.rs b/forc-plugins/forc-client/src/util/pkg.rs index 2d41dab3471..0a100a8ec92 100644 --- a/forc-plugins/forc-client/src/util/pkg.rs +++ b/forc-plugins/forc-client/src/util/pkg.rs @@ -166,3 +166,60 @@ pub(crate) fn built_pkgs(path: &Path, build_opts: &BuildOpts) -> Result 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) +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use forc_pkg::BuildOpts; + use forc_util::user_forc_directory; + + use super::{build_proxy_contract, PROXY_CONTRACT_FOLDER_NAME}; + + #[test] + fn test_build_proxy_contract() { + let owner_address = "0x0000000000000000000000000000000000000000000000000000000000000000"; + let impl_contract_address = + "0x0000000000000000000000000000000000000000000000000000000000000000"; + let target_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("test") + .join("data") + .join("standalone_contract"); + let mut build_opts = BuildOpts::default(); + let target_path = format!("{}", target_path.display()); + build_opts.pkg.path = Some(target_path); + let pkg_name = "standalone_contract"; + + let proxy_contract = + build_proxy_contract(owner_address, impl_contract_address, pkg_name, &build_opts); + // We want to make sure proxy_contract is building + proxy_contract.unwrap(); + + let proxy_contract_dir = user_forc_directory() + .join(PROXY_CONTRACT_FOLDER_NAME) + .join(pkg_name); + // Cleanup the test artifacts + std::fs::remove_dir_all(proxy_contract_dir).expect("failed to clean test artifacts") + } +} From d4474bd3c0cdfd28ceb546229c7e23338495378b Mon Sep 17 00:00:00 2001 From: Kaya Gokalp Date: Wed, 29 May 2024 13:54:35 -0700 Subject: [PATCH 10/15] docs: update docs --- .../src/forc/plugins/forc_client/index.md | 75 +++++++++++++++---- 1 file changed, 61 insertions(+), 14 deletions(-) diff --git a/docs/book/src/forc/plugins/forc_client/index.md b/docs/book/src/forc/plugins/forc_client/index.md index ce9ca27d64e..ffacb917e58 100644 --- a/docs/book/src/forc/plugins/forc_client/index.md +++ b/docs/book/src/forc/plugins/forc_client/index.md @@ -1,26 +1,73 @@ # `forc-client` -Forc plugin for interacting with a Fuel node. +Forc plugin for interacting with a Fuel node. Since transactions are going to require some gas, you need to sign them with an account that has enough tokens to pay for them. -## Initializing the wallet and adding accounts +We offer multiple ways to sign the transaction: + 1. Sign the transaction via your local wallet using forc-client which integrates with our CLI wallet, forc-wallet. + 2. Use the default signer to deploy to a local node + 3. Use forc-wallet to manually sign transactions, and copy the signed transaction back to forc-client. -If you don't have an initialized wallet or any account for your wallet you won't be able to sign transactions. +The easiest and recommended way to interact with deployed networks, such as our testnets is the option 1, using forc-client to sign your transactions which reads your default forc-wallet vault. For interacting with local node, we recommend using the second option, which leads forc-client to sign transactions with the a private key that comes pre-funded in local environments. -To create a wallet you can use `forc wallet new`. It will ask you to choose a password to encrypt your wallet. After the initialization is done you will have your mnemonic phrase. +## Option 1: Sign transactions via forc-client using your local forc-wallet vault +If you used forc-wallet before, a vault which securely holds your private key is created and written to your file-system in a password encrypted format. forc-client is compatible with forc-wallet such that it can read that vault by asking you your password and use your account to sign transactions. -After you have created a wallet, you can derive a new account by running `forc wallet account new`. It will ask your password to decrypt the wallet before deriving an account. +Example: -## Signing transactions using `forc-wallet` CLI +```console +> forc deploy -To submit the transactions created by `forc deploy` or `forc run`, you need to sign them first (unless you are using a client without UTXO validation). To sign a transaction you can use `forc-wallet` CLI. This section is going to walk you through the whole signing process. + Building /Users/test/test-projects/test-contract + Finished release [optimized + fuel] target(s) in 11.39s -By default `fuel-core` runs without UTXO validation, this allows you to send invalid inputs to emulate different conditions. +Please provide the password of your encrypted wallet vault at "/Users/ceylinbormali/.fuel/wallets/.wallet": + Deploying contract: impl-contract -If you want to run `fuel-core` with UTXO validation, you can pass `--utxo-validation` to `fuel-core run`. +--------------------------------------------------------------------------- +Account 0: fuel12pls73y9hnqdqthvduy2x44x48zt8s50pkerf32kq26f2afeqdwq6rj9ar -To install `forc-wallet` please refer to `forc-wallet`'s [GitHub repo](https://github.com/FuelLabs/forc-wallet#forc-wallet). +Asset ID : f8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07 +Amount : 2197245 +--------------------------------------------------------------------------- -1. Construct the transaction by using either `forc deploy` or `forc run`. To do so simply run `forc deploy` or `forc run` with your desired parameters. For a list of parameters please refer to the [forc-deploy](./forc_deploy.md) or [forc-run](./forc_run.md) section of the book. Once you run either command you will be asked the address of the wallet you are going to be signing with. After the address is given the transaction will be generated and you will be given a transaction ID. At this point CLI will actively wait for you to insert the signature. +Please provide the index of account to use for signing: 0 +Do you agree to sign this transaction with fuel12pls73y9hnqdqthvduy2x44x48zt8s50pkerf32kq26f2afeqdwq6rj9ar? [y/N]: y + + +Contract impl-contract Deployed! + +Network: https://testnet.fuel.network +Contract ID: 0x94b712901f04332682d14c998a5fc5a078ed15321438f46d58d0383200cde43d +Deployed in block 00114d4d +``` + +As it can be seen from the example, forc-client asks for your password to decrypt the forc-wallet vault, and list your accounts so that you can select the one you want to fund the transaction with. + +## Option 2: Using default signer + +If you are not interacting with a deployed network, such as testnets, your local fuel-core environment can be structured such that it funds an account by default. Using `--default-signer` flag with forc-client binaries (run, deploy) will instruct forc-client to sign transactions with this pre-funded account. Which makes it a useful command while working against a local node. + +Example: +```console +> forc deploy --default-signer + + Building /Users/test/test-projects/test-contract + Finished release [optimized + fuel] target(s) in 11.40s + Deploying contract: impl-contract + + +Contract impl-contract Deployed! + +Network: http://127.0.0.1:4000 +Contract ID: 0xf9fb08ef18ce226954270d6d4f67677d484b8782a5892b3d436572b405407544 +Deployed in block 00000001 +``` + +## Option 3: Manually signing through forc-wallet (Deprecated) + +This option is for creating the transaction first, signing it manually and supplying the signed transaction back to forc-client. Since it requires multiple steps, it is more error-prone and not recommended for general use case. Also this will be deprecated soon. + +1. Construct the transaction by using either `forc deploy` or `forc run`. To do so simply run `forc deploy --manual-sign` or `forc run --manual-sign` with your desired parameters. For a list of parameters please refer to the [forc-deploy](./forc_deploy.md) or [forc-run](./forc_run.md) section of the book. Once you run either command you will be asked the address of the wallet you are going to be signing with. After the address is given the transaction will be generated and you will be given a transaction ID. At this point CLI will actively wait for you to insert the signature. 2. Take the transaction ID generated in the first step and sign it with `forc wallet sign --account tx-id `. This will generate a signature. 3. Take the signature generated in the second step and provide it to `forc-deploy` (or `forc-run`). Once the signature is provided, the signed transaction will be submitted. @@ -56,7 +103,7 @@ By default `--default-signer` flag would sign your transactions with the followi ## Interacting with the testnet -To interact with the latest testnet, use the `--testnet` flag. When this flag is passed, transactions created by `forc-deploy` will be sent to the `beta-4` testnet. +To interact with the latest testnet, use the `--testnet` flag. When this flag is passed, transactions created by `forc-deploy` will be sent to the latest `testnet`. ```sh forc-deploy --testnet @@ -68,10 +115,10 @@ It is also possible to pass the exact node URL while using `forc-deploy` or `for forc-deploy --node-url https://beta-3.fuel.network ``` -Another alternative is the `--target` option, which provides useful aliases to all targets. For example if you want to deploy to `beta-3` you can use: +Another alternative is the `--target` option, which provides useful aliases to all targets. For example if you want to deploy to `beta-5` you can use: ```sh -forc-deploy --target beta-3 +forc-deploy --target beta-5 ``` Since deploying and running projects on the testnet cost gas, you will need coins to pay for them. You can get some using the [testnet faucet](https://faucet-beta-4.fuel.network/). From 7d3613e06292e4b69402c0baa85dd600746b19d3 Mon Sep 17 00:00:00 2001 From: Kaya Gokalp Date: Wed, 29 May 2024 13:57:05 -0700 Subject: [PATCH 11/15] markdown lint --- docs/book/src/forc/plugins/forc_client/index.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/book/src/forc/plugins/forc_client/index.md b/docs/book/src/forc/plugins/forc_client/index.md index ffacb917e58..ed8cfe9d859 100644 --- a/docs/book/src/forc/plugins/forc_client/index.md +++ b/docs/book/src/forc/plugins/forc_client/index.md @@ -3,6 +3,7 @@ Forc plugin for interacting with a Fuel node. Since transactions are going to require some gas, you need to sign them with an account that has enough tokens to pay for them. We offer multiple ways to sign the transaction: + 1. Sign the transaction via your local wallet using forc-client which integrates with our CLI wallet, forc-wallet. 2. Use the default signer to deploy to a local node 3. Use forc-wallet to manually sign transactions, and copy the signed transaction back to forc-client. @@ -10,6 +11,7 @@ We offer multiple ways to sign the transaction: The easiest and recommended way to interact with deployed networks, such as our testnets is the option 1, using forc-client to sign your transactions which reads your default forc-wallet vault. For interacting with local node, we recommend using the second option, which leads forc-client to sign transactions with the a private key that comes pre-funded in local environments. ## Option 1: Sign transactions via forc-client using your local forc-wallet vault + If you used forc-wallet before, a vault which securely holds your private key is created and written to your file-system in a password encrypted format. forc-client is compatible with forc-wallet such that it can read that vault by asking you your password and use your account to sign transactions. Example: @@ -48,6 +50,7 @@ As it can be seen from the example, forc-client asks for your password to decryp If you are not interacting with a deployed network, such as testnets, your local fuel-core environment can be structured such that it funds an account by default. Using `--default-signer` flag with forc-client binaries (run, deploy) will instruct forc-client to sign transactions with this pre-funded account. Which makes it a useful command while working against a local node. Example: + ```console > forc deploy --default-signer From 64d991fc0aeb505c3c6d4508ae7e755aaaa721d6 Mon Sep 17 00:00:00 2001 From: Kaya Gokalp Date: Wed, 29 May 2024 15:01:37 -0700 Subject: [PATCH 12/15] fix comment typo --- forc-plugins/forc-client/src/util/pkg.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forc-plugins/forc-client/src/util/pkg.rs b/forc-plugins/forc-client/src/util/pkg.rs index 0a100a8ec92..f0ecd6605f3 100644 --- a/forc-plugins/forc-client/src/util/pkg.rs +++ b/forc-plugins/forc-client/src/util/pkg.rs @@ -169,7 +169,7 @@ pub(crate) fn built_pkgs(path: &Path, build_opts: &BuildOpts) -> Result Date: Wed, 29 May 2024 15:27:22 -0700 Subject: [PATCH 13/15] docs: add proxy docs --- .../src/forc/plugins/forc_client/index.md | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/docs/book/src/forc/plugins/forc_client/index.md b/docs/book/src/forc/plugins/forc_client/index.md index ed8cfe9d859..341595348fd 100644 --- a/docs/book/src/forc/plugins/forc_client/index.md +++ b/docs/book/src/forc/plugins/forc_client/index.md @@ -141,3 +141,39 @@ forc-deploy saves the details of each deployment in the `out/deployments` folder "deployed_block_id": "0x915c6f372252be6bc54bd70df6362dae9bf750ba652bf5582d9b31c7023ca6cf" } ``` +## Proxy Contracts + +`forc-deploy` supports deploying proxy contracts automatically if the it is enabled in the `Forc.toml` of the contract. + +```TOML +[project] +name = "test_contract" +authors = ["Fuel Labs "] +entry = "main.sw" +license = "Apache-2.0" +implicit-std = false + +[proxy] +enabled = true +``` + +If there is no `address` field present under the proxy table, like the example above, forc automatically creates a proxy contract based on [SRC14](https://github.com/FuelLabs/sway-standards/pull/94) implementation from [sway-standards](https://github.com/FuelLabs/sway-standards). After generating the proxy contract, the target is set to the current contract, and owner of the proxy is set to the account that is signing the transaction for deployment. + +This means that if you simply enable proxy in the Forc.toml, forc will automatically deploy a proxy contract for you and you do not need to make anything manually. After deploying the proxy contract the `address` of it is added into the `address` field of the proxy table. + +If you want to update the target of a proxy contract rather than deploying a new one, simply add its `address` in the `address` field, like the following example: + +```TOML +[project] +name = "test_contract" +authors = ["Fuel Labs "] +entry = "main.sw" +license = "Apache-2.0" +implicit-std = false + +[proxy] +enabled = true +address = "0xd8c4b07a0d1be57b228f4c18ba7bca0c8655eb6e9d695f14080f2cf4fc7cd946" # example proxy contract address +``` + +If an `address` is present, forc calls into that contract to update its target instead of deploying a new contract. Since a new proxy deployment adds its own `address` into the Forc.toml automatically, you can simply enable the proxy once and after the initial deployment, forc will keep updating the target accordingly for each new deployment of the same contract. From 72e719c31e429b8b2b84b8cafc92738b7463f463 Mon Sep 17 00:00:00 2001 From: Kaya Gokalp Date: Wed, 29 May 2024 15:30:50 -0700 Subject: [PATCH 14/15] trigger ci From 7ae135eb659b121680b169201939cb17127a7024 Mon Sep 17 00:00:00 2001 From: Kaya Gokalp Date: Wed, 29 May 2024 15:32:04 -0700 Subject: [PATCH 15/15] fix docs lints --- docs/book/src/forc/plugins/forc_client/index.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/book/src/forc/plugins/forc_client/index.md b/docs/book/src/forc/plugins/forc_client/index.md index 341595348fd..49cf6c71754 100644 --- a/docs/book/src/forc/plugins/forc_client/index.md +++ b/docs/book/src/forc/plugins/forc_client/index.md @@ -141,6 +141,7 @@ forc-deploy saves the details of each deployment in the `out/deployments` folder "deployed_block_id": "0x915c6f372252be6bc54bd70df6362dae9bf750ba652bf5582d9b31c7023ca6cf" } ``` + ## Proxy Contracts `forc-deploy` supports deploying proxy contracts automatically if the it is enabled in the `Forc.toml` of the contract. @@ -176,4 +177,4 @@ enabled = true address = "0xd8c4b07a0d1be57b228f4c18ba7bca0c8655eb6e9d695f14080f2cf4fc7cd946" # example proxy contract address ``` -If an `address` is present, forc calls into that contract to update its target instead of deploying a new contract. Since a new proxy deployment adds its own `address` into the Forc.toml automatically, you can simply enable the proxy once and after the initial deployment, forc will keep updating the target accordingly for each new deployment of the same contract. +If an `address` is present, forc calls into that contract to update its target instead of deploying a new contract. Since a new proxy deployment adds its own `address` into the Forc.toml automatically, you can simply enable the proxy once and after the initial deployment, forc will keep updating the target accordingly for each new deployment of the same contract.