From 69082ea56b2b49d504438e9bb90ff4df074aa580 Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Tue, 29 Jul 2025 05:48:38 -0700 Subject: [PATCH 1/5] publish: add safe --- Cargo.lock | 7 ++ src/main.rs | 17 ++++- src/publish/mod.rs | 166 +++++++++++++++++++++++++++++++++------------ 3 files changed, 143 insertions(+), 47 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2581b55f..b993ec06 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2316,6 +2316,7 @@ dependencies = [ "tracing-appender", "tracing-error", "tracing-subscriber", + "urlencoding", "walkdir", "wit-bindgen", "zip", @@ -4314,6 +4315,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf-8" version = "0.7.6" diff --git a/src/main.rs b/src/main.rs index 84068641..495b022b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -403,6 +403,9 @@ async fn execute( .and_then(|kp| Some(PathBuf::from(kp))); let ledger = matches.get_one::("LEDGER").unwrap(); let trezor = matches.get_one::("TREZOR").unwrap(); + let safe = matches + .get_one::("SAFE") + .and_then(|gs| Some(gs.as_str())); let rpc_uri = matches.get_one::("RPC_URI").unwrap(); let real = matches.get_one::("REAL").unwrap(); let unpublish = matches.get_one::("UNPUBLISH").unwrap(); @@ -421,6 +424,7 @@ async fn execute( keystore_path, ledger, trezor, + safe, rpc_uri, real, unpublish, @@ -1110,21 +1114,28 @@ async fn make_app(current_dir: &std::ffi::OsString) -> Result { .action(ArgAction::Set) .short('k') .long("keystore-path") - .help("Path to private key keystore (choose 1 of `k`, `l`, `t`)") // TODO: add link to docs? + .help("Path to private key keystore (choose 1 of `k`, `l`, `t`, `s`)") // TODO: add link to docs? .required(false) ) .arg(Arg::new("LEDGER") .action(ArgAction::SetTrue) .short('l') .long("ledger") - .help("Use Ledger private key (choose 1 of `k`, `l`, `t`)") + .help("Use Ledger private key (choose 1 of `k`, `l`, `t`, `s`)") .required(false) ) .arg(Arg::new("TREZOR") .action(ArgAction::SetTrue) .short('t') .long("trezor") - .help("Use Trezor private key (choose 1 of `k`, `l`, `t`)") + .help("Use Trezor private key (choose 1 of `k`, `l`, `t`, `s`)") + .required(false) + ) + .arg(Arg::new("SAFE") + .action(ArgAction::Set) + .short('s') + .long("safe") + .help("Safe contract address (choose 1 of `k`, `l`, `t`, `s`)") .required(false) ) .arg(Arg::new("URI") diff --git a/src/publish/mod.rs b/src/publish/mod.rs index ef72f055..8c6be900 100644 --- a/src/publish/mod.rs +++ b/src/publish/mod.rs @@ -308,40 +308,20 @@ async fn prepare_hypermap_put( Ok((to, call)) } + #[instrument(level = "trace", skip_all)] -pub async fn execute( +pub async fn build_tx( package_dir: &Path, metadata_uri: &str, - keystore_path: Option, - ledger: &bool, - trezor: &bool, - rpc_uri: &str, + provider: &RootProvider, real: &bool, unpublish: &bool, + wallet_address: Address, + chain_id: u64, gas_limit: u64, max_priority_fee_per_gas: Option, max_fee_per_gas: Option, - mock: &bool, -) -> Result<()> { - if !package_dir.join("pkg").exists() { - return Err(eyre!( - "Required `pkg/` dir not found within given input dir {:?} (or cwd, if none given). Please re-run targeting a package.", - package_dir, - )); - } - - let chain_id = if *real { REAL_CHAIN_ID } else { FAKE_CHAIN_ID }; - let (wallet_address, wallet) = match (keystore_path, *ledger, *trezor) { - (Some(ref kp), false, false) => read_keystore(kp)?, - (None, true, false) => read_ledger(chain_id).await?, - (None, false, true) => read_trezor(chain_id).await?, - _ => { - return Err(eyre!( - "Must supply one and only one of `--keystore_path`, `--ledger`, or `--trezor`" - )) - } - }; - +) -> Result<(String, Address, Vec, TransactionRequest)> { let metadata = read_and_update_metadata(package_dir)?; let name = metadata.name.clone().unwrap(); @@ -361,9 +341,6 @@ pub async fn execute( let metadata_hash = check_remote_metadata(&metadata, metadata_uri, package_dir).await?; check_pkg_hash(&metadata, package_dir, metadata_uri)?; - let ws = WsConnect::new(rpc_uri); - let provider: RootProvider = ProviderBuilder::default().on_ws(ws).await?; - let hypermap = Address::from_str(if *real { REAL_KIMAP_ADDRESS } else { @@ -410,7 +387,7 @@ pub async fn execute( let tx = TransactionRequest::default() .to(to) - .input(TransactionInput::new(call.into())) + .input(TransactionInput::new(call.clone().into())) .nonce(nonce) .with_chain_id(chain_id) .with_gas_limit(gas_limit) @@ -419,21 +396,122 @@ pub async fn execute( ) .with_max_fee_per_gas(max_fee_per_gas.unwrap_or_else(|| suggested_max_fee_per_gas)); - let tx_envelope = tx.build(&wallet).await?; - let tx_encoded = tx_envelope.encoded_2718(); - if *mock { - info!( - "{} {name} tx mock successful", - if *unpublish { "unpublish" } else { "publish" } - ); + Ok((name, to, call, tx)) +} + +#[instrument(level = "trace", skip_all)] +pub async fn execute( + package_dir: &Path, + metadata_uri: &str, + keystore_path: Option, + ledger: &bool, + trezor: &bool, + safe: Option<&str>, + rpc_uri: &str, + real: &bool, + unpublish: &bool, + gas_limit: u64, + max_priority_fee_per_gas: Option, + max_fee_per_gas: Option, + mock: &bool, +) -> Result<()> { + if !package_dir.join("pkg").exists() { + return Err(eyre!( + "Required `pkg/` dir not found within given input dir {:?} (or cwd, if none given). Please re-run targeting a package.", + package_dir, + )); + } + + let chain_id = if *real { REAL_CHAIN_ID } else { FAKE_CHAIN_ID }; + + // Check if using Gnosis Safe mode + let is_safe_mode = safe.is_some(); + + let (wallet_address, wallet) = if is_safe_mode { + // In Safe mode, we don't need a wallet for signing + // Parse the Safe address provided by the user + let safe_address = Address::from_str(safe.unwrap())?; + (safe_address, None) } else { - let tx = provider.send_raw_transaction(&tx_encoded).await?; - let tx_hash = format!("{:?}", tx.tx_hash()); - let link = make_remote_link(&format!("https://basescan.org/tx/{tx_hash}"), &tx_hash); - info!( - "{} {name} tx sent: {link}", - if *unpublish { "unpublish" } else { "publish" } - ); + // Traditional wallet mode + let (addr, wallet) = match (keystore_path, *ledger, *trezor) { + (Some(ref kp), false, false) => read_keystore(kp)?, + (None, true, false) => read_ledger(chain_id).await?, + (None, false, true) => read_trezor(chain_id).await?, + _ => { + return Err(eyre!( + "Must supply one and only one of `--keystore_path`, `--ledger`, `--trezor`, or `--safe`" + )) + } + }; + (addr, Some(wallet)) + }; + + let ws = WsConnect::new(rpc_uri); + let provider: RootProvider = ProviderBuilder::default().on_ws(ws).await?; + + let (name, to, call, tx) = build_tx( + package_dir, + metadata_uri, + &provider, + real, + unpublish, + wallet_address, + chain_id, + gas_limit, + max_priority_fee_per_gas, + max_fee_per_gas, + ).await?; + + if is_safe_mode { + // Generate Safe transaction data + let tx_data = hex::encode(call); + + // TODO: can we get URL working? If so this is by far preferable + //// Create Safe App URL - always use Base chain (8453) + //let safe_url = format!( + // "https://app.safe.global/base:{}/transactions/tx?safe={}&to={}&value=0&data=0x{}", + // wallet_address, + // wallet_address, + // to, + // tx_data + //); + + info!("=== Gnosis Safe Transaction Data ==="); + // TODO: can we get URL working? If so this is by far preferable + //info!("Safe App URL (click or copy):"); + //info!("{}", make_remote_link(&safe_url, &safe_url)); + //info!(""); + info!("Manual Steps:"); + info!("1. Go to your Safe at https://app.safe.global"); + info!("2. Click \"New Transaction\" → \"Transaction Builder\""); + info!("3. Enter the contract address in \"Enter Address or ENS Name\": {}", to); + info!("4. Toggle \"Custom data\""); + info!("5. Put in \"ETH value\": 0"); + info!("6. Paste the transaction data in \"Data (Hex encoded)\": 0x{}", tx_data); + info!("7. \"Add new transaction\" -> \"Create Batch\" -> \"Simulate\""); + info!("8. If simulation passes, \"Send Batch\""); + info!("9. Collect signatures from other Safe owners"); + info!("10. Execute once threshold is reached (transaction only goes live in this final step)"); + } else { + // Traditional wallet signing flow + let wallet = wallet.unwrap(); + let tx_envelope = tx.build(&wallet).await?; + let tx_encoded = tx_envelope.encoded_2718(); + if *mock { + info!( + "{} {name} tx mock successful", + if *unpublish { "unpublish" } else { "publish" } + ); + } else { + let tx = provider.send_raw_transaction(&tx_encoded).await?; + let tx_hash = format!("{:?}", tx.tx_hash()); + let link = make_remote_link(&format!("https://basescan.org/tx/{tx_hash}"), &tx_hash); + info!( + "{} {name} tx sent: {link}", + if *unpublish { "unpublish" } else { "publish" } + ); + } } Ok(()) } From 9f19d4058b0eb48009d0835a95c89c3a36065da5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 29 Jul 2025 12:49:07 +0000 Subject: [PATCH 2/5] Format Rust code using rustfmt --- src/publish/mod.rs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/publish/mod.rs b/src/publish/mod.rs index 8c6be900..7df5d874 100644 --- a/src/publish/mod.rs +++ b/src/publish/mod.rs @@ -308,7 +308,6 @@ async fn prepare_hypermap_put( Ok((to, call)) } - #[instrument(level = "trace", skip_all)] pub async fn build_tx( package_dir: &Path, @@ -461,7 +460,8 @@ pub async fn execute( gas_limit, max_priority_fee_per_gas, max_fee_per_gas, - ).await?; + ) + .await?; if is_safe_mode { // Generate Safe transaction data @@ -485,14 +485,22 @@ pub async fn execute( info!("Manual Steps:"); info!("1. Go to your Safe at https://app.safe.global"); info!("2. Click \"New Transaction\" → \"Transaction Builder\""); - info!("3. Enter the contract address in \"Enter Address or ENS Name\": {}", to); + info!( + "3. Enter the contract address in \"Enter Address or ENS Name\": {}", + to + ); info!("4. Toggle \"Custom data\""); info!("5. Put in \"ETH value\": 0"); - info!("6. Paste the transaction data in \"Data (Hex encoded)\": 0x{}", tx_data); + info!( + "6. Paste the transaction data in \"Data (Hex encoded)\": 0x{}", + tx_data + ); info!("7. \"Add new transaction\" -> \"Create Batch\" -> \"Simulate\""); info!("8. If simulation passes, \"Send Batch\""); info!("9. Collect signatures from other Safe owners"); - info!("10. Execute once threshold is reached (transaction only goes live in this final step)"); + info!( + "10. Execute once threshold is reached (transaction only goes live in this final step)" + ); } else { // Traditional wallet signing flow let wallet = wallet.unwrap(); From 2db75af26757a7bee9b2f3bba94b59f0807c0c12 Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Tue, 29 Jul 2025 09:04:28 -0700 Subject: [PATCH 3/5] publish: move some logic around --- Cargo.lock | 7 ------ src/publish/mod.rs | 54 +++++++++++++++++++++++++--------------------- 2 files changed, 29 insertions(+), 32 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b993ec06..2581b55f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2316,7 +2316,6 @@ dependencies = [ "tracing-appender", "tracing-error", "tracing-subscriber", - "urlencoding", "walkdir", "wit-bindgen", "zip", @@ -4315,12 +4314,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "urlencoding" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" - [[package]] name = "utf-8" version = "0.7.6" diff --git a/src/publish/mod.rs b/src/publish/mod.rs index 7df5d874..a2447846 100644 --- a/src/publish/mod.rs +++ b/src/publish/mod.rs @@ -310,8 +310,10 @@ async fn prepare_hypermap_put( #[instrument(level = "trace", skip_all)] pub async fn build_tx( - package_dir: &Path, metadata_uri: &str, + metadata_hash: &str, + name: &str, + publisher: &str, provider: &RootProvider, real: &bool, unpublish: &bool, @@ -320,26 +322,7 @@ pub async fn build_tx( gas_limit: u64, max_priority_fee_per_gas: Option, max_fee_per_gas: Option, -) -> Result<(String, Address, Vec, TransactionRequest)> { - let metadata = read_and_update_metadata(package_dir)?; - - let name = metadata.name.clone().unwrap(); - let publisher = metadata.properties.publisher.clone(); - - if !is_hypermap_safe(&name, false) { - return Err(eyre!( - "The App Store requires package names have only lowercase letters, digits, and `-`s" - )); - } - if !is_hypermap_safe(&publisher, true) { - return Err(eyre!( - "The App Store requires publisher names have only lowercase letters, digits, `-`s, and `.`s" - )); - } - - let metadata_hash = check_remote_metadata(&metadata, metadata_uri, package_dir).await?; - check_pkg_hash(&metadata, package_dir, metadata_uri)?; - +) -> Result<(Address, Vec, TransactionRequest)> { let hypermap = Address::from_str(if *real { REAL_KIMAP_ADDRESS } else { @@ -367,7 +350,7 @@ pub async fn build_tx( prepare_hypermap_put( multicall, - name.clone(), + name.to_string(), &publisher, hypermap, &provider, @@ -395,7 +378,7 @@ pub async fn build_tx( ) .with_max_fee_per_gas(max_fee_per_gas.unwrap_or_else(|| suggested_max_fee_per_gas)); - Ok((name, to, call, tx)) + Ok((to, call, tx)) } #[instrument(level = "trace", skip_all)] @@ -421,6 +404,25 @@ pub async fn execute( )); } + let metadata = read_and_update_metadata(package_dir)?; + + let name = metadata.name.clone().unwrap(); + let publisher = metadata.properties.publisher.clone(); + + if !is_hypermap_safe(&name, false) { + return Err(eyre!( + "The App Store requires package names have only lowercase letters, digits, and `-`s" + )); + } + if !is_hypermap_safe(&publisher, true) { + return Err(eyre!( + "The App Store requires publisher names have only lowercase letters, digits, `-`s, and `.`s" + )); + } + + let metadata_hash = check_remote_metadata(&metadata, metadata_uri, package_dir).await?; + check_pkg_hash(&metadata, package_dir, metadata_uri)?; + let chain_id = if *real { REAL_CHAIN_ID } else { FAKE_CHAIN_ID }; // Check if using Gnosis Safe mode @@ -449,9 +451,11 @@ pub async fn execute( let ws = WsConnect::new(rpc_uri); let provider: RootProvider = ProviderBuilder::default().on_ws(ws).await?; - let (name, to, call, tx) = build_tx( - package_dir, + let (to, call, tx) = build_tx( metadata_uri, + &metadata_hash, + &name, + &publisher, &provider, real, unpublish, From b4e6d8dc8b83e8454364ff6ef6274ecf72909395 Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Tue, 29 Jul 2025 09:13:57 -0700 Subject: [PATCH 4/5] publish: change some var names --- src/publish/mod.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/publish/mod.rs b/src/publish/mod.rs index a2447846..e99a2484 100644 --- a/src/publish/mod.rs +++ b/src/publish/mod.rs @@ -425,10 +425,9 @@ pub async fn execute( let chain_id = if *real { REAL_CHAIN_ID } else { FAKE_CHAIN_ID }; - // Check if using Gnosis Safe mode - let is_safe_mode = safe.is_some(); + let is_safe_tx = safe.is_some(); - let (wallet_address, wallet) = if is_safe_mode { + let (wallet_address, wallet) = if is_safe_tx { // In Safe mode, we don't need a wallet for signing // Parse the Safe address provided by the user let safe_address = Address::from_str(safe.unwrap())?; @@ -467,7 +466,7 @@ pub async fn execute( ) .await?; - if is_safe_mode { + if is_safe_tx { // Generate Safe transaction data let tx_data = hex::encode(call); @@ -481,7 +480,7 @@ pub async fn execute( // tx_data //); - info!("=== Gnosis Safe Transaction Data ==="); + info!("=== Safe Transaction Data ==="); // TODO: can we get URL working? If so this is by far preferable //info!("Safe App URL (click or copy):"); //info!("{}", make_remote_link(&safe_url, &safe_url)); From 592903b30e35a98d79007d0067eb7282d8a74e9d Mon Sep 17 00:00:00 2001 From: hosted-fornet Date: Tue, 29 Jul 2025 09:14:11 -0700 Subject: [PATCH 5/5] publish: be less strict on unpublish checks --- src/publish/mod.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/publish/mod.rs b/src/publish/mod.rs index e99a2484..216bfa6c 100644 --- a/src/publish/mod.rs +++ b/src/publish/mod.rs @@ -421,7 +421,9 @@ pub async fn execute( } let metadata_hash = check_remote_metadata(&metadata, metadata_uri, package_dir).await?; - check_pkg_hash(&metadata, package_dir, metadata_uri)?; + if !unpublish { + check_pkg_hash(&metadata, package_dir, metadata_uri)?; + } let chain_id = if *real { REAL_CHAIN_ID } else { FAKE_CHAIN_ID };