From db8ba0bb6c913b7547111d17c1b5164868df1ed1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kaya=20G=C3=B6kalp?= Date: Wed, 3 Jul 2024 03:39:09 -0700 Subject: [PATCH] fix: deployment estimation deferred to sdk (#6212) --- .github/workflows/ci.yml | 21 +- Cargo.lock | 3 + forc-plugins/forc-client/Cargo.toml | 5 + forc-plugins/forc-client/src/constants.rs | 5 + forc-plugins/forc-client/src/op/deploy.rs | 88 +++--- forc-plugins/forc-client/src/op/mod.rs | 2 +- forc-plugins/forc-client/src/op/run/mod.rs | 24 +- forc-plugins/forc-client/src/util/gas.rs | 63 +---- forc-plugins/forc-client/src/util/tx.rs | 295 ++++++++++++--------- forc-plugins/forc-client/tests/deploy.rs | 128 +++++++++ 10 files changed, 392 insertions(+), 242 deletions(-) create mode 100644 forc-plugins/forc-client/tests/deploy.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4ec166a53ef..3928b4f7aee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -517,6 +517,25 @@ jobs: mv fuel-core-${{ needs.get-fuel-core-version.outputs.fuel_core_version }}-x86_64-unknown-linux-gnu/fuel-core /usr/local/bin/fuel-core - name: Run tests run: cargo test --locked --release -p forc-debug + cargo-test-forc-client: + runs-on: ubuntu-latest + needs: get-fuel-core-version + steps: + - uses: actions/checkout@v3 + - name: Install toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ env.RUST_VERSION }} + targets: "x86_64-unknown-linux-gnu, wasm32-unknown-unknown" + - uses: Swatinem/rust-cache@v2 + - name: Install fuel-core for tests + run: | + curl -sSLf https://github.com/FuelLabs/fuel-core/releases/download/v${{ needs.get-fuel-core-version.outputs.fuel_core_version }}/fuel-core-${{ needs.get-fuel-core-version.outputs.fuel_core_version }}-x86_64-unknown-linux-gnu.tar.gz -L -o fuel-core.tar.gz + tar -xvf fuel-core.tar.gz + chmod +x fuel-core-${{ needs.get-fuel-core-version.outputs.fuel_core_version }}-x86_64-unknown-linux-gnu/fuel-core + mv fuel-core-${{ needs.get-fuel-core-version.outputs.fuel_core_version }}-x86_64-unknown-linux-gnu/fuel-core /usr/local/bin/fuel-core + - name: Run tests + run: cargo test --locked --release -p forc-client cargo-test-sway-lsp: runs-on: ubuntu-latest steps: @@ -538,7 +557,7 @@ jobs: toolchain: ${{ env.RUST_VERSION }} - uses: Swatinem/rust-cache@v2 - name: Run tests - run: cargo test --locked --release --workspace --exclude forc-debug --exclude sway-lsp + run: cargo test --locked --release --workspace --exclude forc-debug --exclude sway-lsp --exclude forc-client cargo-unused-deps-check: runs-on: ubuntu-latest steps: diff --git a/Cargo.lock b/Cargo.lock index 27dfdded259..d19bd9f0989 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2012,6 +2012,7 @@ dependencies = [ "fuels-core", "futures", "hex", + "portpicker", "rand", "rpassword", "serde", @@ -2019,7 +2020,9 @@ dependencies = [ "sway-core", "sway-types", "sway-utils", + "tempfile", "tokio", + "toml_edit 0.21.1", "tracing", ] diff --git a/forc-plugins/forc-client/Cargo.toml b/forc-plugins/forc-client/Cargo.toml index 7ad1be7d2e2..8ff255fef3b 100644 --- a/forc-plugins/forc-client/Cargo.toml +++ b/forc-plugins/forc-client/Cargo.toml @@ -40,6 +40,11 @@ sway-utils = { version = "0.61.1", path = "../../sway-utils" } tokio = { version = "1.8", features = ["macros", "rt-multi-thread", "process"] } tracing = "0.1" +[dev-dependencies] +portpicker = "0.1.1" +tempfile = "3" +toml_edit = "0.21.1" + [[bin]] name = "forc-deploy" path = "src/bin/deploy.rs" diff --git a/forc-plugins/forc-client/src/constants.rs b/forc-plugins/forc-client/src/constants.rs index bd946af2f44..927323a57b3 100644 --- a/forc-plugins/forc-client/src/constants.rs +++ b/forc-plugins/forc-client/src/constants.rs @@ -12,3 +12,8 @@ pub const DEVNET_FAUCET_URL: &str = "https://faucet-devnet.fuel.network"; pub const DEVNET_ENDPOINT_URL: &str = "https://devnet.fuel.network"; pub const TESTNET_FAUCET_URL: &str = "https://faucet-testnet.fuel.network"; pub const TESTNET_ENDPOINT_URL: &str = "https://testnet.fuel.network"; +/// Default PrivateKey to sign transactions submitted to local node. +pub const DEFAULT_PRIVATE_KEY: &str = + "0xde97d8624a438121b86a1956544bd72ed68cd69f2c99555b08b1e8c51ffd511c"; +/// The maximum time to wait for a transaction to be included in a block by the node +pub const TX_SUBMIT_TIMEOUT_MS: u64 = 30_000u64; diff --git a/forc-plugins/forc-client/src/op/deploy.rs b/forc-plugins/forc-client/src/op/deploy.rs index e0270fa5245..bb7a95cc549 100644 --- a/forc-plugins/forc-client/src/op/deploy.rs +++ b/forc-plugins/forc-client/src/op/deploy.rs @@ -1,10 +1,10 @@ use crate::{ cmd, + constants::TX_SUBMIT_TIMEOUT_MS, util::{ - gas::get_estimated_max_fee, node_url::get_node_url, pkg::built_pkgs, - tx::{TransactionBuilderExt, WalletSelectionMode, TX_SUBMIT_TIMEOUT_MS}, + tx::{prompt_forc_wallet_password, select_secret_key, WalletSelectionMode}, }, }; use anyhow::{bail, Context, Result}; @@ -12,12 +12,14 @@ 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_tx::Salt; use fuel_vm::prelude::*; -use fuels_accounts::provider::Provider; +use fuels_accounts::{provider::Provider, wallet::WalletUnlocked, Account}; +use fuels_core::types::{transaction::TxPolicies, transaction_builders::CreateTransactionBuilder}; use futures::FutureExt; use pkg::{manifest::build_profile::ExperimentalFlags, BuildProfile, BuiltPackage}; use serde::{Deserialize, Serialize}; @@ -30,7 +32,7 @@ use sway_core::language::parsed::TreeType; use sway_core::BuildTarget; use tracing::info; -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] pub struct DeployedContract { pub id: fuel_tx::ContractId, } @@ -176,6 +178,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) + }; + for pkg in built_pkgs { if pkg .descriptor @@ -197,8 +206,14 @@ pub async fn deploy(command: cmd::Deploy) -> Result> { bail!("Both `--salt` and `--default-salt` were specified: must choose one") } }; - let contract_id = - deploy_pkg(&command, &pkg.descriptor.manifest_file, &pkg, salt).await?; + let contract_id = deploy_pkg( + &command, + &pkg.descriptor.manifest_file, + &pkg, + salt, + &wallet_mode, + ) + .await?; contract_ids.push(contract_id); } } @@ -211,6 +226,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())?; @@ -232,44 +248,30 @@ 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?; + let tx_policies = TxPolicies::default(); + + let mut tb = CreateTransactionBuilder::prepare_contract_deployment( + bytecode.clone(), + contract_id, + state_root, + salt, + storage_slots.clone(), + tx_policies, + ); + let signing_key = select_secret_key( + wallet_mode, + command.default_signer || command.unsigned, + command.signing_key, + &provider, + ) + .await? + .ok_or_else(|| anyhow::anyhow!("failed to select a signer for the transaction"))?; + let wallet = WalletUnlocked::new_from_private_key(signing_key, Some(provider.clone())); - // We need a tx for estimation without the signature. - let mut tb = - TransactionBuilder::create(bytecode.as_slice().into(), salt, storage_slots.clone()); - tb.maturity(command.maturity.maturity.into()) - .add_output(Output::contract_created(contract_id, state_root)); - let tx_for_estimation = tb.finalize_without_signature_inner(); - - // If user specified max_fee use that but if not, we will estimate with %10 safety margin. - let max_fee = if let Some(max_fee) = command.gas.max_fee { - max_fee - } else { - let estimation_margin = 10; - get_estimated_max_fee( - tx_for_estimation.clone(), - &provider, - &client, - estimation_margin, - ) - .await? - }; - - let tx = tb - .max_fee_limit(max_fee) - .finalize_signed( - provider.clone(), - command.default_signer || command.unsigned, - command.signing_key, - wallet_mode, - ) - .await?; + wallet.add_witnesses(&mut tb)?; + wallet.adjust_for_fee(&mut tb, 0).await?; + let tx = tb.build(provider).await?; let tx = Transaction::from(tx); let chain_id = client.chain_info().await?.consensus_parameters.chain_id(); diff --git a/forc-plugins/forc-client/src/op/mod.rs b/forc-plugins/forc-client/src/op/mod.rs index bb7e5746eb4..44a1b055551 100644 --- a/forc-plugins/forc-client/src/op/mod.rs +++ b/forc-plugins/forc-client/src/op/mod.rs @@ -2,6 +2,6 @@ mod deploy; mod run; mod submit; -pub use deploy::deploy; +pub use deploy::{deploy, DeployedContract}; pub use run::run; pub use submit::submit; diff --git a/forc-plugins/forc-client/src/op/run/mod.rs b/forc-plugins/forc-client/src/op/run/mod.rs index 0ea4f073b2a..317afd80687 100644 --- a/forc-plugins/forc-client/src/op/run/mod.rs +++ b/forc-plugins/forc-client/src/op/run/mod.rs @@ -1,17 +1,19 @@ mod encode; use crate::{ cmd, + constants::TX_SUBMIT_TIMEOUT_MS, util::{ 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}, }, }; 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 +51,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 +64,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 +82,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 +116,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/gas.rs b/forc-plugins/forc-client/src/util/gas.rs index 74d87dd0bf2..9667bee8674 100644 --- a/forc-plugins/forc-client/src/util/gas.rs +++ b/forc-plugins/forc-client/src/util/gas.rs @@ -1,15 +1,11 @@ use anyhow::Result; -use fuel_core_client::client::FuelClient; -use fuel_core_types::services::executor::TransactionExecutionResult; use fuel_tx::{ - field::{Inputs, MaxFeeLimit, Witnesses}, - Buildable, Chargeable, Create, Input, Script, Transaction, TxPointer, + field::{Inputs, Witnesses}, + Buildable, Chargeable, Input, Script, TxPointer, }; use fuels_accounts::provider::Provider; -use fuels_core::{ - constants::DEFAULT_GAS_ESTIMATION_BLOCK_HORIZON, types::transaction::ScriptTransaction, -}; +use fuels_core::types::transaction::ScriptTransaction; fn no_spendable_input<'a, I: IntoIterator>(inputs: I) -> bool { !inputs.into_iter().any(|i| { @@ -55,56 +51,3 @@ pub(crate) async fn get_script_gas_used(mut tx: Script, provider: &Provider) -> .await?; Ok(estimated_tx_cost.gas_used) } - -/// Returns an estimation for the max fee of `Create` transactions. -/// Accepts a `tolerance` which is used to add some safety margin to the estimation. -/// Resulting estimation is calculated as `(dry_run_estimation * tolerance)/100 + dry_run_estimation)`. -pub(crate) async fn get_estimated_max_fee( - tx: Create, - provider: &Provider, - client: &FuelClient, - tolerance: u64, -) -> Result { - let mut tx = tx.clone(); - // Add dummy input to get past validation for dry run. - let no_spendable_input = no_spendable_input(tx.inputs()); - let base_asset_id = provider.base_asset_id(); - if no_spendable_input { - tx.inputs_mut().push(Input::coin_signed( - Default::default(), - Default::default(), - 1_000_000_000, - *base_asset_id, - TxPointer::default(), - 0, - )); - - // Add an empty `Witness` for the `coin_signed` we just added - // and increase the witness limit - tx.witnesses_mut().push(Default::default()); - } - let consensus_params = provider.consensus_parameters(); - let gas_price = provider - .estimate_gas_price(DEFAULT_GAS_ESTIMATION_BLOCK_HORIZON) - .await? - .gas_price; - let max_fee = tx.max_fee( - consensus_params.gas_costs(), - consensus_params.fee_params(), - gas_price, - ); - tx.set_max_fee_limit(max_fee as u64); - let tx = Transaction::from(tx); - - let tx_status = client - .dry_run(&[tx]) - .await - .map(|mut status_vec| status_vec.remove(0))?; - let total_fee = match tx_status.result { - TransactionExecutionResult::Success { total_fee, .. } => total_fee, - TransactionExecutionResult::Failed { total_fee, .. } => total_fee, - }; - - let total_fee_with_tolerance = ((total_fee * tolerance) / 100) + total_fee; - Ok(total_fee_with_tolerance) -} diff --git a/forc-plugins/forc-client/src/util/tx.rs b/forc-plugins/forc-client/src/util/tx.rs index be4206b340e..a79f7359255 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; @@ -23,18 +23,12 @@ use forc_wallet::{ utils::default_wallet_path, }; -use crate::util::target::Target; - -/// The maximum time to wait for a transaction to be included in a block by the node -pub const TX_SUBMIT_TIMEOUT_MS: u64 = 30_000u64; - -/// Default PrivateKey to sign transactions submitted to local node. -pub const DEFAULT_PRIVATE_KEY: &str = - "0xde97d8624a438121b86a1956544bd72ed68cd69f2c99555b08b1e8c51ffd511c"; +use crate::{constants::DEFAULT_PRIVATE_KEY, util::target::Target}; #[derive(PartialEq, Eq)] pub enum WalletSelectionMode { - ForcWallet, + /// Holds the password of forc-wallet instance. + ForcWallet(String), Manual, } @@ -66,6 +60,89 @@ 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 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 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(()) +} + +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, @@ -81,6 +158,78 @@ 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) +} + #[async_trait] pub trait TransactionBuilderExt { fn add_contract(&mut self, contract_id: ContractId) -> &mut Self; @@ -95,9 +244,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; } @@ -169,131 +318,17 @@ 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) => { - // 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 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 = 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 - } - })?; - - // 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 - ); - let accepted = ask_user_yes_no_question(&question)?; - if !accepted { - anyhow::bail!("User refused to sign"); - } - - 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) - } - }; + 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()) } else { + // TODO: Remove this path https://github.com/FuelLabs/sway/issues/6071 Address::from(prompt_address()?) }; diff --git a/forc-plugins/forc-client/tests/deploy.rs b/forc-plugins/forc-client/tests/deploy.rs new file mode 100644 index 00000000000..d6ca5c6c858 --- /dev/null +++ b/forc-plugins/forc-client/tests/deploy.rs @@ -0,0 +1,128 @@ +use std::{ + fs, + path::{Path, PathBuf}, + process::{Child, Command}, + str::FromStr, +}; + +use forc::cli::shared::Pkg; +use forc_client::{ + cmd, + op::{deploy, DeployedContract}, + NodeTarget, +}; +use fuel_tx::{ContractId, Salt}; +use portpicker::Port; +use tempfile::tempdir; +use toml_edit::{Document, InlineTable, Item, Value}; + +fn get_workspace_root() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../") + .join("../") + .canonicalize() + .unwrap() +} + +/// Return the path to the chain config file which is expected to be in +/// `.github/workflows/local-node` from sway repo root. +fn chain_config_path() -> PathBuf { + get_workspace_root() + .join(".github") + .join("workflows") + .join("local-testnode") +} + +fn test_data_path() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("test") + .join("data") + .canonicalize() + .unwrap() +} + +fn run_node() -> (Child, Port) { + let port = portpicker::pick_unused_port().expect("No ports free"); + + let chain_config = chain_config_path(); + let child = Command::new("fuel-core") + .arg("run") + .arg("--debug") + .arg("--db-type") + .arg("in-memory") + .arg("--port") + .arg(port.to_string()) + .arg("--snapshot") + .arg(format!("{}", chain_config.display())) + .spawn() + .expect("Failed to start fuel-core"); + (child, port) +} + +/// Copy a directory recursively from `source` to `dest`. +fn copy_dir(source: &Path, dest: &Path) -> anyhow::Result<()> { + fs::create_dir_all(dest)?; + for e in fs::read_dir(source)? { + let entry = e?; + let file_type = entry.file_type()?; + if file_type.is_dir() { + copy_dir(&entry.path(), &dest.join(entry.file_name()))?; + } else { + fs::copy(entry.path(), dest.join(entry.file_name()))?; + } + } + Ok(()) +} + +fn patch_manifest_file_with_path_std(manifest_dir: &Path) -> anyhow::Result<()> { + let toml_path = manifest_dir.join(sway_utils::constants::MANIFEST_FILE_NAME); + let toml_content = fs::read_to_string(&toml_path).unwrap(); + + let mut doc = toml_content.parse::().unwrap(); + let new_std_path = get_workspace_root().join("sway-lib-std"); + + let mut std_dependency = InlineTable::new(); + std_dependency.insert("path", Value::from(new_std_path.display().to_string())); + doc["dependencies"]["std"] = Item::Value(Value::InlineTable(std_dependency)); + + fs::write(&toml_path, doc.to_string()).unwrap(); + Ok(()) +} + +#[tokio::test] +async fn simple_deploy() { + let (mut node, port) = run_node(); + let tmp_dir = tempdir().unwrap(); + let project_dir = test_data_path().join("standalone_contract"); + copy_dir(&project_dir, tmp_dir.path()).unwrap(); + patch_manifest_file_with_path_std(tmp_dir.path()).unwrap(); + + let pkg = Pkg { + path: Some(tmp_dir.path().display().to_string()), + ..Default::default() + }; + + let node_url = format!("http://127.0.0.1:{}/v1/graphql", port); + let target = NodeTarget { + node_url: Some(node_url), + target: None, + testnet: false, + }; + let cmd = cmd::Deploy { + pkg, + salt: Some(vec![format!("{}", Salt::default())]), + node: target, + default_signer: true, + ..Default::default() + }; + let contract_ids = deploy(cmd).await.unwrap(); + node.kill().unwrap(); + let expected = vec![DeployedContract { + id: ContractId::from_str( + "428896412bda8530282a7b8fca5d20b2a73f30037612ca3a31750cf3bf0e976a", + ) + .unwrap(), + }]; + + assert_eq!(contract_ids, expected) +}