Repository: OffchainLabs/cargo-stylus (language: rust)

Issue:
[feature request] add a subcommand for activation of previously deployed Stylus smart contract
Today, if i have deployed a Stylus smart contract to an Arbitrum chain using cargo-stylus, it gets activated as part of the deployment step. This is awesome but in either scenario where (1) a year has elapsed or (2) a Stylus upgrade has taken place on the chain, I will need to re-activate my Stylus contract that has already been deployed.

To do this, I have to manually call `activateProgram` on the `ArbWasm` precompile. 

I would like the "activation" part of my Stylus dev workflow to be a separate subcommand within cargo-stylus. The UX should be:

```
cargo-stylus activate \
      --private-key-path=$PRIVATE_KEY
      --address=0x123
```


Discussion / hints:

Fix patch:
diff --git a/check/src/activate.rs b/check/src/activate.rs
new file mode 100644
index 0000000..8a80481
--- /dev/null
+++ b/check/src/activate.rs
@@ -0,0 +1,87 @@
+// Copyright 2023-2024, Offchain Labs, Inc.
+// For licensing, see https://github.com/OffchainLabs/cargo-stylus/blob/stylus/licenses/COPYRIGHT.md
+
+use alloy_primitives::Address;
+use alloy_sol_macro::sol;
+use alloy_sol_types::SolCall;
+use cargo_stylus_util::color::{Color, DebugColor};
+use cargo_stylus_util::sys;
+use ethers::middleware::{Middleware, SignerMiddleware};
+use ethers::signers::Signer;
+use ethers::types::transaction::eip2718::TypedTransaction;
+use ethers::types::{Eip1559TransactionRequest, U256};
+use ethers::utils::format_units;
+use eyre::{bail, Context, Result};
+
+use crate::check::check_activate;
+use crate::constants::ARB_WASM_H160;
+use crate::macros::greyln;
+
+use crate::ActivateConfig;
+
+sol! {
+    interface ArbWasm {
+        function activateProgram(address program)
+            external
+            payable
+            returns (uint16 version, uint256 dataFee);
+    }
+}
+
+/// Activates an already deployed Stylus program by address.
+pub async fn activate_program(cfg: &ActivateConfig) -> Result<()> {
+    let provider = sys::new_provider(&cfg.common_cfg.endpoint)?;
+    let chain_id = provider
+        .get_chainid()
+        .await
+        .wrap_err("failed to get chain id")?;
+
+    let wallet = cfg.auth.wallet().wrap_err("failed to load wallet")?;
+    let wallet = wallet.with_chain_id(chain_id.as_u64());
+    let client = SignerMiddleware::new(provider.clone(), wallet);
+
+    let code = client.get_code(cfg.address, None).await?;
+    let data_fee = check_activate(code, cfg.address, &provider).await?;
+    let mut data_fee = alloy_ethers_typecast::alloy_u256_to_ethers(data_fee);
+
+    greyln!(
+        "obtained estimated activation data fee {}",
+        format_units(data_fee, "ether")?.debug_lavender()
+    );
+    greyln!(
+        "bumping estimated activation data fee by {}%",
+        cfg.data_fee_bump_percent.debug_lavender()
+    );
+    data_fee = bump_data_fee(data_fee, cfg.data_fee_bump_percent);
+
+    let program: Address = cfg.address.to_fixed_bytes().into();
+    let data = ArbWasm::activateProgramCall { program }.abi_encode();
+    let tx = Eip1559TransactionRequest::new()
+        .from(client.address())
+        .to(*ARB_WASM_H160)
+        .value(data_fee)
+        .data(data);
+    let tx = TypedTransaction::Eip1559(tx);
+    let tx = client.send_transaction(tx, None).await?;
+    match tx.await? {
+        Some(receipt) => {
+            greyln!(
+                "successfully activated program 0x{} with tx {}",
+                hex::encode(cfg.address),
+                hex::encode(receipt.transaction_hash).debug_lavender()
+            );
+        }
+        None => {
+            bail!(
+                "failed to fetch receipt for program activation {}",
+                cfg.address
+            );
+        }
+    }
+    Ok(())
+}
+
+fn bump_data_fee(fee: U256, pct: u64) -> U256 {
+    let num = 100 + pct;
+    fee * U256::from(num) / U256::from(100)
+}
diff --git a/check/src/cache.rs b/check/src/cache.rs
index b270587..607db6c 100644
--- a/check/src/cache.rs
+++ b/check/src/cache.rs
@@ -56,7 +56,7 @@ pub async fn cache_program(cfg: &CacheConfig) -> Result<()> {
     if cache_manager_addrs.is_empty() {
         bail!("no cache managers found in ArbWasmCache, perhaps the Stylus cache is not yet enabled on this chain");
     }
-    let cache_manager = cache_manager_addrs.last().unwrap().clone();
+    let cache_manager = *cache_manager_addrs.last().unwrap();
     let cache_manager = H160::from_slice(cache_manager.as_slice());
 
     let program: Address = cfg.address.to_fixed_bytes().into();
diff --git a/check/src/check.rs b/check/src/check.rs
index ebe2626..137a14d 100644
--- a/check/src/check.rs
+++ b/check/src/check.rs
@@ -241,7 +241,7 @@ async fn program_exists(codehash: B256, provider: &Provider<Http>) -> Result<boo
 }
 
 /// Checks program activation, returning the data fee.
