diff --git a/Cargo.lock b/Cargo.lock index 9b9923b843f..8a6c0afaf72 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1355,6 +1355,7 @@ dependencies = [ "rlp", "rocksdb", "sc-rpc-api", + "sc-transaction-pool-api", "scale-info", "secp256k1 0.27.0", "serde", diff --git a/engine/Cargo.toml b/engine/Cargo.toml index 44041e83b32..57e18db9ab4 100644 --- a/engine/Cargo.toml +++ b/engine/Cargo.toml @@ -121,6 +121,7 @@ substrate-frame-rpc-system = { git = "https://github.com/chainflip-io/substrate. frame-support = { git = "https://github.com/chainflip-io/substrate.git", tag = "chainflip-monthly-2023-08+1" } frame-system = { git = "https://github.com/chainflip-io/substrate.git", tag = "chainflip-monthly-2023-08+1" } sc-rpc-api = { git = "https://github.com/chainflip-io/substrate.git", tag = "chainflip-monthly-2023-08+1" } +sc-transaction-pool-api = { git = "https://github.com/chainflip-io/substrate.git", tag = 'chainflip-monthly-2023-08+1' } scale-info = { version = "2.5.0", features = ["derive"] } sp-core = { git = "https://github.com/chainflip-io/substrate.git", tag = "chainflip-monthly-2023-08+1" } sp-rpc = { git = "https://github.com/chainflip-io/substrate.git", tag = "chainflip-monthly-2023-08+1" } diff --git a/engine/src/state_chain_observer/client/base_rpc_api.rs b/engine/src/state_chain_observer/client/base_rpc_api.rs index 9812069bde8..8133e8989fb 100644 --- a/engine/src/state_chain_observer/client/base_rpc_api.rs +++ b/engine/src/state_chain_observer/client/base_rpc_api.rs @@ -3,9 +3,10 @@ use async_trait::async_trait; use cf_amm::{common::Tick, range_orders::Liquidity}; use cf_primitives::Asset; use jsonrpsee::core::{ - client::{ClientT, SubscriptionClientT}, + client::{ClientT, Subscription, SubscriptionClientT}, RpcResult, }; +use sc_transaction_pool_api::TransactionStatus; use sp_core::{ storage::{StorageData, StorageKey}, Bytes, @@ -92,6 +93,11 @@ pub trait BaseRpcApi { extrinsic: state_chain_runtime::UncheckedExtrinsic, ) -> RpcResult; + async fn submit_and_watch_extrinsic( + &self, + extrinsic: state_chain_runtime::UncheckedExtrinsic, + ) -> RpcResult>>; + async fn storage( &self, block_hash: state_chain_runtime::Hash, @@ -120,9 +126,7 @@ pub trait BaseRpcApi { async fn subscribe_finalized_block_headers( &self, - ) -> RpcResult< - jsonrpsee::core::client::Subscription>, - >; + ) -> RpcResult>>; async fn runtime_version(&self) -> RpcResult; @@ -171,6 +175,13 @@ impl BaseRpcApi for BaseRpcClient RpcResult>> { + self.raw_rpc_client.watch_extrinsic(Bytes::from(extrinsic.encode())).await + } + async fn storage( &self, block_hash: state_chain_runtime::Hash, @@ -215,9 +226,7 @@ impl BaseRpcApi for BaseRpcClient RpcResult< - jsonrpsee::core::client::Subscription>, - > { + ) -> RpcResult>> { self.raw_rpc_client.subscribe_finalized_heads().await } diff --git a/engine/src/state_chain_observer/client/error_decoder.rs b/engine/src/state_chain_observer/client/error_decoder.rs index 8a68791bff0..0606d4563f8 100644 --- a/engine/src/state_chain_observer/client/error_decoder.rs +++ b/engine/src/state_chain_observer/client/error_decoder.rs @@ -94,7 +94,7 @@ impl ErrorDecoder { } } -#[derive(Error, Debug)] +#[derive(Error, Debug, Clone)] pub enum DispatchError { #[error("{0:?}")] DispatchError(sp_runtime::DispatchError), diff --git a/engine/src/state_chain_observer/client/extrinsic_api/signed.rs b/engine/src/state_chain_observer/client/extrinsic_api/signed.rs index b500ad7d98e..7fee245349a 100644 --- a/engine/src/state_chain_observer/client/extrinsic_api/signed.rs +++ b/engine/src/state_chain_observer/client/extrinsic_api/signed.rs @@ -24,18 +24,51 @@ mod submission_watcher; #[cfg_attr(test, mockall::automock)] #[async_trait] pub trait UntilFinalized { - async fn until_finalized(self) -> submission_watcher::ExtrinsicResult; + async fn until_finalized(self) -> submission_watcher::FinalizationResult; } #[async_trait] impl UntilFinalized for (state_chain_runtime::Hash, W) { - async fn until_finalized(self) -> submission_watcher::ExtrinsicResult { + async fn until_finalized(self) -> submission_watcher::FinalizationResult { self.1.until_finalized().await } } -pub struct UntilFinalizedFuture(oneshot::Receiver); +#[async_trait] +impl UntilFinalized for (T, W) { + async fn until_finalized(self) -> submission_watcher::FinalizationResult { + self.1.until_finalized().await + } +} + +pub struct UntilFinalizedFuture(oneshot::Receiver); #[async_trait] impl UntilFinalized for UntilFinalizedFuture { - async fn until_finalized(self) -> submission_watcher::ExtrinsicResult { + async fn until_finalized(self) -> submission_watcher::FinalizationResult { + self.0.await.expect(OR_CANCEL) + } +} + +// Wrapper type to avoid await.await on submits/finalize calls being possible +#[cfg_attr(test, mockall::automock)] +#[async_trait] +pub trait UntilInBlock { + async fn until_in_block(self) -> submission_watcher::InBlockResult; +} +#[async_trait] +impl UntilInBlock for (state_chain_runtime::Hash, W) { + async fn until_in_block(self) -> submission_watcher::InBlockResult { + self.1.until_in_block().await + } +} +#[async_trait] +impl UntilInBlock for (W, T) { + async fn until_in_block(self) -> submission_watcher::InBlockResult { + self.0.until_in_block().await + } +} +pub struct UntilInBlockFuture(oneshot::Receiver); +#[async_trait] +impl UntilInBlock for UntilInBlockFuture { + async fn until_in_block(self) -> submission_watcher::InBlockResult { self.0.await.expect(OR_CANCEL) } } @@ -44,10 +77,14 @@ impl UntilFinalized for UntilFinalizedFuture { #[async_trait] pub trait SignedExtrinsicApi { type UntilFinalizedFuture: UntilFinalized + Send; + type UntilInBlockFuture: UntilInBlock + Send; fn account_id(&self) -> AccountId; - async fn submit_signed_extrinsic(&self, call: Call) -> (H256, Self::UntilFinalizedFuture) + async fn submit_signed_extrinsic( + &self, + call: Call, + ) -> (H256, (Self::UntilInBlockFuture, Self::UntilFinalizedFuture)) where Call: Into + Clone @@ -56,7 +93,10 @@ pub trait SignedExtrinsicApi { + Sync + 'static; - async fn finalize_signed_extrinsic(&self, call: Call) -> Self::UntilFinalizedFuture + async fn finalize_signed_extrinsic( + &self, + call: Call, + ) -> (Self::UntilInBlockFuture, Self::UntilFinalizedFuture) where Call: Into + Clone @@ -70,7 +110,8 @@ pub struct SignedExtrinsicClient { account_id: AccountId, request_sender: mpsc::Sender<( state_chain_runtime::RuntimeCall, - oneshot::Sender, + oneshot::Sender, + oneshot::Sender, submission_watcher::RequestStrategy, )>, _task_handle: ScopedJoinHandle<()>, @@ -151,9 +192,10 @@ impl SignedExtrinsicClient { ); utilities::loop_select! { - if let Some((call, result_sender, strategy)) = request_receiver.recv() => { - submission_watcher.new_request(&mut requests, call, result_sender, strategy).await?; + if let Some((call, until_in_block_sender, until_finalized_sender, strategy)) = request_receiver.recv() => { + submission_watcher.new_request(&mut requests, call, until_in_block_sender, until_finalized_sender, strategy).await?; } else break Ok(()), + let _ = submission_watcher.watch_for_block_inclusion(&mut requests) => {}, if let Some((block_hash, block_header)) = state_chain_stream.next() => { trace!("Received state chain block: {number} ({block_hash:x?})", number = block_header.number); submission_watcher.on_block_finalized( @@ -171,12 +213,16 @@ impl SignedExtrinsicClient { #[async_trait] impl SignedExtrinsicApi for SignedExtrinsicClient { type UntilFinalizedFuture = UntilFinalizedFuture; + type UntilInBlockFuture = UntilInBlockFuture; fn account_id(&self) -> AccountId { self.account_id.clone() } - async fn submit_signed_extrinsic(&self, call: Call) -> (H256, Self::UntilFinalizedFuture) + async fn submit_signed_extrinsic( + &self, + call: Call, + ) -> (H256, (Self::UntilInBlockFuture, Self::UntilFinalizedFuture)) where Call: Into + Clone @@ -185,23 +231,31 @@ impl SignedExtrinsicApi for SignedExtrinsicClient { + Sync + 'static, { - let (result_sender, result_receiver) = oneshot::channel(); + let (until_in_block_sender, until_in_block_receiver) = oneshot::channel(); + let (until_finalized_sender, until_finalized_receiver) = oneshot::channel(); ( send_request(&self.request_sender, |hash_sender| { ( call.into(), - result_sender, + until_in_block_sender, + until_finalized_sender, submission_watcher::RequestStrategy::StrictlyOneSubmission(hash_sender), ) }) .await .await .expect(OR_CANCEL), - UntilFinalizedFuture(result_receiver), + ( + UntilInBlockFuture(until_in_block_receiver), + UntilFinalizedFuture(until_finalized_receiver), + ), ) } - async fn finalize_signed_extrinsic(&self, call: Call) -> Self::UntilFinalizedFuture + async fn finalize_signed_extrinsic( + &self, + call: Call, + ) -> (Self::UntilInBlockFuture, Self::UntilFinalizedFuture) where Call: Into + Clone @@ -210,15 +264,21 @@ impl SignedExtrinsicApi for SignedExtrinsicClient { + Sync + 'static, { - UntilFinalizedFuture( - send_request(&self.request_sender, |result_sender| { - ( - call.into(), - result_sender, - submission_watcher::RequestStrategy::AllowMultipleSubmissions, - ) - }) - .await, + let (until_finalized_sender, until_finalized_receiver) = oneshot::channel(); + + ( + UntilInBlockFuture( + send_request(&self.request_sender, |until_in_block_sender| { + ( + call.into(), + until_in_block_sender, + until_finalized_sender, + submission_watcher::RequestStrategy::AllowMultipleSubmissions, + ) + }) + .await, + ), + UntilFinalizedFuture(until_finalized_receiver), ) } } diff --git a/engine/src/state_chain_observer/client/extrinsic_api/signed/submission_watcher.rs b/engine/src/state_chain_observer/client/extrinsic_api/signed/submission_watcher.rs index 851c0ee225d..b3d07cb202b 100644 --- a/engine/src/state_chain_observer/client/extrinsic_api/signed/submission_watcher.rs +++ b/engine/src/state_chain_observer/client/extrinsic_api/signed/submission_watcher.rs @@ -2,12 +2,15 @@ use std::{collections::BTreeMap, sync::Arc}; use anyhow::{anyhow, Result}; use frame_support::{dispatch::DispatchInfo, pallet_prelude::InvalidTransaction}; +use futures_util::{stream::FuturesUnordered, FutureExt}; use itertools::Itertools; +use sc_transaction_pool_api::TransactionStatus; use sp_core::H256; use sp_runtime::{traits::Hash, MultiAddress}; use thiserror::Error; use tokio::sync::oneshot; use tracing::{debug, warn}; +use utilities::UnendingStream; use crate::state_chain_observer::client::{ base_rpc_api, @@ -23,24 +26,34 @@ mod tests; const REQUEST_LIFETIME: u32 = 128; +#[derive(Error, Debug)] +pub enum ExtrinsicError { + #[error(transparent)] + Other(OtherError), + #[error(transparent)] + Dispatch(DispatchError), +} + +pub type ExtrinsicResult = Result< + (H256, Vec, state_chain_runtime::Header, DispatchInfo), + ExtrinsicError, +>; + #[derive(Error, Debug)] pub enum FinalizationError { #[error("The requested transaction was not and will not be included in a finalized block")] NotFinalized, } +pub type FinalizationResult = ExtrinsicResult; + #[derive(Error, Debug)] -pub enum ExtrinsicError { - #[error(transparent)] - Finalization(FinalizationError), - #[error(transparent)] - Dispatch(DispatchError), +pub enum InBlockError { + #[error("The requested transaction was not and will not be included in a block")] + NotInBlock, } -pub type ExtrinsicResult = Result< - (H256, Vec, state_chain_runtime::Header, DispatchInfo), - ExtrinsicError, ->; +pub type InBlockResult = ExtrinsicResult; pub type RequestID = u64; @@ -51,7 +64,8 @@ pub struct Request { strictly_one_submission: bool, resubmit_window: std::ops::RangeToInclusive, call: state_chain_runtime::RuntimeCall, - result_sender: oneshot::Sender, + until_in_block_sender: Option>, + until_finalized_sender: oneshot::Sender, } #[derive(Debug)] @@ -68,6 +82,19 @@ pub struct Submission { pub struct SubmissionWatcher { submissions_by_nonce: BTreeMap>, + #[allow(clippy::type_complexity)] + submission_futures: FuturesUnordered< + futures::future::BoxFuture< + 'static, + Result< + Option<( + RequestID, + (H256, Vec, state_chain_runtime::Header), + )>, + anyhow::Error, + >, + >, + >, signer: signer::PairSigner, finalized_nonce: state_chain_runtime::Nonce, finalized_block_hash: state_chain_runtime::Hash, @@ -99,6 +126,7 @@ impl ( Self { submissions_by_nonce: Default::default(), + submission_futures: Default::default(), signer, finalized_nonce, finalized_block_hash, @@ -130,14 +158,71 @@ impl ); assert!(lifetime.contains(&(self.finalized_block_number + 1))); - match self.base_rpc_client.submit_extrinsic(signed_extrinsic).await { - Ok(tx_hash) => { + let tx_hash: H256 = { + use sp_core::{blake2_256, Encode}; + let encoded = signed_extrinsic.encode(); + blake2_256(&encoded).into() + }; + + match self.base_rpc_client.submit_and_watch_extrinsic(signed_extrinsic).await { + Ok(mut transaction_status_stream) => { request.pending_submissions += 1; self.submissions_by_nonce.entry(nonce).or_default().push(Submission { lifetime, tx_hash, request_id: request.id, }); + self.submission_futures.push({ + let request_id = request.id; + let base_rpc_client = self.base_rpc_client.clone(); + async move { + while let Some(result_transaction_status) = + transaction_status_stream.next().await + { + if let TransactionStatus::InBlock((block_hash, extrinsic_index)) = + result_transaction_status? + { + if let Some(block) = base_rpc_client.block(block_hash).await? { + let extrinsic = block + .block + .extrinsics + .get(extrinsic_index) + .expect(SUBSTRATE_BEHAVIOUR); + + let tx_hash = + ::Hashing::hash_of( + extrinsic, + ); + + let extrinsic_events = base_rpc_client + .storage_value::>( + block_hash, + ) + .await? + .into_iter() + .filter_map(|event_record| match *event_record { + frame_system::EventRecord { + phase: + frame_system::Phase::ApplyExtrinsic(index), + event, + .. + } if index as usize == extrinsic_index => Some(event), + _ => None, + }) + .collect::>(); + + return Ok(Some(( + request_id, + (tx_hash, extrinsic_events, block.block.header.clone()), + ))) + } + } + } + + Ok(None) + } + .boxed() + }); break Ok(Ok(tx_hash)) }, Err(rpc_err) => { @@ -194,7 +279,7 @@ impl } } - pub async fn submit_extrinsic(&mut self, request: &mut Request) -> Result { + async fn submit_extrinsic(&mut self, request: &mut Request) -> Result { Ok(loop { let nonce = self.base_rpc_client.next_account_nonce(self.signer.account_id.clone()).await?; @@ -209,7 +294,8 @@ impl &mut self, requests: &mut BTreeMap, call: state_chain_runtime::RuntimeCall, - result_sender: oneshot::Sender, + until_in_block_sender: oneshot::Sender, + until_finalized_sender: oneshot::Sender, strategy: RequestStrategy, ) -> Result<(), anyhow::Error> { let id = requests.keys().next_back().map(|id| id + 1).unwrap_or(0); @@ -225,7 +311,8 @@ impl ), resubmit_window: ..=(self.finalized_block_number + 1 + REQUEST_LIFETIME), call, - result_sender, + until_in_block_sender: Some(until_in_block_sender), + until_finalized_sender, }, ) .unwrap(); @@ -236,6 +323,53 @@ impl Ok(()) } + fn extrinsic_result( + &self, + tx_hash: H256, + extrinsic_events: Vec, + header: state_chain_runtime::Header, + ) -> ExtrinsicResult { + extrinsic_events + .iter() + .find_map(|event| match event { + state_chain_runtime::RuntimeEvent::System( + frame_system::Event::ExtrinsicSuccess { dispatch_info }, + ) => Some(Ok(*dispatch_info)), + state_chain_runtime::RuntimeEvent::System( + frame_system::Event::ExtrinsicFailed { dispatch_error, dispatch_info: _ }, + ) => Some(Err(ExtrinsicError::Dispatch( + self.error_decoder.decode_dispatch_error(*dispatch_error), + ))), + _ => None, + }) + .expect(SUBSTRATE_BEHAVIOUR) + .map(|dispatch_info| (tx_hash, extrinsic_events, header, dispatch_info)) + } + + pub async fn watch_for_block_inclusion( + &mut self, + requests: &mut BTreeMap, + ) -> Result<(), anyhow::Error> { + if let Some((request_id, (tx_hash, extrinsic_events, block_header))) = + self.submission_futures.next_or_pending().await? + { + // Note must not put awaits after this point, due to the requried cancellation safety + // (The above next() call is cancel safe) + + if let Some(request) = requests.get_mut(&request_id) { + if let Some(until_in_block_sender) = request.until_in_block_sender.take() { + let _result = until_in_block_sender.send(self.extrinsic_result( + tx_hash, + extrinsic_events, + block_header, + )); + } + } + } + + Ok(()) + } + pub async fn on_block_finalized( &mut self, requests: &mut BTreeMap, @@ -319,34 +453,27 @@ impl (not_found_matching_submission.take().unwrap(), request) }) { let extrinsic_events = extrinsic_events.collect::>(); - let _result = matching_request.result_sender.send({ - extrinsic_events - .iter() - .find_map(|event| match event { - state_chain_runtime::RuntimeEvent::System( - frame_system::Event::ExtrinsicSuccess { dispatch_info }, - ) => Some(Ok(*dispatch_info)), - state_chain_runtime::RuntimeEvent::System( - frame_system::Event::ExtrinsicFailed { - dispatch_error, - dispatch_info: _, - }, - ) => Some(Err(ExtrinsicError::Dispatch( - self.error_decoder - .decode_dispatch_error(*dispatch_error), - ))), - _ => None, - }) - .expect(SUBSTRATE_BEHAVIOUR) - .map(|dispatch_info| { - ( - tx_hash, - extrinsic_events, - block.header.clone(), - dispatch_info, - ) - }) - }); + let result = self.extrinsic_result( + tx_hash, + extrinsic_events, + block.header.clone(), + ); + if let Some(until_in_block_sender) = + matching_request.until_in_block_sender + { + let _result = until_in_block_sender.send( + result.as_ref().map(Clone::clone).map_err( + |error| match error { + ExtrinsicError::Dispatch(dispatch_error) => + ExtrinsicError::Dispatch(dispatch_error.clone()), + ExtrinsicError::Other( + FinalizationError::NotFinalized, + ) => ExtrinsicError::Other(InBlockError::NotInBlock), + }, + ), + ); + } + let _result = matching_request.until_finalized_sender.send(result); } } } @@ -376,8 +503,8 @@ impl request.strictly_one_submission) }) { let _result = request - .result_sender - .send(Err(ExtrinsicError::Finalization(FinalizationError::NotFinalized))); + .until_finalized_sender + .send(Err(ExtrinsicError::Other(FinalizationError::NotFinalized))); } // Has to be a separate loop from the above due to not being able to await inside diff --git a/engine/src/state_chain_observer/client/extrinsic_api/signed/submission_watcher/tests.rs b/engine/src/state_chain_observer/client/extrinsic_api/signed/submission_watcher/tests.rs index 41d146ebc39..4e777d5a4a5 100644 --- a/engine/src/state_chain_observer/client/extrinsic_api/signed/submission_watcher/tests.rs +++ b/engine/src/state_chain_observer/client/extrinsic_api/signed/submission_watcher/tests.rs @@ -90,7 +90,8 @@ async fn new_watcher_and_submit_test_extrinsic( strictly_one_submission: false, resubmit_window: ..=1, call, - result_sender: oneshot::channel().0, + until_in_block_sender: Some(oneshot::channel().0), + until_finalized_sender: oneshot::channel().0, }; let _result = watcher.submit_extrinsic(&mut request).await; diff --git a/engine/src/state_chain_observer/client/mod.rs b/engine/src/state_chain_observer/client/mod.rs index 4d3aa8a21a8..13a8702e3d9 100644 --- a/engine/src/state_chain_observer/client/mod.rs +++ b/engine/src/state_chain_observer/client/mod.rs @@ -436,13 +436,17 @@ impl< for StateChainClient { type UntilFinalizedFuture = SignedExtrinsicClient::UntilFinalizedFuture; + type UntilInBlockFuture = SignedExtrinsicClient::UntilInBlockFuture; fn account_id(&self) -> AccountId { self.signed_extrinsic_client.account_id() } /// Submit an signed extrinsic, returning the hash of the submission - async fn submit_signed_extrinsic(&self, call: Call) -> (H256, Self::UntilFinalizedFuture) + async fn submit_signed_extrinsic( + &self, + call: Call, + ) -> (H256, (Self::UntilInBlockFuture, Self::UntilFinalizedFuture)) where Call: Into + Clone @@ -455,7 +459,10 @@ impl< } /// Sign, submit, and watch an extrinsic retrying if submissions fail be to finalized - async fn finalize_signed_extrinsic(&self, call: Call) -> Self::UntilFinalizedFuture + async fn finalize_signed_extrinsic( + &self, + call: Call, + ) -> (Self::UntilInBlockFuture, Self::UntilFinalizedFuture) where Call: Into + Clone @@ -521,10 +528,11 @@ pub mod mocks { #[async_trait] impl SignedExtrinsicApi for StateChainClient { type UntilFinalizedFuture = extrinsic_api::signed::MockUntilFinalized; + type UntilInBlockFuture = extrinsic_api::signed::MockUntilInBlock; fn account_id(&self) -> AccountId; - async fn submit_signed_extrinsic(&self, call: Call) -> (H256, ::UntilFinalizedFuture) + async fn submit_signed_extrinsic(&self, call: Call) -> (H256, (::UntilInBlockFuture, ::UntilFinalizedFuture)) where Call: Into + Clone @@ -533,7 +541,7 @@ pub mod mocks { + Sync + 'static; - async fn finalize_signed_extrinsic(&self, call: Call) -> ::UntilFinalizedFuture + async fn finalize_signed_extrinsic(&self, call: Call) -> (::UntilInBlockFuture, ::UntilFinalizedFuture) where Call: Into + Clone diff --git a/engine/src/state_chain_observer/sc_observer/tests.rs b/engine/src/state_chain_observer/sc_observer/tests.rs index 4856c3a82e1..003fd49cd4c 100644 --- a/engine/src/state_chain_observer/sc_observer/tests.rs +++ b/engine/src/state_chain_observer/sc_observer/tests.rs @@ -215,7 +215,12 @@ ChainCrypto>::ThresholdSignature: std::convert::From<::Signat offenders: BTreeSet::default(), })) .once() - .return_once(|_| extrinsic_api::signed::MockUntilFinalized::new()); + .return_once(|_| { + ( + extrinsic_api::signed::MockUntilInBlock::new(), + extrinsic_api::signed::MockUntilFinalized::new(), + ) + }); // ceremony_id_3 is a success and should submit an unsigned extrinsic let ceremony_id_3 = ceremony_id_2 + 1; @@ -328,7 +333,12 @@ where state_chain_client .expect_finalize_signed_extrinsic::>() .once() - .return_once(|_| extrinsic_api::signed::MockUntilFinalized::new()); + .return_once(|_| { + ( + extrinsic_api::signed::MockUntilInBlock::new(), + extrinsic_api::signed::MockUntilFinalized::new(), + ) + }); let state_chain_client = Arc::new(state_chain_client); let mut multisig_client = MockMultisigClientApi::::new(); @@ -450,7 +460,12 @@ where state_chain_client .expect_finalize_signed_extrinsic::>() .once() - .return_once(|_| extrinsic_api::signed::MockUntilFinalized::new()); + .return_once(|_| { + ( + extrinsic_api::signed::MockUntilInBlock::new(), + extrinsic_api::signed::MockUntilFinalized::new(), + ) + }); let state_chain_client = Arc::new(state_chain_client); task_scope(|scope| {