From 9f406fd6346c71f862215167fb1673d3f48c422d Mon Sep 17 00:00:00 2001 From: Kaya Gokalp Date: Tue, 28 May 2024 14:44:01 -0700 Subject: [PATCH 01/37] feat: create and deploy proxy for contracts with proxy enabled chore: use defaults from constants chore: clippy fix feat: update proxy addres after new deployment of it refactor: remove duplicate password prompts and cleanup secret key selection chore: add remove todo comment feat: actually update proxy target if there is an address in the forc.toml clippy fix test: add proxy contract build test docs: update docs markdown lint fix comment typo docs: add proxy docs fix docs lints update docs from review Co-authored-by: K1-R1 <77465250+K1-R1@users.noreply.github.com> Co-authored-by: Sophie Dankel <47993817+sdankel@users.noreply.github.com> pin to tag --- Cargo.lock | 3 + Cargo.toml | 1 + .../src/forc/plugins/forc_client/index.md | 115 +++++- forc-pkg/src/manifest/mod.rs | 17 + forc-pkg/src/pkg.rs | 6 +- forc-plugins/forc-client/Cargo.toml | 3 + 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 | 159 ++++++++- forc-plugins/forc-client/src/op/run/mod.rs | 26 +- forc-plugins/forc-client/src/util/pkg.rs | 193 ++++++++++- .../src/util/proxy_contract-abi.json | 146 ++++++++ forc-plugins/forc-client/src/util/tx.rs | 326 +++++++++++------- 13 files changed, 832 insertions(+), 169 deletions(-) create mode 100644 forc-plugins/forc-client/src/util/proxy_contract-abi.json diff --git a/Cargo.lock b/Cargo.lock index 833a3735f3a..f7bc7d70735 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2032,6 +2032,7 @@ dependencies = [ "async-trait", "chrono", "clap 4.5.4", + "colored", "devault", "forc", "forc-pkg", @@ -2045,6 +2046,7 @@ dependencies = [ "fuel-crypto", "fuel-tx", "fuel-vm", + "fuels", "fuels-accounts", "fuels-core", "futures", @@ -2057,6 +2059,7 @@ dependencies = [ "sway-types", "sway-utils", "tokio", + "toml_edit 0.21.1", "tracing", ] diff --git a/Cargo.toml b/Cargo.toml index ae06a4828c1..f9eb28deda1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,7 @@ fuel-vm = "0.50.0" # Dependencies from the `fuels-rs` repository: fuels-core = "0.63.0" fuels-accounts = "0.63.0" +fuels = "0.63.0" # Dependencies from the `forc-wallet` repository: forc-wallet = "0.8.0" diff --git a/docs/book/src/forc/plugins/forc_client/index.md b/docs/book/src/forc/plugins/forc_client/index.md index ce9ca27d64e..a0a3be877ab 100644 --- a/docs/book/src/forc/plugins/forc_client/index.md +++ b/docs/book/src/forc/plugins/forc_client/index.md @@ -1,26 +1,76 @@ # `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: -If you don't have an initialized wallet or any account for your wallet you won't be able to sign transactions. + 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. -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. +The easiest and recommended way to interact with deployed networks such as our testnets is 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. -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. +## Option 1: Sign transactions via forc-client using your local forc-wallet vault -## Signing transactions using `forc-wallet` CLI +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. -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. +Example: -By default `fuel-core` runs without UTXO validation, this allows you to send invalid inputs to emulate different conditions. +```console +> forc deploy -If you want to run `fuel-core` with UTXO validation, you can pass `--utxo-validation` to `fuel-core run`. + Building /Users/test/test-projects/test-contract + Finished release [optimized + fuel] target(s) in 11.39s -To install `forc-wallet` please refer to `forc-wallet`'s [GitHub repo](https://github.com/FuelLabs/forc-wallet#forc-wallet). +Please provide the password of your encrypted wallet vault at "/Users/ceylinbormali/.fuel/wallets/.wallet": + Deploying contract: impl-contract -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. +--------------------------------------------------------------------------- +Account 0: fuel12pls73y9hnqdqthvduy2x44x48zt8s50pkerf32kq26f2afeqdwq6rj9ar + +Asset ID : f8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07 +Amount : 2197245 +--------------------------------------------------------------------------- + +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 +106,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 +118,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/). @@ -91,3 +141,40 @@ 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 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 [SRC-14](https://github.com/FuelLabs/sway-standards/blob/master/SRCs/src-14.md) 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 an [SRC-14](https://github.com/FuelLabs/sway-standards/blob/master/SRCs/src-14.md) compliant 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. diff --git a/forc-pkg/src/manifest/mod.rs b/forc-pkg/src/manifest/mod.rs index 3f7bc21b1bd..28729aff757 100644 --- a/forc-pkg/src/manifest/mod.rs +++ b/forc-pkg/src/manifest/mod.rs @@ -186,6 +186,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)] @@ -273,6 +274,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. @@ -650,6 +662,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 cccead1051b..c2fcea53e2b 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..7046dfec8ee 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" } @@ -26,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" @@ -38,6 +40,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/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 e0270fa5245..bb610285565 100644 --- a/forc-plugins/forc-client/src/op/deploy.rs +++ b/forc-plugins/forc-client/src/op/deploy.rs @@ -3,28 +3,37 @@ use crate::{ util::{ gas::get_estimated_max_fee, node_url::get_node_url, - pkg::built_pkgs, - tx::{TransactionBuilderExt, WalletSelectionMode, TX_SUBMIT_TIMEOUT_MS}, + 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, + update_proxy_contract_target, 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}, + str::FromStr, }; use sway_core::language::parsed::TreeType; use sway_core::BuildTarget; @@ -113,6 +122,54 @@ fn validate_and_parse_salts<'a>( Ok(contract_salt_map) } +async fn deploy_new_proxy( + pkg: &BuiltPackage, + owner_account_address: &mut Bech32Address, + impl_contract: &DeployedContract, + build_opts: &BuildOpts, + command: &cmd::Deploy, + salt: Salt, + wallet_mode: &WalletSelectionMode, +) -> 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. + 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); + 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, + wallet_mode, + ) + .await?; + Ok(proxy) +} /// Builds and deploys contract(s). If the given path corresponds to a workspace, all deployable members /// will be built and deployed. /// @@ -124,7 +181,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 { @@ -136,7 +193,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 { @@ -156,7 +213,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)) @@ -176,6 +232,14 @@ 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 .descriptor @@ -197,12 +261,79 @@ 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?; - contract_ids.push(contract_id); + let node_url = get_node_url(&command.node, &pkg.descriptor.manifest_file.network)?; + println!( + " {} contract: {}", + "Deploying".bold().green(), + &pkg.descriptor.name + ); + 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 { + 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. + 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( + &pkg, + &mut owner_account_address, + &deployed_contract, + &build_opts, + &command, + salt, + &wallet_mode, + ) + .await?; + + // Update manifest file such that the proxy address field points to the new proxy contract. + update_proxy_address_in_manifest( + &format!("0x{}", deployed_proxy_contract.id), + &pkg.descriptor.manifest_file, + )?; + } + } + } + + deployed_contracts.push(deployed_contract); } } - Ok(contract_ids) + Ok(deployed_contracts) } /// Deploy a single pkg given deploy command and the manifest file @@ -211,10 +342,10 @@ 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())?; - let bytecode = &compiled.bytecode.bytes; let mut storage_slots = @@ -232,12 +363,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 f7b6384cb74..4bb0787ea0a 100644 --- a/forc-plugins/forc-client/src/util/pkg.rs +++ b/forc-plugins/forc-client/src/util/pkg.rs @@ -1,8 +1,142 @@ use anyhow::Result; use forc_pkg::manifest::GenericManifestFile; use forc_pkg::{self as pkg, manifest::ManifestFile, BuildOpts, BuildPlan}; -use pkg::{build_with_options, BuiltPackage}; +use forc_util::user_forc_directory; +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}; + +/// 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/", tag = "v0.5.0" } +"#; + +/// 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, + ); +}} +"# + ) +} + +/// 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_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)?; + 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, + impl_contract_id: &str, + pkg_name: &str, +) -> Result { + // Create the proxy contract folder. + let proxy_contract_dir = user_forc_directory() + .join(PROXY_CONTRACT_FOLDER_NAME) + .join(pkg_name); + std::fs::create_dir_all(&proxy_contract_dir)?; + + // Create the Forc.toml + let mut f = std::fs::OpenOptions::new() + .read(true) + .write(true) + .create(true) + .truncate(true) + .open(proxy_contract_dir.join(MANIFEST_FILE_NAME))?; + write!(f, "{}", PROXY_CONTRACT_FORC_TOML)?; + + // Create the src folder + std::fs::create_dir_all(proxy_contract_dir.join(SRC_DIR))?; + + // Create main.sw + let mut f = std::fs::OpenOptions::new() + .read(true) + .write(true) + .create(true) + .truncate(true) + .open(proxy_contract_dir.join(SRC_DIR).join(MAIN_ENTRY))?; + + let contract_str = generate_proxy_contract_src(addr, impl_contract_id); + write!(f, "{}", contract_str)?; + Ok(proxy_contract_dir) +} pub(crate) fn built_pkgs(path: &Path, build_opts: &BuildOpts) -> Result>> { let manifest_file = ManifestFile::from_dir(path)?; @@ -32,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") + } +} 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 84441b79933..33b65c6f246 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; @@ -6,7 +6,12 @@ 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, @@ -34,7 +39,8 @@ pub const DEFAULT_PRIVATE_KEY: &str = #[derive(PartialEq, Eq)] pub enum WalletSelectionMode { - ForcWallet, + /// Holds the password of forc-wallet instance. + ForcWallet(String), Manual, } @@ -66,6 +72,98 @@ 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 first_user_account(wallet_path: &Path, password: &str) -> Result { + 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(()) +} + +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 +179,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; @@ -95,9 +289,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; } @@ -168,131 +362,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()?) }; From 96d9dd1c5d4d2697a9bd3fc8aa25335e993646ca Mon Sep 17 00:00:00 2001 From: Kaya Gokalp Date: Mon, 10 Jun 2024 13:28:18 -0700 Subject: [PATCH 02/37] update docs link --- docs/book/src/forc/plugins/forc_client/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/book/src/forc/plugins/forc_client/index.md b/docs/book/src/forc/plugins/forc_client/index.md index a0a3be877ab..0eb8de48a54 100644 --- a/docs/book/src/forc/plugins/forc_client/index.md +++ b/docs/book/src/forc/plugins/forc_client/index.md @@ -158,11 +158,11 @@ implicit-std = false 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 [SRC-14](https://github.com/FuelLabs/sway-standards/blob/master/SRCs/src-14.md) 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. +If there is no `address` field present under the proxy table, like the example above, forc automatically creates a proxy contract based on [SRC-14](https://github.com/FuelLabs/sway-standards/blob/master/docs/src/src-14-simple-upgradeable-proxies.md) 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 an [SRC-14](https://github.com/FuelLabs/sway-standards/blob/master/SRCs/src-14.md) compliant proxy contract rather than deploying a new one, simply add its `address` in the `address` field, like the following example: +If you want to update the target of an [SRC-14](https://github.com/FuelLabs/sway-standards/blob/master/docs/src/src-14-simple-upgradeable-proxies.md) compliant proxy contract rather than deploying a new one, simply add its `address` in the `address` field, like the following example: ```TOML [project] From 985162ea449abb04391b85b55fdac7d84b314fc1 Mon Sep 17 00:00:00 2001 From: Kaya Gokalp Date: Thu, 27 Jun 2024 14:12:02 -0500 Subject: [PATCH 03/37] update lock after merge --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 6ceeb7cfdf1..466cbb82720 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1994,8 +1994,8 @@ dependencies = [ "anyhow", "async-trait", "chrono", - "colored", "clap 4.5.7", + "colored", "devault", "forc", "forc-pkg", From 22169f4b457437a921024ed1a069f6c74a4b4fd1 Mon Sep 17 00:00:00 2001 From: Kaya Gokalp Date: Fri, 28 Jun 2024 10:36:46 -0500 Subject: [PATCH 04/37] refactor: remove duplicate deployment logic --- forc-plugins/forc-client/src/op/deploy.rs | 63 ++++++++++------------- forc-plugins/forc-client/src/util/gas.rs | 63 ++--------------------- forc-plugins/forc-client/src/util/tx.rs | 1 + 3 files changed, 31 insertions(+), 96 deletions(-) diff --git a/forc-plugins/forc-client/src/op/deploy.rs b/forc-plugins/forc-client/src/op/deploy.rs index bb610285565..2afcfe33eb7 100644 --- a/forc-plugins/forc-client/src/op/deploy.rs +++ b/forc-plugins/forc-client/src/op/deploy.rs @@ -1,18 +1,16 @@ use crate::{ cmd, util::{ - gas::get_estimated_max_fee, node_url::get_node_url, 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, - update_proxy_contract_target, TransactionBuilderExt, WalletSelectionMode, - TX_SUBMIT_TIMEOUT_MS, + update_proxy_contract_target, WalletSelectionMode, TX_SUBMIT_TIMEOUT_MS, }, }, }; -use anyhow::{bail, Context, Result}; +use anyhow::{anyhow, bail, Context, Result}; use colored::Colorize; use forc_pkg::manifest::GenericManifestFile; use forc_pkg::{self as pkg, PackageManifestFile}; @@ -22,9 +20,10 @@ 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::types::{transaction::TxPolicies, transaction_builders::CreateTransactionBuilder}; +use fuels_accounts::{provider::Provider, wallet::WalletUnlocked, Account}; use fuels_core::types::bech32::Bech32Address; use futures::FutureExt; use pkg::{manifest::build_profile::ExperimentalFlags, BuildOpts, BuildProfile, BuiltPackage}; @@ -283,6 +282,7 @@ pub async fn deploy(command: cmd::Deploy) -> Result> { // Create a contract instance for the proxy contract using default proxy contract abi and // specified address. + println!(" {} proxy contract", "Updating".bold().green()); 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. @@ -357,44 +357,35 @@ pub async fn deploy_pkg( compiled.storage_slots.clone() }; storage_slots.sort(); - let contract = Contract::from(bytecode.clone()); let root = contract.root(); let state_root = Contract::initial_state_root(storage_slots.iter()); let contract_id = contract.id(&salt, &root, &state_root); let provider = Provider::connect(node_url.clone()).await?; + let tx_policies = TxPolicies::default(); - // 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 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!("failed to select a signer for the transaction"))?; + let wallet = WalletUnlocked::new_from_private_key(signing_key, Some(provider.clone())); - 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/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 b41ca5da7ea..176a65a95b6 100644 --- a/forc-plugins/forc-client/src/util/tx.rs +++ b/forc-plugins/forc-client/src/util/tx.rs @@ -337,6 +337,7 @@ impl TransactionBuilderExt for Tran signature_witness_index: u16, ) -> Result<&mut Self> { let asset_id = *provider.base_asset_id(); + dbg!(asset_id.clone()); let wallet = Wallet::from_address(Bech32Address::from(address), Some(provider)); let amount = 1_000_000; From 3980804b0db63a6f1671e7aada372a1600fba7a8 Mon Sep 17 00:00:00 2001 From: Kaya Gokalp Date: Fri, 28 Jun 2024 10:44:41 -0500 Subject: [PATCH 05/37] feat: add a prompt before asking for signature --- forc-plugins/forc-client/src/op/deploy.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/forc-plugins/forc-client/src/op/deploy.rs b/forc-plugins/forc-client/src/op/deploy.rs index 2afcfe33eb7..932aa03e5f0 100644 --- a/forc-plugins/forc-client/src/op/deploy.rs +++ b/forc-plugins/forc-client/src/op/deploy.rs @@ -231,6 +231,7 @@ pub async fn deploy(command: cmd::Deploy) -> Result> { None }; + println!(" {} deployment", "Starting".bold().green()); let wallet_mode = if command.default_signer || command.signing_key.is_some() { WalletSelectionMode::Manual } else { @@ -357,7 +358,7 @@ pub async fn deploy_pkg( compiled.storage_slots.clone() }; storage_slots.sort(); - let contract = Contract::from(bytecode.clone()); + let contract = Contract::from(bytecode.as_slice()); let root = contract.root(); let state_root = Contract::initial_state_root(storage_slots.iter()); let contract_id = contract.id(&salt, &root, &state_root); From 0270010fd296bbea3d983d47c6b2a882052af8ff Mon Sep 17 00:00:00 2001 From: Kaya Gokalp Date: Fri, 28 Jun 2024 10:46:07 -0500 Subject: [PATCH 06/37] remove debug prompts --- forc-plugins/forc-client/src/util/tx.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/forc-plugins/forc-client/src/util/tx.rs b/forc-plugins/forc-client/src/util/tx.rs index 176a65a95b6..b41ca5da7ea 100644 --- a/forc-plugins/forc-client/src/util/tx.rs +++ b/forc-plugins/forc-client/src/util/tx.rs @@ -337,7 +337,6 @@ impl TransactionBuilderExt for Tran signature_witness_index: u16, ) -> Result<&mut Self> { let asset_id = *provider.base_asset_id(); - dbg!(asset_id.clone()); let wallet = Wallet::from_address(Bech32Address::from(address), Some(provider)); let amount = 1_000_000; From aa0fd68fba3817f2da0848aa3bb97b4d36aef445 Mon Sep 17 00:00:00 2001 From: Kaya Gokalp Date: Fri, 28 Jun 2024 11:03:21 -0500 Subject: [PATCH 07/37] feat: add an end prompt --- forc-plugins/forc-client/src/util/tx.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/forc-plugins/forc-client/src/util/tx.rs b/forc-plugins/forc-client/src/util/tx.rs index b41ca5da7ea..fe89f129992 100644 --- a/forc-plugins/forc-client/src/util/tx.rs +++ b/forc-plugins/forc-client/src/util/tx.rs @@ -2,6 +2,7 @@ use std::{collections::BTreeMap, io::Write, path::Path, str::FromStr}; use anyhow::{Error, Result}; use async_trait::async_trait; +use colored::Colorize; use forc_tracing::println_warning; use fuel_crypto::{Message, PublicKey, SecretKey, Signature}; @@ -272,6 +273,11 @@ pub(crate) async fn update_proxy_contract_target( .set_proxy_target(new_target) .call() .await?; + println!( + " 0x{} proxy contract's target to {}", + "Updated".green(), + new_target + ); Ok(()) } From 263c4f939d2336bda702b6236a32f7494fb7a83a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kaya=20G=C3=B6kalp?= Date: Sun, 30 Jun 2024 18:47:07 -0700 Subject: [PATCH 08/37] Apply suggestions-1 Co-authored-by: Joshua Batty Co-authored-by: Sophie Dankel <47993817+sdankel@users.noreply.github.com> --- docs/book/src/forc/plugins/forc_client/index.md | 12 ++++++------ forc-plugins/forc-client/src/op/deploy.rs | 10 +++++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/book/src/forc/plugins/forc_client/index.md b/docs/book/src/forc/plugins/forc_client/index.md index 1d83c769cc7..cb11f55d2c3 100644 --- a/docs/book/src/forc/plugins/forc_client/index.md +++ b/docs/book/src/forc/plugins/forc_client/index.md @@ -4,15 +4,15 @@ Forc plugin for interacting with a Fuel node. Since transactions are going to re 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. + 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. + 3. Use `forc-wallet` to manually sign transactions, and copy the signed transaction back to `forc-client`. -The easiest and recommended way to interact with deployed networks such as our testnets is 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. +The easiest and recommended way to interact with deployed networks such as our testnets is 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. +If you've used `forc-wallet` before, you'll already have a secure, password-protected vault holding your private key written to your file-system. `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: @@ -160,7 +160,7 @@ 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 [SRC-14](https://github.com/FuelLabs/sway-standards/blob/master/docs/src/src-14-simple-upgradeable-proxies.md) 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. +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 do anything manually aside from signing the deployment transactions for the proxy contract. After deploying the proxy contract, the its address is added into the `address` field of the proxy table. If you want to update the target of an [SRC-14](https://github.com/FuelLabs/sway-standards/blob/master/docs/src/src-14-simple-upgradeable-proxies.md) compliant proxy contract rather than deploying a new one, simply add its `address` in the `address` field, like the following example: @@ -177,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. diff --git a/forc-plugins/forc-client/src/op/deploy.rs b/forc-plugins/forc-client/src/op/deploy.rs index 932aa03e5f0..80717065c54 100644 --- a/forc-plugins/forc-client/src/op/deploy.rs +++ b/forc-plugins/forc-client/src/op/deploy.rs @@ -130,7 +130,7 @@ async fn deploy_new_proxy( salt: Salt, wallet_mode: &WalletSelectionMode, ) -> Result { - println!(" {} proxy contract", "Creating".bold().green()); + info!(" {} proxy contract", "Creating".bold().green()); let user_addr = if *owner_account_address != Bech32Address::default() { anyhow::Ok(owner_account_address.clone()) } else { @@ -158,7 +158,7 @@ async fn deploy_new_proxy( 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()); + info!(" {} proxy contract", "Deploying".bold().green()); let proxy = deploy_pkg( command, &pkg.descriptor.manifest_file, @@ -262,8 +262,8 @@ pub async fn deploy(command: cmd::Deploy) -> Result> { } }; let node_url = get_node_url(&command.node, &pkg.descriptor.manifest_file.network)?; - println!( - " {} contract: {}", + info!( + " {} contract: {}", "Deploying".bold().green(), &pkg.descriptor.name ); @@ -283,7 +283,7 @@ pub async fn deploy(command: cmd::Deploy) -> Result> { // Create a contract instance for the proxy contract using default proxy contract abi and // specified address. - println!(" {} proxy contract", "Updating".bold().green()); + info!(" {} proxy contract", "Updating".bold().green()); 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. From be697788b4ac5f2e36df9eb92045fb49866ec366 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kaya=20G=C3=B6kalp?= Date: Sun, 30 Jun 2024 18:49:44 -0700 Subject: [PATCH 09/37] Apply suggestions-2 Co-authored-by: Joshua Batty Co-authored-by: Sophie Dankel <47993817+sdankel@users.noreply.github.com> --- docs/book/src/forc/plugins/forc_client/index.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/book/src/forc/plugins/forc_client/index.md b/docs/book/src/forc/plugins/forc_client/index.md index cb11f55d2c3..d2738091961 100644 --- a/docs/book/src/forc/plugins/forc_client/index.md +++ b/docs/book/src/forc/plugins/forc_client/index.md @@ -43,11 +43,11 @@ 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. +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. +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: @@ -158,7 +158,7 @@ implicit-std = false 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 [SRC-14](https://github.com/FuelLabs/sway-standards/blob/master/docs/src/src-14-simple-upgradeable-proxies.md) 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. +If there is no `address` field present under the proxy table, like the example above, `forc` will automatically create a proxy contract based on the [SRC-14](https://github.com/FuelLabs/sway-standards/blob/master/docs/src/src-14-simple-upgradeable-proxies.md) implementation from [sway-standards](https://github.com/FuelLabs/sway-standards). After generating and deploying 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 do anything manually aside from signing the deployment transactions for the proxy contract. After deploying the proxy contract, the its address is added into the `address` field of the proxy table. From 1a673cef706c365be0c4cfc69f3784ab5008e3a9 Mon Sep 17 00:00:00 2001 From: Kaya Gokalp Date: Mon, 1 Jul 2024 13:45:02 -0500 Subject: [PATCH 10/37] rework deployed text --- forc-plugins/forc-client/src/op/deploy.rs | 15 +++++++-------- forc-plugins/forc-client/src/util/tx.rs | 7 ++++--- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/forc-plugins/forc-client/src/op/deploy.rs b/forc-plugins/forc-client/src/op/deploy.rs index 80717065c54..66ea740f963 100644 --- a/forc-plugins/forc-client/src/op/deploy.rs +++ b/forc-plugins/forc-client/src/op/deploy.rs @@ -231,7 +231,7 @@ pub async fn deploy(command: cmd::Deploy) -> Result> { None }; - println!(" {} deployment", "Starting".bold().green()); + info!(" {} deployment", "Starting".bold().green()); let wallet_mode = if command.default_signer || command.signing_key.is_some() { WalletSelectionMode::Manual } else { @@ -263,7 +263,7 @@ pub async fn deploy(command: cmd::Deploy) -> Result> { }; let node_url = get_node_url(&command.node, &pkg.descriptor.manifest_file.network)?; info!( - " {} contract: {}", + " {} contract: {}", "Deploying".bold().green(), &pkg.descriptor.name ); @@ -283,7 +283,7 @@ pub async fn deploy(command: cmd::Deploy) -> Result> { // Create a contract instance for the proxy contract using default proxy contract abi and // specified address. - info!(" {} proxy contract", "Updating".bold().green()); + info!(" {} proxy contract", "Updating".bold().green()); 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. @@ -398,11 +398,10 @@ pub async fn deploy_pkg( } TransactionStatus::Success { block_height, .. } => { let pkg_name = manifest.project_name(); - info!("\n\nContract {pkg_name} Deployed!"); - - info!("\nNetwork: {node_url}"); - info!("Contract ID: 0x{contract_id}"); - info!("Deployed in block {}", &block_height); + info!("\n\n {} {pkg_name}!", "Deployed".bold().green()); + info!(" {}: {node_url}", "Network".bold().green()); + info!(" {}: 0x{contract_id}", "Contract ID".bold().green()); + info!(" {}: {}\n", "Block".bold().green(), &block_height); // Create a deployment artifact. let deployment_size = bytecode.len(); diff --git a/forc-plugins/forc-client/src/util/tx.rs b/forc-plugins/forc-client/src/util/tx.rs index fe89f129992..c754c29ed3b 100644 --- a/forc-plugins/forc-client/src/util/tx.rs +++ b/forc-plugins/forc-client/src/util/tx.rs @@ -28,6 +28,7 @@ use forc_wallet::{ new::{new_wallet_cli, New}, utils::default_wallet_path, }; +use tracing::info; use crate::util::target::Target; @@ -273,9 +274,9 @@ pub(crate) async fn update_proxy_contract_target( .set_proxy_target(new_target) .call() .await?; - println!( - " 0x{} proxy contract's target to {}", - "Updated".green(), + info!( + " {} proxy contract's target to 0x{}", + "Updated".bold().green(), new_target ); Ok(()) From 7d040503502fc7d63bf63d11fdb6b195ff899adb Mon Sep 17 00:00:00 2001 From: Kaya Gokalp Date: Mon, 1 Jul 2024 22:47:13 -0500 Subject: [PATCH 11/37] test: add simple deploy integration test --- Cargo.lock | 5 + forc-plugins/forc-client/Cargo.toml | 7 ++ forc-plugins/forc-client/src/op/deploy.rs | 4 +- forc-plugins/forc-client/src/op/mod.rs | 2 +- forc-plugins/forc-client/tests/deploy.rs | 135 ++++++++++++++++++++++ 5 files changed, 150 insertions(+), 3 deletions(-) create mode 100644 forc-plugins/forc-client/tests/deploy.rs diff --git a/Cargo.lock b/Cargo.lock index 466cbb82720..0af793142dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1996,7 +1996,9 @@ dependencies = [ "chrono", "clap 4.5.7", "colored", + "dap", "devault", + "escargot", "forc", "forc-pkg", "forc-tracing 0.61.0", @@ -2014,13 +2016,16 @@ dependencies = [ "fuels-core", "futures", "hex", + "portpicker", "rand", + "rexpect", "rpassword", "serde", "serde_json", "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 28aeaefef19..18ac85f942b 100644 --- a/forc-plugins/forc-client/Cargo.toml +++ b/forc-plugins/forc-client/Cargo.toml @@ -43,6 +43,13 @@ tokio = { version = "1.8", features = ["macros", "rt-multi-thread", "process"] } toml_edit = "0.21.1" tracing = "0.1" +[dev-dependencies] +dap = { version = "0.4.1-alpha1", features = ["client"] } +escargot = "0.5.7" +portpicker = "0.1.1" +rexpect = "0.4" +tempfile = "3" + [[bin]] name = "forc-deploy" path = "src/bin/deploy.rs" diff --git a/forc-plugins/forc-client/src/op/deploy.rs b/forc-plugins/forc-client/src/op/deploy.rs index 66ea740f963..5e20ab855ea 100644 --- a/forc-plugins/forc-client/src/op/deploy.rs +++ b/forc-plugins/forc-client/src/op/deploy.rs @@ -38,7 +38,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, } @@ -436,7 +436,7 @@ pub async fn deploy_pkg( }, Err(e) => bail!("{e}"), }); - + println!("here"); // submit contract deployment with a timeout let contract_id = tokio::time::timeout( Duration::from_millis(TX_SUBMIT_TIMEOUT_MS), 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/tests/deploy.rs b/forc-plugins/forc-client/tests/deploy.rs new file mode 100644 index 00000000000..c799768be25 --- /dev/null +++ b/forc-plugins/forc-client/tests/deploy.rs @@ -0,0 +1,135 @@ +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_pkg_path() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("test") + .join("data") + .join("standalone_contract") + .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(manifest_dir: &Path) -> anyhow::Result<()> { + let toml_path = manifest_dir.join(sway_utils::constants::MANIFEST_FILE_NAME); + // Read the existing TOML file + let toml_content = fs::read_to_string(&toml_path).unwrap(); + + // Parse the TOML content + let mut doc = toml_content.parse::().unwrap(); + + // Calculate the new std path relative to the workspace root + let new_std_path = get_workspace_root().join("sway-lib-std"); + + // Update the std dependency path using InlineTable + 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)); + + // Write the modified TOML content back to the file + 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_pkg_path(); + copy_dir(&project_dir, tmp_dir.path()).unwrap(); + patch_manifest_file(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) +} From 1ac2803202ef591275506492edd96a5d1f764c1a Mon Sep 17 00:00:00 2001 From: Kaya Gokalp Date: Tue, 2 Jul 2024 14:23:57 -0700 Subject: [PATCH 12/37] simple deploy and deploy with proxy integration tests added --- forc-plugins/forc-client/src/op/deploy.rs | 3 +- forc-plugins/forc-client/tests/deploy.rs | 85 ++++++++++++++++++++--- 2 files changed, 78 insertions(+), 10 deletions(-) diff --git a/forc-plugins/forc-client/src/op/deploy.rs b/forc-plugins/forc-client/src/op/deploy.rs index 5e20ab855ea..3be2838defe 100644 --- a/forc-plugins/forc-client/src/op/deploy.rs +++ b/forc-plugins/forc-client/src/op/deploy.rs @@ -38,7 +38,7 @@ use sway_core::language::parsed::TreeType; use sway_core::BuildTarget; use tracing::info; -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Clone, PartialOrd, Ord)] pub struct DeployedContract { pub id: fuel_tx::ContractId, } @@ -322,6 +322,7 @@ pub async fn deploy(command: cmd::Deploy) -> Result> { ) .await?; + deployed_contracts.push(deployed_proxy_contract.clone()); // Update manifest file such that the proxy address field points to the new proxy contract. update_proxy_address_in_manifest( &format!("0x{}", deployed_proxy_contract.id), diff --git a/forc-plugins/forc-client/tests/deploy.rs b/forc-plugins/forc-client/tests/deploy.rs index c799768be25..41b9b58e73e 100644 --- a/forc-plugins/forc-client/tests/deploy.rs +++ b/forc-plugins/forc-client/tests/deploy.rs @@ -11,10 +11,11 @@ use forc_client::{ op::{deploy, DeployedContract}, NodeTarget, }; +use forc_pkg::manifest::Proxy; use fuel_tx::{ContractId, Salt}; use portpicker::Port; use tempfile::tempdir; -use toml_edit::{Document, InlineTable, Item, Value}; +use toml_edit::{value, Document, InlineTable, Item, Table, Value}; fn get_workspace_root() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")) @@ -75,34 +76,48 @@ fn copy_dir(source: &Path, dest: &Path) -> anyhow::Result<()> { Ok(()) } -fn patch_manifest_file(manifest_dir: &Path) -> anyhow::Result<()> { +fn patch_manifest_file_with_path_std(manifest_dir: &Path) -> anyhow::Result<()> { let toml_path = manifest_dir.join(sway_utils::constants::MANIFEST_FILE_NAME); - // Read the existing TOML file let toml_content = fs::read_to_string(&toml_path).unwrap(); - // Parse the TOML content let mut doc = toml_content.parse::().unwrap(); - - // Calculate the new std path relative to the workspace root let new_std_path = get_workspace_root().join("sway-lib-std"); - // Update the std dependency path using InlineTable 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)); - // Write the modified TOML content back to the file fs::write(&toml_path, doc.to_string()).unwrap(); Ok(()) } +fn patch_manifest_file_with_proxy_table(manifest_dir: &Path, proxy: Proxy) -> anyhow::Result<()> { + let toml_path = manifest_dir.join(sway_utils::constants::MANIFEST_FILE_NAME); + let toml_content = fs::read_to_string(&toml_path)?; + let mut doc = toml_content.parse::()?; + + let proxy_table = doc.entry("proxy").or_insert(Item::Table(Table::new())); + let proxy_table = proxy_table.as_table_mut().unwrap(); + + proxy_table.insert("enabled", value(proxy.enabled)); + + if let Some(address) = proxy.address { + proxy_table.insert("address", value(address)); + } else { + proxy_table.remove("address"); + } + + fs::write(&toml_path, doc.to_string())?; + Ok(()) +} + #[tokio::test] async fn simple_deploy() { let (mut node, port) = run_node(); let tmp_dir = tempdir().unwrap(); let project_dir = test_pkg_path(); copy_dir(&project_dir, tmp_dir.path()).unwrap(); - patch_manifest_file(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()), @@ -133,3 +148,55 @@ async fn simple_deploy() { assert_eq!(contract_ids, expected) } + +#[tokio::test] +async fn deploy_fresh_proxy() { + let (mut node, port) = run_node(); + let tmp_dir = tempdir().unwrap(); + let project_dir = test_pkg_path(); + copy_dir(&project_dir, tmp_dir.path()).unwrap(); + patch_manifest_file_with_path_std(tmp_dir.path()).unwrap(); + let proxy = Proxy { + enabled: true, + address: None, + }; + patch_manifest_file_with_proxy_table(tmp_dir.path(), proxy).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 mut contract_ids = deploy(cmd).await.unwrap(); + contract_ids.sort(); + node.kill().unwrap(); + let impl_contract = DeployedContract { + id: ContractId::from_str( + "fe084b07f5fd44f837d1fbf043671f0b27caef87503106b799b6a8b1ad5b30bd", + ) + .unwrap(), + }; + let proxy_contract = DeployedContract { + id: ContractId::from_str( + "428896412bda8530282a7b8fca5d20b2a73f30037612ca3a31750cf3bf0e976a", + ) + .unwrap(), + }; + let mut expected = vec![proxy_contract, impl_contract]; + expected.sort(); + + assert_eq!(contract_ids, expected) +} From ad73213618e38462be84f45399792508a63dcbe4 Mon Sep 17 00:00:00 2001 From: Kaya Gokalp Date: Tue, 2 Jul 2024 18:38:36 -0700 Subject: [PATCH 13/37] add routing integration test --- forc-plugins/forc-client/src/constants.rs | 5 ++ forc-plugins/forc-client/src/lib.rs | 2 +- forc-plugins/forc-client/src/op/deploy.rs | 3 +- forc-plugins/forc-client/src/op/run/mod.rs | 6 +- forc-plugins/forc-client/src/util/tx.rs | 9 +-- .../test/data/standalone_contract/Forc.toml | 2 +- forc-plugins/forc-client/tests/deploy.rs | 69 +++++++++++++++++-- 7 files changed, 77 insertions(+), 19 deletions(-) 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/lib.rs b/forc-plugins/forc-client/src/lib.rs index 0c8cb335f5f..815e07d9a67 100644 --- a/forc-plugins/forc-client/src/lib.rs +++ b/forc-plugins/forc-client/src/lib.rs @@ -1,5 +1,5 @@ pub mod cmd; -mod constants; +pub mod constants; pub mod op; mod util; diff --git a/forc-plugins/forc-client/src/op/deploy.rs b/forc-plugins/forc-client/src/op/deploy.rs index 3be2838defe..6a8ae28be27 100644 --- a/forc-plugins/forc-client/src/op/deploy.rs +++ b/forc-plugins/forc-client/src/op/deploy.rs @@ -1,12 +1,13 @@ use crate::{ cmd, + constants::TX_SUBMIT_TIMEOUT_MS, util::{ node_url::get_node_url, 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, - update_proxy_contract_target, WalletSelectionMode, TX_SUBMIT_TIMEOUT_MS, + update_proxy_contract_target, WalletSelectionMode, }, }, }; diff --git a/forc-plugins/forc-client/src/op/run/mod.rs b/forc-plugins/forc-client/src/op/run/mod.rs index b7e9356bce4..317afd80687 100644 --- a/forc-plugins/forc-client/src/op/run/mod.rs +++ b/forc-plugins/forc-client/src/op/run/mod.rs @@ -1,14 +1,12 @@ 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::{ - prompt_forc_wallet_password, TransactionBuilderExt, WalletSelectionMode, - TX_SUBMIT_TIMEOUT_MS, - }, + tx::{prompt_forc_wallet_password, TransactionBuilderExt, WalletSelectionMode}, }, }; use anyhow::{anyhow, bail, Context, Result}; diff --git a/forc-plugins/forc-client/src/util/tx.rs b/forc-plugins/forc-client/src/util/tx.rs index c754c29ed3b..295eb574234 100644 --- a/forc-plugins/forc-client/src/util/tx.rs +++ b/forc-plugins/forc-client/src/util/tx.rs @@ -30,14 +30,7 @@ use forc_wallet::{ }; use tracing::info; -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 { diff --git a/forc-plugins/forc-client/test/data/standalone_contract/Forc.toml b/forc-plugins/forc-client/test/data/standalone_contract/Forc.toml index de97c7bf31f..2324ff98894 100644 --- a/forc-plugins/forc-client/test/data/standalone_contract/Forc.toml +++ b/forc-plugins/forc-client/test/data/standalone_contract/Forc.toml @@ -6,4 +6,4 @@ license = "Apache-2.0" name = "standalone_contract" [dependencies] -std = { path = "../../../../../../../../sway-lib-std/" } +std = { path = "../../../../../sway-lib-std/" } diff --git a/forc-plugins/forc-client/tests/deploy.rs b/forc-plugins/forc-client/tests/deploy.rs index 41b9b58e73e..9cf149300dc 100644 --- a/forc-plugins/forc-client/tests/deploy.rs +++ b/forc-plugins/forc-client/tests/deploy.rs @@ -12,7 +12,10 @@ use forc_client::{ NodeTarget, }; use forc_pkg::manifest::Proxy; +use fuel_crypto::SecretKey; use fuel_tx::{ContractId, Salt}; +use fuels::macros::abigen; +use fuels_accounts::{provider::Provider, wallet::WalletUnlocked}; use portpicker::Port; use tempfile::tempdir; use toml_edit::{value, Document, InlineTable, Item, Table, Value}; @@ -34,11 +37,10 @@ fn chain_config_path() -> PathBuf { .join("local-testnode") } -fn test_pkg_path() -> PathBuf { +fn test_data_path() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("test") .join("data") - .join("standalone_contract") .canonicalize() .unwrap() } @@ -115,7 +117,7 @@ fn patch_manifest_file_with_proxy_table(manifest_dir: &Path, proxy: Proxy) -> an async fn simple_deploy() { let (mut node, port) = run_node(); let tmp_dir = tempdir().unwrap(); - let project_dir = test_pkg_path(); + 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(); @@ -153,7 +155,7 @@ async fn simple_deploy() { async fn deploy_fresh_proxy() { let (mut node, port) = run_node(); let tmp_dir = tempdir().unwrap(); - let project_dir = test_pkg_path(); + 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 proxy = Proxy { @@ -200,3 +202,62 @@ async fn deploy_fresh_proxy() { assert_eq!(contract_ids, expected) } + +#[tokio::test] +async fn proxy_contract_re_routes_call() { + 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 proxy = Proxy { + enabled: true, + address: None, + }; + patch_manifest_file_with_proxy_table(tmp_dir.path(), proxy).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.clone()), + 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(); + // At this point we deployed a contract with proxy. Proxy address is the + // first contract id returned. + let proxy_contract = contract_ids[0].id; + let impl_contract_id = contract_ids[1].id; + // Make a contract call into proxy contract, and check if the initial + // contract returns a true. + let provider = Provider::connect(node_url).await.unwrap(); + let secret_key = SecretKey::from_str(forc_client::constants::DEFAULT_PRIVATE_KEY).unwrap(); + let wallet_unlocked = WalletUnlocked::new_from_private_key(secret_key, Some(provider)); + + abigen!(Contract( + name = "ImplementationContract", + abi = "forc-plugins/forc-client/test/data/standalone_contract/out/debug/standalone_contract-abi.json" + )); + + let impl_contract_a = ImplementationContract::new(proxy_contract, wallet_unlocked); + let res = impl_contract_a + .methods() + .test_function() + .with_contract_ids(&[impl_contract_id.into()]) + .call() + .await + .unwrap(); + node.kill().unwrap(); + assert_eq!(res.value, true) +} From 3150f6e0f14755550d41a65f105a55732c726737 Mon Sep 17 00:00:00 2001 From: Kaya Gokalp Date: Wed, 3 Jul 2024 13:29:15 -0700 Subject: [PATCH 14/37] remove snapshot while running client --- forc-plugins/forc-client/tests/deploy.rs | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/forc-plugins/forc-client/tests/deploy.rs b/forc-plugins/forc-client/tests/deploy.rs index 9cf149300dc..867d171965d 100644 --- a/forc-plugins/forc-client/tests/deploy.rs +++ b/forc-plugins/forc-client/tests/deploy.rs @@ -28,15 +28,6 @@ fn get_workspace_root() -> PathBuf { .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") @@ -47,8 +38,6 @@ fn test_data_path() -> PathBuf { 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") @@ -56,8 +45,6 @@ fn run_node() -> (Child, Port) { .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) From 2b3180ac2ce3062dc248e669715db87312a8c476 Mon Sep 17 00:00:00 2001 From: Kaya Gokalp Date: Wed, 3 Jul 2024 14:58:45 -0700 Subject: [PATCH 15/37] proxy and impl contract packed together at result --- forc-plugins/forc-client/src/op/deploy.rs | 36 ++++++++++++++--------- forc-plugins/forc-client/tests/deploy.rs | 26 ++++++++-------- 2 files changed, 34 insertions(+), 28 deletions(-) diff --git a/forc-plugins/forc-client/src/op/deploy.rs b/forc-plugins/forc-client/src/op/deploy.rs index 6a8ae28be27..9817c84e10d 100644 --- a/forc-plugins/forc-client/src/op/deploy.rs +++ b/forc-plugins/forc-client/src/op/deploy.rs @@ -42,6 +42,7 @@ use tracing::info; #[derive(Debug, PartialEq, Eq, Clone, PartialOrd, Ord)] pub struct DeployedContract { pub id: fuel_tx::ContractId, + pub proxy: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -125,12 +126,12 @@ fn validate_and_parse_salts<'a>( async fn deploy_new_proxy( pkg: &BuiltPackage, owner_account_address: &mut Bech32Address, - impl_contract: &DeployedContract, + impl_contract: &fuel_tx::ContractId, build_opts: &BuildOpts, command: &cmd::Deploy, salt: Salt, wallet_mode: &WalletSelectionMode, -) -> Result { +) -> Result { info!(" {} proxy contract", "Creating".bold().green()); let user_addr = if *owner_account_address != Bech32Address::default() { anyhow::Ok(owner_account_address.clone()) @@ -157,7 +158,7 @@ async fn deploy_new_proxy( 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 contract_addr = format!("0x{}", impl_contract); let proxy_contract = build_proxy_contract(&user_addr, &contract_addr, pkg_name, build_opts)?; info!(" {} proxy contract", "Deploying".bold().green()); let proxy = deploy_pkg( @@ -268,7 +269,7 @@ pub async fn deploy(command: cmd::Deploy) -> Result> { "Deploying".bold().green(), &pkg.descriptor.name ); - let deployed_contract = deploy_pkg( + let deployed_contract_id = deploy_pkg( &command, &pkg.descriptor.manifest_file, &pkg, @@ -277,7 +278,7 @@ pub async fn deploy(command: cmd::Deploy) -> Result> { ) .await?; let proxy = &pkg.descriptor.manifest_file.proxy(); - if let Some(proxy) = proxy { + let proxy_id = if let Some(proxy) = proxy { if proxy.enabled { if let Some(proxy_addr) = &proxy.address { // Make a call into the contract to update impl contract address to 'deployed_contract'. @@ -307,15 +308,16 @@ pub async fn deploy(command: cmd::Deploy) -> Result> { provider, signing_key, proxy_contract, - deployed_contract.id, + deployed_contract_id, ) .await?; + Some(proxy_contract) } else { // Deploy a new proxy contract. let deployed_proxy_contract = deploy_new_proxy( &pkg, &mut owner_account_address, - &deployed_contract, + &deployed_contract_id, &build_opts, &command, salt, @@ -323,16 +325,23 @@ pub async fn deploy(command: cmd::Deploy) -> Result> { ) .await?; - deployed_contracts.push(deployed_proxy_contract.clone()); // Update manifest file such that the proxy address field points to the new proxy contract. update_proxy_address_in_manifest( - &format!("0x{}", deployed_proxy_contract.id), + &format!("0x{}", deployed_proxy_contract), &pkg.descriptor.manifest_file, )?; + Some(deployed_proxy_contract) } + } else { + None } - } - + } else { + None + }; + let deployed_contract = DeployedContract { + id: deployed_contract_id, + proxy: proxy_id, + }; deployed_contracts.push(deployed_contract); } } @@ -346,7 +355,7 @@ pub async fn deploy_pkg( compiled: &BuiltPackage, salt: Salt, wallet_mode: &WalletSelectionMode, -) -> Result { +) -> Result { let node_url = get_node_url(&command.node, &manifest.network)?; let client = FuelClient::new(node_url.clone())?; let bytecode = &compiled.bytecode.bytes; @@ -438,7 +447,6 @@ pub async fn deploy_pkg( }, Err(e) => bail!("{e}"), }); - println!("here"); // submit contract deployment with a timeout let contract_id = tokio::time::timeout( Duration::from_millis(TX_SUBMIT_TIMEOUT_MS), @@ -451,7 +459,7 @@ pub async fn deploy_pkg( &contract_id ) })??; - Ok(DeployedContract { id: contract_id }) + Ok(contract_id) } fn build_opts_from_cmd(cmd: &cmd::Deploy) -> pkg::BuildOpts { diff --git a/forc-plugins/forc-client/tests/deploy.rs b/forc-plugins/forc-client/tests/deploy.rs index 867d171965d..57b09264346 100644 --- a/forc-plugins/forc-client/tests/deploy.rs +++ b/forc-plugins/forc-client/tests/deploy.rs @@ -133,6 +133,7 @@ async fn simple_deploy() { "428896412bda8530282a7b8fca5d20b2a73f30037612ca3a31750cf3bf0e976a", ) .unwrap(), + proxy: None, }]; assert_eq!(contract_ids, expected) @@ -169,23 +170,21 @@ async fn deploy_fresh_proxy() { default_signer: true, ..Default::default() }; - let mut contract_ids = deploy(cmd).await.unwrap(); - contract_ids.sort(); + let contract_ids = deploy(cmd).await.unwrap(); node.kill().unwrap(); let impl_contract = DeployedContract { - id: ContractId::from_str( - "fe084b07f5fd44f837d1fbf043671f0b27caef87503106b799b6a8b1ad5b30bd", - ) - .unwrap(), - }; - let proxy_contract = DeployedContract { id: ContractId::from_str( "428896412bda8530282a7b8fca5d20b2a73f30037612ca3a31750cf3bf0e976a", ) .unwrap(), + proxy: Some( + ContractId::from_str( + "fe084b07f5fd44f837d1fbf043671f0b27caef87503106b799b6a8b1ad5b30bd", + ) + .unwrap(), + ), }; - let mut expected = vec![proxy_contract, impl_contract]; - expected.sort(); + let expected = vec![impl_contract]; assert_eq!(contract_ids, expected) } @@ -222,10 +221,9 @@ async fn proxy_contract_re_routes_call() { ..Default::default() }; let contract_ids = deploy(cmd).await.unwrap(); - // At this point we deployed a contract with proxy. Proxy address is the - // first contract id returned. - let proxy_contract = contract_ids[0].id; - let impl_contract_id = contract_ids[1].id; + // At this point we deployed a contract with proxy. + let proxy_contract = contract_ids[0].proxy.unwrap(); + let impl_contract_id = contract_ids[0].id; // Make a contract call into proxy contract, and check if the initial // contract returns a true. let provider = Provider::connect(node_url).await.unwrap(); From 47c8778f3f68e54d773a66778e43deae5822e9c1 Mon Sep 17 00:00:00 2001 From: Kaya Gokalp Date: Wed, 3 Jul 2024 15:06:23 -0700 Subject: [PATCH 16/37] test updated routing --- forc-plugins/forc-client/tests/deploy.rs | 48 ++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/forc-plugins/forc-client/tests/deploy.rs b/forc-plugins/forc-client/tests/deploy.rs index 57b09264346..e12f13a37db 100644 --- a/forc-plugins/forc-client/tests/deploy.rs +++ b/forc-plugins/forc-client/tests/deploy.rs @@ -100,6 +100,14 @@ fn patch_manifest_file_with_proxy_table(manifest_dir: &Path, proxy: Proxy) -> an Ok(()) } +fn update_main_sw(tmp_dir: &Path) -> anyhow::Result<()> { + let main_sw_path = tmp_dir.join("src").join("main.sw"); + let content = fs::read_to_string(&main_sw_path)?; + let updated_content = content.replace("true", "false"); + fs::write(main_sw_path, updated_content)?; + Ok(()) +} + #[tokio::test] async fn simple_deploy() { let (mut node, port) = run_node(); @@ -226,7 +234,7 @@ async fn proxy_contract_re_routes_call() { let impl_contract_id = contract_ids[0].id; // Make a contract call into proxy contract, and check if the initial // contract returns a true. - let provider = Provider::connect(node_url).await.unwrap(); + let provider = Provider::connect(&node_url).await.unwrap(); let secret_key = SecretKey::from_str(forc_client::constants::DEFAULT_PRIVATE_KEY).unwrap(); let wallet_unlocked = WalletUnlocked::new_from_private_key(secret_key, Some(provider)); @@ -235,7 +243,7 @@ async fn proxy_contract_re_routes_call() { abi = "forc-plugins/forc-client/test/data/standalone_contract/out/debug/standalone_contract-abi.json" )); - let impl_contract_a = ImplementationContract::new(proxy_contract, wallet_unlocked); + let impl_contract_a = ImplementationContract::new(proxy_contract, wallet_unlocked.clone()); let res = impl_contract_a .methods() .test_function() @@ -243,6 +251,40 @@ async fn proxy_contract_re_routes_call() { .call() .await .unwrap(); + assert_eq!(res.value, true); + + update_main_sw(&tmp_dir.path()).unwrap(); + let target = NodeTarget { + node_url: Some(node_url.clone()), + target: None, + testnet: false, + }; + let pkg = Pkg { + path: Some(tmp_dir.path().display().to_string()), + ..Default::default() + }; + + 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(); + // proxy contract id should be the same. + let proxy_contract_after_update = contract_ids[0].proxy.unwrap(); + assert_eq!(proxy_contract, proxy_contract_after_update); + let impl_contract_id_after_update = contract_ids[0].id; + assert!(impl_contract_id != impl_contract_id_after_update); + let impl_contract_a = ImplementationContract::new(proxy_contract_after_update, wallet_unlocked); + let res = impl_contract_a + .methods() + .test_function() + .with_contract_ids(&[impl_contract_id_after_update.into()]) + .call() + .await + .unwrap(); + assert_eq!(res.value, false); node.kill().unwrap(); - assert_eq!(res.value, true) } From 71888b4f37e75b5fc59de19477ebc4acc1dbcc73 Mon Sep 17 00:00:00 2001 From: Kaya Gokalp Date: Wed, 3 Jul 2024 15:08:04 -0700 Subject: [PATCH 17/37] clippy lints --- forc-plugins/forc-client/tests/deploy.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/forc-plugins/forc-client/tests/deploy.rs b/forc-plugins/forc-client/tests/deploy.rs index e12f13a37db..91d333f0804 100644 --- a/forc-plugins/forc-client/tests/deploy.rs +++ b/forc-plugins/forc-client/tests/deploy.rs @@ -52,7 +52,7 @@ fn run_node() -> (Child, Port) { /// Copy a directory recursively from `source` to `dest`. fn copy_dir(source: &Path, dest: &Path) -> anyhow::Result<()> { - fs::create_dir_all(&dest)?; + fs::create_dir_all(dest)?; for e in fs::read_dir(source)? { let entry = e?; let file_type = entry.file_type()?; @@ -251,9 +251,9 @@ async fn proxy_contract_re_routes_call() { .call() .await .unwrap(); - assert_eq!(res.value, true); + assert!(res.value); - update_main_sw(&tmp_dir.path()).unwrap(); + update_main_sw(tmp_dir.path()).unwrap(); let target = NodeTarget { node_url: Some(node_url.clone()), target: None, @@ -285,6 +285,6 @@ async fn proxy_contract_re_routes_call() { .call() .await .unwrap(); - assert_eq!(res.value, false); + assert!(!res.value); node.kill().unwrap(); } From c3ad31bb8510ed3593923ce5b8267f3c1cb6e436 Mon Sep 17 00:00:00 2001 From: Kaya Gokalp Date: Thu, 4 Jul 2024 11:24:39 -0700 Subject: [PATCH 18/37] move abi --- .../standalone_contract-abi.json | 26 +++++++++++++++++++ forc-plugins/forc-client/tests/deploy.rs | 2 +- 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 forc-plugins/forc-client/test/data/standalone_contract/standalone_contract-abi.json diff --git a/forc-plugins/forc-client/test/data/standalone_contract/standalone_contract-abi.json b/forc-plugins/forc-client/test/data/standalone_contract/standalone_contract-abi.json new file mode 100644 index 00000000000..f8ba2707608 --- /dev/null +++ b/forc-plugins/forc-client/test/data/standalone_contract/standalone_contract-abi.json @@ -0,0 +1,26 @@ +{ + "encoding": "1", + "types": [ + { + "typeId": 0, + "type": "bool", + "components": null, + "typeParameters": null + } + ], + "functions": [ + { + "inputs": [], + "name": "test_function", + "output": { + "name": "", + "type": 0, + "typeArguments": null + }, + "attributes": null + } + ], + "loggedTypes": [], + "messagesTypes": [], + "configurables": [] +} \ No newline at end of file diff --git a/forc-plugins/forc-client/tests/deploy.rs b/forc-plugins/forc-client/tests/deploy.rs index ebc5eef99d5..c817c22f723 100644 --- a/forc-plugins/forc-client/tests/deploy.rs +++ b/forc-plugins/forc-client/tests/deploy.rs @@ -241,7 +241,7 @@ async fn proxy_contract_re_routes_call() { abigen!(Contract( name = "ImplementationContract", - abi = "forc-plugins/forc-client/test/data/standalone_contract/out/debug/standalone_contract-abi.json" + abi = "forc-plugins/forc-client/test/data/standalone_contract/standalone_contract-abi.json" )); let impl_contract_a = ImplementationContract::new(proxy_contract, wallet_unlocked.clone()); From a03530e23fe317a1a798c65423c2e313edba2d91 Mon Sep 17 00:00:00 2001 From: Kaya Gokalp Date: Fri, 5 Jul 2024 14:25:32 -0700 Subject: [PATCH 19/37] add fail to change owner by random account --- forc-plugins/forc-client/tests/deploy.rs | 95 +++++++++++++++++++++++- 1 file changed, 92 insertions(+), 3 deletions(-) diff --git a/forc-plugins/forc-client/tests/deploy.rs b/forc-plugins/forc-client/tests/deploy.rs index c817c22f723..2a1c904c632 100644 --- a/forc-plugins/forc-client/tests/deploy.rs +++ b/forc-plugins/forc-client/tests/deploy.rs @@ -13,10 +13,11 @@ use forc_client::{ }; use forc_pkg::manifest::Proxy; use fuel_crypto::SecretKey; -use fuel_tx::{ContractId, Salt}; -use fuels::macros::abigen; -use fuels_accounts::{provider::Provider, wallet::WalletUnlocked}; +use fuel_tx::{ContractId, Receipt, Salt}; +use fuels::{macros::abigen, types::transaction::TxPolicies}; +use fuels_accounts::{provider::Provider, wallet::WalletUnlocked, Account}; use portpicker::Port; +use rand::thread_rng; use tempfile::tempdir; use toml_edit::{value, Document, InlineTable, Item, Table, Value}; @@ -289,3 +290,91 @@ async fn proxy_contract_re_routes_call() { assert!(!res.value); node.kill().unwrap(); } + +#[tokio::test] +async fn non_owner_fails_to_set_target() { + 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 proxy = Proxy { + enabled: true, + address: None, + }; + patch_manifest_file_with_proxy_table(tmp_dir.path(), proxy).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.clone()), + target: None, + testnet: false, + }; + let cmd = cmd::Deploy { + pkg, + salt: Some(vec![format!("{}", Salt::default())]), + node: target, + default_signer: true, + ..Default::default() + }; + let contract_id = deploy(cmd).await.unwrap(); + // Proxy contract's id. + let proxy_id = contract_id.first().and_then(|f| f.proxy).unwrap(); + + // create an another account and fund it. + let provider = Provider::connect(&node_url).await.unwrap(); + let secret_key = SecretKey::random(&mut thread_rng()); + let attacker_wallet = WalletUnlocked::new_from_private_key(secret_key, Some(provider.clone())); + + let secret_key = SecretKey::from_str(forc_client::constants::DEFAULT_PRIVATE_KEY).unwrap(); + let owner_wallet = WalletUnlocked::new_from_private_key(secret_key, Some(provider.clone())); + let base_asset_id = provider.base_asset_id(); + + // fund attacker wallet so that it can try to make a set proxy target call. + owner_wallet + .transfer( + attacker_wallet.address(), + 100000, + *base_asset_id, + TxPolicies::default(), + ) + .await + .unwrap(); + + let dummy_contract_id_target = ContractId::default(); + abigen!(Contract( + name = "ProxyContract", + abi = "forc-plugins/forc-client/src/util/proxy_contract-abi.json" + )); + + let proxy_contract = ProxyContract::new(proxy_id, attacker_wallet); + // try to change target of the proxy with a random wallet which is not the + // owner of the proxy. + let res = proxy_contract + .methods() + .set_proxy_target(dummy_contract_id_target) + .call() + .await + .err() + .unwrap(); + + node.kill().unwrap(); + match res { + fuels::types::errors::Error::Transaction( + fuels::types::errors::transaction::Reason::Reverted { reason, .. }, + ) => { + assert_eq!( + reason, + "NotOwner".to_string(), + "Expected 'NotOwner' error, but got: {}", + reason + ); + } + _ => panic!("Expected a Reverted transaction error, but got: {:?}", res), + } +} From 5f50d12eb33dcceaa2226c529d2c3622990266b5 Mon Sep 17 00:00:00 2001 From: Kaya Gokalp Date: Mon, 8 Jul 2024 16:00:28 -0700 Subject: [PATCH 20/37] remove unused import --- forc-plugins/forc-client/tests/deploy.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forc-plugins/forc-client/tests/deploy.rs b/forc-plugins/forc-client/tests/deploy.rs index 2a1c904c632..fe88b371590 100644 --- a/forc-plugins/forc-client/tests/deploy.rs +++ b/forc-plugins/forc-client/tests/deploy.rs @@ -13,7 +13,7 @@ use forc_client::{ }; use forc_pkg::manifest::Proxy; use fuel_crypto::SecretKey; -use fuel_tx::{ContractId, Receipt, Salt}; +use fuel_tx::{ContractId, Salt}; use fuels::{macros::abigen, types::transaction::TxPolicies}; use fuels_accounts::{provider::Provider, wallet::WalletUnlocked, Account}; use portpicker::Port; From a618d8921be275ba835e3d247be75b68e97f2c8d Mon Sep 17 00:00:00 2001 From: Kaya Gokalp Date: Tue, 9 Jul 2024 18:20:28 -0700 Subject: [PATCH 21/37] re-add manual sign command --- forc-plugins/forc-client/src/cmd/deploy.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/forc-plugins/forc-client/src/cmd/deploy.rs b/forc-plugins/forc-client/src/cmd/deploy.rs index abec6c3ed76..cf1f26e2a2e 100644 --- a/forc-plugins/forc-client/src/cmd/deploy.rs +++ b/forc-plugins/forc-client/src/cmd/deploy.rs @@ -57,6 +57,9 @@ 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 From 0019efab60c833267cc3dc81fbf43093a80100de Mon Sep 17 00:00:00 2001 From: Kaya Gokalp Date: Tue, 9 Jul 2024 18:50:41 -0700 Subject: [PATCH 22/37] update contract ids --- forc-plugins/forc-client/tests/deploy.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/forc-plugins/forc-client/tests/deploy.rs b/forc-plugins/forc-client/tests/deploy.rs index d6cbd0b1a3e..5a98c994f8a 100644 --- a/forc-plugins/forc-client/tests/deploy.rs +++ b/forc-plugins/forc-client/tests/deploy.rs @@ -184,12 +184,12 @@ async fn deploy_fresh_proxy() { node.kill().unwrap(); let impl_contract = DeployedContract { id: ContractId::from_str( - "428896412bda8530282a7b8fca5d20b2a73f30037612ca3a31750cf3bf0e976a", + "822c8d3672471f64f14f326447793c7377b6e430122db23b622880ccbd8a33ef", ) .unwrap(), proxy: Some( ContractId::from_str( - "7fbb4bd7f702cb41bfd9cef8b811895533d1ed59f7a31357ec142546207eb979", + "58ed1aca6e781609b2ff04488f0299fcb9f548874e477938937b5d2a2b001f74", ) .unwrap(), ), From 862828027ae1a662d665aa8b21e5cc21db71b36a Mon Sep 17 00:00:00 2001 From: Kaya Gokalp Date: Wed, 10 Jul 2024 22:38:23 -0700 Subject: [PATCH 23/37] move proxy abi --- .../forc-client/{src/util => abi}/proxy_contract-abi.json | 0 forc-plugins/forc-client/src/util/tx.rs | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename forc-plugins/forc-client/{src/util => abi}/proxy_contract-abi.json (100%) diff --git a/forc-plugins/forc-client/src/util/proxy_contract-abi.json b/forc-plugins/forc-client/abi/proxy_contract-abi.json similarity index 100% rename from forc-plugins/forc-client/src/util/proxy_contract-abi.json rename to forc-plugins/forc-client/abi/proxy_contract-abi.json diff --git a/forc-plugins/forc-client/src/util/tx.rs b/forc-plugins/forc-client/src/util/tx.rs index ce080e51c6d..d8d4191867b 100644 --- a/forc-plugins/forc-client/src/util/tx.rs +++ b/forc-plugins/forc-client/src/util/tx.rs @@ -255,7 +255,7 @@ pub(crate) async fn update_proxy_contract_target( ) -> Result<()> { abigen!(Contract( name = "ProxyContract", - abi = "forc-plugins/forc-client/src/util/proxy_contract-abi.json" + abi = "forc-plugins/forc-client/abi/proxy_contract-abi.json" )); let wallet = WalletUnlocked::new_from_private_key(secret_key, Some(provider)); From e96b9838d7cd0b9df465e5f2e3fae424c84a6956 Mon Sep 17 00:00:00 2001 From: Kaya Gokalp Date: Wed, 10 Jul 2024 22:41:52 -0700 Subject: [PATCH 24/37] update test --- forc-plugins/forc-client/tests/deploy.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forc-plugins/forc-client/tests/deploy.rs b/forc-plugins/forc-client/tests/deploy.rs index 5a98c994f8a..f107d7de37e 100644 --- a/forc-plugins/forc-client/tests/deploy.rs +++ b/forc-plugins/forc-client/tests/deploy.rs @@ -349,7 +349,7 @@ async fn non_owner_fails_to_set_target() { let dummy_contract_id_target = ContractId::default(); abigen!(Contract( name = "ProxyContract", - abi = "forc-plugins/forc-client/src/util/proxy_contract-abi.json" + abi = "forc-plugins/forc-client/abi/proxy_contract-abi.json" )); let proxy_contract = ProxyContract::new(proxy_id, attacker_wallet); From eb53abe2606a15ab6a1c26d40d42e8586932f887 Mon Sep 17 00:00:00 2001 From: Sophie Dankel <47993817+sdankel@users.noreply.github.com> Date: Wed, 24 Jul 2024 20:08:36 -0700 Subject: [PATCH 25/37] Merge master into kayagokalp/deploy-default-proxy (#6296) ## Description Merging in master, specifically the merge conflicts from https://github.com/FuelLabs/sway/pull/6278 ## Checklist - [ ] I have linked to any relevant issues. - [ ] I have commented my code, particularly in hard-to-understand areas. - [ ] I have updated the documentation where relevant (API docs, the reference, and the Sway book). - [ ] If my change requires substantial documentation changes, I have [requested support from the DevRel team](https://github.com/FuelLabs/devrel-requests/issues/new/choose) - [ ] I have added tests that prove my fix is effective or that my feature works. - [ ] I have added (or requested a maintainer to add) the necessary `Breaking*` or `New Feature` labels where relevant. - [ ] I have done my best to ensure that my PR adheres to [the Fuel Labs Code Review Standards](https://github.com/FuelLabs/rfcs/blob/master/text/code-standards/external-contributors.md). - [ ] I have requested a review from the relevant team or maintainers. --- Cargo.lock | 52 ++- forc-plugins/forc-client/Cargo.toml | 3 +- forc-plugins/forc-client/src/constants.rs | 8 +- forc-plugins/forc-client/src/op/deploy.rs | 388 ++++++++++-------- forc-plugins/forc-client/src/op/run/mod.rs | 3 +- forc-plugins/forc-client/src/util/pkg.rs | 34 +- forc-plugins/forc-client/src/util/target.rs | 10 +- forc-plugins/forc-client/src/util/tx.rs | 171 +++++--- .../test/data/contract_with_dep/Forc.toml | 2 +- .../Forc.toml | 2 +- .../test/data/standalone_contract_b/Forc.toml | 2 +- forc-plugins/forc-client/tests/deploy.rs | 57 ++- 12 files changed, 458 insertions(+), 274 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 81be02fd4fe..738fd6732e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -834,6 +834,12 @@ dependencies = [ "unreachable", ] +[[package]] +name = "comma" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55b672471b4e9f9e95499ea597ff64941a309b2cdbffcc46f2cc5e2d971fd335" + [[package]] name = "common-multipart-rfc7578" version = "0.6.0" @@ -897,6 +903,7 @@ dependencies = [ "encode_unicode 0.3.6", "lazy_static", "libc", + "unicode-width", "windows-sys 0.52.0", ] @@ -1412,6 +1419,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "dialoguer" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de" +dependencies = [ + "console", + "shell-words", + "tempfile", + "thiserror", + "zeroize", +] + [[package]] name = "diff" version = "0.1.13" @@ -2012,9 +2032,9 @@ dependencies = [ "anyhow", "async-trait", "chrono", - "colored", "clap 4.5.9", "devault", + "dialoguer", "forc", "forc-pkg", "forc-tracing 0.62.0", @@ -2034,6 +2054,7 @@ dependencies = [ "hex", "portpicker", "rand", + "rexpect 0.5.0", "rpassword", "serde", "serde_json", @@ -2088,7 +2109,7 @@ dependencies = [ "fuel-vm", "portpicker", "rayon", - "rexpect", + "rexpect 0.4.0", "serde", "serde_json", "shellfish", @@ -4319,6 +4340,20 @@ dependencies = [ "memoffset 0.6.5", ] +[[package]] +name = "nix" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" +dependencies = [ + "autocfg", + "bitflags 1.2.1", + "cfg-if 1.0.0", + "libc", + "memoffset 0.6.5", + "pin-utils", +] + [[package]] name = "nix" version = "0.26.4" @@ -5600,6 +5635,19 @@ dependencies = [ "tempfile", ] +[[package]] +name = "rexpect" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01ff60778f96fb5a48adbe421d21bf6578ed58c0872d712e7e08593c195adff8" +dependencies = [ + "comma", + "nix 0.25.1", + "regex", + "tempfile", + "thiserror", +] + [[package]] name = "rfc6979" version = "0.4.0" diff --git a/forc-plugins/forc-client/Cargo.toml b/forc-plugins/forc-client/Cargo.toml index cfb696552f3..7b9e3160333 100644 --- a/forc-plugins/forc-client/Cargo.toml +++ b/forc-plugins/forc-client/Cargo.toml @@ -13,8 +13,8 @@ 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" +dialoguer = "0.11" forc = { version = "0.62.0", path = "../../forc" } forc-pkg = { version = "0.62.0", path = "../../forc-pkg" } forc-tracing = { version = "0.62.0", path = "../../forc-tracing" } @@ -45,6 +45,7 @@ tracing = "0.1" [dev-dependencies] portpicker = "0.1.1" +rexpect = "0.5" tempfile = "3" toml_edit = "0.21.1" diff --git a/forc-plugins/forc-client/src/constants.rs b/forc-plugins/forc-client/src/constants.rs index 927323a57b3..f6e1faa4201 100644 --- a/forc-plugins/forc-client/src/constants.rs +++ b/forc-plugins/forc-client/src/constants.rs @@ -4,14 +4,18 @@ pub const BETA_2_ENDPOINT_URL: &str = "https://node-beta-2.fuel.network"; pub const BETA_3_ENDPOINT_URL: &str = "https://beta-3.fuel.network"; pub const BETA_4_ENDPOINT_URL: &str = "https://beta-4.fuel.network"; pub const BETA_5_ENDPOINT_URL: &str = "https://beta-5.fuel.network"; +pub const DEVNET_ENDPOINT_URL: &str = "https://devnet.fuel.network"; +pub const TESTNET_ENDPOINT_URL: &str = "https://testnet.fuel.network"; + pub const BETA_2_FAUCET_URL: &str = "https://faucet-beta-2.fuel.network"; pub const BETA_3_FAUCET_URL: &str = "https://faucet-beta-3.fuel.network"; pub const BETA_4_FAUCET_URL: &str = "https://faucet-beta-4.fuel.network"; pub const BETA_5_FAUCET_URL: &str = "https://faucet-beta-5.fuel.network"; 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"; + +pub const TESTNET_EXPLORER_URL: &str = "https://app.fuel.network"; + /// Default PrivateKey to sign transactions submitted to local node. pub const DEFAULT_PRIVATE_KEY: &str = "0xde97d8624a438121b86a1956544bd72ed68cd69f2c99555b08b1e8c51ffd511c"; diff --git a/forc-plugins/forc-client/src/op/deploy.rs b/forc-plugins/forc-client/src/op/deploy.rs index 68932fa5573..aed4b959317 100644 --- a/forc-plugins/forc-client/src/op/deploy.rs +++ b/forc-plugins/forc-client/src/op/deploy.rs @@ -4,40 +4,38 @@ use crate::{ util::{ node_url::get_node_url, pkg::{build_proxy_contract, built_pkgs, update_proxy_address_in_manifest}, + target::Target, 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, + bech32_from_secret, prompt_forc_wallet_password, select_secret_key, update_proxy_contract_target, WalletSelectionMode, }, }, }; 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_tracing::{println_action_green, 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::Salt; +use fuel_tx::{Salt, Transaction}; use fuel_vm::prelude::*; -use fuels::types::{transaction::TxPolicies, transaction_builders::CreateTransactionBuilder}; use fuels_accounts::{provider::Provider, wallet::WalletUnlocked, Account}; -use fuels_core::types::bech32::Bech32Address; +use fuels_core::types::{transaction::TxPolicies, transaction_builders::CreateTransactionBuilder}; 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, + sync::Arc, + time::Duration, }; use sway_core::language::parsed::TreeType; use sway_core::BuildTarget; -use tracing::info; #[derive(Debug, PartialEq, Eq, Clone, PartialOrd, Ord)] pub struct DeployedContract { @@ -123,54 +121,30 @@ fn validate_and_parse_salts<'a>( Ok(contract_salt_map) } +/// Deploys a new proxy contract for the given package. async fn deploy_new_proxy( pkg: &BuiltPackage, - owner_account_address: &mut Bech32Address, impl_contract: &fuel_tx::ContractId, build_opts: &BuildOpts, command: &cmd::Deploy, salt: Salt, - wallet_mode: &WalletSelectionMode, + provider: &Provider, + signing_key: &SecretKey, ) -> Result { - info!(" {} 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. - 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); let pkg_name = pkg.descriptor.manifest_file.project_name(); - let contract_addr = format!("0x{}", impl_contract); - let proxy_contract = build_proxy_contract(&user_addr, &contract_addr, pkg_name, build_opts)?; - info!(" {} proxy contract", "Deploying".bold().green()); - let proxy = deploy_pkg( - command, - &pkg.descriptor.manifest_file, - &proxy_contract, - salt, - wallet_mode, - ) - .await?; - Ok(proxy) + println_action_green("Creating", &format!("proxy contract for {pkg_name}")); + let user_addr_bech32 = bech32_from_secret(signing_key)?; + let proxy_built_package = build_proxy_contract( + &user_addr_bech32.into(), + impl_contract, + pkg_name, + build_opts, + )?; + let proxy_contract_id = + deploy_pkg(command, &proxy_built_package, salt, provider, signing_key).await?; + Ok(proxy_contract_id) } + /// Builds and deploys contract(s). If the given path corresponds to a workspace, all deployable members /// will be built and deployed. /// @@ -191,8 +165,17 @@ pub async fn deploy(command: cmd::Deploy) -> Result> { let build_opts = build_opts_from_cmd(&command); let built_pkgs = built_pkgs(&curr_dir, &build_opts)?; - - if built_pkgs.is_empty() { + let pkgs_to_deploy = built_pkgs + .iter() + .filter(|pkg| { + pkg.descriptor + .manifest_file + .check_program_type(&[TreeType::Contract]) + .is_ok() + }) + .collect::>(); + + if pkgs_to_deploy.is_empty() { println_warning("No deployable contracts found in the current directory."); return Ok(deployed_contracts); } @@ -214,6 +197,7 @@ 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)) @@ -233,131 +217,171 @@ pub async fn deploy(command: cmd::Deploy) -> Result> { None }; - info!(" {} deployment", "Starting".bold().green()); - 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) - }; + // Ensure that all packages are being deployed to the same node. + let node_url = get_node_url( + &command.node, + &pkgs_to_deploy[0].descriptor.manifest_file.network, + )?; + if !pkgs_to_deploy.iter().all(|pkg| { + get_node_url(&command.node, &pkg.descriptor.manifest_file.network).ok() + == Some(node_url.clone()) + }) { + bail!("All contracts in a deployment should be deployed to the same node. Please ensure that the network specified in the Forc.toml files of all contracts is the same."); + } - let mut owner_account_address = Bech32Address::default(); - for pkg in built_pkgs { - if pkg - .descriptor - .manifest_file - .check_program_type(&[TreeType::Contract]) - .is_ok() - { - let salt = match (&contract_salt_map, command.default_salt) { - (Some(map), false) => { - if let Some(salt) = map.get(pkg.descriptor.manifest_file.project_name()) { - *salt - } else { - Default::default() - } - } - (None, true) => Default::default(), - (None, false) => rand::random(), - (Some(_), true) => { - bail!("Both `--salt` and `--default-salt` were specified: must choose one") + // Confirmation step. Summarize the transaction(s) for the deployment. + let (provider, signing_key) = + confirm_transaction_details(&pkgs_to_deploy, &command, node_url.clone()).await?; + + for pkg in pkgs_to_deploy { + let salt = match (&contract_salt_map, command.default_salt) { + (Some(map), false) => { + if let Some(salt) = map.get(pkg.descriptor.manifest_file.project_name()) { + *salt + } else { + Default::default() } - }; - let node_url = get_node_url(&command.node, &pkg.descriptor.manifest_file.network)?; - info!( - " {} contract: {}", - "Deploying".bold().green(), - &pkg.descriptor.name - ); - let deployed_contract_id = deploy_pkg( - &command, - &pkg.descriptor.manifest_file, - &pkg, - salt, - &wallet_mode, - ) - .await?; - let proxy = &pkg.descriptor.manifest_file.proxy(); - let proxy_id = if let Some(proxy) = proxy { - if proxy.enabled { - 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. - info!(" {} proxy contract", "Updating".bold().green()); - 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?; - Some(proxy_contract) + } + (None, true) => Default::default(), + (None, false) => rand::random(), + (Some(_), true) => { + bail!("Both `--salt` and `--default-salt` were specified: must choose one") + } + }; + let deployed_contract_id = deploy_pkg(&command, pkg, salt, &provider, &signing_key).await?; + + let proxy_id = match &pkg.descriptor.manifest_file.proxy { + Some(forc_pkg::manifest::Proxy { + enabled: true, + address: Some(proxy_addr), + }) => { + // 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. + 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?; + Some(proxy_contract) + } + Some(forc_pkg::manifest::Proxy { + enabled: true, + address: None, + }) => { + // Deploy a new proxy contract. + let deployed_proxy_contract = deploy_new_proxy( + pkg, + &deployed_contract_id, + &build_opts, + &command, + salt, + &provider, + &signing_key, + ) + .await?; + + // Update manifest file such that the proxy address field points to the new proxy contract. + update_proxy_address_in_manifest( + &format!("0x{}", deployed_proxy_contract), + &pkg.descriptor.manifest_file, + )?; + Some(deployed_proxy_contract) + } + // Proxy not enabled. + _ => None, + }; + + let deployed_contract = DeployedContract { + id: deployed_contract_id, + proxy: proxy_id, + }; + deployed_contracts.push(deployed_contract); + } + Ok(deployed_contracts) +} + +/// Prompt the user to confirm the transactions required for deployment, as well as the signing key. +async fn confirm_transaction_details( + pkgs_to_deploy: &[&Arc], + command: &cmd::Deploy, + node_url: String, +) -> Result<(Provider, SecretKey)> { + // Confirmation step. Summarize the transaction(s) for the deployment. + let mut tx_count = 0; + let tx_summary = pkgs_to_deploy + .iter() + .map(|pkg| { + tx_count += 1; + let proxy_text = match &pkg.descriptor.manifest_file.proxy { + Some(forc_pkg::manifest::Proxy { + enabled: true, + address, + }) => { + if address.is_some() { + " + update proxy" } else { - // Deploy a new proxy contract. - let deployed_proxy_contract = deploy_new_proxy( - &pkg, - &mut owner_account_address, - &deployed_contract_id, - &build_opts, - &command, - salt, - &wallet_mode, - ) - .await?; - - // Update manifest file such that the proxy address field points to the new proxy contract. - update_proxy_address_in_manifest( - &format!("0x{}", deployed_proxy_contract), - &pkg.descriptor.manifest_file, - )?; - Some(deployed_proxy_contract) + " + deploy proxy" } - } else { - None } - } else { - None - }; - let deployed_contract = DeployedContract { - id: deployed_contract_id, - proxy: proxy_id, + _ => "", }; - deployed_contracts.push(deployed_contract); - } - } - Ok(deployed_contracts) + + format!( + "deploy {}{proxy_text}", + pkg.descriptor.manifest_file.project_name() + ) + }) + .collect::>() + .join(" + "); + + println_action_green("Confirming", &format!("transactions [{tx_summary}]")); + println_action_green("", &format!("Network: {node_url}")); + + let provider = Provider::connect(node_url.clone()).await?; + + let wallet_mode = if command.default_signer || command.signing_key.is_some() { + WalletSelectionMode::Manual + } else { + println_action_green("", &format!("Wallet: {}", default_wallet_path().display())); + let password = prompt_forc_wallet_password()?; + WalletSelectionMode::ForcWallet(password) + }; + + // TODO: Display the estimated gas cost of the transaction(s). + // https://github.com/FuelLabs/sway/issues/6277 + + let signing_key = select_secret_key( + &wallet_mode, + command.default_signer || command.unsigned, + command.signing_key, + &provider, + tx_count, + ) + .await? + .ok_or_else(|| anyhow::anyhow!("failed to select a signer for the transaction"))?; + + Ok((provider.clone(), signing_key)) } /// Deploy a single pkg given deploy command and the manifest file pub async fn deploy_pkg( command: &cmd::Deploy, - manifest: &PackageManifestFile, compiled: &BuiltPackage, salt: Salt, - wallet_mode: &WalletSelectionMode, + provider: &Provider, + signing_key: &SecretKey, ) -> Result { - let node_url = get_node_url(&command.node, &manifest.network)?; - let client = FuelClient::new(node_url.clone())?; + let manifest = &compiled.descriptor.manifest_file; + let node_url = provider.url(); + let client = FuelClient::new(node_url)?; + let bytecode = &compiled.bytecode.bytes; let mut storage_slots = @@ -369,12 +393,11 @@ pub async fn deploy_pkg( compiled.storage_slots.clone() }; storage_slots.sort(); - let contract = Contract::from(bytecode.as_slice()); + + let contract = Contract::from(bytecode.clone()); let root = contract.root(); let state_root = Contract::initial_state_root(storage_slots.iter()); let contract_id = contract.id(&salt, &root, &state_root); - - let provider = Provider::connect(node_url.clone()).await?; let tx_policies = TxPolicies::default(); let mut tb = CreateTransactionBuilder::prepare_contract_deployment( @@ -385,22 +408,15 @@ pub async fn deploy_pkg( 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())); + let wallet = WalletUnlocked::new_from_private_key(*signing_key, Some(provider.clone())); 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(); + let chain_info = client.chain_info().await?; + let chain_id = chain_info.consensus_parameters.chain_id(); let deployment_request = client.submit_and_await_commit(&tx).map(|res| match res { Ok(logs) => match logs { @@ -409,10 +425,25 @@ pub async fn deploy_pkg( } TransactionStatus::Success { block_height, .. } => { let pkg_name = manifest.project_name(); - info!("\n\n {} {pkg_name}!", "Deployed".bold().green()); - info!(" {}: {node_url}", "Network".bold().green()); - info!(" {}: 0x{contract_id}", "Contract ID".bold().green()); - info!(" {}: {}\n", "Block".bold().green(), &block_height); + let target = Target::from_str(&chain_info.name).unwrap_or(Target::testnet()); + let (contract_url, block_url) = match target.explorer_url() { + Some(explorer_url) => ( + format!("{explorer_url}/contract/0x"), + format!("{explorer_url}/block/"), + ), + None => ("".to_string(), "".to_string()), + }; + println_action_green( + "Finished", + &format!("deploying {pkg_name} {contract_url}{contract_id}"), + ); + let block_height_formatted = + match u32::from_str_radix(&block_height.to_string(), 16) { + Ok(decimal) => format!("{block_url}{decimal}"), + Err(_) => block_height.to_string(), + }; + + println_action_green("Deployed", &format!("in block {block_height_formatted}")); // Create a deployment artifact. let deployment_size = bytecode.len(); @@ -447,6 +478,7 @@ pub async fn deploy_pkg( }, Err(e) => bail!("{e}"), }); + // submit contract deployment with a timeout let contract_id = tokio::time::timeout( Duration::from_millis(TX_SUBMIT_TIMEOUT_MS), diff --git a/forc-plugins/forc-client/src/op/run/mod.rs b/forc-plugins/forc-client/src/op/run/mod.rs index 49047de3a37..6144399dfb9 100644 --- a/forc-plugins/forc-client/src/op/run/mod.rs +++ b/forc-plugins/forc-client/src/op/run/mod.rs @@ -13,7 +13,6 @@ 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; @@ -54,7 +53,7 @@ pub async fn run(command: cmd::Run) -> Result> { let wallet_mode = if command.default_signer || command.signing_key.is_some() { WalletSelectionMode::Manual } else { - let password = prompt_forc_wallet_password(&default_wallet_path())?; + let password = prompt_forc_wallet_password()?; WalletSelectionMode::ForcWallet(password) }; for built in built_pkgs_with_manifest { diff --git a/forc-plugins/forc-client/src/util/pkg.rs b/forc-plugins/forc-client/src/util/pkg.rs index 4bb0787ea0a..91cd175606b 100644 --- a/forc-plugins/forc-client/src/util/pkg.rs +++ b/forc-plugins/forc-client/src/util/pkg.rs @@ -103,10 +103,13 @@ pub(crate) fn update_proxy_address_in_manifest( /// 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, + owner_addr: &fuels_core::types::Address, + impl_contract_id: &fuel_tx::ContractId, pkg_name: &str, ) -> Result { + let owner_addr = &format!("0x{}", owner_addr); + let impl_contract_id = &format!("0x{}", impl_contract_id); + // Create the proxy contract folder. let proxy_contract_dir = user_forc_directory() .join(PROXY_CONTRACT_FOLDER_NAME) @@ -133,7 +136,7 @@ pub(crate) fn create_proxy_contract( .truncate(true) .open(proxy_contract_dir.join(SRC_DIR).join(MAIN_ENTRY))?; - let contract_str = generate_proxy_contract_src(addr, impl_contract_id); + let contract_str = generate_proxy_contract_src(owner_addr, impl_contract_id); write!(f, "{}", contract_str)?; Ok(proxy_contract_dir) } @@ -171,8 +174,8 @@ pub(crate) fn built_pkgs(path: &Path, build_opts: &BuildOpts) -> Result Result> { @@ -190,18 +193,17 @@ pub fn build_proxy_contract( #[cfg(test)] mod tests { - use std::path::PathBuf; - + use super::{build_proxy_contract, PROXY_CONTRACT_FOLDER_NAME}; use forc_pkg::BuildOpts; use forc_util::user_forc_directory; - - use super::{build_proxy_contract, PROXY_CONTRACT_FOLDER_NAME}; + use fuel_tx::ContractId; + use fuels_core::types::Address; + use std::path::PathBuf; #[test] fn test_build_proxy_contract() { - let owner_address = "0x0000000000000000000000000000000000000000000000000000000000000000"; - let impl_contract_address = - "0x0000000000000000000000000000000000000000000000000000000000000000"; + let owner_address = Address::new([0u8; 32]); + let impl_contract_address = ContractId::new([0u8; 32]); let target_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("test") .join("data") @@ -211,8 +213,12 @@ mod tests { 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); + 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(); diff --git a/forc-plugins/forc-client/src/util/target.rs b/forc-plugins/forc-client/src/util/target.rs index 8c9d598ab4d..86b9b947666 100644 --- a/forc-plugins/forc-client/src/util/target.rs +++ b/forc-plugins/forc-client/src/util/target.rs @@ -1,7 +1,8 @@ use crate::constants::{ BETA_2_ENDPOINT_URL, BETA_2_FAUCET_URL, BETA_3_ENDPOINT_URL, BETA_3_FAUCET_URL, BETA_4_ENDPOINT_URL, BETA_4_FAUCET_URL, BETA_5_ENDPOINT_URL, BETA_5_FAUCET_URL, - DEVNET_ENDPOINT_URL, DEVNET_FAUCET_URL, NODE_URL, TESTNET_ENDPOINT_URL, TESTNET_FAUCET_URL, + DEVNET_ENDPOINT_URL, DEVNET_FAUCET_URL, NODE_URL, TESTNET_ENDPOINT_URL, TESTNET_EXPLORER_URL, + TESTNET_FAUCET_URL, }; use anyhow::{bail, Result}; use serde::{Deserialize, Serialize}; @@ -67,6 +68,13 @@ impl Target { Target::Local => "http://localhost:3000".to_string(), } } + + pub fn explorer_url(&self) -> Option { + match self { + Target::Testnet | Target::Devnet => Some(TESTNET_EXPLORER_URL.to_string()), + _ => None, + } + } } impl FromStr for Target { diff --git a/forc-plugins/forc-client/src/util/tx.rs b/forc-plugins/forc-client/src/util/tx.rs index d8d4191867b..1a958a8c839 100644 --- a/forc-plugins/forc-client/src/util/tx.rs +++ b/forc-plugins/forc-client/src/util/tx.rs @@ -1,12 +1,20 @@ -use std::{collections::BTreeMap, io::Write, path::Path, str::FromStr}; - +use crate::{constants::DEFAULT_PRIVATE_KEY, util::target::Target}; use anyhow::{Error, Result}; use async_trait::async_trait; -use colored::Colorize; -use forc_tracing::println_warning; - +use dialoguer::{theme::ColorfulTheme, Confirm, Password, Select}; +use forc_tracing::{println_action_green, println_warning}; +use forc_wallet::{ + account::{derive_secret_key, new_at_index_cli}, + balance::{ + collect_accounts_with_verification, AccountBalances, AccountVerification, AccountsMap, + }, + new::{new_wallet_cli, New}, + utils::default_wallet_path, +}; use fuel_crypto::{Message, PublicKey, SecretKey, Signature}; -use fuel_tx::{field, Address, Buildable, ContractId, Input, Output, TransactionBuilder, Witness}; +use fuel_tx::{ + field, Address, AssetId, Buildable, ContractId, Input, Output, TransactionBuilder, Witness, +}; use fuels::macros::abigen; use fuels_accounts::{ provider::Provider, @@ -18,19 +26,7 @@ use fuels_core::types::{ coin_type::CoinType, transaction_builders::{create_coin_input, create_coin_message_input}, }; - -use forc_wallet::{ - account::{derive_secret_key, new_at_index_cli}, - balance::{ - collect_accounts_with_verification, print_account_balances, AccountBalances, - AccountVerification, AccountsMap, - }, - new::{new_wallet_cli, New}, - utils::default_wallet_path, -}; -use tracing::info; - -use crate::{constants::DEFAULT_PRIVATE_KEY, util::target::Target}; +use std::{collections::BTreeMap, io::Write, path::Path, str::FromStr}; #[derive(PartialEq, Eq)] pub enum WalletSelectionMode { @@ -57,15 +53,12 @@ fn prompt_signature(tx_id: fuel_tx::Bytes32) -> Result { } fn ask_user_yes_no_question(question: &str) -> Result { - print!("{question}"); - std::io::stdout().flush()?; - let mut ans = String::new(); - std::io::stdin().read_line(&mut ans)?; - // Pop trailing \n as users press enter to submit their answers. - ans.pop(); - // Trim the user input as it might have an additional space. - let ans = ans.trim(); - Ok(ans == "y" || ans == "Y") + let answer = Confirm::with_theme(&ColorfulTheme::default()) + .with_prompt(question) + .default(false) + .show_default(false) + .interact()?; + Ok(answer) } fn collect_user_accounts( @@ -83,22 +76,13 @@ fn collect_user_accounts( 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 first_user_account(wallet_path: &Path, password: &str) -> Result { - let accounts = collect_user_accounts(wallet_path, password)?; +pub(crate) fn prompt_forc_wallet_password() -> Result { + let password = Password::with_theme(&ColorfulTheme::default()) + .with_prompt("Wallet password") + .allow_empty_password(true) + .interact()?; - let account = accounts - .get(&0) - .ok_or_else(|| anyhow::anyhow!("No account derived for this wallet"))? - .clone(); - Ok(account) + Ok(password) } pub(crate) fn check_and_create_wallet_at_default_path(wallet_path: &Path) -> Result<()> { @@ -175,12 +159,33 @@ async fn collect_account_balances( .map_err(|e| anyhow::anyhow!("{e}")) } +/// Format collected account balances for each asset type, including only the balance of the base asset that can be used to pay gas. +pub fn format_base_asset_account_balances( + accounts_map: &AccountsMap, + account_balances: &AccountBalances, + base_asset_id: &AssetId, +) -> Vec { + accounts_map + .iter() + .zip(account_balances) + .map(|((ix, address), balance)| { + let base_asset_amount = balance + .get(&base_asset_id.to_string()) + .copied() + .unwrap_or(0); + let eth_amount = base_asset_amount as f64 / 1_000_000_000.0; + format!("[{ix}] {address} - {eth_amount} ETH") + }) + .collect() +} + // 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, + tx_count: usize, ) -> Result> { let chain_info = provider.chain_info().await?; let signing_key = match wallet_mode { @@ -191,6 +196,7 @@ pub(crate) async fn select_secret_key( // capabilities for selections and answer collection. let accounts = collect_user_accounts(&wallet_path, password)?; let account_balances = collect_account_balances(&accounts, provider).await?; + let base_asset_id = provider.base_asset_id(); let total_balance = account_balances .iter() @@ -207,15 +213,18 @@ pub(crate) async fn select_secret_key( \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 selections = + format_base_asset_account_balances(&accounts, &account_balances, base_asset_id); 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::()?; + account_index = Select::with_theme(&ColorfulTheme::default()) + .with_prompt("Wallet account") + .max_length(5) + .items(&selections[..]) + .default(0) + .interact()?; + if accounts.contains_key(&account_index) { break; } @@ -229,11 +238,11 @@ pub(crate) async fn select_secret_key( 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. + // TODO: calculate the number of transactions to sign and ask the user to confirm. let question = format!( - "Do you agree to sign this transaction with {}? [y/N]: ", - bech32 + "Do you agree to sign {tx_count} transaction{}?", + if tx_count > 1 { "s" } else { "" } ); let accepted = ask_user_yes_no_question(&question)?; if !accepted { @@ -248,7 +257,7 @@ pub(crate) async fn select_secret_key( } pub(crate) async fn update_proxy_contract_target( - provider: Provider, + provider: &Provider, secret_key: SecretKey, proxy_contract_id: ContractId, new_target: ContractId, @@ -258,7 +267,7 @@ pub(crate) async fn update_proxy_contract_target( abi = "forc-plugins/forc-client/abi/proxy_contract-abi.json" )); - let wallet = WalletUnlocked::new_from_private_key(secret_key, Some(provider)); + let wallet = WalletUnlocked::new_from_private_key(secret_key, Some(provider.clone())); let proxy_contract = ProxyContract::new(proxy_contract_id, wallet); @@ -268,10 +277,9 @@ pub(crate) async fn update_proxy_contract_target( .set_proxy_target(new_target) .call() .await?; - info!( - " {} proxy contract's target to 0x{}", - "Updated".bold().green(), - new_target + println_action_green( + "Updated", + &format!("proxy contract target to 0x{new_target}"), ); Ok(()) } @@ -369,7 +377,7 @@ impl TransactionBuilderExt for Tran let chain_info = provider.chain_info().await?; let params = chain_info.consensus_parameters; let signing_key = - select_secret_key(wallet_mode, default_sign, signing_key, &provider).await?; + select_secret_key(wallet_mode, default_sign, signing_key, &provider, 1).await?; // Get the address let address = if let Some(key) = signing_key { Address::from(*key.public_key().hash()) @@ -422,3 +430,48 @@ impl TransactionExt for T { self } } + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::BTreeMap; + use std::collections::HashMap; + + #[test] + fn test_format_base_asset_account_balances() { + let mut accounts_map: AccountsMap = BTreeMap::new(); + + let address1 = Bech32Address::from_str( + "fuel1dved7k25uxadatl7l5kql309jnw07dcn4t3a6x9hm9nxyjcpqqns50p7n2", + ) + .expect("address1"); + let address2 = Bech32Address::from_str( + "fuel1x9f3ysyk7fmey5ac23s2p4rwg4gjye2kke3nu3pvrs5p4qc4m4qqwx56k3", + ) + .expect("address2"); + + let base_asset_id = AssetId::zeroed(); + + accounts_map.insert(0, address1.clone()); + accounts_map.insert(1, address2.clone()); + + let mut account_balances: AccountBalances = Vec::new(); + let mut balance1 = HashMap::new(); + balance1.insert(base_asset_id.to_string(), 1_500_000_000); + balance1.insert("other_asset".to_string(), 2_000_000_000); + account_balances.push(balance1); + + let mut balance2 = HashMap::new(); + balance2.insert("other_asset".to_string(), 3_000_000_000); + account_balances.push(balance2); + + let expected = vec![ + format!("[0] {address1} - 1.5 ETH"), + format!("[1] {address2} - 0 ETH"), + ]; + + let result = + format_base_asset_account_balances(&accounts_map, &account_balances, &base_asset_id); + assert_eq!(result, expected); + } +} diff --git a/forc-plugins/forc-client/test/data/contract_with_dep/Forc.toml b/forc-plugins/forc-client/test/data/contract_with_dep/Forc.toml index 45be87cb3b6..4d1d6e8308b 100644 --- a/forc-plugins/forc-client/test/data/contract_with_dep/Forc.toml +++ b/forc-plugins/forc-client/test/data/contract_with_dep/Forc.toml @@ -6,7 +6,7 @@ license = "Apache-2.0" name = "contract_with_dep" [dependencies] -std = { path = "../../../../../../../../sway-lib-std/" } +std = { path = "../../../../../sway-lib-std/" } [contract-dependencies] standalone_contract = { path = "../standalone_contract", salt = "0x0000000000000000000000000000000000000000000000000000000000000001" } diff --git a/forc-plugins/forc-client/test/data/contract_with_dep_with_salt_conflict/Forc.toml b/forc-plugins/forc-client/test/data/contract_with_dep_with_salt_conflict/Forc.toml index 4123009928a..fe10c17663d 100644 --- a/forc-plugins/forc-client/test/data/contract_with_dep_with_salt_conflict/Forc.toml +++ b/forc-plugins/forc-client/test/data/contract_with_dep_with_salt_conflict/Forc.toml @@ -6,7 +6,7 @@ license = "Apache-2.0" name = "contract_with_dep_with_salt_conflict" [dependencies] -std = { path = "../../../../../../../../sway-lib-std/" } +std = { path = "../../../../../sway-lib-std/" } [contract-dependencies] contract_with_dep = { path = "../contract_with_dep" } diff --git a/forc-plugins/forc-client/test/data/standalone_contract_b/Forc.toml b/forc-plugins/forc-client/test/data/standalone_contract_b/Forc.toml index ccca650f3d5..e61b046039b 100644 --- a/forc-plugins/forc-client/test/data/standalone_contract_b/Forc.toml +++ b/forc-plugins/forc-client/test/data/standalone_contract_b/Forc.toml @@ -6,4 +6,4 @@ license = "Apache-2.0" name = "standalone_contract_b" [dependencies] -std = { path = "../../../../../../../../sway-lib-std/" } +std = { path = "../../../../../sway-lib-std/" } diff --git a/forc-plugins/forc-client/tests/deploy.rs b/forc-plugins/forc-client/tests/deploy.rs index f107d7de37e..ee817f5805b 100644 --- a/forc-plugins/forc-client/tests/deploy.rs +++ b/forc-plugins/forc-client/tests/deploy.rs @@ -1,10 +1,3 @@ -use std::{ - fs, - path::{Path, PathBuf}, - process::{Child, Command}, - str::FromStr, -}; - use forc::cli::shared::Pkg; use forc_client::{ cmd, @@ -18,6 +11,13 @@ use fuels::{macros::abigen, types::transaction::TxPolicies}; use fuels_accounts::{provider::Provider, wallet::WalletUnlocked, Account}; use portpicker::Port; use rand::thread_rng; +use rexpect::spawn; +use std::{ + fs, + path::{Path, PathBuf}, + process::{Child, Command}, + str::FromStr, +}; use tempfile::tempdir; use toml_edit::{value, Document, InlineTable, Item, Table, Value}; @@ -111,7 +111,7 @@ fn update_main_sw(tmp_dir: &Path) -> anyhow::Result<()> { } #[tokio::test] -async fn simple_deploy() { +async fn test_simple_deploy() { let (mut node, port) = run_node(); let tmp_dir = tempdir().unwrap(); let project_dir = test_data_path().join("standalone_contract"); @@ -150,7 +150,7 @@ async fn simple_deploy() { } #[tokio::test] -async fn deploy_fresh_proxy() { +async fn test_deploy_fresh_proxy() { let (mut node, port) = run_node(); let tmp_dir = tempdir().unwrap(); let project_dir = test_data_path().join("standalone_contract"); @@ -189,7 +189,7 @@ async fn deploy_fresh_proxy() { .unwrap(), proxy: Some( ContractId::from_str( - "58ed1aca6e781609b2ff04488f0299fcb9f548874e477938937b5d2a2b001f74", + "2b49d9d74f2d54274c01584523f64e0a5f8d2c4df4bdda760efe5c7b451f6b2c", ) .unwrap(), ), @@ -200,7 +200,7 @@ async fn deploy_fresh_proxy() { } #[tokio::test] -async fn proxy_contract_re_routes_call() { +async fn test_proxy_contract_re_routes_call() { let (mut node, port) = run_node(); let tmp_dir = tempdir().unwrap(); let project_dir = test_data_path().join("standalone_contract"); @@ -292,7 +292,7 @@ async fn proxy_contract_re_routes_call() { } #[tokio::test] -async fn non_owner_fails_to_set_target() { +async fn test_non_owner_fails_to_set_target() { let (mut node, port) = run_node(); let tmp_dir = tempdir().unwrap(); let project_dir = test_data_path().join("standalone_contract"); @@ -378,3 +378,36 @@ async fn non_owner_fails_to_set_target() { _ => panic!("Expected a Reverted transaction error, but got: {:?}", res), } } + +// TODO: https://github.com/FuelLabs/sway/issues/6283 +// Add interactive tests for the happy path cases. This requires starting the node with funded accounts and setting up +// the wallet with the correct password. The tests should be run in a separate test suite that is not run by default. +// It would also require overriding `default_wallet_path` function for tests, so as not to interfere with the user's wallet. + +#[test] +fn test_deploy_interactive_wrong_password() -> Result<(), rexpect::error::Error> { + let (mut node, port) = run_node(); + let node_url = format!("http://127.0.0.1:{}/v1/graphql", port); + + // Spawn the forc-deploy binary using cargo run + let project_dir = test_data_path().join("standalone_contract"); + let mut process = spawn( + &format!( + "cargo run --bin forc-deploy -- --node-url {node_url} -p {}", + project_dir.display() + ), + Some(300000), + )?; + + // Confirmation prompts + process + .exp_string("\u{1b}[1;32mConfirming\u{1b}[0m transactions [deploy standalone_contract]")?; + process.exp_string(&format!("Network: {node_url}"))?; + process.exp_string("Wallet: ")?; + process.exp_string("Wallet password")?; + process.send_line("mock_password")?; + + process.process.exit()?; + node.kill().unwrap(); + Ok(()) +} From 65a380d8219a63dbbda2f3ae40f96320eb1cec74 Mon Sep 17 00:00:00 2001 From: Sophie Dankel <47993817+sdankel@users.noreply.github.com> Date: Wed, 24 Jul 2024 20:24:27 -0700 Subject: [PATCH 26/37] Fix merge conflicts (#6298) ## Description ## Checklist - [ ] I have linked to any relevant issues. - [ ] I have commented my code, particularly in hard-to-understand areas. - [ ] I have updated the documentation where relevant (API docs, the reference, and the Sway book). - [ ] If my change requires substantial documentation changes, I have [requested support from the DevRel team](https://github.com/FuelLabs/devrel-requests/issues/new/choose) - [ ] I have added tests that prove my fix is effective or that my feature works. - [ ] I have added (or requested a maintainer to add) the necessary `Breaking*` or `New Feature` labels where relevant. - [ ] I have done my best to ensure that my PR adheres to [the Fuel Labs Code Review Standards](https://github.com/FuelLabs/rfcs/blob/master/text/code-standards/external-contributors.md). - [ ] I have requested a review from the relevant team or maintainers. --- forc-plugins/forc-client/src/op/deploy.rs | 2 +- forc-plugins/forc-client/src/util/tx.rs | 7 +++++++ .../test/data/contract_with_dep/Forc.lock | 19 ------------------- .../test/data/standalone_contract/Forc.lock | 13 ------------- 4 files changed, 8 insertions(+), 33 deletions(-) delete mode 100644 forc-plugins/forc-client/test/data/contract_with_dep/Forc.lock delete mode 100644 forc-plugins/forc-client/test/data/standalone_contract/Forc.lock diff --git a/forc-plugins/forc-client/src/op/deploy.rs b/forc-plugins/forc-client/src/op/deploy.rs index ca0cdfeaf5b..aed4b959317 100644 --- a/forc-plugins/forc-client/src/op/deploy.rs +++ b/forc-plugins/forc-client/src/op/deploy.rs @@ -34,7 +34,6 @@ use std::{ sync::Arc, time::Duration, }; -use std::{sync::Arc, time::Duration}; use sway_core::language::parsed::TreeType; use sway_core::BuildTarget; @@ -382,6 +381,7 @@ pub async fn deploy_pkg( let manifest = &compiled.descriptor.manifest_file; let node_url = provider.url(); let client = FuelClient::new(node_url)?; + let bytecode = &compiled.bytecode.bytes; let mut storage_slots = diff --git a/forc-plugins/forc-client/src/util/tx.rs b/forc-plugins/forc-client/src/util/tx.rs index 9e33b9e8087..1a958a8c839 100644 --- a/forc-plugins/forc-client/src/util/tx.rs +++ b/forc-plugins/forc-client/src/util/tx.rs @@ -121,6 +121,13 @@ pub(crate) fn secret_key_from_forc_wallet( 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, diff --git a/forc-plugins/forc-client/test/data/contract_with_dep/Forc.lock b/forc-plugins/forc-client/test/data/contract_with_dep/Forc.lock deleted file mode 100644 index 9722a217392..00000000000 --- a/forc-plugins/forc-client/test/data/contract_with_dep/Forc.lock +++ /dev/null @@ -1,19 +0,0 @@ -[[package]] -name = "contract_with_dep" -source = "member" -dependencies = ["std"] -contract-dependencies = ["standalone_contract (0000000000000000000000000000000000000000000000000000000000000001)"] - -[[package]] -name = "core" -source = "path+from-root-9B9D657E3F1FCA11" - -[[package]] -name = "standalone_contract" -source = "path+from-root-9B9D657E3F1FCA11" -dependencies = ["std"] - -[[package]] -name = "std" -source = "path+from-root-9B9D657E3F1FCA11" -dependencies = ["core"] diff --git a/forc-plugins/forc-client/test/data/standalone_contract/Forc.lock b/forc-plugins/forc-client/test/data/standalone_contract/Forc.lock deleted file mode 100644 index 7b517045569..00000000000 --- a/forc-plugins/forc-client/test/data/standalone_contract/Forc.lock +++ /dev/null @@ -1,13 +0,0 @@ -[[package]] -name = "core" -source = "path+from-root-79BB3EA8498403DE" - -[[package]] -name = "standalone_contract" -source = "member" -dependencies = ["std"] - -[[package]] -name = "std" -source = "path+from-root-79BB3EA8498403DE" -dependencies = ["core"] From 38b3d434d274d4ca980f3e02a80269068ebc2100 Mon Sep 17 00:00:00 2001 From: Sophie Date: Wed, 24 Jul 2024 20:38:26 -0700 Subject: [PATCH 27/37] Update docs --- .../src/forc/plugins/forc_client/index.md | 50 +++++++------------ 1 file changed, 18 insertions(+), 32 deletions(-) diff --git a/docs/book/src/forc/plugins/forc_client/index.md b/docs/book/src/forc/plugins/forc_client/index.md index d2738091961..fc552e6bece 100644 --- a/docs/book/src/forc/plugins/forc_client/index.md +++ b/docs/book/src/forc/plugins/forc_client/index.md @@ -19,28 +19,18 @@ Example: ```console > forc deploy - Building /Users/test/test-projects/test-contract - Finished release [optimized + fuel] target(s) in 11.39s - -Please provide the password of your encrypted wallet vault at "/Users/ceylinbormali/.fuel/wallets/.wallet": - Deploying contract: impl-contract - ---------------------------------------------------------------------------- -Account 0: fuel12pls73y9hnqdqthvduy2x44x48zt8s50pkerf32kq26f2afeqdwq6rj9ar - -Asset ID : f8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07 -Amount : 2197245 ---------------------------------------------------------------------------- - -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 + Building /Users/yourname/test-projects/test-contract + Finished release [optimized + fuel] target(s) in 11.39s + Confirming transactions [deploy impl-contract] + Network: https://testnet.fuel.network + Wallet: /Users/yourname/.fuel/wallets/.wallet +✔ Wallet password · ******** +? Wallet account › +❯ [0] fuel12pls73y9hnqdqthvduy2x44x48zt8s50pkerf32kq26f2afeqdwq6rj9ar - 0.002197245 ETH + [1] fuel1vzrm6kw9s3tv85gl25lpptsxrdguyzfhq6c8rk07tr6ft5g45nwqqh0uty - 0.001963631 ETH +? Do you agree to sign 1 transaction? (y/n) › yes + Finished deploying impl-contract https://app.fuel.network/contract/0x94b712901f04332682d14c998a5fc5a078ed15321438f46d58d0383200cde43d + Deployed in block https://app.fuel.network/block/5958351 ``` 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. @@ -54,16 +44,12 @@ 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 + Building /Users/test/test-projects/test-contract + Finished release [optimized + fuel] target(s) in 11.40s + Confirming transactions [deploy impl-contract] + Network: http://127.0.0.1:4000 + Finished deploying impl-contract 0xf9fb08ef18ce226954270d6d4f67677d484b8782a5892b3d436572b405407544 + Deployed in block 00000001 ``` ## Option 3: Manually signing through forc-wallet (Deprecated) From 17b2bd70e1bd71030545308cffa0d3b15a285cb7 Mon Sep 17 00:00:00 2001 From: Sophie Date: Thu, 25 Jul 2024 09:50:18 -0700 Subject: [PATCH 28/37] Update non owner test to use update proxy method --- forc-plugins/forc-client/src/lib.rs | 2 +- forc-plugins/forc-client/src/util/mod.rs | 2 +- forc-plugins/forc-client/src/util/tx.rs | 10 ++--- forc-plugins/forc-client/tests/deploy.rs | 54 +++++++++++------------- 4 files changed, 31 insertions(+), 37 deletions(-) diff --git a/forc-plugins/forc-client/src/lib.rs b/forc-plugins/forc-client/src/lib.rs index 815e07d9a67..cce5e5ab95e 100644 --- a/forc-plugins/forc-client/src/lib.rs +++ b/forc-plugins/forc-client/src/lib.rs @@ -1,7 +1,7 @@ pub mod cmd; pub mod constants; pub mod op; -mod util; +pub mod util; use clap::Parser; use serde::{Deserialize, Serialize}; diff --git a/forc-plugins/forc-client/src/util/mod.rs b/forc-plugins/forc-client/src/util/mod.rs index 3b99a89659f..d8024081b28 100644 --- a/forc-plugins/forc-client/src/util/mod.rs +++ b/forc-plugins/forc-client/src/util/mod.rs @@ -3,4 +3,4 @@ pub(crate) mod gas; pub(crate) mod node_url; pub(crate) mod pkg; pub(crate) mod target; -pub(crate) mod tx; +pub mod tx; diff --git a/forc-plugins/forc-client/src/util/tx.rs b/forc-plugins/forc-client/src/util/tx.rs index 1a958a8c839..c71a18fa022 100644 --- a/forc-plugins/forc-client/src/util/tx.rs +++ b/forc-plugins/forc-client/src/util/tx.rs @@ -15,7 +15,7 @@ use fuel_crypto::{Message, PublicKey, SecretKey, Signature}; use fuel_tx::{ field, Address, AssetId, Buildable, ContractId, Input, Output, TransactionBuilder, Witness, }; -use fuels::macros::abigen; +use fuels::{macros::abigen, programs::responses::CallResponse}; use fuels_accounts::{ provider::Provider, wallet::{Wallet, WalletUnlocked}, @@ -256,12 +256,12 @@ pub(crate) async fn select_secret_key( Ok(signing_key) } -pub(crate) async fn update_proxy_contract_target( +pub async fn update_proxy_contract_target( provider: &Provider, secret_key: SecretKey, proxy_contract_id: ContractId, new_target: ContractId, -) -> Result<()> { +) -> Result> { abigen!(Contract( name = "ProxyContract", abi = "forc-plugins/forc-client/abi/proxy_contract-abi.json" @@ -272,7 +272,7 @@ pub(crate) async fn update_proxy_contract_target( 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 + let result = proxy_contract .methods() .set_proxy_target(new_target) .call() @@ -281,7 +281,7 @@ pub(crate) async fn update_proxy_contract_target( "Updated", &format!("proxy contract target to 0x{new_target}"), ); - Ok(()) + Ok(result) } #[async_trait] diff --git a/forc-plugins/forc-client/tests/deploy.rs b/forc-plugins/forc-client/tests/deploy.rs index ee817f5805b..8454603b721 100644 --- a/forc-plugins/forc-client/tests/deploy.rs +++ b/forc-plugins/forc-client/tests/deploy.rs @@ -2,6 +2,7 @@ use forc::cli::shared::Pkg; use forc_client::{ cmd, op::{deploy, DeployedContract}, + util::tx::update_proxy_contract_target, NodeTarget, }; use forc_pkg::manifest::Proxy; @@ -326,16 +327,19 @@ async fn test_non_owner_fails_to_set_target() { // Proxy contract's id. let proxy_id = contract_id.first().and_then(|f| f.proxy).unwrap(); - // create an another account and fund it. + // Create and fund an owner account and an attacker account. let provider = Provider::connect(&node_url).await.unwrap(); - let secret_key = SecretKey::random(&mut thread_rng()); - let attacker_wallet = WalletUnlocked::new_from_private_key(secret_key, Some(provider.clone())); - - let secret_key = SecretKey::from_str(forc_client::constants::DEFAULT_PRIVATE_KEY).unwrap(); - let owner_wallet = WalletUnlocked::new_from_private_key(secret_key, Some(provider.clone())); + let attacker_secret_key = SecretKey::random(&mut thread_rng()); + let attacker_wallet = + WalletUnlocked::new_from_private_key(attacker_secret_key, Some(provider.clone())); + + let owner_secret_key = + SecretKey::from_str(forc_client::constants::DEFAULT_PRIVATE_KEY).unwrap(); + let owner_wallet = + WalletUnlocked::new_from_private_key(owner_secret_key, Some(provider.clone())); let base_asset_id = provider.base_asset_id(); - // fund attacker wallet so that it can try to make a set proxy target call. + // Fund attacker wallet so that it can try to make a set proxy target call. owner_wallet .transfer( attacker_wallet.address(), @@ -352,31 +356,21 @@ async fn test_non_owner_fails_to_set_target() { abi = "forc-plugins/forc-client/abi/proxy_contract-abi.json" )); - let proxy_contract = ProxyContract::new(proxy_id, attacker_wallet); - // try to change target of the proxy with a random wallet which is not the - // owner of the proxy. - let res = proxy_contract - .methods() - .set_proxy_target(dummy_contract_id_target) - .call() - .await - .err() - .unwrap(); + // Try to change target of the proxy with a random wallet which is not the owner of the proxy. + let res = update_proxy_contract_target( + &provider, + attacker_secret_key, + proxy_id, + dummy_contract_id_target, + ) + .await + .err() + .unwrap(); node.kill().unwrap(); - match res { - fuels::types::errors::Error::Transaction( - fuels::types::errors::transaction::Reason::Reverted { reason, .. }, - ) => { - assert_eq!( - reason, - "NotOwner".to_string(), - "Expected 'NotOwner' error, but got: {}", - reason - ); - } - _ => panic!("Expected a Reverted transaction error, but got: {:?}", res), - } + assert!(res + .to_string() + .starts_with("transaction reverted: NotOwner")); } // TODO: https://github.com/FuelLabs/sway/issues/6283 From 4e5e421cc4608645a667d39f302f6d6c01ed0a67 Mon Sep 17 00:00:00 2001 From: Kaya Gokalp Date: Mon, 5 Aug 2024 15:09:36 +0300 Subject: [PATCH 29/37] use audited proxy contract --- .../forc-client/abi/proxy_contract-abi.json | 646 +++++++++++++++++- forc-plugins/forc-client/src/op/deploy.rs | 8 + forc-plugins/forc-client/src/util/pkg.rs | 130 ++-- forc-plugins/forc-client/tests/deploy.rs | 2 +- 4 files changed, 706 insertions(+), 80 deletions(-) diff --git a/forc-plugins/forc-client/abi/proxy_contract-abi.json b/forc-plugins/forc-client/abi/proxy_contract-abi.json index be94c534664..10ad2eeedc4 100644 --- a/forc-plugins/forc-client/abi/proxy_contract-abi.json +++ b/forc-plugins/forc-client/abi/proxy_contract-abi.json @@ -31,12 +31,12 @@ "components": [ { "name": "Address", - "type": 5, + "type": 10, "typeArguments": null }, { "name": "ContractId", - "type": 6, + "type": 11, "typeArguments": null } ], @@ -44,6 +44,49 @@ }, { "typeId": 4, + "type": "enum InitializationError", + "components": [ + { + "name": "CannotReinitialized", + "type": 0, + "typeArguments": null + } + ], + "typeParameters": null + }, + { + "typeId": 5, + "type": "enum Option", + "components": [ + { + "name": "None", + "type": 0, + "typeArguments": null + }, + { + "name": "Some", + "type": 8, + "typeArguments": null + } + ], + "typeParameters": [ + 8 + ] + }, + { + "typeId": 6, + "type": "enum SetProxyOwnerError", + "components": [ + { + "name": "CannotUninitialize", + "type": 0, + "typeArguments": null + } + ], + "typeParameters": null + }, + { + "typeId": 7, "type": "enum State", "components": [ { @@ -65,7 +108,19 @@ "typeParameters": null }, { - "typeId": 5, + "typeId": 8, + "type": "generic T", + "components": null, + "typeParameters": null + }, + { + "typeId": 9, + "type": "str", + "components": null, + "typeParameters": null + }, + { + "typeId": 10, "type": "struct Address", "components": [ { @@ -77,7 +132,7 @@ "typeParameters": null }, { - "typeId": 6, + "typeId": 11, "type": "struct ContractId", "components": [ { @@ -87,18 +142,102 @@ } ], "typeParameters": null + }, + { + "typeId": 12, + "type": "struct ProxyOwnerSet", + "components": [ + { + "name": "new_proxy_owner", + "type": 7, + "typeArguments": null + } + ], + "typeParameters": null + }, + { + "typeId": 13, + "type": "struct ProxyTargetSet", + "components": [ + { + "name": "new_target", + "type": 11, + "typeArguments": null + } + ], + "typeParameters": null } ], "functions": [ { "inputs": [], - "name": "owner", + "name": "proxy_target", "output": { "name": "", - "type": 4, - "typeArguments": null + "type": 5, + "typeArguments": [ + { + "name": "", + "type": 11, + "typeArguments": null + } + ] }, "attributes": [ + { + "name": "doc-comment", + "arguments": [ + " Returns the target contract of the proxy contract." + ] + }, + { + "name": "doc-comment", + "arguments": [ + "" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " # Returns" + ] + }, + { + "name": "doc-comment", + "arguments": [ + "" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " * [Option] - The new proxy contract to which all fallback calls will be passed or `None`." + ] + }, + { + "name": "doc-comment", + "arguments": [ + "" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " # Number of Storage Accesses" + ] + }, + { + "name": "doc-comment", + "arguments": [ + "" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " * Reads: `1`" + ] + }, { "name": "storage", "arguments": [ @@ -111,7 +250,7 @@ "inputs": [ { "name": "new_target", - "type": 6, + "type": 11, "typeArguments": null } ], @@ -122,6 +261,430 @@ "typeArguments": null }, "attributes": [ + { + "name": "doc-comment", + "arguments": [ + " Change the target contract of the proxy contract." + ] + }, + { + "name": "doc-comment", + "arguments": [ + "" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " # Additional Information" + ] + }, + { + "name": "doc-comment", + "arguments": [ + "" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " This method can only be called by the `proxy_owner`." + ] + }, + { + "name": "doc-comment", + "arguments": [ + "" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " # Arguments" + ] + }, + { + "name": "doc-comment", + "arguments": [ + "" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " * `new_target`: [ContractId] - The new proxy contract to which all fallback calls will be passed." + ] + }, + { + "name": "doc-comment", + "arguments": [ + "" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " # Reverts" + ] + }, + { + "name": "doc-comment", + "arguments": [ + "" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " * When not called by `proxy_owner`." + ] + }, + { + "name": "doc-comment", + "arguments": [ + "" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " # Number of Storage Accesses" + ] + }, + { + "name": "doc-comment", + "arguments": [ + "" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " * Reads: `1`" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " * Write: `1`" + ] + }, + { + "name": "storage", + "arguments": [ + "read", + "write" + ] + } + ] + }, + { + "inputs": [], + "name": "proxy_owner", + "output": { + "name": "", + "type": 7, + "typeArguments": null + }, + "attributes": [ + { + "name": "doc-comment", + "arguments": [ + " Returns the owner of the proxy contract." + ] + }, + { + "name": "doc-comment", + "arguments": [ + "" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " # Returns" + ] + }, + { + "name": "doc-comment", + "arguments": [ + "" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " * [State] - Represents the state of ownership for this contract." + ] + }, + { + "name": "doc-comment", + "arguments": [ + "" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " # Number of Storage Accesses" + ] + }, + { + "name": "doc-comment", + "arguments": [ + "" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " * Reads: `1`" + ] + }, + { + "name": "storage", + "arguments": [ + "read" + ] + } + ] + }, + { + "inputs": [], + "name": "initialize_proxy", + "output": { + "name": "", + "type": 0, + "typeArguments": null + }, + "attributes": [ + { + "name": "doc-comment", + "arguments": [ + " Initializes the proxy contract." + ] + }, + { + "name": "doc-comment", + "arguments": [ + "" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " # Additional Information" + ] + }, + { + "name": "doc-comment", + "arguments": [ + "" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " This method sets the storage values using the values of the configurable constants `INITIAL_TARGET` and `INITIAL_OWNER`." + ] + }, + { + "name": "doc-comment", + "arguments": [ + " This then allows methods that write to storage to be called." + ] + }, + { + "name": "doc-comment", + "arguments": [ + " This method can only be called once." + ] + }, + { + "name": "doc-comment", + "arguments": [ + "" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " # Reverts" + ] + }, + { + "name": "doc-comment", + "arguments": [ + "" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " * When `storage.proxy_owner` is not [State::Uninitialized]." + ] + }, + { + "name": "doc-comment", + "arguments": [ + "" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " # Number of Storage Accesses" + ] + }, + { + "name": "doc-comment", + "arguments": [ + "" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " * Writes: `2`" + ] + }, + { + "name": "storage", + "arguments": [ + "write" + ] + } + ] + }, + { + "inputs": [ + { + "name": "new_proxy_owner", + "type": 7, + "typeArguments": null + } + ], + "name": "set_proxy_owner", + "output": { + "name": "", + "type": 0, + "typeArguments": null + }, + "attributes": [ + { + "name": "doc-comment", + "arguments": [ + " Changes proxy ownership to the passed State." + ] + }, + { + "name": "doc-comment", + "arguments": [ + "" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " # Additional Information" + ] + }, + { + "name": "doc-comment", + "arguments": [ + "" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " This method can be used to transfer ownership between Identities or to revoke ownership." + ] + }, + { + "name": "doc-comment", + "arguments": [ + "" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " # Arguments" + ] + }, + { + "name": "doc-comment", + "arguments": [ + "" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " * `new_proxy_owner`: [State] - The new state of the proxy ownership." + ] + }, + { + "name": "doc-comment", + "arguments": [ + "" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " # Reverts" + ] + }, + { + "name": "doc-comment", + "arguments": [ + "" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " * When the sender is not the current proxy owner." + ] + }, + { + "name": "doc-comment", + "arguments": [ + " * When the new state of the proxy ownership is [State::Uninitialized]." + ] + }, + { + "name": "doc-comment", + "arguments": [ + "" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " # Number of Storage Accesses" + ] + }, + { + "name": "doc-comment", + "arguments": [ + "" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " * Reads: `1`" + ] + }, + { + "name": "doc-comment", + "arguments": [ + " * Writes: `1`" + ] + }, { "name": "storage", "arguments": [ @@ -139,8 +702,73 @@ "type": 2, "typeArguments": [] } + }, + { + "logId": "2151606668983994881", + "loggedType": { + "name": "", + "type": 13, + "typeArguments": [] + } + }, + { + "logId": "2161305517876418151", + "loggedType": { + "name": "", + "type": 4, + "typeArguments": [] + } + }, + { + "logId": "4354576968059844266", + "loggedType": { + "name": "", + "type": 6, + "typeArguments": [] + } + }, + { + "logId": "10870989709723147660", + "loggedType": { + "name": "", + "type": 12, + "typeArguments": [] + } + }, + { + "logId": "10098701174489624218", + "loggedType": { + "name": "", + "type": 9, + "typeArguments": null + } } ], "messagesTypes": [], - "configurables": [] + "configurables": [ + { + "name": "INITIAL_TARGET", + "configurableType": { + "name": "", + "type": 5, + "typeArguments": [ + { + "name": "", + "type": 11, + "typeArguments": [] + } + ] + }, + "offset": 16224 + }, + { + "name": "INITIAL_OWNER", + "configurableType": { + "name": "", + "type": 7, + "typeArguments": [] + }, + "offset": 16264 + } + ] } \ No newline at end of file diff --git a/forc-plugins/forc-client/src/op/deploy.rs b/forc-plugins/forc-client/src/op/deploy.rs index aed4b959317..510037404fb 100644 --- a/forc-plugins/forc-client/src/op/deploy.rs +++ b/forc-plugins/forc-client/src/op/deploy.rs @@ -142,6 +142,14 @@ async fn deploy_new_proxy( )?; let proxy_contract_id = deploy_pkg(command, &proxy_built_package, salt, provider, signing_key).await?; + fuels::macros::abigen!(Contract( + name = "ProxyContract", + abi = "forc-plugins/forc-client/abi/proxy_contract-abi.json" + )); + let wallet = WalletUnlocked::new_from_private_key(*signing_key, Some(provider.clone())); + let instance = ProxyContract::new(proxy_contract_id, wallet); + instance.methods().initialize_proxy().call().await?; + println_action_green("Initialized", &format!("proxy contract for {pkg_name}")); Ok(proxy_contract_id) } diff --git a/forc-plugins/forc-client/src/util/pkg.rs b/forc-plugins/forc-client/src/util/pkg.rs index 91cd175606b..c88148bac9c 100644 --- a/forc-plugins/forc-client/src/util/pkg.rs +++ b/forc-plugins/forc-client/src/util/pkg.rs @@ -17,10 +17,11 @@ pub const PROXY_CONTRACT_FORC_TOML: &str = r#" authors = ["Fuel Labs "] entry = "main.sw" license = "Apache-2.0" -name = "proxy_contract" +name = "src14_owned_proxy" [dependencies] -standards = { git = "https://github.com/FuelLabs/sway-standards/", tag = "v0.5.0" } +standards = { git = "https://github.com/FuelLabs/sway-standards", tag = "v0.5.1" } +sway_libs = { git = "https://github.com/FuelLabs/sway-libs", tag = "v0.23.0" } "#; /// Generates source code for proxy contract owner set to the given 'addr'. @@ -28,54 +29,82 @@ pub(crate) fn generate_proxy_contract_src(addr: &str, impl_contract_id: &str) -> format!( r#" contract; - +use ::sway_libs::{{ + ownership::errors::InitializationError, + upgradability::{{ + _proxy_owner, + _proxy_target, + _set_proxy_owner, + _set_proxy_target, + only_proxy_owner, + }}, +}}; +use standards::{{src14::{{SRC14, SRC14Extension}}, src5::State}}; 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})); +abi OwnedProxy {{ + #[storage(write)] + fn initialize_proxy(); + + #[storage(write)] + fn set_proxy_owner(new_proxy_owner: State); +}} + +configurable {{ + INITIAL_TARGET: Option = Some(ContractId::from({impl_contract_id})), + INITIAL_OWNER: State = State::Initialized(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), + target: Option = None, + proxy_owner: State = State::Uninitialized, }} -impl SRC5 for Contract {{ +impl SRC14 for Contract {{ + #[storage(read, write)] + fn set_proxy_target(new_target: ContractId) {{ + only_proxy_owner(storage.proxy_owner); + _set_proxy_target(new_target); + }} + #[storage(read)] - fn owner() -> State {{ - storage.owner.read() + fn proxy_target() -> Option {{ + _proxy_target() }} }} -impl SRC14 for Contract {{ +impl SRC14Extension for Contract {{ + #[storage(read)] + fn proxy_owner() -> State {{ + _proxy_owner(storage.proxy_owner) + }} +}} + +impl OwnedProxy for Contract {{ #[storage(write)] - fn set_proxy_target(new_target: ContractId) {{ - only_owner(); - storage.target.write(new_target); + fn initialize_proxy() {{ + require( + _proxy_owner(storage.proxy_owner) == State::Uninitialized, + InitializationError::CannotReinitialized, + ); + + storage.target.write(INITIAL_TARGET); + storage.proxy_owner.write(INITIAL_OWNER); + }} + + #[storage(write)] + fn set_proxy_owner(new_proxy_owner: State) {{ + _set_proxy_owner(new_proxy_owner, storage.proxy_owner); }} }} #[fallback] #[storage(read)] fn fallback() {{ - // pass through any other method call to the target - run_external(storage.target.read()) + run_external(_proxy_target().expect("FallbackError::TargetNotSet")) }} - -#[storage(read)] -fn only_owner() {{ - require( - storage - .owner - .read() == State::Initialized(msg_sender().unwrap()), - AccessError::NotOwner, - ); -}} -"# + "# ) } @@ -190,42 +219,3 @@ pub fn build_proxy_contract( .ok_or_else(|| anyhow::anyhow!("could not get proxy contract"))?; Ok(built_pkg) } - -#[cfg(test)] -mod tests { - use super::{build_proxy_contract, PROXY_CONTRACT_FOLDER_NAME}; - use forc_pkg::BuildOpts; - use forc_util::user_forc_directory; - use fuel_tx::ContractId; - use fuels_core::types::Address; - use std::path::PathBuf; - - #[test] - fn test_build_proxy_contract() { - let owner_address = Address::new([0u8; 32]); - let impl_contract_address = ContractId::new([0u8; 32]); - 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") - } -} diff --git a/forc-plugins/forc-client/tests/deploy.rs b/forc-plugins/forc-client/tests/deploy.rs index 8454603b721..ed65f63074f 100644 --- a/forc-plugins/forc-client/tests/deploy.rs +++ b/forc-plugins/forc-client/tests/deploy.rs @@ -190,7 +190,7 @@ async fn test_deploy_fresh_proxy() { .unwrap(), proxy: Some( ContractId::from_str( - "2b49d9d74f2d54274c01584523f64e0a5f8d2c4df4bdda760efe5c7b451f6b2c", + "c8dad72a0b0e67f2fc1b92fc603ff1ea3ddc53eddf8eb0ae4bc9effd0bfe519e", ) .unwrap(), ), From ef35fb9cd8f645fa1043fb0cf90aee8356a1d2a6 Mon Sep 17 00:00:00 2001 From: Kaya Gokalp Date: Mon, 5 Aug 2024 15:46:59 +0300 Subject: [PATCH 30/37] update tests --- forc-plugins/forc-client/src/op/deploy.rs | 2 ++ forc-plugins/forc-client/tests/deploy.rs | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/forc-plugins/forc-client/src/op/deploy.rs b/forc-plugins/forc-client/src/op/deploy.rs index 510037404fb..e92ed5cb6f9 100644 --- a/forc-plugins/forc-client/src/op/deploy.rs +++ b/forc-plugins/forc-client/src/op/deploy.rs @@ -295,6 +295,8 @@ pub async fn deploy(command: cmd::Deploy) -> Result> { ) .await?; + println!("--==-- deployed proxy contract {deployed_proxy_contract:?}"); + // Update manifest file such that the proxy address field points to the new proxy contract. update_proxy_address_in_manifest( &format!("0x{}", deployed_proxy_contract), diff --git a/forc-plugins/forc-client/tests/deploy.rs b/forc-plugins/forc-client/tests/deploy.rs index ed65f63074f..e5009740929 100644 --- a/forc-plugins/forc-client/tests/deploy.rs +++ b/forc-plugins/forc-client/tests/deploy.rs @@ -233,7 +233,7 @@ async fn test_proxy_contract_re_routes_call() { }; let contract_ids = deploy(cmd).await.unwrap(); // At this point we deployed a contract with proxy. - let proxy_contract = contract_ids[0].proxy.unwrap(); + let proxy_contract_id = contract_ids[0].proxy.unwrap(); let impl_contract_id = contract_ids[0].id; // Make a contract call into proxy contract, and check if the initial // contract returns a true. @@ -246,7 +246,7 @@ async fn test_proxy_contract_re_routes_call() { abi = "forc-plugins/forc-client/test/data/standalone_contract/standalone_contract-abi.json" )); - let impl_contract_a = ImplementationContract::new(proxy_contract, wallet_unlocked.clone()); + let impl_contract_a = ImplementationContract::new(proxy_contract_id, wallet_unlocked.clone()); let res = impl_contract_a .methods() .test_function() @@ -277,7 +277,7 @@ async fn test_proxy_contract_re_routes_call() { let contract_ids = deploy(cmd).await.unwrap(); // proxy contract id should be the same. let proxy_contract_after_update = contract_ids[0].proxy.unwrap(); - assert_eq!(proxy_contract, proxy_contract_after_update); + assert_eq!(proxy_contract_id, proxy_contract_after_update); let impl_contract_id_after_update = contract_ids[0].id; assert!(impl_contract_id != impl_contract_id_after_update); let impl_contract_a = ImplementationContract::new(proxy_contract_after_update, wallet_unlocked); From 680cb0b6651d4effa305eb8c7962be608bba376e Mon Sep 17 00:00:00 2001 From: Kaya Gokalp Date: Mon, 5 Aug 2024 19:02:06 +0300 Subject: [PATCH 31/37] use built proxy contract --- .../abi/proxy_contract-storage_slots.json | 18 ++ .../forc-client/abi/proxy_contract.bin | Bin 0 -> 16680 bytes forc-plugins/forc-client/src/op/deploy.rs | 65 ++++---- forc-plugins/forc-client/src/util/pkg.rs | 157 +----------------- forc-plugins/forc-client/tests/deploy.rs | 91 ++++++++++ 5 files changed, 153 insertions(+), 178 deletions(-) create mode 100644 forc-plugins/forc-client/abi/proxy_contract-storage_slots.json create mode 100644 forc-plugins/forc-client/abi/proxy_contract.bin diff --git a/forc-plugins/forc-client/abi/proxy_contract-storage_slots.json b/forc-plugins/forc-client/abi/proxy_contract-storage_slots.json new file mode 100644 index 00000000000..72849c97055 --- /dev/null +++ b/forc-plugins/forc-client/abi/proxy_contract-storage_slots.json @@ -0,0 +1,18 @@ +[ + { + "key": "7bb458adc1d118713319a5baa00a2d049dd64d2916477d2688d76970c898cd55", + "value": "0000000000000000000000000000000000000000000000000000000000000000" + }, + { + "key": "7bb458adc1d118713319a5baa00a2d049dd64d2916477d2688d76970c898cd56", + "value": "0000000000000000000000000000000000000000000000000000000000000000" + }, + { + "key": "bb79927b15d9259ea316f2ecb2297d6cc8851888a98278c0a2e03e1a091ea754", + "value": "0000000000000000000000000000000000000000000000000000000000000000" + }, + { + "key": "bb79927b15d9259ea316f2ecb2297d6cc8851888a98278c0a2e03e1a091ea755", + "value": "0000000000000000000000000000000000000000000000000000000000000000" + } +] \ No newline at end of file diff --git a/forc-plugins/forc-client/abi/proxy_contract.bin b/forc-plugins/forc-client/abi/proxy_contract.bin new file mode 100644 index 0000000000000000000000000000000000000000..ea52800465d15de0e1ecd19085568823d1b464c6 GIT binary patch literal 16680 zcmd5@e{dY-dHznilTWgV7bIh`hIpOWW&vY1AR7TG-s|bO6SBA!vd8k+*OQ?%CZ$-Y zvLl*ie1Xu>w4~#ZgnBv&Bg!;!(=@YZ%RjNF*J=N7re#J+GmV>ssYuP#Zmd=?b;br- z_j$h`d*AKt$v8u2>KV`O-R*w+zR&yXdEakk&N*z1u_ngA6oEDONFihFPI+?nWciQ8LdP%(`?B8+w1H$$Nh$KE|3qZf2)harXMj;)U3& z-nLWBJU<+|X3;LXrqDYaGkRCLg?_9zFdSRvEn!CgI_3@fR&mye7iW!xmp`>{bQ3e| z{)}7PYQ?<1Q~SL9QFqj%?^a=LC$x6UPP1>Cd3oO`&RR`aquI-zoEy#I`b<}e&Wp2* zL6@aC#=Lad=f9<=N?zZQ(&$E9*V|F?vPbsu^Gv*Ap0}9!c397*|1{InY>&%g!030q z^r0E^JPXOv{8_rj%T67r$EoeRgn`$0$iZ`LyEk4br2XG&Pcd5a4teh4ya}>)u6G&3 zGaN6)l91EKJ)J_%L#NGi2IPZ`Z-r%SUMX-kSfSsD6*5MXH~9J-@U-lKgzKeG0;3hr z6=y&7JnZ!)FMD{xJnv#H*t~A7>;8juDP(|MFN+oWSrhI*1KkZAE5NYRPIUm^L%w+q z_eZYL%mf}M&(yASv;sR_g1s)o?(YFFo3O0Md_h^ha#*b4q}G_YNSt)5l)z*Z9q+j?9UKBC#m)oisBHtGkqCXSE5 zR$>iqtK$@BGPX|~&&Q2|cGO^%%5%OXP*u4`5A)o&g)3!8yejeDd&!uzggFE_#L54Pq;W7|kC& zgV=f>vGoBk`EM^h)lE5*;xlBgpGfS3Sc1Jg*lR@DuU^)~b?P42i!kB%*4wM?=bF9J zn!RQq({>qK1>`rz*WmU#nN#Z_4zChhu@^}W8C!2)y&h?=4A~2{>LH`osuc2bS)<>n z6pC{>qcBn_Y@6#fwhmWNZ)Q|Zggq@U<=XsnYKaSb4hehqkUhs6*i+`UGWkQdH+bYU z?sr6vJ#vYkV=a|iYjJP7TVQz9#=UPxuODxRFIy@Oab3>KFdKHUL`^$11RWnH9f6r` zXDBxv9EaWW$S=rS2H8>gKmYCUX|g5Nj9OXZHw)i8b`I<0xbNks&Io@C=bF~y|$@SrUvqU+6pLLftCgcr?hX^>G=$Ro#QQq&(6fE?Ym);&Nj( zE)Rhhu*VQbt8siBacS{2L|jg)^$?dnKW~^v*|$gz8JCY>y?z;&kI0(m)#r+MLEwCf zaONBjHP7<$hv%a0()@wAzDzaE5jC%Dd-?K^umP`mch%Os6X)Pt4%IX-`+6sE%~2dh z$6o7?ku#GGZ1Y8lbC4rpn-SP%QrW8BHq8}v4{UQ(@2g=ZHC~A16!7W?uU60f8xE)Ynm%03!z&`Ra4MPR*ZF_W&cReU1Egqrzw<`joFT*8Fl3brJoFnVw{3 zc7_$er_GD_f7xLB@!Ox^nujm(Yd5HCzXED4uHVPZz8S`!Hw)hX3qO<}nPcX`*#~_C zIo+@aL{6`bS9Ys8IL+G8$F%o5%=EzLH?o_-!HAQ=pQG|5yIrp9&a>_cZfEpH;+h0< znj4lcNx0oM(B+vIUG@^`b=gvSZ;*N~(DXJGPTUr=@sgSL39}eshWiZ-w*}X5KgYbm z{d=|N=c>4un3sU_EeI*q3-M=gL0y z9>aEe3eWLLUU!e^kI?h_H^|Q&IA0`nq25XT#37uqHSMXmy1$w3SKrC& zVrCD=;ReVs>`@<`#W7q>R4rk&=NNoEetgyOz=+T4bj%LN2{ z`d}w8wFy&mJ7YJdc9($B3i=-UQe6T+jF*5M&rz;! z$9!Q=4{TbsXOiD@)(GjG{7bVq&2}?(FV+>;)b%up^`ed(bd}x$hw^FlxxV%!|Bmhf zW~iYD!4Jyg>RRZ1I=cR`2jp`)q5mY0Szc@XbE>|EYQ$Z#W_Jg;rNLLEKZtv3Yb^Bq znv`n+3e9fYq?X>N%kkd_?*|gX=fR>;L#A2A{j^s$6*`!oO>B z4%Z!V&TT2Iv7%<6X&I~r*P)i9QP6M3cX_ZNsepr-k#WtP2pRSEv&@blhG z3G2?iHn8bc)SPKvi+Xvkn)3$B#%lvBEqXTWik#9@0^Y$n{|>bsvo$_b1%{6OIAK_# z{54`;9+CEqDf{XeR`XX87;XbUBHzJ!(#8}kw8yFbCnY%Ce0dw37S~GEHbQ z*@CxPb?ZQ;+aqK;R+Y)eY{gmznQn$mw?L+$d1R`c%}AMw1%nj}R;*ZXz)eQT9;gAs z^Tk_DTp$Nf-jCrwX5Flr_#2OvYMe#H2-7)WP5i1laBZ9+_88%fT%=oBY^c^Z zwlj!V@Nm@RZuog4&iu6G%sJQtTs(>oxAgG5JSaID_myzWwXEgWey?9;GdpN7s0 zTjuUI1~SyQfY+>ZizzoYPmWgj=>FHJ2Z4+Ydk_uZSm*rsCVr&hy8<}7z!{v8_+l63 zE`c>TA#(m2#PTBGbqq0t`CYsM3^8x_o#F+f1>YlY!!O>hvqOgakg$Wz=YF&XEK2kl?{dPBN{t5Lb#gxU0z>sXE^(LyGYj$GCu41RpYIbTA2gv#L_(RvM zhQFxEuoZNMEqEM&$081}57GduE&TpGIEn{9rXE1uUmr8aBJ88<3HyNSWA=bLV}xk3K`O}AHa{2rq=y>hY5gRRZ^SV5jEiM$yBX(g zviDK_B>m=6e@OWN*He$^sU8RYp)GsFnZQ5NWya4N^oWR$5^;xl)|#xY#d}Ziw&;En zYkig0lKrM$&zB26{-HT<*N5c1T^|+a1&3Noek*dPQDI)G2x&z$Y$+ zxUcS2ng8n7VZP>KzQ)viZCIPHDLJlGV;S=|KA%ON>fCR1J96{ZLbW0-bZ~6*SuYI%VRkAd1H)7!a z2G7mesS0eh4>q*rtdHh~m|tVBJxle6V!*2FHO5#A?|)D;Mz9X{mOC)3_jB~@`u89b zGsMr)|DM)-OXdQm`BrSGiZkca`ymV8tAj5&(LHF>KgxPA17FBR^Xldmyj~9ak(&LcZ0)-&I-$6atwG)= z9?Rb;0?yO?ococSMWANXK8t;fW8R0F&G$#?{lU3)*r#bN z@0E0Zqh#)eEv~|Eny2Wwe0E6ljc9%YOzU#5CWkh67Ck!k4XW-o@Go{z`j;*ID{7t< zOLLnKqF<187x9TaiG<2$TXl95*XJ?s^jNprdybfEeNtkL8mRg)nhV#>wU$?NI+Jr3 z9;fS9=MaOr5mubFC~nu|$Y+vfpI_7K7E=yp2H=V*3tW+Nm`=? zunzEyz;dtV3*gL?n0eI19r1Y{cL9zeZn&EXzK429HAdp-kK^+J4aaui*wGkA|G<3% zn?26UGMii@gZK2dVDDoHId}7_lGv|0?FwP*L|_~BzE3uEG8y z#<+T zn!O*-MXxO8Cc2!MO&_3{5%y?lw!(X5ti6-V*)M9ms4-4R))@5(;$0c!3GCa8bDDED z?jL_4V}6)(Lfi*VcrRjn5jf#>;Dm#c6Ao*Bv7x@7i0xN)MStR9t(4Z$^P zb!>W&p38Z4AMw>%u8R5PbApGLEnzG4Z1}qbfqmVzuZe4UFJa3WN(C~Ym*Vr@Iewm| zcju#IX|*^8ebiTid-DB)@H1LB3LKzky*~!|8u5`-IPA!=SbM)kJyW8+*K#rY8Tsw~Oi-6hNs>3&*|_Wv{+skqZ+ zld&t-L9YhC#{7CCTZkj=A+~JKzjGSzL{;`kjs#y=!)0~RgQEYzIsS&nPns@dN6wJ* zO2m7>`x?j&&PVU+P>z7!wEp(KcZNdotz-`L4%boKPxoHa`)S||_ww9W-D|!d{{1Vw zpQg_`O1xLl<{Wz8-wB_ko`IR?fD!TRIv&&3MKWEbXF&Z5I7US7W0SgHz&>?-7FzTVUHHqsJIWm_pzI`z06}jTkh1~{eYjV z_dX6l5BYaRupHAPaq$US9`GV zTFD;2v3i-_kh(d#ZV~T~n>yZQAfLwr{0eK`lH`sX2-gcI10Wkgu=j`kbUXMT2oSn6ZIuSW%*L6Y7A%EvQ ziuT(}#G@Q=B6FjQKPVbtU-KBd+r_-kW$tRqx3@T~;EpgW)yy7EvG~LIPQ1?+)QzYd zF%)4tL;0o3$%vn&h^Ho%U$t1u!Pl^V+(CUncY|K?2C^;sP~w9w?tMVduku|!gJz+) zhUQh^i8PO9Lt1`QIYIY3U_uy)dWHOknUNFX5%Hy3e&bvl@j+Zk!4Z1bD# z)DsmiOnsD*FIIy<4 zP4Razm>uM7yqjWPbjY^IDX!w}%C->?6J zht~e7`1!$MaeQDx9=qgmjX2(qH`nRkee(DFO8g`1J}Hj(o{`6Cc|0eNGxB&$9({TI lhB&VOkvMjpl}A?|yT$RIA#uFxyW+S^UU#D-KF5Fme*sN7MkD|L literal 0 HcmV?d00001 diff --git a/forc-plugins/forc-client/src/op/deploy.rs b/forc-plugins/forc-client/src/op/deploy.rs index e92ed5cb6f9..222e4422fb4 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::{ constants::TX_SUBMIT_TIMEOUT_MS, util::{ node_url::get_node_url, - pkg::{build_proxy_contract, built_pkgs, update_proxy_address_in_manifest}, + pkg::{built_pkgs, create_proxy_contract, update_proxy_address_in_manifest}, target::Target, tx::{ bech32_from_secret, prompt_forc_wallet_password, select_secret_key, @@ -22,10 +22,11 @@ use fuel_core_client::client::FuelClient; use fuel_crypto::fuel_types::ChainId; use fuel_tx::{Salt, Transaction}; use fuel_vm::prelude::*; +use fuels::programs::contract::{LoadConfiguration, StorageConfiguration}; 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, BuildOpts, BuildProfile, BuiltPackage}; +use pkg::{manifest::build_profile::ExperimentalFlags, BuildProfile, BuiltPackage}; use serde::{Deserialize, Serialize}; use std::{ collections::BTreeMap, @@ -123,34 +124,43 @@ fn validate_and_parse_salts<'a>( /// Deploys a new proxy contract for the given package. async fn deploy_new_proxy( - pkg: &BuiltPackage, + pkg_name: &str, impl_contract: &fuel_tx::ContractId, - build_opts: &BuildOpts, - command: &cmd::Deploy, - salt: Salt, provider: &Provider, signing_key: &SecretKey, ) -> Result { - let pkg_name = pkg.descriptor.manifest_file.project_name(); - println_action_green("Creating", &format!("proxy contract for {pkg_name}")); - let user_addr_bech32 = bech32_from_secret(signing_key)?; - let proxy_built_package = build_proxy_contract( - &user_addr_bech32.into(), - impl_contract, - pkg_name, - build_opts, - )?; - let proxy_contract_id = - deploy_pkg(command, &proxy_built_package, salt, provider, signing_key).await?; fuels::macros::abigen!(Contract( name = "ProxyContract", abi = "forc-plugins/forc-client/abi/proxy_contract-abi.json" )); + let proxy_dir_output = create_proxy_contract(pkg_name)?; + let address = bech32_from_secret(signing_key)?; let wallet = WalletUnlocked::new_from_private_key(*signing_key, Some(provider.clone())); - let instance = ProxyContract::new(proxy_contract_id, wallet); - instance.methods().initialize_proxy().call().await?; + + let storage_path = proxy_dir_output.join("proxy-storage_slots.json"); + let storage_configuration = + StorageConfiguration::default().add_slot_overrides_from_file(storage_path)?; + + let configurables = ProxyContractConfigurables::default() + .with_INITIAL_TARGET(Some(*impl_contract))? + .with_INITIAL_OWNER(State::Initialized(Address::from(address).into()))?; + + let configuration = LoadConfiguration::default() + .with_storage_configuration(storage_configuration) + .with_configurables(configurables); + + let proxy_contract_id = fuels::programs::contract::Contract::load_from( + proxy_dir_output.join("proxy.bin"), + configuration, + )? + .deploy(&wallet, TxPolicies::default()) + .await?; + + let instance = ProxyContract::new(&proxy_contract_id, wallet); + let response = instance.methods().initialize_proxy().call().await?; + println!("{response:?}"); println_action_green("Initialized", &format!("proxy contract for {pkg_name}")); - Ok(proxy_contract_id) + Ok(proxy_contract_id.into()) } /// Builds and deploys contract(s). If the given path corresponds to a workspace, all deployable members @@ -283,17 +293,12 @@ pub async fn deploy(command: cmd::Deploy) -> Result> { enabled: true, address: None, }) => { + let pkg_name = &pkg.descriptor.name; + println_action_green("Creating", &format!("proxy contract for {pkg_name}")); // Deploy a new proxy contract. - let deployed_proxy_contract = deploy_new_proxy( - pkg, - &deployed_contract_id, - &build_opts, - &command, - salt, - &provider, - &signing_key, - ) - .await?; + let deployed_proxy_contract = + deploy_new_proxy(pkg_name, &deployed_contract_id, &provider, &signing_key) + .await?; println!("--==-- deployed proxy contract {deployed_proxy_contract:?}"); diff --git a/forc-plugins/forc-client/src/util/pkg.rs b/forc-plugins/forc-client/src/util/pkg.rs index c88148bac9c..39968bb8cc2 100644 --- a/forc-plugins/forc-client/src/util/pkg.rs +++ b/forc-plugins/forc-client/src/util/pkg.rs @@ -7,106 +7,12 @@ 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}; /// 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 = "src14_owned_proxy" - -[dependencies] -standards = { git = "https://github.com/FuelLabs/sway-standards", tag = "v0.5.1" } -sway_libs = { git = "https://github.com/FuelLabs/sway-libs", tag = "v0.23.0" } -"#; - -/// 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 ::sway_libs::{{ - ownership::errors::InitializationError, - upgradability::{{ - _proxy_owner, - _proxy_target, - _set_proxy_owner, - _set_proxy_target, - only_proxy_owner, - }}, -}}; -use standards::{{src14::{{SRC14, SRC14Extension}}, src5::State}}; -use std::execution::run_external; - -abi OwnedProxy {{ - #[storage(write)] - fn initialize_proxy(); - - #[storage(write)] - fn set_proxy_owner(new_proxy_owner: State); -}} - -configurable {{ - INITIAL_TARGET: Option = Some(ContractId::from({impl_contract_id})), - INITIAL_OWNER: State = State::Initialized(Identity::Address(Address::from({addr}))), -}} - -#[namespace(SRC14)] -storage {{ - target: Option = None, - proxy_owner: State = State::Uninitialized, -}} - -impl SRC14 for Contract {{ - #[storage(read, write)] - fn set_proxy_target(new_target: ContractId) {{ - only_proxy_owner(storage.proxy_owner); - _set_proxy_target(new_target); - }} - - #[storage(read)] - fn proxy_target() -> Option {{ - _proxy_target() - }} -}} - -impl SRC14Extension for Contract {{ - #[storage(read)] - fn proxy_owner() -> State {{ - _proxy_owner(storage.proxy_owner) - }} -}} - -impl OwnedProxy for Contract {{ - #[storage(write)] - fn initialize_proxy() {{ - require( - _proxy_owner(storage.proxy_owner) == State::Uninitialized, - InitializationError::CannotReinitialized, - ); - - storage.target.write(INITIAL_TARGET); - storage.proxy_owner.write(INITIAL_OWNER); - }} - - #[storage(write)] - fn set_proxy_owner(new_proxy_owner: State) {{ - _set_proxy_owner(new_proxy_owner, storage.proxy_owner); - }} -}} - -#[fallback] -#[storage(read)] -fn fallback() {{ - run_external(_proxy_target().expect("FallbackError::TargetNotSet")) -}} - "# - ) -} +pub const PROXY_CONTRACT_BIN: &[u8] = include_bytes!("../../abi/proxy_contract.bin"); +pub const PROXY_CONTRACT_STORAGE_SLOTS: &str = + include_str!("../../abi/proxy_contract-storage_slots.json"); /// 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. @@ -131,42 +37,18 @@ pub(crate) fn update_proxy_address_in_manifest( } /// Creates a proxy contract project at the given path, adds a forc.toml and source file. -pub(crate) fn create_proxy_contract( - owner_addr: &fuels_core::types::Address, - impl_contract_id: &fuel_tx::ContractId, - pkg_name: &str, -) -> Result { - let owner_addr = &format!("0x{}", owner_addr); - let impl_contract_id = &format!("0x{}", impl_contract_id); - +pub(crate) fn create_proxy_contract(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)?; + std::fs::write(proxy_contract_dir.join("proxy.bin"), PROXY_CONTRACT_BIN)?; + std::fs::write( + proxy_contract_dir.join("proxy-storage_slots.json"), + PROXY_CONTRACT_STORAGE_SLOTS, + )?; - // Create the Forc.toml - let mut f = std::fs::OpenOptions::new() - .read(true) - .write(true) - .create(true) - .truncate(true) - .open(proxy_contract_dir.join(MANIFEST_FILE_NAME))?; - write!(f, "{}", PROXY_CONTRACT_FORC_TOML)?; - - // Create the src folder - std::fs::create_dir_all(proxy_contract_dir.join(SRC_DIR))?; - - // Create main.sw - let mut f = std::fs::OpenOptions::new() - .read(true) - .write(true) - .create(true) - .truncate(true) - .open(proxy_contract_dir.join(SRC_DIR).join(MAIN_ENTRY))?; - - let contract_str = generate_proxy_contract_src(owner_addr, impl_contract_id); - write!(f, "{}", contract_str)?; Ok(proxy_contract_dir) } @@ -198,24 +80,3 @@ 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) -} diff --git a/forc-plugins/forc-client/tests/deploy.rs b/forc-plugins/forc-client/tests/deploy.rs index e5009740929..5d5842172c2 100644 --- a/forc-plugins/forc-client/tests/deploy.rs +++ b/forc-plugins/forc-client/tests/deploy.rs @@ -292,6 +292,97 @@ async fn test_proxy_contract_re_routes_call() { node.kill().unwrap(); } +#[tokio::test] +async fn test_proxy_contract_update_target() { + 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 proxy = Proxy { + enabled: true, + address: None, + }; + patch_manifest_file_with_proxy_table(tmp_dir.path(), proxy).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.clone()), + target: None, + testnet: false, + }; + // Make a contract call into proxy contract, and check if the initial + // contract returns a true. + let provider = Provider::connect(&node_url).await.unwrap(); + let secret_key = SecretKey::from_str(forc_client::constants::DEFAULT_PRIVATE_KEY).unwrap(); + let wallet_unlocked = WalletUnlocked::new_from_private_key(secret_key, Some(provider)); + 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(); + // At this point we deployed a contract with proxy. + let proxy_contract_id = contract_ids[0].proxy.unwrap(); + let impl_contract_id = contract_ids[0].id; + abigen!(Contract( + name = "ProxyContract", + abi = "forc-plugins/forc-client/abi/proxy_contract-abi.json" + )); + let p_instance = ProxyContract::new(proxy_contract_id, wallet_unlocked); + let current_target = p_instance + .methods() + .proxy_target() + .simulate() + .await + .unwrap() + .value + .unwrap(); + assert_eq!(current_target, impl_contract_id); + + update_main_sw(tmp_dir.path()).unwrap(); + let target = NodeTarget { + node_url: Some(node_url.clone()), + target: None, + testnet: false, + }; + let pkg = Pkg { + path: Some(tmp_dir.path().display().to_string()), + ..Default::default() + }; + + 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(); + // proxy contract id should be the same. + let proxy_contract_after_update = contract_ids[0].proxy.unwrap(); + assert_eq!(proxy_contract_id, proxy_contract_after_update); + let impl_contract_id_after_update = contract_ids[0].id; + assert!(impl_contract_id != impl_contract_id_after_update); + let current_target = p_instance + .methods() + .proxy_target() + .simulate() + .await + .unwrap() + .value + .unwrap(); + assert_eq!(current_target, impl_contract_id_after_update); + node.kill().unwrap(); +} + #[tokio::test] async fn test_non_owner_fails_to_set_target() { let (mut node, port) = run_node(); From 34f0f02d3ef861d076e4174fc3941b75a7c57a0e Mon Sep 17 00:00:00 2001 From: Kaya Gokalp Date: Mon, 5 Aug 2024 19:07:01 +0300 Subject: [PATCH 32/37] remove debug prompts --- forc-plugins/forc-client/src/op/deploy.rs | 8 +++----- forc-plugins/forc-client/src/util/pkg.rs | 9 +++++++-- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/forc-plugins/forc-client/src/op/deploy.rs b/forc-plugins/forc-client/src/op/deploy.rs index 222e4422fb4..6f73b978d42 100644 --- a/forc-plugins/forc-client/src/op/deploy.rs +++ b/forc-plugins/forc-client/src/op/deploy.rs @@ -156,9 +156,9 @@ async fn deploy_new_proxy( .deploy(&wallet, TxPolicies::default()) .await?; + println_action_green("Initializing", &format!("proxy contract for {pkg_name}")); let instance = ProxyContract::new(&proxy_contract_id, wallet); - let response = instance.methods().initialize_proxy().call().await?; - println!("{response:?}"); + instance.methods().initialize_proxy().call().await?; println_action_green("Initialized", &format!("proxy contract for {pkg_name}")); Ok(proxy_contract_id.into()) } @@ -294,14 +294,12 @@ pub async fn deploy(command: cmd::Deploy) -> Result> { address: None, }) => { let pkg_name = &pkg.descriptor.name; - println_action_green("Creating", &format!("proxy contract for {pkg_name}")); + println_action_green("Deploying", &format!("proxy contract for {pkg_name}")); // Deploy a new proxy contract. let deployed_proxy_contract = deploy_new_proxy(pkg_name, &deployed_contract_id, &provider, &signing_key) .await?; - println!("--==-- deployed proxy contract {deployed_proxy_contract:?}"); - // Update manifest file such that the proxy address field points to the new proxy contract. update_proxy_address_in_manifest( &format!("0x{}", deployed_proxy_contract), diff --git a/forc-plugins/forc-client/src/util/pkg.rs b/forc-plugins/forc-client/src/util/pkg.rs index 39968bb8cc2..dd9de80d9db 100644 --- a/forc-plugins/forc-client/src/util/pkg.rs +++ b/forc-plugins/forc-client/src/util/pkg.rs @@ -13,6 +13,8 @@ pub const PROXY_CONTRACT_FOLDER_NAME: &str = ".generated_proxy_contracts"; pub const PROXY_CONTRACT_BIN: &[u8] = include_bytes!("../../abi/proxy_contract.bin"); pub const PROXY_CONTRACT_STORAGE_SLOTS: &str = include_str!("../../abi/proxy_contract-storage_slots.json"); +pub const PROXY_BIN_FILE_NAME: &str = "proxy.bin"; +pub const PROXY_STORAGE_SLOTS_FILE_NAME: &str = "proxy-storage_slots.json"; /// 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. @@ -43,9 +45,12 @@ pub(crate) fn create_proxy_contract(pkg_name: &str) -> Result { .join(PROXY_CONTRACT_FOLDER_NAME) .join(pkg_name); std::fs::create_dir_all(&proxy_contract_dir)?; - std::fs::write(proxy_contract_dir.join("proxy.bin"), PROXY_CONTRACT_BIN)?; std::fs::write( - proxy_contract_dir.join("proxy-storage_slots.json"), + proxy_contract_dir.join(PROXY_BIN_FILE_NAME), + PROXY_CONTRACT_BIN, + )?; + std::fs::write( + proxy_contract_dir.join(PROXY_STORAGE_SLOTS_FILE_NAME), PROXY_CONTRACT_STORAGE_SLOTS, )?; From 9253ba73b13ec748eb84d68df3feb9828fb31145 Mon Sep 17 00:00:00 2001 From: Kaya Gokalp Date: Tue, 6 Aug 2024 00:46:57 +0300 Subject: [PATCH 33/37] updated test proxy id --- forc-plugins/forc-client/tests/deploy.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forc-plugins/forc-client/tests/deploy.rs b/forc-plugins/forc-client/tests/deploy.rs index 5d5842172c2..a8513312b56 100644 --- a/forc-plugins/forc-client/tests/deploy.rs +++ b/forc-plugins/forc-client/tests/deploy.rs @@ -190,7 +190,7 @@ async fn test_deploy_fresh_proxy() { .unwrap(), proxy: Some( ContractId::from_str( - "c8dad72a0b0e67f2fc1b92fc603ff1ea3ddc53eddf8eb0ae4bc9effd0bfe519e", + "3da2f8ee967c62496db4b71df0acd7c3fea1e494fee1de0cd16e7abd22e6057f", ) .unwrap(), ), From ba2f92e3d1263ff648bba87f6a4e158dec01ed45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kaya=20G=C3=B6kalp?= Date: Mon, 5 Aug 2024 14:48:29 -0700 Subject: [PATCH 34/37] remove comment Co-authored-by: Sophie Dankel <47993817+sdankel@users.noreply.github.com> --- forc-plugins/forc-client/src/util/tx.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/forc-plugins/forc-client/src/util/tx.rs b/forc-plugins/forc-client/src/util/tx.rs index c71a18fa022..705def88342 100644 --- a/forc-plugins/forc-client/src/util/tx.rs +++ b/forc-plugins/forc-client/src/util/tx.rs @@ -271,7 +271,6 @@ pub async fn update_proxy_contract_target( 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? let result = proxy_contract .methods() .set_proxy_target(new_target) From 0aad88d458f585598ccf9b9488cf37ce64707fd4 Mon Sep 17 00:00:00 2001 From: Kaya Gokalp Date: Tue, 6 Aug 2024 16:26:30 +0300 Subject: [PATCH 35/37] rename abi to proxy_abi, remove duplicate test --- forc-plugins/forc-client/proxy_abi/README.md | 8 ++ .../proxy_contract-abi.json | 0 .../proxy_contract-storage_slots.json | 0 .../{abi => proxy_abi}/proxy_contract.bin | Bin 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 | 2 +- forc-plugins/forc-client/tests/deploy.rs | 93 +----------------- 8 files changed, 13 insertions(+), 96 deletions(-) create mode 100644 forc-plugins/forc-client/proxy_abi/README.md rename forc-plugins/forc-client/{abi => proxy_abi}/proxy_contract-abi.json (100%) rename forc-plugins/forc-client/{abi => proxy_abi}/proxy_contract-storage_slots.json (100%) rename forc-plugins/forc-client/{abi => proxy_abi}/proxy_contract.bin (100%) diff --git a/forc-plugins/forc-client/proxy_abi/README.md b/forc-plugins/forc-client/proxy_abi/README.md new file mode 100644 index 00000000000..f32b5df34bd --- /dev/null +++ b/forc-plugins/forc-client/proxy_abi/README.md @@ -0,0 +1,8 @@ +# Proxy Contract + +This folder contains, pre-built version of the owned proxy contract, its abi and `storage-slots.json` file. + +*contract url*: [sway-standard-implementation/src-14/owned_proxy](https://github.com/FuelLabs/sway-standard-implementations/tree/61fd4ad8f69d21cec0d5cd8135bdc4495e0c125c). +*commit hash*: `61fd4ad8f69d21cec0d5cd8135bdc4495e0c125c` +*forc version*: `v0.62.0` +*build command*: `forc build --release` diff --git a/forc-plugins/forc-client/abi/proxy_contract-abi.json b/forc-plugins/forc-client/proxy_abi/proxy_contract-abi.json similarity index 100% rename from forc-plugins/forc-client/abi/proxy_contract-abi.json rename to forc-plugins/forc-client/proxy_abi/proxy_contract-abi.json diff --git a/forc-plugins/forc-client/abi/proxy_contract-storage_slots.json b/forc-plugins/forc-client/proxy_abi/proxy_contract-storage_slots.json similarity index 100% rename from forc-plugins/forc-client/abi/proxy_contract-storage_slots.json rename to forc-plugins/forc-client/proxy_abi/proxy_contract-storage_slots.json diff --git a/forc-plugins/forc-client/abi/proxy_contract.bin b/forc-plugins/forc-client/proxy_abi/proxy_contract.bin similarity index 100% rename from forc-plugins/forc-client/abi/proxy_contract.bin rename to forc-plugins/forc-client/proxy_abi/proxy_contract.bin diff --git a/forc-plugins/forc-client/src/op/deploy.rs b/forc-plugins/forc-client/src/op/deploy.rs index 6f73b978d42..18d857fc804 100644 --- a/forc-plugins/forc-client/src/op/deploy.rs +++ b/forc-plugins/forc-client/src/op/deploy.rs @@ -131,7 +131,7 @@ async fn deploy_new_proxy( ) -> Result { fuels::macros::abigen!(Contract( name = "ProxyContract", - abi = "forc-plugins/forc-client/abi/proxy_contract-abi.json" + abi = "forc-plugins/forc-client/proxy_abi/proxy_contract-abi.json" )); let proxy_dir_output = create_proxy_contract(pkg_name)?; let address = bech32_from_secret(signing_key)?; diff --git a/forc-plugins/forc-client/src/util/pkg.rs b/forc-plugins/forc-client/src/util/pkg.rs index dd9de80d9db..94a068a5bea 100644 --- a/forc-plugins/forc-client/src/util/pkg.rs +++ b/forc-plugins/forc-client/src/util/pkg.rs @@ -10,9 +10,9 @@ 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"; -pub const PROXY_CONTRACT_BIN: &[u8] = include_bytes!("../../abi/proxy_contract.bin"); +pub const PROXY_CONTRACT_BIN: &[u8] = include_bytes!("../../proxy_abi/proxy_contract.bin"); pub const PROXY_CONTRACT_STORAGE_SLOTS: &str = - include_str!("../../abi/proxy_contract-storage_slots.json"); + include_str!("../../proxy_abi/proxy_contract-storage_slots.json"); pub const PROXY_BIN_FILE_NAME: &str = "proxy.bin"; pub const PROXY_STORAGE_SLOTS_FILE_NAME: &str = "proxy-storage_slots.json"; diff --git a/forc-plugins/forc-client/src/util/tx.rs b/forc-plugins/forc-client/src/util/tx.rs index 705def88342..f71501a0941 100644 --- a/forc-plugins/forc-client/src/util/tx.rs +++ b/forc-plugins/forc-client/src/util/tx.rs @@ -264,7 +264,7 @@ pub async fn update_proxy_contract_target( ) -> Result> { abigen!(Contract( name = "ProxyContract", - abi = "forc-plugins/forc-client/abi/proxy_contract-abi.json" + abi = "forc-plugins/forc-client/proxy_abi/proxy_contract-abi.json" )); let wallet = WalletUnlocked::new_from_private_key(secret_key, Some(provider.clone())); diff --git a/forc-plugins/forc-client/tests/deploy.rs b/forc-plugins/forc-client/tests/deploy.rs index a8513312b56..8f45d4a3478 100644 --- a/forc-plugins/forc-client/tests/deploy.rs +++ b/forc-plugins/forc-client/tests/deploy.rs @@ -292,97 +292,6 @@ async fn test_proxy_contract_re_routes_call() { node.kill().unwrap(); } -#[tokio::test] -async fn test_proxy_contract_update_target() { - 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 proxy = Proxy { - enabled: true, - address: None, - }; - patch_manifest_file_with_proxy_table(tmp_dir.path(), proxy).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.clone()), - target: None, - testnet: false, - }; - // Make a contract call into proxy contract, and check if the initial - // contract returns a true. - let provider = Provider::connect(&node_url).await.unwrap(); - let secret_key = SecretKey::from_str(forc_client::constants::DEFAULT_PRIVATE_KEY).unwrap(); - let wallet_unlocked = WalletUnlocked::new_from_private_key(secret_key, Some(provider)); - 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(); - // At this point we deployed a contract with proxy. - let proxy_contract_id = contract_ids[0].proxy.unwrap(); - let impl_contract_id = contract_ids[0].id; - abigen!(Contract( - name = "ProxyContract", - abi = "forc-plugins/forc-client/abi/proxy_contract-abi.json" - )); - let p_instance = ProxyContract::new(proxy_contract_id, wallet_unlocked); - let current_target = p_instance - .methods() - .proxy_target() - .simulate() - .await - .unwrap() - .value - .unwrap(); - assert_eq!(current_target, impl_contract_id); - - update_main_sw(tmp_dir.path()).unwrap(); - let target = NodeTarget { - node_url: Some(node_url.clone()), - target: None, - testnet: false, - }; - let pkg = Pkg { - path: Some(tmp_dir.path().display().to_string()), - ..Default::default() - }; - - 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(); - // proxy contract id should be the same. - let proxy_contract_after_update = contract_ids[0].proxy.unwrap(); - assert_eq!(proxy_contract_id, proxy_contract_after_update); - let impl_contract_id_after_update = contract_ids[0].id; - assert!(impl_contract_id != impl_contract_id_after_update); - let current_target = p_instance - .methods() - .proxy_target() - .simulate() - .await - .unwrap() - .value - .unwrap(); - assert_eq!(current_target, impl_contract_id_after_update); - node.kill().unwrap(); -} - #[tokio::test] async fn test_non_owner_fails_to_set_target() { let (mut node, port) = run_node(); @@ -444,7 +353,7 @@ async fn test_non_owner_fails_to_set_target() { let dummy_contract_id_target = ContractId::default(); abigen!(Contract( name = "ProxyContract", - abi = "forc-plugins/forc-client/abi/proxy_contract-abi.json" + abi = "forc-plugins/forc-client/proxy_abi/proxy_contract-abi.json" )); // Try to change target of the proxy with a random wallet which is not the owner of the proxy. From 8a1ab5dcab8f5408a67648fbf4835e4ec4c90849 Mon Sep 17 00:00:00 2001 From: Kaya Gokalp Date: Tue, 6 Aug 2024 16:47:21 +0300 Subject: [PATCH 36/37] remove , --- forc-plugins/forc-client/proxy_abi/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forc-plugins/forc-client/proxy_abi/README.md b/forc-plugins/forc-client/proxy_abi/README.md index f32b5df34bd..47a9b800ff7 100644 --- a/forc-plugins/forc-client/proxy_abi/README.md +++ b/forc-plugins/forc-client/proxy_abi/README.md @@ -1,6 +1,6 @@ # Proxy Contract -This folder contains, pre-built version of the owned proxy contract, its abi and `storage-slots.json` file. +This folder contains pre-built version of the owned proxy contract, its abi and `storage-slots.json` file. *contract url*: [sway-standard-implementation/src-14/owned_proxy](https://github.com/FuelLabs/sway-standard-implementations/tree/61fd4ad8f69d21cec0d5cd8135bdc4495e0c125c). *commit hash*: `61fd4ad8f69d21cec0d5cd8135bdc4495e0c125c` From f302d7404377a96e1720b470d0301d08cbc49fd2 Mon Sep 17 00:00:00 2001 From: Sophie Date: Tue, 6 Aug 2024 11:49:07 -0700 Subject: [PATCH 37/37] Fix tx count and display deployed url for proxy --- forc-plugins/forc-client/src/op/deploy.rs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/forc-plugins/forc-client/src/op/deploy.rs b/forc-plugins/forc-client/src/op/deploy.rs index 18d857fc804..f41fd80532c 100644 --- a/forc-plugins/forc-client/src/op/deploy.rs +++ b/forc-plugins/forc-client/src/op/deploy.rs @@ -156,7 +156,18 @@ async fn deploy_new_proxy( .deploy(&wallet, TxPolicies::default()) .await?; - println_action_green("Initializing", &format!("proxy contract for {pkg_name}")); + let chain_info = provider.chain_info().await?; + let target = Target::from_str(&chain_info.name).unwrap_or(Target::testnet()); + let contract_url = match target.explorer_url() { + Some(explorer_url) => format!("{explorer_url}/contract/0x"), + None => "".to_string(), + }; + + println_action_green( + "Finished", + &format!("deploying proxy contract for {pkg_name} {contract_url}{proxy_contract_id}"), + ); + let instance = ProxyContract::new(&proxy_contract_id, wallet); instance.methods().initialize_proxy().call().await?; println_action_green("Initialized", &format!("proxy contract for {pkg_name}")); @@ -294,7 +305,6 @@ pub async fn deploy(command: cmd::Deploy) -> Result> { address: None, }) => { let pkg_name = &pkg.descriptor.name; - println_action_green("Deploying", &format!("proxy contract for {pkg_name}")); // Deploy a new proxy contract. let deployed_proxy_contract = deploy_new_proxy(pkg_name, &deployed_contract_id, &provider, &signing_key) @@ -337,6 +347,7 @@ async fn confirm_transaction_details( enabled: true, address, }) => { + tx_count += 1; if address.is_some() { " + update proxy" } else {