Skip to content

Commit

Permalink
Allow creating/submitting unsigned transactions, too. (#625)
Browse files Browse the repository at this point in the history
* add tx.create_unsigned method

* check unsigned extrinsic shape against pjs output to give us some confidence it's ok
  • Loading branch information
jsdw authored Aug 18, 2022
1 parent 4f39f6f commit a71223a
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 17 deletions.
2 changes: 1 addition & 1 deletion subxt/src/tx/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ pub use self::{
Signer,
},
tx_client::{
SignedSubmittableExtrinsic,
SubmittableExtrinsic,
TxClient,
},
tx_payload::{
Expand Down
66 changes: 55 additions & 11 deletions subxt/src/tx/tx_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
{
Expand Down Expand Up @@ -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,
Expand All @@ -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,
{
Expand Down Expand Up @@ -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>,
Expand Down Expand Up @@ -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
}
}
41 changes: 36 additions & 5 deletions testing/integration-tests/src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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");
Expand All @@ -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);
}

0 comments on commit a71223a

Please sign in to comment.