Skip to content

Commit

Permalink
feat: logic for parachain registration (#410)
Browse files Browse the repository at this point in the history
* feat: deploy a parachain commands

* feat: build specs in pop up

* feat: logic to interact with a chain

* feat: register parachain

* refactor: clean code and improve docs

* test: unit tests for pop up methods

* refactor: small fixes with visibility and removing logs

* feat: return events in submit_signed_extrinsic

* feat: get para_id from event

* test: fix detects_parachain_correctly

* refactor: improve docs and code

* test: fix change_working_directory_works

* fix: clippy warnings

* refactor: move submit_extrinsic_with_wallet in a common file

* refactor: remove unnecesary code

* refactor: UpChainCommand structure

* test: adjust tests to refactored struct

* refactor: renaming prepare_register_parachain_call_data and prepare_rerve_parachain_call_data

* refactor: move events module

* fix: submit_extrinsic_with_wallet under parachain feature

* refactor: remove unnecesary code

* test: increase coverage with reserve_parachain_id_fails_wrong_chain and resolve_genesis_files_fails_wrong_path

* refactor: remove unnecesary clones

* refactor: minor improvements

* test: refactor tests and include comments

* refactor: map errors in submit_extrinsic_with_wallet

* test: fix prepare_register_parachain_call_data_works

* refactor: move configure_chain into a common folder

* refactor: function visibility

* fix: error message and include test for it

* refactor: build specs removing repetitive code

* refactor: use prepare_extrinsic from Call module to prepare a call

* docs: improve comments and messages

* refactor: rename variables and structs

* refactor: relay_chain_url

* refactor: rename prepare_reserve_call_data and prepare_register_call_data

* test: remove unnecesary test

* refactor: remove events module

* refactor: rename parachain to rollup

* chore: improve succesful message

* chore: change intro title to use rollup

* docs: comments for Reserved event

* refactor: improve readability with chain::configure

* refactor: improve message and clean code
  • Loading branch information
AlexD10S committed Mar 10, 2025
1 parent 840b52e commit 317427d
Show file tree
Hide file tree
Showing 14 changed files with 558 additions and 120 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ subxt = "0.38.0"
ink_env = "5.0.0"
sp-core = "32.0.0"
sp-weights = "31.0.0"
scale = { package = "parity-scale-codec", version = "3.0.0", features = ["derive"] }
scale-info = { version = "2.11.4", default-features = false, features = ["derive"] }
scale-value = { version = "0.17.0", default-features = false, features = ["from-string", "parser-ss58"] }
contract-build = "5.0.2"
Expand Down
32 changes: 22 additions & 10 deletions crates/pop-cli/src/commands/build/spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ use std::{thread::sleep, time::Duration};
use strum::{EnumMessage, VariantArray};
use strum_macros::{AsRefStr, Display, EnumString};

pub(crate) type CodePathBuf = PathBuf;
pub(crate) type StatePathBuf = PathBuf;

const DEFAULT_CHAIN: &str = "dev";
const DEFAULT_PARA_ID: u32 = 2000;
const DEFAULT_PROTOCOL_ID: &str = "my-protocol";
Expand Down Expand Up @@ -179,21 +182,21 @@ impl BuildSpecCommand {
// Checks for appchain project in `./`.
if is_supported(None)? {
let build_spec = self.configure_build_spec(&mut cli).await?;
build_spec.build(&mut cli)
build_spec.build(&mut cli)?;
} else {
cli.outro_cancel(
"🚫 Can't build a specification for target. Maybe not a chain project ?",
)?;
Ok("spec")
}
Ok("spec")
}

/// Configure chain specification requirements by prompting for missing inputs, validating
/// provided values, and preparing a BuildSpec to generate file(s).
///
/// # Arguments
/// * `cli` - The cli.
async fn configure_build_spec(
pub(crate) async fn configure_build_spec(
self,
cli: &mut impl cli::traits::Cli,
) -> anyhow::Result<BuildSpec> {
Expand Down Expand Up @@ -439,7 +442,7 @@ impl BuildSpecCommand {

// Represents the configuration for building a chain specification.
#[derive(Debug)]
struct BuildSpec {
pub(crate) struct BuildSpec {
output_file: PathBuf,
profile: Profile,
id: u32,
Expand All @@ -458,7 +461,10 @@ impl BuildSpec {
// This function generates plain and raw chain spec files based on the provided configuration,
// optionally including genesis state and runtime artifacts. If the node binary is missing,
// it triggers a build process.
fn build(self, cli: &mut impl cli::traits::Cli) -> anyhow::Result<&'static str> {
pub(crate) fn build(
self,
cli: &mut impl cli::traits::Cli,
) -> anyhow::Result<(Option<CodePathBuf>, Option<StatePathBuf>)> {
cli.intro("Building your chain spec")?;
let mut generated_files = vec![];
let BuildSpec {
Expand Down Expand Up @@ -500,21 +506,27 @@ impl BuildSpec {
));

// Generate genesis artifacts.
if genesis_code {
let genesis_code_file = if genesis_code {
spinner.set_message("Generating genesis code...");
let wasm_file_name = format!("para-{}.wasm", id);
let wasm_file = export_wasm_file(&binary_path, &raw_chain_spec, &wasm_file_name)?;
generated_files
.push(format!("WebAssembly runtime file exported at: {}", wasm_file.display()));
}
if genesis_state {
Some(wasm_file)
} else {
None
};
let genesis_state_file = if genesis_state {
spinner.set_message("Generating genesis state...");
let genesis_file_name = format!("para-{}-genesis-state", id);
let genesis_state_file =
generate_genesis_state_file(&binary_path, &raw_chain_spec, &genesis_file_name)?;
generated_files
.push(format!("Genesis State file exported at: {}", genesis_state_file.display()));
}
Some(genesis_state_file)
} else {
None
};

spinner.stop("Chain specification built successfully.");
let generated_files: Vec<_> = generated_files
Expand All @@ -526,7 +538,7 @@ impl BuildSpec {
"Need help? Learn more at {}\n",
style("https://learn.onpop.io").magenta().underlined()
))?;
Ok("spec")
Ok((genesis_code_file, genesis_state_file))
}

// Customize a chain specification.
Expand Down
160 changes: 59 additions & 101 deletions crates/pop-cli/src/commands/call/chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,18 @@ use std::path::Path;

use crate::{
cli::{self, traits::*},
common::wallet::{prompt_to_use_wallet, request_signature},
common::{
chain::{self, Chain},
wallet::{self, prompt_to_use_wallet},
},
};
use anyhow::{anyhow, Result};
use clap::Args;
use pop_parachains::{
construct_extrinsic, construct_sudo_extrinsic, decode_call_data, encode_call_data,
find_dispatchable_by_name, find_pallet_by_name, parse_chain_metadata, set_up_client,
sign_and_submit_extrinsic, submit_signed_extrinsic, supported_actions, Action, CallData,
DynamicPayload, Function, OnlineClient, Pallet, Param, Payload, SubstrateConfig,
find_dispatchable_by_name, find_pallet_by_name, sign_and_submit_extrinsic, supported_actions,
Action, CallData, DynamicPayload, Function, OnlineClient, Pallet, Param, Payload,
SubstrateConfig,
};
use url::Url;

Expand Down Expand Up @@ -67,10 +70,17 @@ impl CallChainCommand {
/// Executes the command.
pub(crate) async fn execute(mut self) -> Result<()> {
let mut cli = cli::Cli;
cli.intro("Call a chain")?;
// Check if all fields are specified via the command line.
let prompt_to_repeat_call = self.requires_user_input();
// Configure the chain.
let chain = self.configure_chain(&mut cli).await?;
let chain = chain::configure(
"Which chain would you like to interact with?",
DEFAULT_URL,
&self.url,
&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
Expand Down Expand Up @@ -109,7 +119,9 @@ impl CallChainCommand {
// Sign and submit the extrinsic.
let result = if self.use_wallet {
let call_data = xt.encode_call_data(&chain.client.metadata())?;
submit_extrinsic_with_wallet(&chain.client, &chain.url, call_data, &mut cli).await
wallet::submit_extrinsic(&chain.client, &chain.url, call_data, &mut cli)
.await
.map(|_| ()) // Mapping to `()` since we don't need events returned
} else {
call.submit_extrinsic(&chain.client, &chain.url, xt, &mut cli).await
};
Expand All @@ -132,33 +144,6 @@ impl CallChainCommand {
Ok(())
}

// Configures the chain by resolving the URL and fetching its metadata.
async fn configure_chain(&self, cli: &mut impl Cli) -> Result<Chain> {
cli.intro("Call a chain")?;
// Resolve url.
let url = match &self.url {
Some(url) => url.clone(),
None => {
// Prompt for url.
let url: String = cli
.input("Which chain would you like to interact with?")
.default_input(DEFAULT_URL)
.interact()?;
Url::parse(&url)?
},
};

// Parse metadata from chain url.
let client = set_up_client(url.as_str()).await?;
let mut pallets = parse_chain_metadata(&client).map_err(|e| {
anyhow!(format!("Unable to fetch the chain metadata: {}", e.to_string()))
})?;
// Sort by name for display.
pallets.sort_by(|a, b| a.name.cmp(&b.name));
pallets.iter_mut().for_each(|p| p.functions.sort_by(|a, b| a.name.cmp(&b.name)));
Ok(Chain { url, client, pallets })
}

// Configure the call based on command line arguments/call UI.
fn configure_call(&mut self, chain: &Chain, cli: &mut impl Cli) -> Result<Call> {
loop {
Expand Down Expand Up @@ -244,7 +229,7 @@ impl CallChainCommand {
if use_wallet {
let call_data_bytes =
decode_call_data(call_data).map_err(|err| anyhow!("{}", format!("{err:?}")))?;
submit_extrinsic_with_wallet(client, url, call_data_bytes, cli)
wallet::submit_extrinsic(client, url, call_data_bytes, cli)
.await
.map_err(|err| anyhow!("{}", format!("{err:?}")))?;
display_message("Call complete.", true, cli)?;
Expand Down Expand Up @@ -357,41 +342,31 @@ impl CallChainCommand {
}
}

// Represents a chain, including its URL, client connection, and available pallets.
struct Chain {
// Websocket endpoint of the node.
url: Url,
// The client used to interact with the chain.
client: OnlineClient<SubstrateConfig>,
// A list of pallets available on the chain.
pallets: Vec<Pallet>,
}

/// Represents a configured dispatchable function call, including the pallet, function, arguments,
/// and signing options.
#[derive(Clone)]
struct Call {
#[derive(Clone, Default)]
pub(crate) struct Call {
/// The dispatchable function to execute.
function: Function,
pub(crate) function: Function,
/// The dispatchable function arguments, encoded as strings.
args: Vec<String>,
pub(crate) args: Vec<String>,
/// Secret key URI for the account signing the extrinsic.
///
/// e.g.
/// - for a dev account "//Alice"
/// - with a password "//Alice///SECRET_PASSWORD"
suri: String,
pub(crate) suri: String,
/// Whether to use your browser wallet to sign the extrinsic.
use_wallet: bool,
pub(crate) use_wallet: bool,
/// Whether to automatically sign and submit the extrinsic without prompting for confirmation.
skip_confirm: bool,
pub(crate) skip_confirm: bool,
/// Whether to dispatch the function call with `Root` origin.
sudo: bool,
pub(crate) sudo: bool,
}

impl Call {
// Prepares the extrinsic.
fn prepare_extrinsic(
pub(crate) fn prepare_extrinsic(
&self,
client: &OnlineClient<SubstrateConfig>,
cli: &mut impl Cli,
Expand Down Expand Up @@ -473,32 +448,6 @@ impl Call {
}
}

// Sign and submit an extrinsic using wallet integration.
async fn submit_extrinsic_with_wallet(
client: &OnlineClient<SubstrateConfig>,
url: &Url,
call_data: Vec<u8>,
cli: &mut impl Cli,
) -> Result<()> {
let maybe_payload = request_signature(call_data, 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(client.clone(), payload)
.await
.map_err(|err| anyhow!("{}", format!("{err:?}")))?;

spinner.stop(format!("Extrinsic submitted with hash: {:?}", result));
} else {
display_message("No signed payload received.", 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 {
Expand Down Expand Up @@ -670,33 +619,20 @@ fn parse_function_name(name: &str) -> Result<String, String> {
mod tests {
use super::*;
use crate::{cli::MockCli, common::wallet::USE_WALLET_PROMPT};
use pop_parachains::{parse_chain_metadata, set_up_client};
use tempfile::tempdir;
use url::Url;

const BOB_SURI: &str = "//Bob";
const POP_NETWORK_TESTNET_URL: &str = "wss://rpc1.paseo.popnetwork.xyz";
const POLKADOT_NETWORK_URL: &str = "wss://polkadot-rpc.publicnode.com";

#[tokio::test]
async fn configure_chain_works() -> Result<()> {
let call_config =
CallChainCommand { suri: Some(DEFAULT_URI.to_string()), ..Default::default() };
let mut cli = MockCli::new().expect_intro("Call a chain").expect_input(
"Which chain would you like to interact with?",
POP_NETWORK_TESTNET_URL.into(),
);
let chain = call_config.configure_chain(&mut cli).await?;
assert_eq!(chain.url, Url::parse(POP_NETWORK_TESTNET_URL)?);
cli.verify()
}

#[tokio::test]
async fn guide_user_to_call_chain_works() -> Result<()> {
let mut call_config =
CallChainCommand { pallet: Some("System".to_string()), ..Default::default() };

let mut cli = MockCli::new()
.expect_intro("Call a chain")
.expect_input("Which chain would you like to interact with?", POP_NETWORK_TESTNET_URL.into())
.expect_select(
"Select the function to call:",
Expand Down Expand Up @@ -725,7 +661,13 @@ mod tests {
.expect_confirm("Would you like to dispatch this function call with `Root` origin?", true)
.expect_confirm(USE_WALLET_PROMPT, true);

let chain = call_config.configure_chain(&mut cli).await?;
let chain = chain::configure(
"Which chain would you like to interact with?",
POP_NETWORK_TESTNET_URL,
&None,
&mut cli,
)
.await?;
assert_eq!(chain.url, Url::parse(POP_NETWORK_TESTNET_URL)?);

let call_chain = call_config.configure_call(&chain, &mut cli)?;
Expand All @@ -743,11 +685,17 @@ mod tests {
async fn guide_user_to_configure_predefined_action_works() -> Result<()> {
let mut call_config = CallChainCommand::default();

let mut cli = MockCli::new().expect_intro("Call a chain").expect_input(
let mut cli = MockCli::new().expect_input(
"Which chain would you like to interact with?",
POLKADOT_NETWORK_URL.into(),
);
let chain = call_config.configure_chain(&mut cli).await?;
let chain = chain::configure(
"Which chain would you like to interact with?",
POP_NETWORK_TESTNET_URL,
&None,
&mut cli,
)
.await?;
assert_eq!(chain.url, Url::parse(POLKADOT_NETWORK_URL)?);
cli.verify()?;

Expand Down Expand Up @@ -898,20 +846,30 @@ mod tests {
sudo: true,
};
let mut cli = MockCli::new()
.expect_intro("Call a chain")
.expect_warning("NOTE: sudo is not supported by the chain. Ignoring `--sudo` flag.");
let chain = call_config.configure_chain(&mut cli).await?;
let chain = chain::configure(
"Which chain would you like to interact with?",
POP_NETWORK_TESTNET_URL,
&Some(Url::parse(POLKADOT_NETWORK_URL)?),
&mut cli,
)
.await?;
call_config.configure_sudo(&chain, &mut cli)?;
assert!(!call_config.sudo);
cli.verify()?;

// Test when sudo pallet exist.
cli = MockCli::new().expect_intro("Call a chain").expect_confirm(
cli = MockCli::new().expect_confirm(
"Would you like to dispatch this function call with `Root` origin?",
true,
);
call_config.url = Some(Url::parse(POP_NETWORK_TESTNET_URL)?);
let chain = call_config.configure_chain(&mut cli).await?;
let chain = chain::configure(
"Which chain would you like to interact with?",
POP_NETWORK_TESTNET_URL,
&Some(Url::parse(POP_NETWORK_TESTNET_URL)?),
&mut cli,
)
.await?;
call_config.configure_sudo(&chain, &mut cli)?;
assert!(call_config.sudo);
cli.verify()
Expand Down
Loading

0 comments on commit 317427d

Please sign in to comment.