mod account_nonce;
mod default_params;
mod payload;
mod signer;
mod transaction_progress;
mod validation_result;
use crate::backend::TransactionStatus as BackendTransactionStatus;
use crate::client::{OfflineClientAtBlockT, OnlineClientAtBlockT};
use crate::config::transaction_extensions::Params;
use crate::config::{ClientState, Config, HashFor, Hasher, TransactionExtensions};
use crate::error::{ExtrinsicError, TransactionStatusError};
use codec::{Compact, Decode, Encode};
use core::marker::PhantomData;
use scale_value::Composite;
pub use default_params::DefaultParams;
pub use payload::{DynamicPayload, Payload, StaticPayload, ValidationDetails, dynamic};
pub use signer::Signer;
pub use transaction_progress::{TransactionInBlock, TransactionProgress, TransactionStatus};
pub use validation_result::{
TransactionInvalid, TransactionUnknown, TransactionValid, ValidationResult,
};
#[derive(Clone)]
pub struct TransactionsClient<T, Client> {
client: Client,
marker: PhantomData<T>,
}
impl<T, Client> TransactionsClient<T, Client> {
pub(crate) fn new(client: Client) -> Self {
TransactionsClient {
client,
marker: PhantomData,
}
}
}
impl<T: Config, Client: OfflineClientAtBlockT<T>> TransactionsClient<T, Client> {
pub fn validate<Call>(&self, call: &Call) -> Result<(), ExtrinsicError>
where
Call: Payload,
{
let Some(details) = call.validation_details() else {
return Ok(());
};
let pallet_name = details.pallet_name;
let call_name = details.call_name;
let expected_hash = self
.client
.metadata_ref()
.pallet_by_name(pallet_name)
.ok_or_else(|| ExtrinsicError::PalletNameNotFound(pallet_name.to_string()))?
.call_hash(call_name)
.ok_or_else(|| ExtrinsicError::CallNameNotFound {
pallet_name: pallet_name.to_string(),
call_name: call_name.to_string(),
})?;
if details.hash != expected_hash {
Err(ExtrinsicError::IncompatibleCodegen)
} else {
Ok(())
}
}
pub fn from_bytes(&self, tx_bytes: Vec<u8>) -> SubmittableTransaction<T, Client> {
SubmittableTransaction {
client: self.client.clone(),
encoded: tx_bytes,
marker: PhantomData,
}
}
pub fn call_data<Call>(&self, call: &Call) -> Result<Vec<u8>, ExtrinsicError>
where
Call: Payload,
{
let encoded = frame_decode::extrinsics::encode_call_data(
call.pallet_name(),
call.call_name(),
call.call_data(),
self.client.metadata_ref(),
self.client.metadata_ref().types(),
)?;
Ok(encoded)
}
pub fn from_call_data_bytes(
&self,
call_data: &[u8],
) -> Result<DynamicPayload<Composite<()>>, ExtrinsicError> {
use frame_decode::extrinsics::ExtrinsicTypeInfo;
use scale_decode::DecodeAsFields;
let &[pallet_index, call_index, ..] = call_data else {
return Err(ExtrinsicError::CallDataTooShort(call_data.len()));
};
let metadata = self.client.metadata_ref();
let call_info = metadata
.extrinsic_call_info_by_index(pallet_index, call_index)
.map_err(|e| ExtrinsicError::CannotDecodeCallData(e.into_owned()))?;
let mut fields = call_info.args.iter().map(|arg| {
let name = (!arg.name.is_empty()).then_some(&*arg.name);
scale_decode::Field::new(arg.id, name)
});
let cursor = &mut &call_data[2..];
let args = Composite::<()>::decode_as_fields(cursor, &mut fields, metadata.types())
.map_err(ExtrinsicError::CannotDecodeCallDataFields)?;
if !cursor.is_empty() {
return Err(ExtrinsicError::LeftoverBytesDecodingCallData(cursor.len()));
}
Ok(dynamic(call_info.pallet_name, call_info.call_name, args))
}
pub fn create_unsigned<Call>(
&self,
call: &Call,
) -> Result<SubmittableTransaction<T, Client>, ExtrinsicError>
where
Call: Payload,
{
let tx = match self.default_transaction_version()? {
SupportedTransactionVersion::V4 => self.create_v4_unsigned(call),
SupportedTransactionVersion::V5 => self.create_v5_unsigned(call),
}?;
Ok(tx)
}
pub fn create_v4_unsigned<Call>(
&self,
call: &Call,
) -> Result<SubmittableTransaction<T, Client>, ExtrinsicError>
where
Call: Payload,
{
self.validate(call)?;
let encoded = frame_decode::extrinsics::encode_v4_unsigned(
call.pallet_name(),
call.call_name(),
call.call_data(),
self.client.metadata_ref(),
self.client.metadata_ref().types(),
)?;
Ok(SubmittableTransaction {
client: self.client.clone(),
encoded,
marker: PhantomData,
})
}
pub fn create_v5_unsigned<Call>(
&self,
call: &Call,
) -> Result<SubmittableTransaction<T, Client>, ExtrinsicError>
where
Call: Payload,
{
self.validate(call)?;
let encoded = frame_decode::extrinsics::encode_v5_bare(
call.pallet_name(),
call.call_name(),
call.call_data(),
self.client.metadata_ref(),
self.client.metadata_ref().types(),
)?;
Ok(SubmittableTransaction {
client: self.client.clone(),
encoded,
marker: PhantomData,
})
}
pub fn create_signable_offline<'call, Call>(
&self,
call: &'call Call,
params: <T::TransactionExtensions as TransactionExtensions<T>>::Params,
) -> Result<SignableTransaction<'call, T, Client, Call>, ExtrinsicError>
where
Call: Payload,
{
match self.default_transaction_version()? {
SupportedTransactionVersion::V4 => self.create_v4_signable_offline(call, params),
SupportedTransactionVersion::V5 => self.create_v5_signable_offline(call, params),
}
}
pub fn create_v4_signable_offline<'call, Call>(
&self,
call: &'call Call,
params: <T::TransactionExtensions as TransactionExtensions<T>>::Params,
) -> Result<SignableTransaction<'call, T, Client, Call>, ExtrinsicError>
where
Call: Payload,
{
self.create_signable_at_version(call, params, SupportedTransactionVersion::V4)
}
pub fn create_v5_signable_offline<'call, Call>(
&self,
call: &'call Call,
params: <T::TransactionExtensions as TransactionExtensions<T>>::Params,
) -> Result<SignableTransaction<'call, T, Client, Call>, ExtrinsicError>
where
Call: Payload,
{
self.create_signable_at_version(call, params, SupportedTransactionVersion::V5)
}
pub fn default_transaction_version(
&self,
) -> Result<SupportedTransactionVersion, ExtrinsicError> {
let metadata = self.client.metadata_ref();
let versions = metadata.extrinsic().supported_versions();
if versions.contains(&4) {
Ok(SupportedTransactionVersion::V4)
} else if versions.contains(&5) {
Ok(SupportedTransactionVersion::V5)
} else {
Err(ExtrinsicError::UnsupportedVersion)
}
}
fn create_signable_at_version<'call, Call>(
&self,
call: &'call Call,
params: <T::TransactionExtensions as TransactionExtensions<T>>::Params,
tx_version: SupportedTransactionVersion,
) -> Result<SignableTransaction<'call, T, Client, Call>, ExtrinsicError>
where
Call: Payload,
{
self.validate(call)?;
let client_state = ClientState {
genesis_hash: self
.client
.genesis_hash()
.ok_or(ExtrinsicError::GenesisHashNotProvided)?,
spec_version: self.client.spec_version(),
transaction_version: self.client.transaction_version(),
metadata: self.client.metadata(),
};
let tx_extensions =
<T::TransactionExtensions as TransactionExtensions<T>>::new(&client_state, params)?;
let tx_extension_version = match tx_version {
SupportedTransactionVersion::V4 => None,
SupportedTransactionVersion::V5 => {
let metadata = self.client.metadata_ref();
let v = frame_decode::extrinsics::best_v5_general_transaction_extension_version(
&tx_extensions,
metadata,
metadata.types(),
)?;
Some(v)
}
};
Ok(SignableTransaction {
client: self.client.clone(),
call,
tx_extensions,
tx_extension_version,
})
}
}
impl<T: Config, Client: OnlineClientAtBlockT<T>> TransactionsClient<T, Client> {
pub async fn account_nonce(&self, account_id: &T::AccountId) -> Result<u64, ExtrinsicError> {
account_nonce::get_account_nonce(&self.client, account_id)
.await
.map_err(|e| ExtrinsicError::AccountNonceError {
block_hash: self.client.block_ref().hash().into(),
account_id: account_id.clone().encode().into(),
reason: e,
})
}
pub async fn create_signable<'call, Call>(
&self,
call: &'call Call,
account_id: &T::AccountId,
mut params: <T::TransactionExtensions as TransactionExtensions<T>>::Params,
) -> Result<SignableTransaction<'call, T, Client, Call>, ExtrinsicError>
where
Call: Payload,
{
self.inject_account_nonce_and_block(account_id, &mut params)
.await?;
self.create_signable_offline(call, params)
}
pub async fn create_v4_signable<'call, Call>(
&self,
call: &'call Call,
account_id: &T::AccountId,
mut params: <T::TransactionExtensions as TransactionExtensions<T>>::Params,
) -> Result<SignableTransaction<'call, T, Client, Call>, ExtrinsicError>
where
Call: Payload,
{
self.inject_account_nonce_and_block(account_id, &mut params)
.await?;
self.create_v4_signable_offline(call, params)
}
pub async fn create_v5_signable<'call, Call>(
&self,
call: &'call Call,
account_id: &T::AccountId,
mut params: <T::TransactionExtensions as TransactionExtensions<T>>::Params,
) -> Result<SignableTransaction<'call, T, Client, Call>, ExtrinsicError>
where
Call: Payload,
{
self.inject_account_nonce_and_block(account_id, &mut params)
.await?;
self.create_v5_signable_offline(call, params)
}
pub async fn create_signed<Call, S>(
&mut self,
call: &Call,
signer: &S,
params: <T::TransactionExtensions as TransactionExtensions<T>>::Params,
) -> Result<SubmittableTransaction<T, Client>, ExtrinsicError>
where
Call: Payload,
S: Signer<T>,
{
let mut signable = self
.create_signable(call, &signer.account_id(), params)
.await?;
signable.sign(signer)
}
pub async fn sign_and_submit_then_watch_default<Call, S>(
&mut self,
call: &Call,
signer: &S,
) -> Result<TransactionProgress<T, Client>, ExtrinsicError>
where
Call: Payload,
S: Signer<T>,
<T::TransactionExtensions as TransactionExtensions<T>>::Params: DefaultParams,
{
self.sign_and_submit_then_watch(call, signer, DefaultParams::default_params())
.await
}
pub async fn sign_and_submit_then_watch<Call, S>(
&mut self,
call: &Call,
signer: &S,
params: <T::TransactionExtensions as TransactionExtensions<T>>::Params,
) -> Result<TransactionProgress<T, Client>, ExtrinsicError>
where
Call: Payload,
S: Signer<T>,
{
self.create_signed(call, signer, params)
.await?
.submit_and_watch()
.await
}
pub async fn sign_and_submit_default<Call, S>(
&mut self,
call: &Call,
signer: &S,
) -> Result<HashFor<T>, ExtrinsicError>
where
Call: Payload,
S: Signer<T>,
<T::TransactionExtensions as TransactionExtensions<T>>::Params: DefaultParams,
{
self.sign_and_submit(call, signer, DefaultParams::default_params())
.await
}
pub async fn sign_and_submit<Call, S>(
&mut self,
call: &Call,
signer: &S,
params: <T::TransactionExtensions as TransactionExtensions<T>>::Params,
) -> Result<HashFor<T>, ExtrinsicError>
where
Call: Payload,
S: Signer<T>,
{
self.create_signed(call, signer, params)
.await?
.submit()
.await
}
async fn inject_account_nonce_and_block(
&self,
account_id: &T::AccountId,
params: &mut <T::TransactionExtensions as TransactionExtensions<T>>::Params,
) -> Result<(), ExtrinsicError> {
let block_number = self.client.block_number();
let block_hash = self.client.block_ref().hash();
let account_nonce = self.account_nonce(account_id).await?;
params.inject_account_nonce(account_nonce);
params.inject_block(block_number, block_hash);
Ok(())
}
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
#[repr(u8)]
pub enum SupportedTransactionVersion {
V4 = 4u8,
V5 = 5u8,
}
pub struct SignableTransaction<'call, T: Config, Client, Call> {
client: Client,
call: &'call Call,
tx_extensions: <T as Config>::TransactionExtensions,
tx_extension_version: Option<u8>,
}
impl<'call, T: Config, Client: OfflineClientAtBlockT<T>, Call: Payload>
SignableTransaction<'call, T, Client, Call>
{
pub fn signer_payload(&self) -> Result<Vec<u8>, ExtrinsicError> {
let signer_payload = if let Some(transaction_extension_version) = self.tx_extension_version
{
frame_decode::extrinsics::encode_v5_signer_payload(
self.call.pallet_name(),
self.call.call_name(),
self.call.call_data(),
transaction_extension_version,
&self.tx_extensions,
self.client.metadata_ref(),
self.client.metadata_ref().types(),
)?
.to_vec()
} else {
frame_decode::extrinsics::encode_v4_signer_payload(
self.call.pallet_name(),
self.call.call_name(),
self.call.call_data(),
&self.tx_extensions,
self.client.metadata_ref(),
self.client.metadata_ref().types(),
)?
};
Ok(signer_payload)
}
pub fn sign<S: Signer<T>>(
&mut self,
signer: &S,
) -> Result<SubmittableTransaction<T, Client>, ExtrinsicError> {
let signature = signer.sign(&self.signer_payload()?);
self.sign_with_account_and_signature(&signer.account_id(), &signature)
}
pub fn sign_with_account_and_signature(
&mut self,
account_id: &T::AccountId,
signature: &T::Signature,
) -> Result<SubmittableTransaction<T, Client>, ExtrinsicError> {
let encoded = if let Some(transaction_extension_version) = self.tx_extension_version {
self.tx_extensions.inject_signature(account_id, signature);
frame_decode::extrinsics::encode_v5_general(
self.call.pallet_name(),
self.call.call_name(),
self.call.call_data(),
transaction_extension_version,
&self.tx_extensions,
self.client.metadata_ref(),
self.client.metadata_ref().types(),
)
} else {
let address: T::Address = account_id.clone().into();
frame_decode::extrinsics::encode_v4_signed(
self.call.pallet_name(),
self.call.call_name(),
self.call.call_data(),
&self.tx_extensions,
&address,
&signature,
self.client.metadata_ref(),
self.client.metadata_ref().types(),
)
}?;
Ok(SubmittableTransaction {
client: self.client.clone(),
encoded,
marker: PhantomData,
})
}
}
#[derive(Debug, Clone)]
pub struct SubmittableTransaction<T, Client> {
client: Client,
encoded: Vec<u8>,
marker: PhantomData<T>,
}
impl<T, Client> SubmittableTransaction<T, Client>
where
T: Config,
Client: OfflineClientAtBlockT<T>,
{
pub fn hash(&self) -> HashFor<T> {
self.client.hasher().hash(&self.encoded)
}
pub fn encoded(&self) -> &[u8] {
&self.encoded
}
pub fn into_encoded(self) -> Vec<u8> {
self.encoded.clone()
}
}
impl<T: Config, Client: OnlineClientAtBlockT<T>> SubmittableTransaction<T, Client> {
pub async fn submit_and_watch(&self) -> Result<TransactionProgress<T, Client>, ExtrinsicError> {
let ext_hash = self.hash();
let sub = self
.client
.backend()
.submit_transaction(self.encoded())
.await
.map_err(ExtrinsicError::ErrorSubmittingTransaction)?;
Ok(TransactionProgress::new(sub, self.client.clone(), ext_hash))
}
pub async fn submit(&self) -> Result<HashFor<T>, ExtrinsicError> {
let ext_hash = self.hash();
let mut sub = self
.client
.backend()
.submit_transaction(self.encoded())
.await
.map_err(ExtrinsicError::ErrorSubmittingTransaction)?;
match sub.next().await {
Some(Ok(status)) => match status {
BackendTransactionStatus::Validated
| BackendTransactionStatus::Broadcasted
| BackendTransactionStatus::InBestBlock { .. }
| BackendTransactionStatus::NoLongerInBestBlock
| BackendTransactionStatus::InFinalizedBlock { .. } => Ok(ext_hash),
BackendTransactionStatus::Error { message } => Err(
ExtrinsicError::TransactionStatusError(TransactionStatusError::Error(message)),
),
BackendTransactionStatus::Invalid { message } => {
Err(ExtrinsicError::TransactionStatusError(
TransactionStatusError::Invalid(message),
))
}
BackendTransactionStatus::Dropped { message } => {
Err(ExtrinsicError::TransactionStatusError(
TransactionStatusError::Dropped(message),
))
}
},
Some(Err(e)) => Err(ExtrinsicError::TransactionStatusStreamError(e)),
None => Err(ExtrinsicError::UnexpectedEndOfTransactionStatusStream),
}
}
pub async fn validate(&self) -> Result<ValidationResult, ExtrinsicError> {
let block_hash = self.client.block_ref().hash();
let mut params = Vec::with_capacity(8 + self.encoded().len() + 8);
2u8.encode_to(&mut params);
params.extend(self.encoded().iter());
block_hash.encode_to(&mut params);
let res: Vec<u8> = self
.client
.backend()
.call(
"TaggedTransactionQueue_validate_transaction",
Some(¶ms),
block_hash,
)
.await
.map_err(ExtrinsicError::CannotGetValidationInfo)?;
ValidationResult::try_from_bytes(res)
}
pub async fn partial_fee_estimate(&self) -> Result<u128, ExtrinsicError> {
let block_hash = self.client.block_ref().hash();
let mut params = self.encoded().to_vec();
(self.encoded().len() as u32).encode_to(&mut params);
let response = self
.client
.backend()
.call(
"TransactionPaymentApi_query_info",
Some(¶ms),
block_hash,
)
.await
.map_err(ExtrinsicError::CannotGetFeeInfo)?;
let (_, _, _, partial_fee) =
<(Compact<u64>, Compact<u64>, u8, u128)>::decode(&mut &*response)
.map_err(ExtrinsicError::CannotDecodeFeeInfo)?;
Ok(partial_fee)
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::client::{OfflineClient, OfflineClientAtBlock};
use crate::config::SubstrateConfig;
use crate::config::substrate::SpecVersionForRange;
use crate::metadata::Metadata;
use assert_matches::assert_matches;
use scale_value::Value;
use std::sync::Arc;
fn test_client_with_metadata(metadata_bytes: &[u8]) -> OfflineClientAtBlock<SubstrateConfig> {
test_client_with_metadata_and_genesis(metadata_bytes, None)
}
fn test_client_with_metadata_and_genesis(
metadata_bytes: &[u8],
genesis_hash: Option<crate::utils::H256>,
) -> OfflineClientAtBlock<SubstrateConfig> {
let metadata = Metadata::decode(&mut &*metadata_bytes).unwrap();
test_client_with_decoded_metadata(metadata, genesis_hash)
}
fn test_client_with_decoded_metadata(
metadata: Metadata,
genesis_hash: Option<crate::utils::H256>,
) -> OfflineClientAtBlock<SubstrateConfig> {
let mut builder = SubstrateConfig::builder()
.set_metadata_for_spec_versions([(0, Arc::new(metadata))])
.set_spec_version_for_block_ranges([SpecVersionForRange {
block_range: 0..u64::MAX,
spec_version: 0,
transaction_version: 0,
}]);
if let Some(hash) = genesis_hash {
builder = builder.set_genesis_hash(hash);
}
OfflineClient::new_with_config(builder.build())
.at_block(0u64)
.unwrap()
}
fn test_client() -> OfflineClientAtBlock<SubstrateConfig> {
test_client_with_metadata(include_bytes!(
"../../artifacts/polkadot_metadata_small.scale"
))
}
#[test]
fn from_call_data_bytes_roundtrips() {
let client = test_client();
let tx = client.tx();
let dest = Value::unnamed_variant("Id", [Value::from_bytes([7u8; 32])]);
let original = dynamic(
"Balances",
"transfer_keep_alive",
(dest, 12_345_000_000u128),
);
let call_data = tx.call_data(&original).unwrap();
let recovered = tx.from_call_data_bytes(&call_data).unwrap();
assert_eq!(recovered.pallet_name(), "Balances");
assert_eq!(recovered.call_name(), "transfer_keep_alive");
assert_eq!(tx.call_data(&recovered).unwrap(), call_data);
}
#[test]
fn from_call_data_bytes_decodes_call_without_arguments() {
let client = test_client_with_metadata(include_bytes!(
"../../artifacts/polkadot_metadata_full.scale"
));
let tx = client.tx();
let pallet = client
.metadata_ref()
.pallet_by_name("Sudo")
.expect("the test metadata should contain the Sudo pallet");
let call = pallet
.call_variant_by_name("remove_key")
.expect("the test metadata should contain Sudo.remove_key");
assert!(call.fields.is_empty());
let call_data = [pallet.call_index(), call.index];
let recovered = tx.from_call_data_bytes(&call_data).unwrap();
assert_eq!(recovered.pallet_name(), "Sudo");
assert_eq!(recovered.call_name(), "remove_key");
assert_eq!(recovered.call_data(), &Composite::Unnamed(Vec::new()));
assert_eq!(tx.call_data(&recovered).unwrap(), call_data);
}
#[test]
fn from_call_data_bytes_decodes_known_scale_fixture() {
let client = test_client();
let tx = client.tx();
let call_data = [0x00, 0x00, 0x0c, 0x01, 0x02, 0x03];
let recovered = tx.from_call_data_bytes(&call_data).unwrap();
assert_eq!(recovered.pallet_name(), "System");
assert_eq!(recovered.call_name(), "remark");
assert_eq!(
recovered.call_data(),
&Composite::Named(vec![("remark".to_owned(), Value::from_bytes([1u8, 2, 3]),)])
);
assert_eq!(tx.call_data(&recovered).unwrap(), call_data);
}
#[test]
fn from_call_data_bytes_rejects_invalid_bytes() {
let client = test_client();
let tx = client.tx();
assert_matches!(
tx.from_call_data_bytes(&[]),
Err(ExtrinsicError::CallDataTooShort(0))
);
assert_matches!(
tx.from_call_data_bytes(&[0]),
Err(ExtrinsicError::CallDataTooShort(1))
);
let unused_pallet_index = (0..=u8::MAX)
.find(|i| client.metadata_ref().pallet_by_call_index(*i).is_none())
.unwrap();
assert_matches!(
tx.from_call_data_bytes(&[unused_pallet_index, 0]),
Err(ExtrinsicError::CannotDecodeCallData(_))
);
let system_pallet_index = client
.metadata_ref()
.pallet_by_name("System")
.unwrap()
.call_index();
assert_matches!(
tx.from_call_data_bytes(&[system_pallet_index, 0xEE]),
Err(ExtrinsicError::CannotDecodeCallData(_))
);
let valid = tx
.call_data(&dynamic("System", "remark", (vec![1u8, 2, 3],)))
.unwrap();
assert_matches!(
tx.from_call_data_bytes(&valid[..valid.len() - 1]),
Err(ExtrinsicError::CannotDecodeCallDataFields(_))
);
let mut extended = valid.clone();
extended.push(0);
assert_matches!(
tx.from_call_data_bytes(&extended),
Err(ExtrinsicError::LeftoverBytesDecodingCallData(1))
);
}
#[allow(dead_code, non_camel_case_types)]
#[derive(scale_info::TypeInfo)]
enum TestCall {
call,
}
fn two_version_metadata() -> Metadata {
use frame_metadata::v16;
use scale_info::meta_type;
v16::RuntimeMetadataV16::new(
vec![v16::PalletMetadata {
name: "Test",
storage: None,
calls: Some(v16::PalletCallMetadata {
ty: meta_type::<TestCall>(),
deprecation_info: v16::EnumDeprecationInfo::nothing_deprecated(),
}),
event: None,
constants: Vec::new(),
error: None,
associated_types: Vec::new(),
view_functions: Vec::new(),
index: 0,
docs: Vec::new(),
deprecation_info: v16::ItemDeprecationInfo::NotDeprecated,
}],
v16::ExtrinsicMetadata {
versions: vec![5],
address_ty: meta_type::<u8>(),
call_ty: meta_type::<TestCall>(),
signature_ty: meta_type::<u8>(),
transaction_extensions_by_version: [
(0u8, Vec::new()),
(1u8, vec![codec::Compact(0u32)]),
]
.into_iter()
.collect(),
transaction_extensions: vec![v16::TransactionExtensionMetadata {
identifier: "NewExt",
ty: meta_type::<bool>(),
implicit: meta_type::<()>(),
}],
},
Vec::new(),
v16::OuterEnums {
call_enum_ty: meta_type::<TestCall>(),
event_enum_ty: meta_type::<()>(),
error_enum_ty: meta_type::<()>(),
},
v16::CustomMetadata {
map: Default::default(),
},
)
.try_into()
.expect("can build valid metadata")
}
#[test]
fn v5_extension_version_falls_back_when_values_are_missing() {
let client = test_client_with_decoded_metadata(
two_version_metadata(),
Some(crate::utils::H256::zero()),
);
let tx = client.tx();
let call = dynamic("Test", "call", ());
let params = crate::config::DefaultExtrinsicParamsBuilder::<SubstrateConfig>::new().build();
let signable = tx.create_v5_signable_offline(&call, params).unwrap();
assert_eq!(signable.tx_extension_version, Some(0));
}
#[test]
fn v5_extension_version_prefers_newest_version_it_can_encode() {
let client = test_client_with_decoded_metadata(
two_version_metadata(),
Some(crate::utils::H256::zero()),
);
let tx = client.tx();
let call = dynamic("Test", "call", ());
let params = crate::config::DefaultExtrinsicParamsBuilder::<SubstrateConfig>::new()
.custom_extension("NewExt", true)
.build();
let mut signable = tx.create_v5_signable_offline(&call, params).unwrap();
assert_eq!(signable.tx_extension_version, Some(1));
let submittable = signable
.sign_with_account_and_signature(
&crate::utils::AccountId32::from([0u8; 32]),
&crate::utils::MultiSignature::Sr25519([0u8; 64]),
)
.unwrap();
let inner = Vec::<u8>::decode(&mut submittable.encoded()).unwrap();
assert_eq!(
inner,
[
0b0100_0000 + 5, 1, 1, 0, 0, ]
);
}
#[test]
fn v5_signer_payload_succeeds() {
let client = test_client_with_metadata_and_genesis(
include_bytes!("../../artifacts/polkadot_metadata_small.scale"),
Some(crate::utils::H256::zero()),
);
let tx = client.tx();
let call = dynamic("System", "remark", (vec![1u8, 2, 3],));
let params = crate::config::DefaultExtrinsicParamsBuilder::<SubstrateConfig>::new().build();
let signable = tx.create_v5_signable_offline(&call, params);
let signable = match signable {
Ok(s) => s,
Err(ExtrinsicError::UnsupportedVersion) => return,
Err(e) => panic!("unexpected error creating signable: {e}"),
};
let payload = signable.signer_payload().unwrap();
assert_eq!(payload.len(), 32);
}
}