diff --git a/subxt/src/tx/mod.rs b/subxt/src/tx/mod.rs index 95a60a8f98..8288898efb 100644 --- a/subxt/src/tx/mod.rs +++ b/subxt/src/tx/mod.rs @@ -42,7 +42,7 @@ pub use self::{ Signer, }, tx_client::{ - SignedSubmittableExtrinsic, + SubmittableExtrinsic, TxClient, }, tx_payload::{ diff --git a/subxt/src/tx/tx_client.rs b/subxt/src/tx/tx_client.rs index 1c281c39e6..b35e0586b3 100644 --- a/subxt/src/tx/tx_client.rs +++ b/subxt/src/tx/tx_client.rs @@ -79,14 +79,52 @@ impl> TxClient { Ok(bytes) } - /// Creates a raw signed extrinsic, without submitting it. + /// Creates an unsigned extrinsic without submitting it. + pub async fn create_unsigned( + &self, + call: &Call, + ) -> Result, 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( &self, call: &Call, signer: &(dyn Signer + Send + Sync), account_nonce: T::Index, other_params: >::OtherParams, - ) -> Result, Error> + ) -> Result, Error> where Call: TxPayload, { @@ -160,7 +198,7 @@ impl> TxClient { // 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> TxClient { call: &Call, signer: &(dyn Signer + Send + Sync), other_params: >::OtherParams, - ) -> Result, Error> + ) -> Result, Error> where Call: TxPayload, { @@ -277,13 +315,24 @@ impl> TxClient { } /// This represents an extrinsic that has been signed and is ready to submit. -pub struct SignedSubmittableExtrinsic { +pub struct SubmittableExtrinsic { client: C, encoded: Encoded, marker: std::marker::PhantomData, } -impl SignedSubmittableExtrinsic +impl SubmittableExtrinsic +where + T: Config, + C: OfflineClientT, +{ + /// Returns the SCALE encoded extrinsic bytes. + pub fn encoded(&self) -> &[u8] { + &self.encoded.0 + } +} + +impl SubmittableExtrinsic where T: Config, C: OnlineClientT, @@ -323,9 +372,4 @@ where ) -> 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/testing/integration-tests/src/client/mod.rs b/testing/integration-tests/src/client/mod.rs index 9e68c75af2..25a9bf8615 100644 --- a/testing/integration-tests/src/client/mod.rs +++ b/testing/integration-tests/src/client/mod.rs @@ -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); +}