From c9059d1a3200c5409ed66488d8589e5f5352491c Mon Sep 17 00:00:00 2001 From: emostov <32168567+emostov@users.noreply.github.com> Date: Sun, 13 Dec 2020 20:13:16 -0800 Subject: [PATCH] feat: Mortal extrinsic construction --- Cargo.toml | 2 + examples/kusama_balance_transfer.rs | 9 ++++- examples/submit_and_watch.rs | 9 ++++- examples/transfer_subscribe.rs | 9 ++++- proc-macro/src/call.rs | 8 +++- src/extrinsic/extra.rs | 14 ++++--- src/extrinsic/mod.rs | 18 ++++++++- src/lib.rs | 57 ++++++++++++++++++++++++++++- 8 files changed, 112 insertions(+), 14 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index bbf4508748..907fcbed27 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,8 @@ serde = { version = "1.0.115", features = ["derive"] } serde_json = "1.0.57" url = "2.1.1" codec = { package = "parity-scale-codec", version = "1.3.5", default-features = false, features = ["derive", "full"] } +quote = "1.0.7" +proc-macro2 = "1.0.19" frame-metadata = { version = "12", package = "frame-metadata" } frame-support = { version = "2.0.0", package = "frame-support" } diff --git a/examples/kusama_balance_transfer.rs b/examples/kusama_balance_transfer.rs index dba35b8771..d5bd647d0a 100644 --- a/examples/kusama_balance_transfer.rs +++ b/examples/kusama_balance_transfer.rs @@ -20,6 +20,8 @@ use substrate_subxt::{ ClientBuilder, KusamaRuntime, PairSigner, + SignedOptions, + DEFAULT_ERA_PERIOD }; #[async_std::main] @@ -30,7 +32,12 @@ async fn main() -> Result<(), Box> { let dest = AccountKeyring::Bob.to_account_id().into(); let client = ClientBuilder::::new().build().await?; - let hash = client.transfer(&signer, &dest, 10_000).await?; + let hash = client.transfer( + &signer, + SignedOptions { era_period: Some(DEFAULT_ERA_PERIOD) }, + &dest, + 10_000 + ).await?; println!("Balance transfer extrinsic submitted: {}", hash); diff --git a/examples/submit_and_watch.rs b/examples/submit_and_watch.rs index c75137f994..fe310dea04 100644 --- a/examples/submit_and_watch.rs +++ b/examples/submit_and_watch.rs @@ -23,6 +23,8 @@ use substrate_subxt::{ ClientBuilder, DefaultNodeRuntime, PairSigner, + SignedOptions, + DEFAULT_ERA_PERIOD, }; #[async_std::main] @@ -33,7 +35,12 @@ async fn main() -> Result<(), Box> { let dest = AccountKeyring::Bob.to_account_id().into(); let client = ClientBuilder::::new().build().await?; - let result = client.transfer_and_watch(&signer, &dest, 10_000).await?; + let result = client.transfer_and_watch( + &signer, + SignedOptions { era_period: Some(DEFAULT_ERA_PERIOD) }, + &dest, + 10_000 + ).await?; if let Some(event) = result.transfer()? { println!("Balance transfer success: value: {:?}", event.amount); diff --git a/examples/transfer_subscribe.rs b/examples/transfer_subscribe.rs index 706d3fdc6f..0a9ca71ca3 100644 --- a/examples/transfer_subscribe.rs +++ b/examples/transfer_subscribe.rs @@ -27,6 +27,8 @@ use substrate_subxt::{ EventSubscription, EventsDecoder, PairSigner, + SignedOptions, + DEFAULT_ERA_PERIOD, }; #[async_std::main] @@ -42,7 +44,12 @@ async fn main() -> Result<(), Box> { decoder.with_balances(); let mut sub = EventSubscription::::new(sub, decoder); sub.filter_event::>(); - client.transfer(&signer, &dest, 10_000).await?; + client.transfer( + &signer, + SignedOptions { era_period: Some(DEFAULT_ERA_PERIOD) }, + &dest, + 10_000 + ).await?; let raw = sub.next().await.unwrap().unwrap(); let event = TransferEvent::::decode(&mut &raw.data[..]); if let Ok(e) = event { diff --git a/proc-macro/src/call.rs b/proc-macro/src/call.rs index cedf993fe0..1cea39521a 100644 --- a/proc-macro/src/call.rs +++ b/proc-macro/src/call.rs @@ -64,6 +64,7 @@ pub fn call(s: Structure) -> TokenStream { fn #call<'a>( &'a self, signer: &'a (dyn #subxt::Signer + Send + Sync), + opts: #subxt::SignedOptions, #args ) -> core::pin::Pin> + Send + 'a>>; @@ -71,6 +72,7 @@ pub fn call(s: Structure) -> TokenStream { fn #call_and_watch<'a>( &'a self, signer: &'a (dyn #subxt::Signer + Send + Sync), + opts: #subxt::SignedOptions, #args ) -> core::pin::Pin, #subxt::Error>> + Send + 'a>>; } @@ -82,19 +84,21 @@ pub fn call(s: Structure) -> TokenStream { fn #call<'a>( &'a self, signer: &'a (dyn #subxt::Signer + Send + Sync), + opts: #subxt::SignedOptions, #args ) -> core::pin::Pin> + Send + 'a>> { let #marker = core::marker::PhantomData::; - Box::pin(self.submit(#build_struct, signer)) + Box::pin(self.submit(#build_struct, signer, opts)) } fn #call_and_watch<'a>( &'a self, signer: &'a (dyn #subxt::Signer + Send + Sync), + opts: #subxt::SignedOptions, #args ) -> core::pin::Pin, #subxt::Error>> + Send + 'a>> { let #marker = core::marker::PhantomData::; - Box::pin(self.watch(#build_struct, signer)) + Box::pin(self.watch(#build_struct, signer, opts)) } } } diff --git a/src/extrinsic/extra.rs b/src/extrinsic/extra.rs index f31cec25b8..68180b5848 100644 --- a/src/extrinsic/extra.rs +++ b/src/extrinsic/extra.rs @@ -138,14 +138,13 @@ where /// /// # Note /// -/// This is modified from the substrate version to allow passing in of the genesis hash, which is -/// returned via `additional_signed()`. It assumes therefore `Era::Immortal` (The transaction is -/// valid forever) +/// This is modified from the substrate version to allow passing in a hash (either the genesis hash +/// if immortal or current hash if mortal. The hash is returned via `additional_signed()`. #[derive(Encode, Decode, Clone, Eq, PartialEq, Debug)] pub struct CheckEra( /// The default structure for the Extra encoding pub (Era, PhantomData), - /// Local genesis hash to be used for `AdditionalSigned` + /// Local hash to be used for `AdditionalSigned` #[codec(skip)] pub T::Hash, ); @@ -238,6 +237,7 @@ pub trait SignedExtra: SignedExtension { tx_version: u32, nonce: T::Index, genesis_hash: T::Hash, + era_info: (Era, T::Hash) ) -> Self; /// Returns the transaction extra. @@ -251,6 +251,8 @@ pub struct DefaultExtra { tx_version: u32, nonce: T::Index, genesis_hash: T::Hash, + // Era and either the genesis_hash if immortal or the current hash if mortal + era_info: (Era, T::Hash) } impl SignedExtra @@ -271,12 +273,14 @@ impl SignedExtra tx_version: u32, nonce: T::Index, genesis_hash: T::Hash, + era_info: (Era, T::Hash) ) -> Self { DefaultExtra { spec_version, tx_version, nonce, genesis_hash, + era_info, } } @@ -285,7 +289,7 @@ impl SignedExtra CheckSpecVersion(PhantomData, self.spec_version), CheckTxVersion(PhantomData, self.tx_version), CheckGenesis(PhantomData, self.genesis_hash), - CheckEra((Era::Immortal, PhantomData), self.genesis_hash), + CheckEra((self.era_info.0, PhantomData), self.era_info.1), CheckNonce(self.nonce), CheckWeight(PhantomData), ChargeTransactionPayment(::Balance::default()), diff --git a/src/extrinsic/mod.rs b/src/extrinsic/mod.rs index 70b50c9a11..2e13b89f8c 100644 --- a/src/extrinsic/mod.rs +++ b/src/extrinsic/mod.rs @@ -31,7 +31,10 @@ pub use self::{ }, }; -use sp_runtime::traits::SignedExtension; +use sp_runtime::{ + traits::SignedExtension, + generic::Era +}; use sp_version::RuntimeVersion; use crate::{ @@ -41,6 +44,10 @@ use crate::{ Error, }; +/// A reasonable default for `era_period` +pub const DEFAULT_ERA_PERIOD: u64 = 64; + + /// UncheckedExtrinsic type. pub type UncheckedExtrinsic = sp_runtime::generic::UncheckedExtrinsic< ::Address, @@ -59,6 +66,7 @@ pub async fn create_signed( nonce: T::Index, call: Encoded, signer: &(dyn Signer + Send + Sync), + era_opts: Option<(u64, u64, T::Hash)>, ) -> Result, Error> where T: Runtime, @@ -67,7 +75,13 @@ where { let spec_version = runtime_version.spec_version; let tx_version = runtime_version.transaction_version; - let extra: T::Extra = T::Extra::new(spec_version, tx_version, nonce, genesis_hash); + let era_info = match era_opts { + Some((period, cur_num, cur_hash)) => { + (Era::mortal(period, cur_num), cur_hash) + }, + None => (Era::Immortal, genesis_hash) + }; + let extra: T::Extra = T::Extra::new(spec_version, tx_version, nonce, genesis_hash, era_info); let payload = SignedPayload::::new(call, extra.extra())?; let signed = signer.sign(payload).await?; Ok(signed) diff --git a/src/lib.rs b/src/lib.rs index e2a7ccf4e9..516ba6ad35 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -63,6 +63,12 @@ use sp_core::{ pub use sp_runtime::traits::SignedExtension; pub use sp_version::RuntimeVersion; use std::marker::PhantomData; +use sp_runtime::{ + traits::{Block, Header}, + SaturatedConversion +}; +use quote::{TokenStreamExt, quote}; +use proc_macro2::TokenStream; mod error; mod events; @@ -84,6 +90,7 @@ pub use crate::{ SignedExtra, Signer, UncheckedExtrinsic, + DEFAULT_ERA_PERIOD }, frame::*, metadata::{ @@ -193,6 +200,37 @@ pub struct Client { page_size: u32, } +/// Construction options for a signed extrinsic +// TODO: tip can go in here https://github.com/paritytech/substrate-subxt/issues/187 +#[derive(Clone)] +pub struct SignedOptions { + /// The period, measured in blocks, that transaction will live for, starting from a checkpoint + /// block. A good default is 64 (64 * 6secs = 6min 40sec). + /// + /// `era_period == None`: immortal transaction. + /// `0 <= era_period <= 65536`: rounded up to the closest power of 2, starting at 4. + /// `65536 < era_period`: 65536. + // pub era_period: Option, + pub era_period: Option, +} +// https://github.com/dtolnay/quote/issues/129#issue-481909264 +fn options_to_tokens(input: &core::option::Option) -> TokenStream { + match input { + Some(value) => quote!(core::option::Option::Some(#value)), + None => quote!(core::option::Option::None) + } +} +impl quote::ToTokens for self::SignedOptions { + fn to_tokens(&self, tokens: &mut TokenStream) { + let era_period = options_to_tokens(&self.era_period); + tokens.append_all(quote!( + SignedOptions { + era_period: #era_period + } + )); + } +} + impl Clone for Client { fn clone(&self) -> Self { Self { @@ -444,6 +482,7 @@ impl Client { &self, call: C, signer: &(dyn Signer + Send + Sync), + opts: SignedOptions, ) -> Result, Error> where <>::Extra as SignedExtension>::AdditionalSigned: @@ -455,12 +494,24 @@ impl Client { self.account(signer.account_id(), None).await?.nonce }; let call = self.encode(call)?; + let era_opts = if opts.era_period.is_some() { + let era_period = opts.era_period.unwrap(); + let current_block = self.block(None::).await?.unwrap().block; + let current_number = (*current_block.header().number()).saturated_into::(); + let current_hash = current_block.hash(); + + Some((era_period, current_number, current_hash)) + } else { + None + }; + let signed = extrinsic::create_signed( &self.runtime_version, self.genesis_hash, account_nonce, call, signer, + era_opts, ) .await?; Ok(signed) @@ -498,12 +549,13 @@ impl Client { &self, call: C, signer: &(dyn Signer + Send + Sync), + opts: SignedOptions, ) -> Result where <>::Extra as SignedExtension>::AdditionalSigned: Send + Sync, { - let extrinsic = self.create_signed(call, signer).await?; + let extrinsic = self.create_signed(call, signer, opts).await?; self.submit_extrinsic(extrinsic).await } @@ -512,12 +564,13 @@ impl Client { &self, call: C, signer: &(dyn Signer + Send + Sync), + opts: SignedOptions, ) -> Result, Error> where <>::Extra as SignedExtension>::AdditionalSigned: Send + Sync, { - let extrinsic = self.create_signed(call, signer).await?; + let extrinsic = self.create_signed(call, signer, opts).await?; let decoder = self.events_decoder::(); self.submit_and_watch_extrinsic(extrinsic, decoder).await }