Skip to content

Commit

Permalink
feat: submit extrinsic from call_data (#348)
Browse files Browse the repository at this point in the history
* feat: submit extrinsic from call_data

* test: unit test for initialize_api_client

* test: unit test for send_extrinsic_from_call_data

* fix: CallData struct

* fix: skip_confirm for send_extrinsic_from_call_data

* chore: clippy

* chore: fmt

* refactor: minor doc and naming changes

* refactor: remove unnecesary clones and return early when submit_extrinsic_from_call_data

* chore: fmt

* refactor: split decode_call_data logic outside sign_and_submit_extrinsic_with_call_data
  • Loading branch information
AlexD10S authored Dec 9, 2024
1 parent 69410bd commit c37580f
Show file tree
Hide file tree
Showing 5 changed files with 165 additions and 5 deletions.
87 changes: 84 additions & 3 deletions crates/pop-cli/src/commands/call/parachain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -35,6 +36,9 @@ pub struct CallParachainCommand {
/// - with a password "//Alice///SECRET_PASSWORD"
#[arg(short, long)]
suri: Option<String>,
/// SCALE encoded bytes representing the call data of the extrinsic.
#[arg(name = "call", long, conflicts_with_all = ["pallet", "extrinsic", "args"])]
call_data: Option<String>,
/// Automatically signs and submits the extrinsic without prompting for confirmation.
#[arg(short('y'), long)]
skip_confirm: bool,
Expand All @@ -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 {
Expand Down Expand Up @@ -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<SubstrateConfig>,
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;
Expand Down Expand Up @@ -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?",
Expand All @@ -484,6 +538,7 @@ mod tests {
url: None,
suri: None,
skip_confirm: false,
call_data: None,
};

let mut cli = MockCli::new()
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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 {
Expand All @@ -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);
Expand All @@ -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;
Expand Down
18 changes: 18 additions & 0 deletions crates/pop-cli/tests/parachain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()?;
Expand Down
58 changes: 58 additions & 0 deletions crates/pop-parachains/src/call/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Vec<u8>, 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<u8>);

impl Payload for CallData {
fn encode_call_data_to(
&self,
_: &subxt::Metadata,
out: &mut Vec<u8>,
) -> 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<SubstrateConfig>,
call_data: Vec<u8>,
suri: &str,
) -> Result<String, Error> {
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::*;
Expand Down Expand Up @@ -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?;
Expand Down
3 changes: 3 additions & 0 deletions crates/pop-parachains/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ pub enum Error {
Aborted,

Check warning on line 10 in crates/pop-parachains/src/errors.rs

View workflow job for this annotation

GitHub Actions / clippy

missing documentation for a variant

warning: missing documentation for a variant --> crates/pop-parachains/src/errors.rs:10:2 | 10 | Aborted, | ^^^^^^^ | = note: requested on the command line with `-W missing-docs`
#[error("Anyhow error: {0}")]
AnyhowError(#[from] anyhow::Error),

Check warning on line 12 in crates/pop-parachains/src/errors.rs

View workflow job for this annotation

GitHub Actions / clippy

missing documentation for a variant

warning: missing documentation for a variant --> crates/pop-parachains/src/errors.rs:12:2 | 12 | 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),
Expand Down
4 changes: 2 additions & 2 deletions crates/pop-parachains/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit c37580f

Please sign in to comment.