diff --git a/subxt/src/tx/tx_client.rs b/subxt/src/tx/tx_client.rs index 5abe69affd..edcc6b1eb4 100644 --- a/subxt/src/tx/tx_client.rs +++ b/subxt/src/tx/tx_client.rs @@ -28,6 +28,7 @@ use codec::{ Encode, }; use derivative::Derivative; +use std::borrow::Cow; // This is returned from an API below, so expose it here. pub use crate::rpc::types::DryRunResult; @@ -122,24 +123,22 @@ impl> TxClient { )) } - /// Creates a raw signed extrinsic without submitting it. - pub fn create_signed_with_nonce( + /// Create a partial extrinsic. + pub fn create_partial_signed_with_nonce( &self, call: &Call, - signer: &Signer, account_nonce: T::Index, other_params: >::OtherParams, - ) -> Result, Error> + ) -> Result, Error> where Call: TxPayload, - Signer: SignerT, { // 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. SCALE encode call data to bytes (pallet u8, call u8, call params). - let call_data = Encoded(self.call_data(call)?); + let call_data = self.call_data(call)?; // 3. Construct our custom additional/extra params. let additional_and_extra_params = { @@ -154,60 +153,37 @@ impl> TxClient { ) }; - tracing::debug!( - "tx additional_and_extra_params: {:?}", - additional_and_extra_params - ); - - // 4. Construct signature. This is compatible with the Encode impl - // for SignedPayload (which is this payload of bytes that we'd like) - // to sign. See: - // https://github.com/paritytech/substrate/blob/9a6d706d8db00abb6ba183839ec98ecd9924b1f8/primitives/runtime/src/generic/unchecked_extrinsic.rs#L215) - let signature = { - let mut bytes = Vec::new(); - call_data.encode_to(&mut bytes); - additional_and_extra_params.encode_extra_to(&mut bytes); - additional_and_extra_params.encode_additional_to(&mut bytes); - if bytes.len() > 256 { - signer.sign(T::Hasher::hash_of(&Encoded(bytes)).as_ref()) - } else { - signer.sign(&bytes) - } - }; + // Return these details, ready to construct a signed extrinsic from. + Ok(PartialExtrinsic { + client: self.client.clone(), + call_data, + additional_and_extra_params, + }) + } - tracing::debug!("tx signature: {}", hex::encode(signature.encode())); + /// Creates a signed extrinsic without submitting it. + pub fn create_signed_with_nonce( + &self, + call: &Call, + signer: &Signer, + account_nonce: T::Index, + other_params: >::OtherParams, + ) -> Result, Error> + where + Call: TxPayload, + Signer: SignerT, + { + // 1. Validate this call against the current node metadata if the call comes + // with a hash allowing us to do so. + self.validate(call)?; - // 5. Encode extrinsic, now that we have the parts we need. This is compatible - // with the Encode impl for UncheckedExtrinsic (protocol version 4). - let extrinsic = { - let mut encoded_inner = Vec::new(); - // "is signed" + transaction protocol version (4) - (0b10000000 + 4u8).encode_to(&mut encoded_inner); - // from address for signature - signer.address().encode_to(&mut encoded_inner); - // the signature bytes - signature.encode_to(&mut encoded_inner); - // attach custom extra params - additional_and_extra_params.encode_extra_to(&mut encoded_inner); - // and now, call data - call_data.encode_to(&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 - }; + // 2. Gather the "additional" and "extra" params along with the encoded call data, + // ready to be signed. + let partial_signed = + self.create_partial_signed_with_nonce(call, account_nonce, other_params)?; - // 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(SubmittableExtrinsic::from_bytes( - self.client.clone(), - extrinsic, - )) + // 3. Sign and construct an extrinsic from these details. + Ok(partial_signed.sign(signer)) } } @@ -216,7 +192,32 @@ where T: Config, C: OnlineClientT, { - /// Creates a raw signed extrinsic, without submitting it. + // Get the next account nonce to use. + async fn next_account_nonce( + &self, + account_id: &T::AccountId, + ) -> Result { + self.client + .rpc() + .system_account_next_index(account_id) + .await + } + + /// Creates a partial signed extrinsic, without submitting it. + pub async fn create_partial_signed( + &self, + call: &Call, + account_id: &T::AccountId, + other_params: >::OtherParams, + ) -> Result, Error> + where + Call: TxPayload, + { + let account_nonce = self.next_account_nonce(account_id).await?; + self.create_partial_signed_with_nonce(call, account_nonce, other_params) + } + + /// Creates a signed extrinsic, without submitting it. pub async fn create_signed( &self, call: &Call, @@ -227,13 +228,7 @@ where Call: TxPayload, Signer: SignerT, { - // Get nonce from the node. - let account_nonce = self - .client - .rpc() - .system_account_next_index(signer.account_id()) - .await?; - + let account_nonce = self.next_account_nonce(signer.account_id()).await?; self.create_signed_with_nonce(call, signer, account_nonce, other_params) } @@ -324,6 +319,100 @@ where } } +/// This payload contains the information needed to produce an extrinsic. +pub struct PartialExtrinsic { + client: C, + call_data: Vec, + additional_and_extra_params: T::ExtrinsicParams, +} + +impl PartialExtrinsic +where + T: Config, + C: OfflineClientT, +{ + // Obtain bytes representing the signer payload and run call some function + // with them. This can avoid an allocation in some cases when compared to + // [`PartialExtrinsic::signer_payload()`]. + fn with_signer_payload(&self, f: F) -> R + where + F: for<'a> FnOnce(Cow<'a, [u8]>) -> R, + { + let mut bytes = self.call_data.clone(); + self.additional_and_extra_params.encode_extra_to(&mut bytes); + self.additional_and_extra_params + .encode_additional_to(&mut bytes); + if bytes.len() > 256 { + f(Cow::Borrowed(T::Hasher::hash_of(&Encoded(bytes)).as_ref())) + } else { + f(Cow::Owned(bytes)) + } + } + + /// Return the signer payload for this extrinsic. These are the bytes that must + /// be signed in order to produce a valid signature for the extrinsic. + pub fn signer_payload(&self) -> Vec { + self.with_signer_payload(|bytes| bytes.to_vec()) + } + + /// Return the bytes representing the call data for this partially constructed + /// extrinsic. + pub fn call_data(&self) -> &[u8] { + &self.call_data + } + + /// Convert this [`PartialExtrinsic`] into a [`SubmittableExtrinsic`], ready to submit. + /// The provided `signer` is responsible for providing the "from" address for the transaction, + /// as well as providing a signature to attach to it. + pub fn sign(&self, signer: &Signer) -> SubmittableExtrinsic + where + Signer: SignerT, + { + // Given our signer, we can sign the payload representing this extrinsic. + let signature = self.with_signer_payload(|bytes| signer.sign(&bytes)); + // Now, use the signature and "from" address to build the extrinsic. + self.sign_with_address_and_signature(&signer.address(), &signature) + } + + /// Convert this [`PartialExtrinsic`] into a [`SubmittableExtrinsic`], ready to submit. + /// An address, and something representing a signature that can be SCALE encoded, are both + /// needed in order to construct it. If you have a `Signer` to hand, you can use + /// [`PartialExtrinsic::sign()`] instead. + pub fn sign_with_address_and_signature( + &self, + address: &T::Address, + signature: &S, + ) -> SubmittableExtrinsic { + // Encode the extrinsic (into the format expected by protocol version 4) + let extrinsic = { + let mut encoded_inner = Vec::new(); + // "is signed" + transaction protocol version (4) + (0b10000000 + 4u8).encode_to(&mut encoded_inner); + // from address for signature + address.encode_to(&mut encoded_inner); + // the signature + signature.encode_to(&mut encoded_inner); + // attach custom extra params + self.additional_and_extra_params + .encode_extra_to(&mut encoded_inner); + // and now, call data (remembering that it's been encoded already and just needs appending) + encoded_inner.extend(&self.call_data); + // 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 + }; + + // Return an extrinsic ready to be submitted. + SubmittableExtrinsic::from_bytes(self.client.clone(), extrinsic) + } +} + /// This represents an extrinsic that has been signed and is ready to submit. pub struct SubmittableExtrinsic { client: C, diff --git a/testing/integration-tests/src/client/mod.rs b/testing/integration-tests/src/client/mod.rs index 297588d2e8..8273a9d4df 100644 --- a/testing/integration-tests/src/client/mod.rs +++ b/testing/integration-tests/src/client/mod.rs @@ -34,6 +34,7 @@ use subxt::{ RuntimeEvent, RuntimeVersionEvent, }, + tx::Signer, utils::AccountId32, }; @@ -244,6 +245,39 @@ async fn dry_run_fails() { } } +#[tokio::test] +async fn external_signing() { + let ctx = test_context().await; + let api = ctx.client(); + let alice = pair_signer(AccountKeyring::Alice.pair()); + + // Create a partial extrinsic. We can get the signer payload at this point, to be + // signed externally. + let tx = node_runtime::tx().preimage().note_preimage(vec![0u8]); + let partial_extrinsic = api + .tx() + .create_partial_signed(&tx, alice.account_id(), Default::default()) + .await + .unwrap(); + + // Get the signer payload. + let signer_payload = partial_extrinsic.signer_payload(); + // Sign it (possibly externally). + let signature = alice.sign(&signer_payload); + // Use this to build a signed extrinsic. + let extrinsic = + partial_extrinsic.sign_with_address_and_signature(&alice.address(), &signature); + + // And now submit it. + extrinsic + .submit_and_watch() + .await + .unwrap() + .wait_for_finalized_success() + .await + .unwrap(); +} + #[tokio::test] async fn submit_large_extrinsic() { let ctx = test_context().await;