Allow creating/submitting unsigned transactions, too. (#625)

* add tx.create_unsigned method

* check unsigned extrinsic shape against pjs output to give us some confidence it's ok
This commit is contained in:
James Wilson
2022-08-18 17:51:45 +01:00
committed by GitHub
parent 4f39f6f8fc
commit a71223ab4a
3 changed files with 92 additions and 17 deletions
+1 -1
View File
@@ -42,7 +42,7 @@ pub use self::{
Signer,
},
tx_client::{
SignedSubmittableExtrinsic,
SubmittableExtrinsic,
TxClient,
},
tx_payload::{
+55 -11
View File
@@ -79,14 +79,52 @@ impl<T: Config, C: OfflineClientT<T>> TxClient<T, C> {
Ok(bytes)
}
/// Creates a raw signed extrinsic, without submitting it.
/// Creates an unsigned extrinsic without submitting it.
pub async fn create_unsigned<Call>(
&self,
call: &Call,
) -> Result<SubmittableExtrinsic<T, C>, Error>
where
Call: TxPayload,
{
// 1. Validate this call against the current node metadata if the call comes
// with a hash allowing us to do so.
self.validate(call)?;
// 2. Encode extrinsic
let extrinsic = {
let mut encoded_inner = Vec::new();
// transaction protocol version (4) (is not signed, so no 1 bit at the front).
4u8.encode_to(&mut encoded_inner);
// encode call data after this byte.
call.encode_call_data(&self.client.metadata(), &mut encoded_inner)?;
// now, prefix byte length:
let len = Compact(
u32::try_from(encoded_inner.len())
.expect("extrinsic size expected to be <4GB"),
);
let mut encoded = Vec::new();
len.encode_to(&mut encoded);
encoded.extend(encoded_inner);
encoded
};
// Wrap in Encoded to ensure that any more "encode" calls leave it in the right state.
Ok(SubmittableExtrinsic {
client: self.client.clone(),
encoded: Encoded(extrinsic),
marker: std::marker::PhantomData,
})
}
/// Creates a raw signed extrinsic without submitting it.
pub async fn create_signed_with_nonce<Call>(
&self,
call: &Call,
signer: &(dyn Signer<T> + Send + Sync),
account_nonce: T::Index,
other_params: <T::ExtrinsicParams as ExtrinsicParams<T::Index, T::Hash>>::OtherParams,
) -> Result<SignedSubmittableExtrinsic<T, C>, Error>
) -> Result<SubmittableExtrinsic<T, C>, Error>
where
Call: TxPayload,
{
@@ -160,7 +198,7 @@ impl<T: Config, C: OfflineClientT<T>> TxClient<T, C> {
// 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(SignedSubmittableExtrinsic {
Ok(SubmittableExtrinsic {
client: self.client.clone(),
encoded: Encoded(extrinsic),
marker: std::marker::PhantomData,
@@ -175,7 +213,7 @@ impl<T: Config, C: OnlineClientT<T>> TxClient<T, C> {
call: &Call,
signer: &(dyn Signer<T> + Send + Sync),
other_params: <T::ExtrinsicParams as ExtrinsicParams<T::Index, T::Hash>>::OtherParams,
) -> Result<SignedSubmittableExtrinsic<T, C>, Error>
) -> Result<SubmittableExtrinsic<T, C>, Error>
where
Call: TxPayload,
{
@@ -277,13 +315,24 @@ impl<T: Config, C: OnlineClientT<T>> TxClient<T, C> {
}
/// This represents an extrinsic that has been signed and is ready to submit.
pub struct SignedSubmittableExtrinsic<T, C> {
pub struct SubmittableExtrinsic<T, C> {
client: C,
encoded: Encoded,
marker: std::marker::PhantomData<T>,
}
impl<T, C> SignedSubmittableExtrinsic<T, C>
impl<T, C> SubmittableExtrinsic<T, C>
where
T: Config,
C: OfflineClientT<T>,
{
/// Returns the SCALE encoded extrinsic bytes.
pub fn encoded(&self) -> &[u8] {
&self.encoded.0
}
}
impl<T, C> SubmittableExtrinsic<T, C>
where
T: Config,
C: OnlineClientT<T>,
@@ -323,9 +372,4 @@ where
) -> Result<ApplyExtrinsicResult, Error> {
self.client.rpc().dry_run(self.encoded(), at).await
}
/// Returns the SCALE encoded extrinsic bytes.
pub fn encoded(&self) -> &[u8] {
&self.encoded.0
}
}
+36 -5
View File
@@ -149,8 +149,8 @@ async fn dry_run_passes() {
.await
.unwrap();
api.rpc()
.dry_run(signed_extrinsic.encoded(), None)
signed_extrinsic
.dry_run(None)
.await
.expect("dryrunning failed")
.expect("expected dryrunning to be successful")
@@ -186,9 +186,8 @@ async fn dry_run_fails() {
.await
.unwrap();
let dry_run_res = api
.rpc()
.dry_run(signed_extrinsic.encoded(), None)
let dry_run_res = signed_extrinsic
.dry_run(None)
.await
.expect("dryrunning failed")
.expect("expected dryrun transaction to be valid");
@@ -214,3 +213,35 @@ async fn dry_run_fails() {
panic!("expected a runtime module error");
}
}
#[tokio::test]
async fn unsigned_extrinsic_is_same_shape_as_polkadotjs() {
let ctx = test_context().await;
let api = ctx.client();
let tx = node_runtime::tx().balances().transfer(
pair_signer(AccountKeyring::Alice.pair())
.account_id()
.clone()
.into(),
12345,
);
let actual_tx = api.tx().create_unsigned(&tx).await.unwrap();
let actual_tx_bytes = actual_tx.encoded();
// How these were obtained:
// - start local substrate node.
// - open polkadot.js UI in browser and point at local node.
// - open dev console (may need to refresh page now) and find the WS connection.
// - create a balances.transfer to ALICE with 12345 and "submit unsigned".
// - find the submitAndWatchExtrinsic call in the WS connection to get these bytes:
let expected_tx_bytes = hex::decode(
"9804060000d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27de5c0",
)
.unwrap();
// Make sure our encoding is the same as the encoding polkadot UI created.
assert_eq!(actual_tx_bytes, expected_tx_bytes);
}