Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: refactor signing in order to more easily be able to dryrun #547

Merged
merged 10 commits into from
Jun 17, 2022
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 @@ -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<T> + Send + Sync),
) -> Result<TransactionProgress<'client, T, E, Evs>, BasicError>
where
Expand All @@ -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<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 @@ -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<T> + Send + Sync),
) -> Result<T::Hash, BasicError>
where
Expand All @@ -304,20 +299,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 @@ -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<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 @@ -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
Expand Down Expand Up @@ -570,6 +573,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