Skip to content

Commit

Permalink
feat: flag sudo to wrap extrinsic (#349)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
AlexD10S authored Dec 9, 2024
1 parent 06622ec commit 562e783
Show file tree
Hide file tree
Showing 3 changed files with 128 additions and 10 deletions.
106 changes: 97 additions & 9 deletions crates/pop-cli/src/commands/call/parachain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<String>,
/// 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,
Expand Down Expand Up @@ -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(),
Expand All @@ -203,6 +210,7 @@ impl CallParachainCommand {
args,
suri,
skip_confirm: self.skip_confirm,
sudo: self.sudo,
});
}
}
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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
}
}
Expand Down Expand Up @@ -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)?);
Expand All @@ -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()
}

Expand Down Expand Up @@ -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()
}

Expand All @@ -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.
Expand All @@ -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()
}

Expand All @@ -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)
Expand All @@ -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())
Expand All @@ -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 {
Expand All @@ -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(())
}

Expand All @@ -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;
Expand All @@ -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()?,
Expand Down
30 changes: 30 additions & 0 deletions crates/pop-parachains/src/call/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<DynamicPayload, Error> {
Ok(subxt::dynamic::tx("Sudo", "sudo", [tx.into_value()].to_vec()))
}

/// Signs and submits a given extrinsic.
///
/// # Arguments
Expand Down Expand Up @@ -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(())
}
}
2 changes: 1 addition & 1 deletion crates/pop-parachains/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit 562e783

Please sign in to comment.