diff --git a/integration-tests/src/client/mod.rs b/integration-tests/src/client/mod.rs index 050ef2e32b..f06708f05a 100644 --- a/integration-tests/src/client/mod.rs +++ b/integration-tests/src/client/mod.rs @@ -17,14 +17,24 @@ use crate::{ test_node_process, test_node_process_with, - utils::node_runtime::system, + utils::{ + node_runtime::system, + pair_signer, + test_context, + }, }; -use sp_core::storage::{ - well_known_keys, - StorageKey, +use sp_core::{ + sr25519::Pair, + storage::{ + well_known_keys, + StorageKey, + }, + Pair as _, }; use sp_keyring::AccountKeyring; +use sp_runtime::DispatchOutcome; +use subxt::Error; #[tokio::test] async fn insert_key() { @@ -131,3 +141,86 @@ async fn fetch_system_info() { assert_eq!(client.rpc().system_name().await.unwrap(), "Substrate Node"); assert!(!client.rpc().system_version().await.unwrap().is_empty()); } + +#[tokio::test] +async fn dry_run_passes() { + let node_process = test_node_process().await; + let client = node_process.client(); + + let alice = pair_signer(AccountKeyring::Alice.pair()); + let bob = pair_signer(AccountKeyring::Bob.pair()); + let bob_address = bob.account_id().clone().into(); + let cxt = test_context().await; + let api = &cxt.api; + let signed_extrinsic = api + .tx() + .balances() + .transfer(bob_address, 10_000) + .unwrap() + .create_signed(&alice, Default::default()) + .await + .unwrap(); + + client + .rpc() + .dry_run(signed_extrinsic.encoded(), None) + .await + .expect("dryrunning failed") + .expect("expected dryrunning to be successful") + .unwrap(); + signed_extrinsic + .submit_and_watch() + .await + .unwrap() + .wait_for_finalized_success() + .await + .unwrap(); +} + +#[tokio::test] +async fn dry_run_fails() { + let node_process = test_node_process().await; + let client = node_process.client(); + + let alice = pair_signer(AccountKeyring::Alice.pair()); + let hans = pair_signer(Pair::generate().0); + let hans_address = hans.account_id().clone().into(); + let cxt = test_context().await; + let api = &cxt.api; + let signed_extrinsic = api + .tx() + .balances() + .transfer( + hans_address, + 100_000_000_000_000_000_000_000_000_000_000_000, + ) + .unwrap() + .create_signed(&alice, Default::default()) + .await + .unwrap(); + + let dry_run_res: DispatchOutcome = client + .rpc() + .dry_run(signed_extrinsic.encoded(), None) + .await + .expect("dryrunning failed") + .expect("expected dryrun transaction to be valid"); + if let Err(sp_runtime::DispatchError::Module(module_error)) = dry_run_res { + assert_eq!(module_error.index, 6); + assert_eq!(module_error.error, 2); + } else { + panic!("expected a module error when dryrunning"); + } + let res = signed_extrinsic + .submit_and_watch() + .await + .unwrap() + .wait_for_finalized_success() + .await; + if let Err(Error::Module(err)) = res { + assert_eq!(err.pallet, "Balances"); + assert_eq!(err.error, "InsufficientBalance"); + } else { + panic!("expected a runtime module error"); + } +} diff --git a/subxt/src/client.rs b/subxt/src/client.rs index 89d6cd2315..eaf3854ab8 100644 --- a/subxt/src/client.rs +++ b/subxt/src/client.rs @@ -15,8 +15,11 @@ // along with subxt. If not, see . use futures::future; -use sp_runtime::traits::Hash; pub use sp_runtime::traits::SignedExtension; +use sp_runtime::{ + traits::Hash, + ApplyExtrinsicResult, +}; use crate::{ error::{ @@ -242,7 +245,7 @@ where /// Returns a [`TransactionProgress`], which can be used to track the status of the transaction /// and obtain details about it, once it has made it into a block. pub async fn sign_and_submit_then_watch_default( - self, + &self, signer: &(dyn Signer + Send + Sync), ) -> Result, BasicError> where @@ -257,22 +260,14 @@ where /// Returns a [`TransactionProgress`], which can be used to track the status of the transaction /// and obtain details about it, once it has made it into a block. pub async fn sign_and_submit_then_watch( - self, + &self, signer: &(dyn Signer + Send + Sync), other_params: X::OtherParams, ) -> Result, BasicError> { - // Sign the call data to create our extrinsic. - let extrinsic = self.create_signed(signer, other_params).await?; - - // Get a hash of the extrinsic (we'll need this later). - let ext_hash = T::Hashing::hash_of(&extrinsic); - - tracing::info!("xt hash: {}", hex::encode(ext_hash.encode())); - - // Submit and watch for transaction progress. - let sub = self.client.rpc().watch_extrinsic(extrinsic).await?; - - Ok(TransactionProgress::new(sub, self.client, ext_hash)) + self.create_signed(signer, other_params) + .await? + .submit_and_watch() + .await } /// Creates and signs an extrinsic and submits to the chain for block inclusion. Passes @@ -286,7 +281,7 @@ where /// Success does not mean the extrinsic has been included in the block, just that it is valid /// and has been included in the transaction pool. pub async fn sign_and_submit_default( - self, + &self, signer: &(dyn Signer + Send + Sync), ) -> Result where @@ -304,12 +299,14 @@ where /// Success does not mean the extrinsic has been included in the block, just that it is valid /// and has been included in the transaction pool. pub async fn sign_and_submit( - self, + &self, signer: &(dyn Signer + Send + Sync), other_params: X::OtherParams, ) -> Result { - let extrinsic = self.create_signed(signer, other_params).await?; - self.client.rpc().submit_extrinsic(extrinsic).await + self.create_signed(signer, other_params) + .await? + .submit() + .await } /// Creates a returns a raw signed extrinsic, without submitting it. @@ -317,7 +314,7 @@ where &self, signer: &(dyn Signer + Send + Sync), other_params: X::OtherParams, - ) -> Result { + ) -> Result, BasicError> { // 1. Get nonce let account_nonce = if let Some(nonce) = signer.nonce() { nonce @@ -404,6 +401,67 @@ where // Wrap in Encoded to ensure that any more "encode" calls leave it in the right state. // maybe we can just return the raw bytes.. - Ok(Encoded(extrinsic)) + Ok(SignedSubmittableExtrinsic { + client: self.client, + encoded: Encoded(extrinsic), + marker: self.marker, + }) + } +} + +pub struct SignedSubmittableExtrinsic<'client, T: Config, X, E: Decode, Evs: Decode> { + client: &'client Client, + encoded: Encoded, + marker: std::marker::PhantomData<(X, E, Evs)>, +} + +impl<'client, T, X, E, Evs> SignedSubmittableExtrinsic<'client, T, X, E, Evs> +where + T: Config, + X: ExtrinsicParams, + E: Decode + HasModuleError, + Evs: Decode, +{ + /// Submits the extrinsic to the chain. + /// + /// Returns a [`TransactionProgress`], which can be used to track the status of the transaction + /// and obtain details about it, once it has made it into a block. + pub async fn submit_and_watch( + &self, + ) -> Result, BasicError> { + // Get a hash of the extrinsic (we'll need this later). + let ext_hash = T::Hashing::hash_of(&self.encoded); + + // Submit and watch for transaction progress. + let sub = self.client.rpc().watch_extrinsic(&self.encoded).await?; + + Ok(TransactionProgress::new(sub, self.client, ext_hash)) + } + + /// Submits the extrinsic to the chain for block inclusion. + /// + /// Returns `Ok` with the extrinsic hash if it is valid extrinsic. + /// + /// # Note + /// + /// Success does not mean the extrinsic has been included in the block, just that it is valid + /// and has been included in the transaction pool. + pub async fn submit(&self) -> Result { + self.client.rpc().submit_extrinsic(&self.encoded).await + } + + /// Submits the extrinsic to the dry_run RPC, to test if it would succeed. + /// + /// Returns `Ok` with an [`ApplyExtrinsicResult`], which is the result of applying of an extrinsic. + pub async fn dry_run( + &self, + at: Option, + ) -> Result { + self.client.rpc().dry_run(self.encoded(), at).await + } + + /// Returns the SCALE encoded extrinsic bytes. + pub fn encoded(&self) -> &[u8] { + &self.encoded.0 } } diff --git a/subxt/src/rpc.rs b/subxt/src/rpc.rs index 1aff88e8a9..73d99fe158 100644 --- a/subxt/src/rpc.rs +++ b/subxt/src/rpc.rs @@ -73,9 +73,12 @@ use sp_core::{ Bytes, U256, }; -use sp_runtime::generic::{ - Block, - SignedBlock, +use sp_runtime::{ + generic::{ + Block, + SignedBlock, + }, + ApplyExtrinsicResult, }; /// A number type that can be serialized both as a number or a string that encodes a number in a @@ -570,6 +573,21 @@ impl Rpc { let params = rpc_params![public_key, key_type]; Ok(self.client.request("author_hasKey", params).await?) } + + /// Submits the extrinsic to the dry_run RPC, to test if it would succeed. + /// + /// Returns `Ok` with an [`ApplyExtrinsicResult`], which is the result of applying of an extrinsic. + pub async fn dry_run( + &self, + encoded_signed: &[u8], + at: Option, + ) -> Result { + let params = rpc_params![format!("0x{}", hex::encode(encoded_signed)), at]; + let result_bytes: Bytes = self.client.request("system_dryRun", params).await?; + let data: ApplyExtrinsicResult = + codec::Decode::decode(&mut result_bytes.0.as_slice())?; + Ok(data) + } } /// Build WS RPC client from URL