From 562e7834f0d75a8d9a54bd0de8891611ce3cbdb5 Mon Sep 17 00:00:00 2001 From: Alex Bean Date: Mon, 9 Dec 2024 18:37:11 +0100 Subject: [PATCH] feat: flag sudo to wrap extrinsic (#349) * feat: submit extrinsic from call_data * test: unit test for initialize_api_client * feat: wrap call into a sudo call * test: add unit test to the new logic * fix: skip_confirm for send_extrinsic_from_call_data * chore: clippy * docs: renaming and improve docs * test: use force_transfer for testing * fix: check if sudo exist before prompt the user * chore: fmt * chore: fmt * test: fix wrong assert * docs: improve comments and output messages * refactor: split decode_call_data logic outside sign_and_submit_extrinsic_with_call_data * fix: test construct_sudo_extrinsic_works and formatting --- crates/pop-cli/src/commands/call/parachain.rs | 106 ++++++++++++++++-- crates/pop-parachains/src/call/mod.rs | 30 +++++ crates/pop-parachains/src/lib.rs | 2 +- 3 files changed, 128 insertions(+), 10 deletions(-) diff --git a/crates/pop-cli/src/commands/call/parachain.rs b/crates/pop-cli/src/commands/call/parachain.rs index db0e7c41..d14787fd 100644 --- a/crates/pop-cli/src/commands/call/parachain.rs +++ b/crates/pop-cli/src/commands/call/parachain.rs @@ -6,10 +6,10 @@ use crate::cli::{self, traits::*}; use anyhow::{anyhow, Result}; use clap::Args; use pop_parachains::{ - 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, + construct_extrinsic, construct_sudo_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; @@ -42,6 +42,9 @@ pub struct CallParachainCommand { /// SCALE encoded bytes representing the call data of the extrinsic. #[arg(name = "call", long, conflicts_with_all = ["pallet", "extrinsic", "args"])] call_data: Option, + /// Authenticates the sudo key and dispatches a function call with `Root` origin. + #[arg(short = 'S', long)] + sudo: bool, /// Automatically signs and submits the extrinsic without prompting for confirmation. #[arg(short('y'), long)] skip_confirm: bool, @@ -190,6 +193,10 @@ impl CallParachainCommand { self.expand_file_arguments()? }; + // If chain has sudo prompt the user to confirm if they want to execute the call via + // sudo. + self.configure_sudo(chain, cli).await?; + // Resolve who is signing the extrinsic. let suri = match self.suri.as_ref() { Some(suri) => suri.clone(), @@ -203,6 +210,7 @@ impl CallParachainCommand { args, suri, skip_confirm: self.skip_confirm, + sudo: self.sudo, }); } } @@ -246,11 +254,36 @@ impl CallParachainCommand { Ok(()) } + // Checks if the chain has the Sudo pallet and prompt the user to confirm if they want to + // execute the call via sudo. + async fn configure_sudo(&mut self, chain: &Chain, cli: &mut impl Cli) -> Result<()> { + match find_extrinsic_by_name(&chain.pallets, "Sudo", "sudo").await { + Ok(_) => + if !self.sudo { + self.sudo = cli + .confirm( + "Would you like to dispatch this function call with `Root` origin?", + ) + .initial_value(false) + .interact()?; + }, + Err(_) => + if self.sudo { + cli.warning( + "NOTE: sudo is not supported by the chain. Ignoring `--sudo` flag.", + )?; + self.sudo = false; + }, + } + Ok(()) + } + // Resets specific fields to default values for a new call. fn reset_for_new_call(&mut self) { self.pallet = None; self.extrinsic = None; self.args.clear(); + self.sudo = false; } // Function to check if all required fields are specified. @@ -306,6 +339,8 @@ struct CallParachain { suri: String, /// Whether to automatically sign and submit the extrinsic without prompting for confirmation. skip_confirm: bool, + /// Whether to dispatch the function call with `Root` origin. + sudo: bool, } impl CallParachain { @@ -327,6 +362,8 @@ impl CallParachain { return Err(anyhow!("Error: {}", e)); }, }; + // If sudo is required, wrap the call in a sudo call. + let tx = if self.sudo { construct_sudo_extrinsic(tx).await? } else { tx }; let encoded_data = encode_call_data(client, &tx)?; // If the encoded call data is too long, don't display it all. if encoded_data.len() < ENCODED_CALL_DATA_MAX_LEN { @@ -383,6 +420,9 @@ impl CallParachain { full_message.push_str(&format!(" --args {}", args.join(" "))); } full_message.push_str(&format!(" --url {} --suri {}", chain.url, self.suri)); + if self.sudo { + full_message.push_str(" --sudo"); + } full_message } } @@ -612,7 +652,8 @@ mod tests { 0, // "remark" extrinsic ) .expect_input("The value for `remark` might be too large to enter. You may enter the path to a file instead.", "0x11".into()) - .expect_input("Signer of the extrinsic:", BOB_SURI.into()); + .expect_confirm("Would you like to dispatch this function call with `Root` origin?", true) + .expect_input("Signer of the extrinsic:", "//Bob".into()); let chain = call_config.configure_chain(&mut cli).await?; assert_eq!(chain.url, Url::parse(POP_NETWORK_TESTNET_URL)?); @@ -621,8 +662,9 @@ mod tests { assert_eq!(call_parachain.pallet.name, "System"); assert_eq!(call_parachain.extrinsic.name, "remark"); assert_eq!(call_parachain.args, ["0x11".to_string()].to_vec()); - assert_eq!(call_parachain.suri, BOB_SURI); - assert_eq!(call_parachain.display(&chain), format!("pop call parachain --pallet System --extrinsic remark --args \"0x11\" --url {}/ --suri {}", POP_NETWORK_TESTNET_URL, BOB_SURI)); + assert_eq!(call_parachain.suri, "//Bob"); + assert!(call_parachain.sudo); + assert_eq!(call_parachain.display(&chain), "pop call parachain --pallet System --extrinsic remark --args \"0x11\" --url wss://rpc1.paseo.popnetwork.xyz/ --suri //Bob --sudo"); cli.verify() } @@ -667,8 +709,9 @@ mod tests { assert_eq!(call_parachain.pallet.name, "OnDemand"); assert_eq!(call_parachain.extrinsic.name, "place_order_allow_death"); assert_eq!(call_parachain.args, ["10000".to_string(), "2000".to_string()].to_vec()); - assert_eq!(call_parachain.suri, BOB_SURI); - assert_eq!(call_parachain.display(&chain), format!("pop call parachain --pallet OnDemand --extrinsic place_order_allow_death --args \"10000\" \"2000\" --url {}/ --suri {}", POLKADOT_NETWORK_URL, BOB_SURI)); + assert_eq!(call_parachain.suri, "//Bob"); + assert!(!call_parachain.sudo); + assert_eq!(call_parachain.display(&chain), "pop call parachain --pallet OnDemand --extrinsic place_order_allow_death --args \"10000\" \"2000\" --url wss://polkadot-rpc.publicnode.com/ --suri //Bob"); cli.verify() } @@ -681,6 +724,7 @@ mod tests { args: vec!["0x11".to_string()].to_vec(), suri: DEFAULT_URI.to_string(), skip_confirm: false, + sudo: false, }; let mut cli = MockCli::new(); // Error, wrong name of the pallet. @@ -700,6 +744,11 @@ mod tests { assert_eq!(tx.call_name(), "remark"); assert_eq!(tx.pallet_name(), "System"); + // Prepare extrinsic wrapped in sudo works. + cli = MockCli::new().expect_info("Encoded call data: 0x0f0000000411"); + call_config.sudo = true; + call_config.prepare_extrinsic(&client, &mut cli).await?; + cli.verify() } @@ -713,6 +762,7 @@ mod tests { args: vec!["0x11".to_string()].to_vec(), suri: DEFAULT_URI.to_string(), skip_confirm: false, + sudo: false, }; let mut cli = MockCli::new() .expect_confirm("Do you want to submit the extrinsic?", false) @@ -734,6 +784,7 @@ mod tests { suri: None, skip_confirm: false, call_data: Some("0x00000411".to_string()), + sudo: false, }; let mut cli = MockCli::new() .expect_input("Signer of the extrinsic:", "//Bob".into()) @@ -746,6 +797,39 @@ mod tests { cli.verify() } + #[tokio::test] + async fn configure_sudo_works() -> Result<()> { + // Test when sudo pallet doesn't exist. + let mut call_config = CallParachainCommand { + pallet: None, + extrinsic: None, + args: vec![].to_vec(), + url: Some(Url::parse("wss://polkadot-rpc.publicnode.com")?), + suri: Some("//Alice".to_string()), + skip_confirm: false, + call_data: Some("0x00000411".to_string()), + sudo: true, + }; + let mut cli = MockCli::new() + .expect_intro("Call a parachain") + .expect_warning("NOTE: sudo is not supported by the chain. Ignoring `--sudo` flag."); + let chain = call_config.configure_chain(&mut cli).await?; + call_config.configure_sudo(&chain, &mut cli).await?; + assert!(!call_config.sudo); + cli.verify()?; + + // Test when sudo pallet exist. + cli = MockCli::new().expect_intro("Call a parachain").expect_confirm( + "Would you like to dispatch this function call with `Root` origin?", + true, + ); + call_config.url = Some(Url::parse("wss://rpc1.paseo.popnetwork.xyz")?); + let chain = call_config.configure_chain(&mut cli).await?; + call_config.configure_sudo(&chain, &mut cli).await?; + assert!(call_config.sudo); + cli.verify() + } + #[test] fn reset_for_new_call_works() -> Result<()> { let mut call_config = CallParachainCommand { @@ -756,11 +840,13 @@ mod tests { suri: Some(DEFAULT_URI.to_string()), skip_confirm: false, call_data: None, + sudo: true, }; call_config.reset_for_new_call(); assert_eq!(call_config.pallet, None); assert_eq!(call_config.extrinsic, None); assert_eq!(call_config.args.len(), 0); + assert!(!call_config.sudo); Ok(()) } @@ -774,6 +860,7 @@ mod tests { suri: Some(DEFAULT_URI.to_string()), skip_confirm: false, call_data: None, + sudo: false, }; assert!(!call_config.requires_user_input()); call_config.pallet = None; @@ -791,6 +878,7 @@ mod tests { suri: Some(DEFAULT_URI.to_string()), skip_confirm: false, call_data: None, + sudo: false, }; assert_eq!( call_config.expand_file_arguments()?, diff --git a/crates/pop-parachains/src/call/mod.rs b/crates/pop-parachains/src/call/mod.rs index 136d0d2d..967e8b74 100644 --- a/crates/pop-parachains/src/call/mod.rs +++ b/crates/pop-parachains/src/call/mod.rs @@ -36,6 +36,15 @@ pub async fn construct_extrinsic( Ok(subxt::dynamic::tx(pallet_name, extrinsic.name.clone(), parsed_args)) } +/// Constructs a Sudo extrinsic. +/// +/// # Arguments +/// * `tx`: The transaction payload representing the function call to be dispatched with `Root` +/// privileges. +pub async fn construct_sudo_extrinsic(tx: DynamicPayload) -> Result { + Ok(subxt::dynamic::tx("Sudo", "sudo", [tx.into_value()].to_vec())) +} + /// Signs and submits a given extrinsic. /// /// # Arguments @@ -215,4 +224,25 @@ mod tests { )); Ok(()) } + + #[tokio::test] + async fn construct_sudo_extrinsic_works() -> Result<()> { + let client = set_up_client("wss://rpc1.paseo.popnetwork.xyz").await?; + let pallets = parse_chain_metadata(&client).await?; + let force_transfer = find_extrinsic_by_name(&pallets, "Balances", "force_transfer").await?; + let extrinsic = construct_extrinsic( + "Balances", + &force_transfer, + vec![ + "Id(5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty)".to_string(), + "Id(5DAAnrj7VHTznn2AWBemMuyBwZWs6FNFjdyVXUeYum3PTXFy)".to_string(), + "100".to_string(), + ], + ) + .await?; + let sudo_extrinsic = construct_sudo_extrinsic(extrinsic).await?; + assert_eq!(sudo_extrinsic.call_name(), "sudo"); + assert_eq!(sudo_extrinsic.pallet_name(), "Sudo"); + Ok(()) + } } diff --git a/crates/pop-parachains/src/lib.rs b/crates/pop-parachains/src/lib.rs index 00f24975..b2bbfa64 100644 --- a/crates/pop-parachains/src/lib.rs +++ b/crates/pop-parachains/src/lib.rs @@ -17,7 +17,7 @@ pub use build::{ generate_plain_chain_spec, generate_raw_chain_spec, is_supported, ChainSpec, }; pub use call::{ - construct_extrinsic, decode_call_data, encode_call_data, + construct_extrinsic, construct_sudo_extrinsic, decode_call_data, encode_call_data, metadata::{ action::{supported_actions, Action}, find_extrinsic_by_name, find_pallet_by_name,