diff --git a/crates/pop-cli/src/commands/call/parachain.rs b/crates/pop-cli/src/commands/call/parachain.rs index a6503d6e..9ed95da1 100644 --- a/crates/pop-cli/src/commands/call/parachain.rs +++ b/crates/pop-cli/src/commands/call/parachain.rs @@ -4,9 +4,10 @@ use crate::cli::{self, traits::*}; use anyhow::{anyhow, Result}; use clap::Args; use pop_parachains::{ - construct_extrinsic, encode_call_data, find_extrinsic_by_name, find_pallet_by_name, - parse_chain_metadata, set_up_client, sign_and_submit_extrinsic, supported_actions, Action, - DynamicPayload, Extrinsic, OnlineClient, Pallet, Param, SubstrateConfig, + construct_extrinsic, decode_call_data, encode_call_data, find_extrinsic_by_name, + find_pallet_by_name, parse_chain_metadata, set_up_client, sign_and_submit_extrinsic, + sign_and_submit_extrinsic_with_call_data, supported_actions, Action, DynamicPayload, Extrinsic, + OnlineClient, Pallet, Param, SubstrateConfig, }; use url::Url; @@ -35,6 +36,9 @@ pub struct CallParachainCommand { /// - with a password "//Alice///SECRET_PASSWORD" #[arg(short, long)] suri: Option, + /// SCALE encoded bytes representing the call data of the extrinsic. + #[arg(name = "call", long, conflicts_with_all = ["pallet", "extrinsic", "args"])] + call_data: Option, /// Automatically signs and submits the extrinsic without prompting for confirmation. #[arg(short('y'), long)] skip_confirm: bool, @@ -48,6 +52,16 @@ impl CallParachainCommand { let prompt_to_repeat_call = self.requires_user_input(); // Configure the chain. let chain = self.configure_chain(&mut cli).await?; + // Execute the call if call_data is provided. + if let Some(call_data) = self.call_data.as_ref() { + if let Err(e) = self + .submit_extrinsic_from_call_data(&chain.client, call_data, &mut cli::Cli) + .await + { + display_message(&e.to_string(), false, &mut cli::Cli)?; + } + return Ok(()); + } loop { // Configure the call based on command line arguments/call UI. let mut call = match self.configure_call(&chain, &mut cli).await { @@ -190,6 +204,45 @@ impl CallParachainCommand { } } + // Submits an extrinsic to the chain using the provided call data. + async fn submit_extrinsic_from_call_data( + &self, + client: &OnlineClient, + call_data: &str, + cli: &mut impl Cli, + ) -> Result<()> { + // Resolve who is signing the extrinsic. + let suri = match self.suri.as_ref() { + Some(suri) => suri, + None => &cli.input("Signer of the extrinsic:").default_input(DEFAULT_URI).interact()?, + }; + cli.info(format!("Encoded call data: {}", call_data))?; + if !self.skip_confirm && + !cli.confirm("Do you want to submit the extrinsic?") + .initial_value(true) + .interact()? + { + display_message( + &format!("Extrinsic with call data {call_data} was not submitted."), + false, + cli, + )?; + return Ok(()); + } + let spinner = cliclack::spinner(); + spinner.start("Signing and submitting the extrinsic, please wait..."); + let call_data_bytes = + decode_call_data(call_data).map_err(|err| anyhow!("{}", format!("{err:?}")))?; + let result = + sign_and_submit_extrinsic_with_call_data(client.clone(), call_data_bytes, suri) + .await + .map_err(|err| anyhow!("{}", format!("{err:?}")))?; + + spinner.stop(format!("Extrinsic submitted successfully with hash: {:?}", result)); + display_message("Call complete.", true, cli)?; + Ok(()) + } + // Resets specific fields to default values for a new call. fn reset_for_new_call(&mut self) { self.pallet = None; @@ -465,6 +518,7 @@ mod tests { url: None, suri: Some(DEFAULT_URI.to_string()), skip_confirm: false, + call_data: None, }; let mut cli = MockCli::new().expect_intro("Call a parachain").expect_input( "Which chain would you like to interact with?", @@ -484,6 +538,7 @@ mod tests { url: None, suri: None, skip_confirm: false, + call_data: None, }; let mut cli = MockCli::new() @@ -535,6 +590,7 @@ mod tests { url: None, suri: None, skip_confirm: false, + call_data: None, }; let mut cli = MockCli::new().expect_intro("Call a parachain").expect_input( @@ -639,6 +695,29 @@ mod tests { cli.verify() } + #[tokio::test] + async fn user_cancel_submit_extrinsic_from_call_data_works() -> Result<()> { + let client = set_up_client("wss://rpc1.paseo.popnetwork.xyz").await?; + let call_config = CallParachainCommand { + pallet: None, + extrinsic: None, + args: vec![].to_vec(), + url: Some(Url::parse("wss://rpc1.paseo.popnetwork.xyz")?), + suri: None, + skip_confirm: false, + call_data: Some("0x00000411".to_string()), + }; + let mut cli = MockCli::new() + .expect_input("Signer of the extrinsic:", "//Bob".into()) + .expect_confirm("Do you want to submit the extrinsic?", false) + .expect_outro_cancel("Extrinsic with call data 0x00000411 was not submitted."); + call_config + .submit_extrinsic_from_call_data(&client, "0x00000411", &mut cli) + .await?; + + cli.verify() + } + #[test] fn reset_for_new_call_works() -> Result<()> { let mut call_config = CallParachainCommand { @@ -648,6 +727,7 @@ mod tests { url: Some(Url::parse("wss://rpc1.paseo.popnetwork.xyz")?), suri: Some(DEFAULT_URI.to_string()), skip_confirm: false, + call_data: None, }; call_config.reset_for_new_call(); assert_eq!(call_config.pallet, None); @@ -665,6 +745,7 @@ mod tests { url: Some(Url::parse("wss://rpc1.paseo.popnetwork.xyz")?), suri: Some(DEFAULT_URI.to_string()), skip_confirm: false, + call_data: None, }; assert!(!call_config.requires_user_input()); call_config.pallet = None; diff --git a/crates/pop-cli/tests/parachain.rs b/crates/pop-cli/tests/parachain.rs index 62c25269..5fabadce 100644 --- a/crates/pop-cli/tests/parachain.rs +++ b/crates/pop-cli/tests/parachain.rs @@ -155,6 +155,24 @@ name = "collator-01" .assert() .success(); + // pop call parachain --call 0x00000411 --url ws://127.0.0.1:random_port --suri //Alice + // --skip-confirm + Command::cargo_bin("pop") + .unwrap() + .args(&[ + "call", + "parachain", + "--call", + "0x00000411", + "--url", + &localhost_url, + "--suri", + "//Alice", + "--skip-confirm", + ]) + .assert() + .success(); + assert!(cmd.try_wait().unwrap().is_none(), "the process should still be running"); // Stop the process Cmd::new("kill").args(["-s", "TERM", &cmd.id().to_string()]).spawn()?; diff --git a/crates/pop-parachains/src/call/mod.rs b/crates/pop-parachains/src/call/mod.rs index 5ca10cd0..2368c31f 100644 --- a/crates/pop-parachains/src/call/mod.rs +++ b/crates/pop-parachains/src/call/mod.rs @@ -73,6 +73,54 @@ pub fn encode_call_data( Ok(format!("0x{}", hex::encode(call_data))) } +/// Decodes a hex-encoded string into a vector of bytes representing the call data. +/// +/// # Arguments +/// * `call_data` - The hex-encoded string representing call data. +pub fn decode_call_data(call_data: &str) -> Result, Error> { + hex::decode(call_data.trim_start_matches("0x")) + .map_err(|e| Error::CallDataDecodingError(e.to_string())) +} + +// This struct implements the [`Payload`] trait and is used to submit +// pre-encoded SCALE call data directly, without the dynamic construction of transactions. +struct CallData(Vec); + +impl Payload for CallData { + fn encode_call_data_to( + &self, + _: &subxt::Metadata, + out: &mut Vec, + ) -> Result<(), subxt::ext::subxt_core::Error> { + out.extend_from_slice(&self.0); + Ok(()) + } +} + +/// Signs and submits a given extrinsic. +/// +/// # Arguments +/// * `client` - Reference to an `OnlineClient` connected to the chain. +/// * `call_data` - SCALE encoded bytes representing the extrinsic's call data. +/// * `suri` - The secret URI (e.g., mnemonic or private key) for signing the extrinsic. +pub async fn sign_and_submit_extrinsic_with_call_data( + client: OnlineClient, + call_data: Vec, + suri: &str, +) -> Result { + let signer = create_signer(suri)?; + let payload = CallData(call_data); + let result = client + .tx() + .sign_and_submit_then_watch_default(&payload, &signer) + .await + .map_err(|e| Error::ExtrinsicSubmissionError(format!("{:?}", e)))? + .wait_for_finalized_success() + .await + .map_err(|e| Error::ExtrinsicSubmissionError(format!("{:?}", e)))?; + Ok(format!("{:?}", result.extrinsic_hash())) +} + #[cfg(test)] mod tests { use super::*; @@ -125,6 +173,16 @@ mod tests { Ok(()) } + #[tokio::test] + async fn decode_call_data_works() -> Result<()> { + assert!(matches!(decode_call_data("wrongcalldata"), Err(Error::CallDataDecodingError(..)))); + let client = set_up_client("wss://rpc1.paseo.popnetwork.xyz").await?; + let extrinsic = construct_extrinsic("System", "remark", vec!["0x11".to_string()]).await?; + let expected_call_data = extrinsic.encode_call_data(&client.metadata())?; + assert_eq!(decode_call_data("0x00000411")?, expected_call_data); + Ok(()) + } + #[tokio::test] async fn sign_and_submit_wrong_extrinsic_fails() -> Result<()> { let client = set_up_client("wss://rpc1.paseo.popnetwork.xyz").await?; diff --git a/crates/pop-parachains/src/errors.rs b/crates/pop-parachains/src/errors.rs index fdef8ac7..ac9fe0f9 100644 --- a/crates/pop-parachains/src/errors.rs +++ b/crates/pop-parachains/src/errors.rs @@ -10,6 +10,9 @@ pub enum Error { Aborted, #[error("Anyhow error: {0}")] AnyhowError(#[from] anyhow::Error), + /// An error occurred while decoding the call data. + #[error("Failed to decode call data. {0}")] + CallDataDecodingError(String), /// An error occurred while encoding the call data. #[error("Failed to encode call data. {0}")] CallDataEncodingError(String), diff --git a/crates/pop-parachains/src/lib.rs b/crates/pop-parachains/src/lib.rs index 3e6da06a..00f24975 100644 --- a/crates/pop-parachains/src/lib.rs +++ b/crates/pop-parachains/src/lib.rs @@ -17,14 +17,14 @@ pub use build::{ generate_plain_chain_spec, generate_raw_chain_spec, is_supported, ChainSpec, }; pub use call::{ - construct_extrinsic, encode_call_data, + construct_extrinsic, decode_call_data, encode_call_data, metadata::{ action::{supported_actions, Action}, find_extrinsic_by_name, find_pallet_by_name, params::Param, parse_chain_metadata, Extrinsic, Pallet, }, - set_up_client, sign_and_submit_extrinsic, + set_up_client, sign_and_submit_extrinsic, sign_and_submit_extrinsic_with_call_data, }; pub use errors::Error; pub use indexmap::IndexSet;