From 73126fe09d5a3e80a7799edfb9f6e371a3308165 Mon Sep 17 00:00:00 2001 From: AlexD10S Date: Thu, 12 Dec 2024 15:23:26 +0100 Subject: [PATCH] feat: secure signing for call from call_data --- crates/pop-cli/src/commands/call/chain.rs | 153 +++++++++++++--------- 1 file changed, 89 insertions(+), 64 deletions(-) diff --git a/crates/pop-cli/src/commands/call/chain.rs b/crates/pop-cli/src/commands/call/chain.rs index b492fffe..d01d58eb 100644 --- a/crates/pop-cli/src/commands/call/chain.rs +++ b/crates/pop-cli/src/commands/call/chain.rs @@ -68,9 +68,8 @@ impl CallChainCommand { 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 + if let Err(e) = + self.submit_extrinsic_from_call_data(&chain, call_data, &mut cli::Cli).await { display_message(&e.to_string(), false, &mut cli::Cli)?; } @@ -98,7 +97,8 @@ impl CallChainCommand { if self.use_wallet { // Sign and submit the extrinsic. - if let Err(e) = call.submit_extrinsic_secure_signing(&chain, &xt, &mut cli).await { + let call_data = call_data(&chain.client, &xt)?; + if let Err(e) = submit_extrinsic_secure_signing(&chain, call_data, &mut cli).await { display_message(&e.to_string(), false, &mut cli)?; break; } @@ -110,9 +110,8 @@ impl CallChainCommand { } } - if !prompt_to_repeat_call - || !cli - .confirm("Do you want to perform another call?") + if !prompt_to_repeat_call || + !cli.confirm("Do you want to perform another call?") .initial_value(false) .interact()? { @@ -212,7 +211,7 @@ impl CallChainCommand { // skip the prompt. let suri = match self.suri.as_ref() { Some(suri) => suri.clone(), - None => { + None => if !self.use_wallet { if cli.confirm("Do you want to use your browser wallet to sign the transaction? (Selecting 'No' will prompt you to manually enter the secret key URI for signing, e.g., '//Alice')") .initial_value(true) @@ -225,8 +224,7 @@ impl CallChainCommand { } } else { DEFAULT_URI.to_string() - } - }, + }, }; return Ok(Call { @@ -243,20 +241,43 @@ impl CallChainCommand { // Submits an extrinsic to the chain using the provided encoded call data. async fn submit_extrinsic_from_call_data( &self, - client: &OnlineClient, + chain: &Chain, call_data: &str, cli: &mut impl Cli, ) -> Result<()> { - // Resolve who is signing the extrinsic. - // TODO: HERE TOO + // Resolve who is signing the extrinsic. If a `suri` was provided via the command line, + // skip the prompt. + let mut use_wallet = self.use_wallet; let suri = match self.suri.as_ref() { - Some(suri) => suri, - None => &cli.input("Signer of the extrinsic:").default_input(DEFAULT_URI).interact()?, + Some(suri) => suri.clone(), + None => + if !self.use_wallet { + if cli.confirm("Do you want to use your browser wallet to sign the transaction? (Selecting 'No' will prompt you to manually enter the secret key URI for signing, e.g., '//Alice')") + .initial_value(true) + .interact()? { + use_wallet = true; + DEFAULT_URI.to_string() + } + else { + cli.input("Signer of the extrinsic:").default_input(DEFAULT_URI).interact()? + } + } else { + DEFAULT_URI.to_string() + }, }; + // Return early + if use_wallet { + let call_data_bytes = + decode_call_data(call_data).map_err(|err| anyhow!("{}", format!("{err:?}")))?; + submit_extrinsic_secure_signing(chain, call_data_bytes, cli) + .await + .map_err(|err| anyhow!("{}", format!("{err:?}")))?; + display_message("Call complete.", true, cli)?; + return Ok(()); + } cli.info(format!("Encoded call data: {}", call_data))?; - if !self.skip_confirm - && !cli - .confirm("Do you want to submit the extrinsic?") + if !self.skip_confirm && + !cli.confirm("Do you want to submit the extrinsic?") .initial_value(true) .interact()? { @@ -271,9 +292,10 @@ impl CallChainCommand { spinner.start("Signing and submitting the extrinsic and then waiting for finalization, please be patient..."); 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, call_data_bytes, suri) - .await - .map_err(|err| anyhow!("{}", format!("{err:?}")))?; + let result = + sign_and_submit_extrinsic_with_call_data(&chain.client, 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)?; @@ -284,7 +306,7 @@ impl CallChainCommand { // execute the call via `sudo`. fn configure_sudo(&mut self, chain: &Chain, cli: &mut impl Cli) -> Result<()> { match find_dispatchable_by_name(&chain.pallets, "Sudo", "sudo") { - Ok(_) => { + Ok(_) => if !self.sudo { self.sudo = cli .confirm( @@ -292,16 +314,14 @@ impl CallChainCommand { ) .initial_value(false) .interact()?; - } - }, - Err(_) => { + }, + Err(_) => if self.sudo { cli.warning( "NOTE: sudo is not supported by the chain. Ignoring `--sudo` flag.", )?; self.sudo = false; - } - }, + }, } Ok(()) } @@ -317,11 +337,11 @@ impl CallChainCommand { // Function to check if all required fields are specified. fn requires_user_input(&self) -> bool { - self.pallet.is_none() - || self.function.is_none() - || self.args.is_empty() - || self.url.is_none() - || self.suri.is_none() + self.pallet.is_none() || + self.function.is_none() || + self.args.is_empty() || + self.url.is_none() || + self.suri.is_none() } /// Replaces file arguments with their contents, leaving other arguments unchanged. @@ -402,9 +422,8 @@ impl Call { tx: DynamicPayload, cli: &mut impl Cli, ) -> Result<()> { - if !self.skip_confirm - && !cli - .confirm("Do you want to submit the extrinsic?") + if !self.skip_confirm && + !cli.confirm("Do you want to submit the extrinsic?") .initial_value(true) .interact()? { @@ -425,31 +444,6 @@ impl Call { Ok(()) } - // Sign and submit an extrinsic. - async fn submit_extrinsic_secure_signing( - &mut self, - chain: &Chain, - xt: &DynamicPayload, - cli: &mut impl Cli, - ) -> Result<()> { - let call_data = call_data(&chain.client, xt)?; - let maybe_payload = wait_for_signature(call_data, chain.url.to_string()).await?; - if let Some(payload) = maybe_payload { - cli.success("Signed payload received.")?; - let spinner = cliclack::spinner(); - spinner.start("Signing and submitting the extrinsic and then waiting for finalization, please be patient..."); - - let result = submit_signed_extrinsic(chain.client.clone(), payload) - .await - .map_err(|err| anyhow!("{}", format!("{err:?}")))?; - - spinner.stop(format!("Extrinsic submitted with hash: {:?}", result)); - } else { - display_message("Signed payload doesn't exist.", false, cli)?; - } - Ok(()) - } - fn display(&self, chain: &Chain) -> String { let mut full_message = "pop call chain".to_string(); full_message.push_str(&format!(" --pallet {}", self.function.pallet)); @@ -482,6 +476,31 @@ impl Call { } } +// Sign and submit an extrinsic. +async fn submit_extrinsic_secure_signing( + chain: &Chain, + call_data: Vec, + cli: &mut impl Cli, +) -> Result<()> { + let maybe_payload = wait_for_signature(call_data, chain.url.to_string()).await?; + if let Some(payload) = maybe_payload { + cli.success("Signed payload received.")?; + let spinner = cliclack::spinner(); + spinner.start( + "Submitting the extrinsic and then waiting for finalization, please be patient...", + ); + + let result = submit_signed_extrinsic(chain.client.clone(), payload) + .await + .map_err(|err| anyhow!("{}", format!("{err:?}")))?; + + spinner.stop(format!("Extrinsic submitted with hash: {:?}", result)); + } else { + display_message("Signed payload doesn't exist.", false, cli)?; + } + Ok(()) +} + // Displays a message to the user, with formatting based on the success status. fn display_message(message: &str, success: bool, cli: &mut impl Cli) -> Result<()> { if success { @@ -833,12 +852,17 @@ mod tests { #[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 client = set_up_client(POP_NETWORK_TESTNET_URL).await?; + let chain = Chain { + url: Url::parse(POP_NETWORK_TESTNET_URL)?, + client: client.clone(), + pallets: [].to_vec(), + }; let call_config = CallChainCommand { pallet: None, function: None, args: vec![].to_vec(), - url: Some(Url::parse("wss://rpc1.paseo.popnetwork.xyz")?), + url: Some(Url::parse(POP_NETWORK_TESTNET_URL)?), suri: None, use_wallet: false, skip_confirm: false, @@ -846,11 +870,12 @@ mod tests { sudo: false, }; let mut cli = MockCli::new() + .expect_confirm("Do you want to use your browser wallet to sign the transaction? (Selecting 'No' will prompt you to manually enter the secret key URI for signing, e.g., '//Alice')", false) .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) + .submit_extrinsic_from_call_data(&chain, "0x00000411", &mut cli) .await?; cli.verify() @@ -863,7 +888,7 @@ mod tests { pallet: None, function: None, args: vec![].to_vec(), - url: Some(Url::parse("wss://polkadot-rpc.publicnode.com")?), + url: Some(Url::parse(POLKADOT_NETWORK_URL)?), suri: Some("//Alice".to_string()), use_wallet: false, skip_confirm: false,