diff --git a/Cargo.lock b/Cargo.lock index 6ff7331d9..cd8dfee0c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4572,6 +4572,12 @@ dependencies = [ "password-hash", ] +[[package]] +name = "peekmore" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9163e1259760e83d528d1b3171e5100c1767f10c52e1c4d6afad26e63d47d758" + [[package]] name = "pem" version = "3.0.4" @@ -4856,10 +4862,13 @@ version = "0.5.0" dependencies = [ "anyhow", "cargo_toml", + "contract-build", + "contract-extrinsics", "duct", "flate2", "git2", "git2_credentials", + "ink_env", "mockito", "regex", "reqwest 0.12.7", @@ -5856,6 +5865,24 @@ dependencies = [ "thiserror", ] +[[package]] +name = "scale-typegen-description" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ff4cabffc407f6378bc7a164fab8280bfcd862b2dd063cc5c9914a520ea8566" +dependencies = [ + "anyhow", + "peekmore", + "proc-macro2", + "quote", + "rand", + "rand_chacha", + "scale-info", + "scale-typegen", + "scale-value", + "smallvec", +] + [[package]] name = "scale-value" version = "0.16.2" diff --git a/crates/pop-cli/src/commands/call/chain.rs b/crates/pop-cli/src/commands/call/chain.rs index ce7050a29..9751b7dd9 100644 --- a/crates/pop-cli/src/commands/call/chain.rs +++ b/crates/pop-cli/src/commands/call/chain.rs @@ -8,8 +8,8 @@ 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, sign_and_submit_extrinsic_with_call_data, supported_actions, Action, - DynamicPayload, Function, OnlineClient, Pallet, Param, SubstrateConfig, + sign_and_submit_extrinsic, supported_actions, Action, CallData, DynamicPayload, Function, + OnlineClient, Pallet, Param, SubstrateConfig, }; use url::Url; @@ -62,7 +62,12 @@ impl CallChainCommand { // 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) + .submit_extrinsic_from_call_data( + &chain.client, + &chain.url, + call_data, + &mut cli::Cli, + ) .await { display_message(&e.to_string(), false, &mut cli::Cli)?; @@ -90,7 +95,7 @@ impl CallChainCommand { }; // Sign and submit the extrinsic. - if let Err(e) = call.submit_extrinsic(&chain.client, xt, &mut cli).await { + if let Err(e) = call.submit_extrinsic(&chain.client, &chain.url, xt, &mut cli).await { display_message(&e.to_string(), false, &mut cli)?; break; } @@ -213,6 +218,7 @@ impl CallChainCommand { async fn submit_extrinsic_from_call_data( &self, client: &OnlineClient, + url: &Url, call_data: &str, cli: &mut impl Cli, ) -> Result<()> { @@ -238,11 +244,11 @@ 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) + let result = sign_and_submit_extrinsic(client, url, CallData::new(call_data_bytes), suri) .await .map_err(|err| anyhow!("{}", format!("{err:?}")))?; - spinner.stop(format!("Extrinsic submitted successfully with hash: {:?}", result)); + spinner.stop(result); display_message("Call complete.", true, cli)?; Ok(()) } @@ -361,6 +367,7 @@ impl Call { async fn submit_extrinsic( &mut self, client: &OnlineClient, + url: &Url, tx: DynamicPayload, cli: &mut impl Cli, ) -> Result<()> { @@ -378,11 +385,10 @@ impl Call { } let spinner = cliclack::spinner(); spinner.start("Signing and submitting the extrinsic and then waiting for finalization, please be patient..."); - let result = sign_and_submit_extrinsic(client, tx, &self.suri) + let result = sign_and_submit_extrinsic(client, url, tx, &self.suri) .await .map_err(|err| anyhow!("{}", format!("{err:?}")))?; - - spinner.stop(format!("Extrinsic submitted with hash: {:?}", result)); + spinner.stop(result); Ok(()) } @@ -754,19 +760,21 @@ mod tests { .expect_confirm("Do you want to submit the extrinsic?", false) .expect_outro_cancel("Extrinsic for `remark` was not submitted."); let xt = call_config.prepare_extrinsic(&client, &mut cli)?; - call_config.submit_extrinsic(&client, xt, &mut cli).await?; + call_config + .submit_extrinsic(&client, &Url::parse(POP_NETWORK_TESTNET_URL)?, xt, &mut cli) + .await?; 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 client = set_up_client(POP_NETWORK_TESTNET_URL).await?; 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, skip_confirm: false, call_data: Some("0x00000411".to_string()), @@ -777,7 +785,12 @@ mod tests { .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( + &client, + &Url::parse(POP_NETWORK_TESTNET_URL)?, + "0x00000411", + &mut cli, + ) .await?; cli.verify() @@ -809,7 +822,7 @@ mod tests { "Would you like to dispatch this function call with `Root` origin?", true, ); - call_config.url = Some(Url::parse("wss://rpc1.paseo.popnetwork.xyz")?); + call_config.url = Some(Url::parse(POP_NETWORK_TESTNET_URL)?); let chain = call_config.configure_chain(&mut cli).await?; call_config.configure_sudo(&chain, &mut cli)?; assert!(call_config.sudo); diff --git a/crates/pop-common/Cargo.toml b/crates/pop-common/Cargo.toml index 03e5035d7..f81f29827 100644 --- a/crates/pop-common/Cargo.toml +++ b/crates/pop-common/Cargo.toml @@ -10,10 +10,13 @@ repository.workspace = true [dependencies] anyhow.workspace = true cargo_toml.workspace = true +contract-build.workspace = true +contract-extrinsics.workspace = true duct.workspace = true flate2.workspace = true git2.workspace = true git2_credentials.workspace = true +ink_env.workspace = true regex.workspace = true reqwest.workspace = true scale-info.workspace = true @@ -27,9 +30,9 @@ tar.workspace = true tempfile.workspace = true thiserror.workspace = true tokio.workspace = true +toml.workspace = true toml_edit.workspace = true url.workspace = true -toml.workspace = true [dev-dependencies] mockito.workspace = true diff --git a/crates/pop-common/src/lib.rs b/crates/pop-common/src/lib.rs index 6f1991960..b0c85ddd3 100644 --- a/crates/pop-common/src/lib.rs +++ b/crates/pop-common/src/lib.rs @@ -2,6 +2,8 @@ use std::net::TcpListener; +use std::net::TcpListener; + pub use build::Profile; pub use errors::Error; pub use git::{Git, GitHub, Release}; @@ -73,6 +75,15 @@ pub fn find_free_port() -> u16 { .port() } +/// Provides functionality for making calls to parachains or smart contracts. +pub mod call { + // Note: parsing events after calling a chain is done using cargo contract logic. This could be + // refactored in the future. + pub use contract_build::Verbosity; + pub use contract_extrinsics::{DisplayEvents, TokenMetadata}; + pub use ink_env::DefaultEnvironment; +} + #[cfg(test)] mod test { use super::*; diff --git a/crates/pop-contracts/src/call.rs b/crates/pop-contracts/src/call.rs index 03effcb0a..f9dd83631 100644 --- a/crates/pop-contracts/src/call.rs +++ b/crates/pop-contracts/src/call.rs @@ -20,6 +20,8 @@ use sp_weights::Weight; use std::path::PathBuf; use url::Url; +pub mod metadata; + /// Attributes for the `call` command. pub struct CallOpts { /// Path to the contract build directory. diff --git a/crates/pop-contracts/src/lib.rs b/crates/pop-contracts/src/lib.rs index c5e76e2f1..1cde57095 100644 --- a/crates/pop-contracts/src/lib.rs +++ b/crates/pop-contracts/src/lib.rs @@ -14,7 +14,9 @@ mod utils; pub use build::{build_smart_contract, is_supported, Verbosity}; pub use call::{ - call_smart_contract, dry_run_call, dry_run_gas_estimate_call, set_up_call, CallOpts, + call_smart_contract, dry_run_call, dry_run_gas_estimate_call, + metadata::{get_messages, Message}, + set_up_call, CallOpts, }; pub use new::{create_smart_contract, is_valid_contract_name}; pub use node::{contracts_node_generator, is_chain_alive, run_contracts_node}; diff --git a/crates/pop-contracts/src/utils/metadata.rs b/crates/pop-contracts/src/utils/metadata.rs index 111468f0e..e4da73cca 100644 --- a/crates/pop-contracts/src/utils/metadata.rs +++ b/crates/pop-contracts/src/utils/metadata.rs @@ -174,17 +174,18 @@ pub fn process_function_args

( where P: AsRef, { + let parsed_args = args.into_iter().map(|arg| arg.replace(",", "")).collect::>(); let function = match function_type { FunctionType::Message => get_message(path, label)?, FunctionType::Constructor => get_constructor(path, label)?, }; - if args.len() != function.args.len() { + if parsed_args.len() != function.args.len() { return Err(Error::IncorrectArguments { expected: function.args.len(), - provided: args.len(), + provided: parsed_args.len(), }); } - Ok(args + Ok(parsed_args .into_iter() .zip(&function.args) .map(|(arg, param)| match (param.type_name.starts_with("Option<"), arg.is_empty()) { diff --git a/crates/pop-parachains/src/call/metadata/mod.rs b/crates/pop-parachains/src/call/metadata/mod.rs index 1f53f523c..ec4f664f7 100644 --- a/crates/pop-parachains/src/call/metadata/mod.rs +++ b/crates/pop-parachains/src/call/metadata/mod.rs @@ -332,3 +332,249 @@ mod tests { Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + + use crate::set_up_api; + use anyhow::Result; + use subxt::ext::scale_bits; + + #[tokio::test] + async fn parse_chain_metadata_works() -> Result<()> { + let api = set_up_api("wss://rpc1.paseo.popnetwork.xyz").await?; + let pallets = parse_chain_metadata(&api).await?; + // Test the first pallet is parsed correctly + let first_pallet = pallets.first().unwrap(); + assert_eq!(first_pallet.name, "System"); + assert_eq!(first_pallet.docs, ""); + assert_eq!(first_pallet.extrinsics.len(), 11); + let first_extrinsic = first_pallet.extrinsics.first().unwrap(); + assert_eq!(first_extrinsic.name, "remark"); + assert_eq!( + first_extrinsic.docs, + "Make some on-chain remark.Can be executed by every `origin`." + ); + assert!(first_extrinsic.is_supported); + assert_eq!(first_extrinsic.params.first().unwrap().name, "remark"); + assert_eq!(first_extrinsic.params.first().unwrap().type_name, "[u8]"); + assert_eq!(first_extrinsic.params.first().unwrap().sub_params.len(), 0); + assert!(!first_extrinsic.params.first().unwrap().is_optional); + assert!(!first_extrinsic.params.first().unwrap().is_tuple); + assert!(!first_extrinsic.params.first().unwrap().is_variant); + Ok(()) + } + + #[tokio::test] + async fn find_pallet_by_name_works() -> Result<()> { + let api = set_up_api("wss://rpc1.paseo.popnetwork.xyz").await?; + let pallets = parse_chain_metadata(&api).await?; + assert!(matches!( + find_pallet_by_name(&pallets, "WrongName").await, + Err(Error::PalletNotFound(pallet)) if pallet == "WrongName".to_string())); + let pallet = find_pallet_by_name(&pallets, "Balances").await?; + assert_eq!(pallet.name, "Balances"); + assert_eq!(pallet.extrinsics.len(), 9); + Ok(()) + } + + #[tokio::test] + async fn find_extrinsic_by_name_works() -> Result<()> { + let api = set_up_api("wss://rpc1.paseo.popnetwork.xyz").await?; + let pallets = parse_chain_metadata(&api).await?; + assert!(matches!( + find_extrinsic_by_name(&pallets, "WrongName", "wrong_extrinsic").await, + Err(Error::PalletNotFound(pallet)) if pallet == "WrongName".to_string())); + assert!(matches!( + find_extrinsic_by_name(&pallets, "Balances", "wrong_extrinsic").await, + Err(Error::ExtrinsicNotSupported(extrinsic)) if extrinsic == "wrong_extrinsic".to_string())); + let extrinsic = find_extrinsic_by_name(&pallets, "Balances", "force_transfer").await?; + assert_eq!(extrinsic.name, "force_transfer"); + assert_eq!(extrinsic.docs, "Exactly as `transfer_allow_death`, except the origin must be root and the source accountmay be specified."); + assert_eq!(extrinsic.is_supported, true); + assert_eq!(extrinsic.params.len(), 3); + Ok(()) + } + + #[tokio::test] + async fn parse_extrinsic_arguments_works() -> Result<()> { + // Values for testing from: https://docs.rs/scale-value/0.18.0/scale_value/stringify/fn.from_str.html + // and https://docs.rs/scale-value/0.18.0/scale_value/stringify/fn.from_str_custom.html + let args = [ + "1".to_string(), + "-1".to_string(), + "true".to_string(), + "'a'".to_string(), + "\"hi\"".to_string(), + "{ a: true, b: \"hello\" }".to_string(), + "MyVariant { a: true, b: \"hello\" }".to_string(), + "<0101>".to_string(), + "(1,2,0x030405)".to_string(), + r#"{ + name: "Alice", + address: 5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty + }"# + .to_string(), + ] + .to_vec(); + let addr: Vec<_> = + hex::decode("8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48") + .unwrap() + .into_iter() + .map(|b| Value::u128(b as u128)) + .collect(); + assert_eq!( + parse_extrinsic_arguments(args).await?, + [ + Value::u128(1), + Value::i128(-1), + Value::bool(true), + Value::char('a'), + Value::string("hi"), + Value::named_composite(vec![ + ("a", Value::bool(true)), + ("b", Value::string("hello")) + ]), + Value::named_variant( + "MyVariant", + vec![("a", Value::bool(true)), ("b", Value::string("hello"))] + ), + Value::bit_sequence(scale_bits::Bits::from_iter([false, true, false, true])), + Value::unnamed_composite(vec![ + Value::u128(1), + Value::u128(2), + Value::unnamed_composite(vec![Value::u128(3), Value::u128(4), Value::u128(5),]) + ]), + Value::named_composite(vec![ + ("name", Value::string("Alice")), + ("address", Value::unnamed_composite(addr)) + ]) + ] + ); + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::set_up_api; + use anyhow::Result; + use subxt::ext::scale_bits; + + #[tokio::test] + async fn parse_chain_metadata_works() -> Result<()> { + let api = set_up_api("wss://rpc1.paseo.popnetwork.xyz").await?; + let pallets = parse_chain_metadata(&api).await?; + // Test the first pallet is parsed correctly + let first_pallet = pallets.first().unwrap(); + assert_eq!(first_pallet.name, "System"); + assert_eq!(first_pallet.docs, ""); + assert_eq!(first_pallet.extrinsics.len(), 11); + let first_extrinsic = first_pallet.extrinsics.first().unwrap(); + assert_eq!(first_extrinsic.name, "remark"); + assert_eq!( + first_extrinsic.docs, + "Make some on-chain remark.Can be executed by every `origin`." + ); + assert!(first_extrinsic.is_supported); + assert_eq!(first_extrinsic.params.first().unwrap().name, "remark"); + assert_eq!(first_extrinsic.params.first().unwrap().type_name, "[u8]"); + assert_eq!(first_extrinsic.params.first().unwrap().sub_params.len(), 0); + assert!(!first_extrinsic.params.first().unwrap().is_optional); + assert!(!first_extrinsic.params.first().unwrap().is_tuple); + assert!(!first_extrinsic.params.first().unwrap().is_variant); + Ok(()) + } + + #[tokio::test] + async fn find_pallet_by_name_works() -> Result<()> { + let api = set_up_api("wss://rpc1.paseo.popnetwork.xyz").await?; + let pallets = parse_chain_metadata(&api).await?; + assert!(matches!( + find_pallet_by_name(&pallets, "WrongName").await, + Err(Error::PalletNotFound(pallet)) if pallet == "WrongName".to_string())); + let pallet = find_pallet_by_name(&pallets, "Balances").await?; + assert_eq!(pallet.name, "Balances"); + assert_eq!(pallet.extrinsics.len(), 9); + Ok(()) + } + + #[tokio::test] + async fn find_extrinsic_by_name_works() -> Result<()> { + let api = set_up_api("wss://rpc1.paseo.popnetwork.xyz").await?; + let pallets = parse_chain_metadata(&api).await?; + assert!(matches!( + find_extrinsic_by_name(&pallets, "WrongName", "wrong_extrinsic").await, + Err(Error::PalletNotFound(pallet)) if pallet == "WrongName".to_string())); + assert!(matches!( + find_extrinsic_by_name(&pallets, "Balances", "wrong_extrinsic").await, + Err(Error::ExtrinsicNotSupported(extrinsic)) if extrinsic == "wrong_extrinsic".to_string())); + let extrinsic = find_extrinsic_by_name(&pallets, "Balances", "force_transfer").await?; + assert_eq!(extrinsic.name, "force_transfer"); + assert_eq!(extrinsic.docs, "Exactly as `transfer_allow_death`, except the origin must be root and the source accountmay be specified."); + assert_eq!(extrinsic.is_supported, true); + assert_eq!(extrinsic.params.len(), 3); + Ok(()) + } + + #[tokio::test] + async fn parse_extrinsic_arguments_works() -> Result<()> { + // Values for testing from: https://docs.rs/scale-value/0.18.0/scale_value/stringify/fn.from_str.html + // and https://docs.rs/scale-value/0.18.0/scale_value/stringify/fn.from_str_custom.html + let args = [ + "1".to_string(), + "-1".to_string(), + "true".to_string(), + "'a'".to_string(), + "\"hi\"".to_string(), + "{ a: true, b: \"hello\" }".to_string(), + "MyVariant { a: true, b: \"hello\" }".to_string(), + "<0101>".to_string(), + "(1,2,0x030405)".to_string(), + r#"{ + name: "Alice", + address: 5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty + }"# + .to_string(), + ] + .to_vec(); + let addr: Vec<_> = + hex::decode("8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48") + .unwrap() + .into_iter() + .map(|b| Value::u128(b as u128)) + .collect(); + assert_eq!( + parse_extrinsic_arguments(args).await?, + [ + Value::u128(1), + Value::i128(-1), + Value::bool(true), + Value::char('a'), + Value::string("hi"), + Value::named_composite(vec![ + ("a", Value::bool(true)), + ("b", Value::string("hello")) + ]), + Value::named_variant( + "MyVariant", + vec![("a", Value::bool(true)), ("b", Value::string("hello"))] + ), + Value::bit_sequence(scale_bits::Bits::from_iter([false, true, false, true])), + Value::unnamed_composite(vec![ + Value::u128(1), + Value::u128(2), + Value::unnamed_composite(vec![Value::u128(3), Value::u128(4), Value::u128(5),]) + ]), + Value::named_composite(vec![ + ("name", Value::string("Alice")), + ("address", Value::unnamed_composite(addr)) + ]) + ] + ); + Ok(()) + } +} diff --git a/crates/pop-parachains/src/call/mod.rs b/crates/pop-parachains/src/call/mod.rs index be9b4f69f..a9a5eb12c 100644 --- a/crates/pop-parachains/src/call/mod.rs +++ b/crates/pop-parachains/src/call/mod.rs @@ -1,7 +1,10 @@ // SPDX-License-Identifier: GPL-3.0 use crate::{errors::Error, Function}; -use pop_common::create_signer; +use pop_common::{ + call::{DefaultEnvironment, DisplayEvents, TokenMetadata, Verbosity}, + create_signer, +}; use subxt::{ dynamic::Value, tx::{DynamicPayload, Payload}, @@ -42,15 +45,26 @@ pub fn construct_sudo_extrinsic(xt: DynamicPayload) -> Result Result { + Ok(subxt::dynamic::tx("Sudo", "sudo", [tx.into_value()].to_vec())) +} + /// Signs and submits a given extrinsic. /// /// # Arguments /// * `client` - The client used to interact with the chain. -/// * `xt` - The extrinsic to be signed and submitted. +/// * `url` - Endpoint of the node. +/// * `xt` - The (encoded) extrinsic to be signed and submitted. /// * `suri` - The secret URI (e.g., mnemonic or private key) for signing the extrinsic. -pub async fn sign_and_submit_extrinsic( +pub async fn sign_and_submit_extrinsic( client: &OnlineClient, - xt: DynamicPayload, + url: &url::Url, + xt: Xt, suri: &str, ) -> Result { let signer = create_signer(suri)?; @@ -62,7 +76,19 @@ pub async fn sign_and_submit_extrinsic( .wait_for_finalized_success() .await .map_err(|e| Error::ExtrinsicSubmissionError(format!("{:?}", e)))?; - Ok(format!("{:?}", result.extrinsic_hash())) + + // Obtain required metadata and parse events. The following is using existing logic from + // `cargo-contract`, also used in calling contracts, due to simplicity and can be refactored in + // the future. + let metadata = client.metadata(); + let token_metadata = TokenMetadata::query::(url).await?; + let events = DisplayEvents::from_events::( + &result, None, &metadata, + )?; + let events = + events.display_events::(Verbosity::Default, &token_metadata)?; + + Ok(format!("Extrinsic Submitted with hash: {:?}\n\n{}", result.extrinsic_hash(), events)) } /// Encodes the call data for a given extrinsic into a hexadecimal string. @@ -89,9 +115,16 @@ pub fn decode_call_data(call_data: &str) -> Result, Error> { .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); +/// This struct implements the [`Payload`] trait and is used to submit +/// pre-encoded SCALE call data directly, without the dynamic construction of transactions. +pub struct CallData(Vec); + +impl CallData { + /// Create a new instance of `CallData`. + pub fn new(data: Vec) -> CallData { + CallData(data) + } +} impl Payload for CallData { fn encode_call_data_to( @@ -104,35 +137,12 @@ impl Payload for CallData { } } -/// 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, - call_data: Vec, - suri: &str, -) -> Result { - 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::*; use crate::{find_dispatchable_by_name, parse_chain_metadata, set_up_client}; use anyhow::Result; + use url::Url; const ALICE_SURI: &str = "//Alice"; pub(crate) const POP_NETWORK_TESTNET_URL: &str = "wss://rpc1.paseo.popnetwork.xyz"; @@ -214,7 +224,7 @@ mod tests { }; let xt = construct_extrinsic(&function, vec!["0x11".to_string()])?; assert!(matches!( - sign_and_submit_extrinsic(&client, xt, ALICE_SURI).await, + sign_and_submit_extrinsic(&client, &Url::parse(POP_NETWORK_TESTNET_URL)?, xt, ALICE_SURI).await, Err(Error::ExtrinsicSubmissionError(message)) if message.contains("PalletNameNotFound(\"WrongPallet\"))") )); Ok(()) @@ -238,4 +248,37 @@ mod tests { assert_eq!(xt.pallet_name(), "Sudo"); Ok(()) } + + #[tokio::test] + async fn sign_and_submit_wrong_extrinsic_fails() -> Result<()> { + let client = set_up_client("wss://rpc1.paseo.popnetwork.xyz").await?; + let tx = + construct_extrinsic("WrongPallet", "wrongExtrinsic", vec!["0x11".to_string()]).await?; + assert!(matches!( + sign_and_submit_extrinsic(client, tx, "//Alice").await, + Err(Error::ExtrinsicSubmissionError(message)) if message.contains("PalletNameNotFound(\"WrongPallet\"))") + )); + 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 31a86ade7..5bba81542 100644 --- a/crates/pop-parachains/src/lib.rs +++ b/crates/pop-parachains/src/lib.rs @@ -24,7 +24,7 @@ pub use call::{ params::Param, parse_chain_metadata, Function, Pallet, }, - set_up_client, sign_and_submit_extrinsic, sign_and_submit_extrinsic_with_call_data, + set_up_client, sign_and_submit_extrinsic, CallData, }; pub use errors::Error; pub use indexmap::IndexSet; diff --git a/tests/files/testing.contract b/tests/files/testing.contract new file mode 100644 index 000000000..fbd8e6fba --- /dev/null +++ b/tests/files/testing.contract @@ -0,0 +1 @@ +{"source":{"hash":"0xf4f0cafd08d8e362141b3c64e3c651ad6a38225dbf0b66f691c15bb0ea00eac3","language":"ink! 5.0.0","compiler":"rustc 1.78.0","wasm":"0x0061736d0100000001270760027f7f0060037f7f7f017f60000060047f7f7f7f017f60037f7f7f0060017f017f6000017f027406057365616c310b6765745f73746f726167650003057365616c3005696e7075740000057365616c320b7365745f73746f726167650003057365616c300b7365616c5f72657475726e0004057365616c301176616c75655f7472616e73666572726564000003656e76066d656d6f7279020102100313120101010100050000040006000200000002020616037f01418080040b7f00418080050b7f00418080050b0711020463616c6c0015066465706c6f7900160ad910122b01017f037f2002200346047f200005200020036a200120036a2d00003a0000200341016a21030c010b0b0b6f01017f0240200020014d04402000210303402002450d02200320012d00003a0000200341016a2103200141016a2101200241016b21020c000b000b200041016b2103200141016b210103402002450d01200220036a200120026a2d00003a0000200241016b21020c000b000b20000b2501017f037f2002200346047f200005200020036a20013a0000200341016a21030c010b0b0b3f01027f0340200245044041000f0b200241016b210220012d0000210320002d00002104200141016a2101200041016a210020032004460d000b200420036b0bc00101057f230041106b22022400410221044104210502402001100a220641ff01714102460d00200241086a2001100b20022d00080d000240024020022d000922030e020100020b20012802042203410449047f4101052001200341046b36020420012001280200220141046a3602002001280000210341000b2101200220033602042002200136020020022802000d0141012103200228020421040b20002003360204200020063a0000410821050b200020056a2004360200200241106a24000b3f01027f230041106b22012400200141086a2000100b20012d0009210020012d00082102200141106a240041024101410220004101461b410020001b20021b0b3c01017f200020012802042202047f2001200241016b36020420012001280200220141016a36020020012d00000520010b3a000120002002453a00000b2601017f230041106b220224002002200036020c20012002410c6a4104100d200241106a24000b4801027f024002402000280208220320026a22042003490d00200420002802044b0d00200420036b2002470d01200028020020036a2001200210051a200020043602080f0b000b000b2601017f230041106b22022400200220003a000f20012002410f6a4101100d200241106a24000b6102027f027e230041206b22002400200041106a22014200370300200042003703082000411036021c200041086a2000411c6a1004200028021c41114f0440000b2001290300210220002903082103200041206a2400410541042002200384501b0b3c01027f027f200145044041808004210141010c010b410121024180800441013a000041818004210141020b2103200120023a0000200020031013000b12004180800441003b0100410041021013000b920101057f230041106b220224002002428080013702082002418080043602044100200241046a2204100c024020022802082206200228020c2203490d00200228020421052002410036020c2002200620036b3602082002200320056a36020420012004100e20002004100c200228020c220020022802084b0d00200520032002280204200010021a200241106a24000f0b000b0d0020004180800420011003000b08002000200110120bb60501087f230041206b2200240020004180800136021441808004200041146a100102400240024020002802142201418180014f0d0041042104027f410420014104490d001a20004184800436020c2000200141046b360210418380042d00002101418280042d00002102418180042d00002103024002400240418080042d00002205412f470440200541ec00460d014104200541e300470d041a41042003413a470d041a4104200241a501470d041a41042202200141d100470d041a410221010c020b41042003418601470d031a4104200241db00470d031a41042202200141d901470d031a410321010c010b41042003410f470d021a41042002411d470d021a4104200141f701470d021a200041146a2000410c6a1009200028021822014102460d01200028021c210420002d001421020b20002001360204200020023a000041080c010b41040b20006a2004360200200028020422024104460d012000280208210520002d000021072000428080013702182000418080043602144100200041146a2203100c20002802182206200028021c2201490d00200028021421042000200620016b220636021420042001200120046a2201200310002000280214220420064b720d0020002004360218200020013602142003100a220141ff01714102460d002000280218220341034d2003410447720d00200028021428000021000240024002404102200241026b2203200341024f1b41016b0e020100020b2005200020021b20074100471014410041001010000b100f41ff01714105470d01230041106b220024002000418080043602044180800441003a00002000428080818010370208200141ff0171410047200041046a100e200028020c2200418180014f0440000b410020001013000b100f41ff01714105460d020b000b410141011010000b2000200141ff0171451014410041001010000be90201067f230041206b22002400024002400240100f41ff01714105470d0020004180800136021441808004200041146a100120002802142201418180014f0d004104210220014104490d0120004184800436020c2000200141046b360210418380042d00002101418280042d00002103418180042d00002104027f418080042d0000220541ed014704402005419b0147200441ae0147722003419d0147200141de004772720d0341022103410322012000410c6a100a220441ff01714102470d011a0c040b200441cb00472003419d0147722001411b47720d02200041146a2000410c6a1009200028021822034102460d0220002d00142104200028021c0b210120002003360204200020043a0000410821020c020b000b410321010b200020026a2001360200024020002802042201410347044020002d0000210220014102460d012000280208410020011b200241004710121011000b410141011010000b4100200210121011000b","build_info":{"rust_toolchain":"stable-aarch64-apple-darwin","cargo_contract_version":"5.0.0-alpha","build_mode":"Release","wasm_opt_settings":{"optimization_passes":"Z","keep_debug_symbols":false}}},"contract":{"name":"testing","version":"0.1.0","authors":["[your_name] <[your_email]>"]},"image":null,"version":5,"types":[{"id":0,"type":{"def":{"primitive":"bool"}}},{"id":1,"type":{"def":{"primitive":"u32"}}},{"id":2,"type":{"path":["testing","testing","Testing"],"def":{"composite":{"fields":[{"name":"value","type":0,"typeName":",>>::Type"},{"name":"number","type":1,"typeName":",>>::Type"}]}}}},{"id":3,"type":{"path":["Result"],"params":[{"name":"T","type":4},{"name":"E","type":5}],"def":{"variant":{"variants":[{"name":"Ok","fields":[{"type":4}],"index":0},{"name":"Err","fields":[{"type":5}],"index":1}]}}}},{"id":4,"type":{"def":{"tuple":[]}}},{"id":5,"type":{"path":["ink_primitives","LangError"],"def":{"variant":{"variants":[{"name":"CouldNotReadInput","index":1}]}}}},{"id":6,"type":{"path":["Option"],"params":[{"name":"T","type":1}],"def":{"variant":{"variants":[{"name":"None","index":0},{"name":"Some","fields":[{"type":1}],"index":1}]}}}},{"id":7,"type":{"path":["Result"],"params":[{"name":"T","type":0},{"name":"E","type":5}],"def":{"variant":{"variants":[{"name":"Ok","fields":[{"type":0}],"index":0},{"name":"Err","fields":[{"type":5}],"index":1}]}}}},{"id":8,"type":{"path":["ink_primitives","types","AccountId"],"def":{"composite":{"fields":[{"type":9,"typeName":"[u8; 32]"}]}}}},{"id":9,"type":{"def":{"array":{"len":32,"type":10}}}},{"id":10,"type":{"def":{"primitive":"u8"}}},{"id":11,"type":{"def":{"primitive":"u128"}}},{"id":12,"type":{"path":["ink_primitives","types","Hash"],"def":{"composite":{"fields":[{"type":9,"typeName":"[u8; 32]"}]}}}},{"id":13,"type":{"def":{"primitive":"u64"}}},{"id":14,"type":{"path":["ink_env","types","NoChainExtension"],"def":{"variant":{}}}}],"storage":{"root":{"root_key":"0x00000000","layout":{"struct":{"name":"Testing","fields":[{"name":"value","layout":{"leaf":{"key":"0x00000000","ty":0}}},{"name":"number","layout":{"leaf":{"key":"0x00000000","ty":1}}}]}},"ty":2}},"spec":{"constructors":[{"label":"new","selector":"0x9bae9d5e","payable":false,"args":[{"label":"init_value","type":{"type":0,"displayName":["bool"]}}],"returnType":{"type":3,"displayName":["ink_primitives","ConstructorResult"]},"docs":["Constructor that initializes the `bool` value to the given `init_value`."],"default":false},{"label":"default","selector":"0xed4b9d1b","payable":false,"args":[{"label":"init_value","type":{"type":0,"displayName":["bool"]}},{"label":"number","type":{"type":6,"displayName":["Option"]}}],"returnType":{"type":3,"displayName":["ink_primitives","ConstructorResult"]},"docs":["Constructor that initializes the `bool` value to `false`.","","Constructors can delegate to other constructors."],"default":false}],"messages":[{"label":"flip","selector":"0x633aa551","mutates":true,"payable":false,"args":[],"returnType":{"type":3,"displayName":["ink","MessageResult"]},"docs":[" A message that can be called on instantiated contracts."," This one flips the value of the stored `bool` from `true`"," to `false` and vice versa."],"default":false},{"label":"get","selector":"0x2f865bd9","mutates":false,"payable":false,"args":[],"returnType":{"type":7,"displayName":["ink","MessageResult"]},"docs":[" Simply returns the current value of our `bool`."],"default":false},{"label":"specific_flip","selector":"0x6c0f1df7","mutates":true,"payable":true,"args":[{"label":"new_value","type":{"type":0,"displayName":["bool"]}},{"label":"number","type":{"type":6,"displayName":["Option"]}}],"returnType":{"type":3,"displayName":["ink","MessageResult"]},"docs":[" A message for testing, flips the value of the stored `bool` with `new_value`"," and is payable"],"default":false}],"events":[],"docs":[],"lang_error":{"type":5,"displayName":["ink","LangError"]},"environment":{"accountId":{"type":8,"displayName":["AccountId"]},"balance":{"type":11,"displayName":["Balance"]},"hash":{"type":12,"displayName":["Hash"]},"timestamp":{"type":13,"displayName":["Timestamp"]},"blockNumber":{"type":1,"displayName":["BlockNumber"]},"chainExtension":{"type":14,"displayName":["ChainExtension"]},"maxEventTopics":4,"staticBufferSize":16384}}} \ No newline at end of file