-async fn check_activate(code: Bytes, address: H160, provider: &Provider<Http>) -> Result<U256> {
+pub async fn check_activate(code: Bytes, address: H160, provider: &Provider<Http>) -> Result<U256> {
     let program = Address::from(address.to_fixed_bytes());
     let data = ArbWasm::activateProgramCall { program }.abi_encode();
     let tx = Eip1559TransactionRequest::new()
diff --git a/check/src/docker.rs b/check/src/docker.rs
index 0c327d6..89a433d 100644
--- a/check/src/docker.rs
+++ b/check/src/docker.rs
@@ -41,7 +41,7 @@ a reproducible deployment, or opt out by using the --no-verify flag for local bu
 }
 
 fn create_image(version: &str) -> Result<()> {
-    let name = version_to_image_name(&version);
+    let name = version_to_image_name(version);
     if image_exists(&name)? {
         return Ok(());
     }
diff --git a/check/src/main.rs b/check/src/main.rs
index c7f60bf..a57b1c3 100644
--- a/check/src/main.rs
+++ b/check/src/main.rs
@@ -8,6 +8,7 @@ use std::fmt;
 use std::path::PathBuf;
 use tokio::runtime::Builder;
 
+mod activate;
 mod cache;
 mod check;
 mod constants;
@@ -50,6 +51,9 @@ enum Apis {
         #[arg(long)]
         json: bool,
     },
+    /// Activate an already deployed contract.
+    #[command(alias = "a")]
+    Activate(ActivateConfig),
     /// Cache a contract using the Stylus CacheManager for Arbitrum chains.
     Cache(CacheConfig),
     /// Check a contract.
@@ -98,6 +102,21 @@ pub struct CacheConfig {
     bid: Option<u64>,
 }
 
+#[derive(Args, Clone, Debug)]
+pub struct ActivateConfig {
+    #[command(flatten)]
+    common_cfg: CommonConfig,
+    /// Wallet source to use.
+    #[command(flatten)]
+    auth: AuthOpts,
+    /// Deployed Stylus program address to activate.
+    #[arg(long)]
+    address: H160,
+    /// Percent to bump the estimated activation data fee by. Default of 20%
+    #[arg(long, default_value = "20")]
+    data_fee_bump_percent: u64,
+}
+
 #[derive(Args, Clone, Debug)]
 pub struct CheckConfig {
     #[command(flatten)]
@@ -188,9 +207,9 @@ impl fmt::Display for CheckConfig {
         write!(
             f,
             "{} {} {} {}",
-            self.common_cfg.to_string(),
+            self.common_cfg,
             match &self.wasm_file {
-                Some(path) => format!("--wasm-file={}", path.display().to_string()),
+                Some(path) => format!("--wasm-file={}", path.display()),
                 None => "".to_string(),
             },
             match &self.program_address {
@@ -210,8 +229,8 @@ impl fmt::Display for DeployConfig {
         write!(
             f,
             "{} {} {}",
-            self.check_config.to_string(),
-            self.auth.to_string(),
+            self.check_config,
+            self.auth,
             match self.estimate_gas {
                 true => "--estimate-gas".to_string(),
                 false => "".to_string(),
@@ -226,7 +245,7 @@ impl fmt::Display for AuthOpts {
             f,
             "{} {} {} {}",
             match &self.private_key_path {
-                Some(path) => format!("--private-key-path={}", path.display().to_string()),
+                Some(path) => format!("--private-key-path={}", path.display()),
                 None => "".to_string(),
             },
             match &self.private_key {
@@ -238,7 +257,7 @@ impl fmt::Display for AuthOpts {
                 None => "".to_string(),
             },
             match &self.keystore_password_path {
-                Some(path) => format!("--keystore-password-path={}", path.display().to_string()),
+                Some(path) => format!("--keystore-password-path={}", path.display()),
                 None => "".to_string(),
             }
         )
@@ -250,7 +269,7 @@ impl fmt::Display for VerifyConfig {
         write!(
             f,
             "{} --deployment-tx={} {}",
-            self.common_cfg.to_string(),
+            self.common_cfg,
             self.deployment_tx,
             match self.no_verify {
                 true => "--no-verify".to_string(),
@@ -280,6 +299,12 @@ async fn main_impl(args: Opts) -> Result<()> {
         Apis::ExportAbi { json, output } => {
             run!(export_abi::export_abi(output, json), "failed to export abi");
         }
+        Apis::Activate(config) => {
+            run!(
+                activate::activate_program(&config).await,
+                "stylus activate failed"
+            );
+        }
         Apis::Cache(config) => {
             run!(cache::cache_program(&config).await, "stylus cache failed");
         }
diff --git a/check/src/verify.rs b/check/src/verify.rs
index 668deee..8eee240 100644
--- a/check/src/verify.rs
+++ b/check/src/verify.rs
@@ -72,7 +72,7 @@ pub async fn verify(cfg: VerifyConfig) -> eyre::Result<()> {
     if deployment_data == *result.input {
         println!("Verified - program matches local project's file hashes");
     } else {
-        let tx_prelude = extract_program_evm_deployment_prelude(&*result.input);
+        let tx_prelude = extract_program_evm_deployment_prelude(&result.input);
         let reconstructed_prelude = extract_program_evm_deployment_prelude(&deployment_data);
         println!(
             "{} - program deployment did not verify against local project's file hashes",
@@ -94,7 +94,7 @@ pub async fn verify(cfg: VerifyConfig) -> eyre::Result<()> {
         );
         println!(
             "Compressed code length of deployment tx {}",
-            extract_compressed_wasm(&*result.input).len()
+            extract_compressed_wasm(&result.input).len()
         );
     }
     Ok(())
diff --git a/main/src/main.rs b/main/src/main.rs
index 175ce04..a6ee7d6 100644
--- a/main/src/main.rs
+++ b/main/src/main.rs
@@ -38,6 +38,9 @@ enum Subcommands {
     /// Check a contract.
     #[command(alias = "c")]
     Check,
+    /// Activate an already deployed program
+    #[command(alias = "a")]
+    Activate,
     /// Deploy a contract.
     #[command(alias = "d")]
     Deploy,
@@ -71,6 +74,7 @@ const COMMANDS: &[Binary] = &[
             "check",
             "deploy",
             "verify",
+            "a",
             "n",
             "x",
             "c",


Test patch: