Skip to content

Commit

Permalink
feat: refactor signing in order to more easily be able to dryrun (#547)
Browse files Browse the repository at this point in the history
* feat: refactor signing in order to more easily be able to dryrun

Co-authored-by: Sander Bosma <[email protected]>
Co-authored-by: Daniel Savu <[email protected]>

* chore: move dry_run to rpc file

* fix: failing dryrun test

* fix: run cargo fmt

* chore(dryrun): Replace complex SubmittableExtrinsic type with bytes array

* cargo fmt

* feat: add dry_run method to signed submittable extrinsic

* fmt

Co-authored-by: Sander Bosma <[email protected]>
  • Loading branch information
daniel-savu and sander2 authored Jun 17, 2022
1 parent 3baea19 commit cb18ad7
Show file tree
Hide file tree
Showing 3 changed files with 197 additions and 28 deletions.
101 changes: 97 additions & 4 deletions integration-tests/src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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");
}
}
100 changes: 79 additions & 21 deletions subxt/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@
// along with subxt. If not, see <http://www.gnu.org/licenses/>.

use futures::future;
use sp_runtime::traits::Hash;
pub use sp_runtime::traits::SignedExtension;
use sp_runtime::{
traits::Hash,
ApplyExtrinsicResult,
};

use crate::{
error::{
Expand Down Expand Up @@ -287,7 +290,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<T> + Send + Sync),
) -> Result<TransactionProgress<'client, T, E, Evs>, BasicError>
where
Expand All @@ -302,22 +305,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<T> + Send + Sync),
other_params: X::OtherParams,
) -> Result<TransactionProgress<'client, T, E, Evs>, 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
Expand All @@ -331,7 +326,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<T> + Send + Sync),
) -> Result<T::Hash, BasicError>
where
Expand All @@ -349,20 +344,22 @@ 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<T> + Send + Sync),
other_params: X::OtherParams,
) -> Result<T::Hash, BasicError> {
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.
pub async fn create_signed(
&self,
signer: &(dyn Signer<T> + Send + Sync),
other_params: X::OtherParams,
) -> Result<Encoded, BasicError> {
) -> Result<SignedSubmittableExtrinsic<'client, T, X, E, Evs>, BasicError> {
// 1. Get nonce
let account_nonce = if let Some(nonce) = signer.nonce() {
nonce
Expand Down Expand Up @@ -449,6 +446,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<T>,
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<T>,
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<TransactionProgress<'client, T, E, Evs>, 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<T::Hash, BasicError> {
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<T::Hash>,
) -> Result<ApplyExtrinsicResult, BasicError> {
self.client.rpc().dry_run(self.encoded(), at).await
}

/// Returns the SCALE encoded extrinsic bytes.
pub fn encoded(&self) -> &[u8] {
&self.encoded.0
}
}
24 changes: 21 additions & 3 deletions subxt/src/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,9 +146,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
Expand Down Expand Up @@ -643,6 +646,21 @@ impl<T: Config> Rpc<T> {
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<T::Hash>,
) -> Result<ApplyExtrinsicResult, BasicError> {
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
Expand Down

0 comments on commit cb18ad7

Please sign in to comment.