chainHead: Propagate results on the chainHead_follow (#1116)

* rpc/types: Update chainHead events

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* rpc: Sync chainHead methods with spec

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* testing: Adjust chainHead tests

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* tests: Ignore block related events to avoid flaky tests

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* tests: Adjust clippy

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* test: Remove unused OfflineClientT

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

* Update subxt/src/rpc/types.rs

Co-authored-by: Niklas Adolfsson <niklasadolfsson1@gmail.com>

* Update subxt/src/rpc/types.rs

Co-authored-by: Niklas Adolfsson <niklasadolfsson1@gmail.com>

* Update subxt/src/rpc/types.rs

Co-authored-by: Niklas Adolfsson <niklasadolfsson1@gmail.com>

* Update subxt/src/rpc/types.rs

Co-authored-by: Niklas Adolfsson <niklasadolfsson1@gmail.com>

* types: Remove serde flags for serialization

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>

---------

Signed-off-by: Alexandru Vasile <alexandru.vasile@parity.io>
Co-authored-by: Niklas Adolfsson <niklasadolfsson1@gmail.com>
This commit is contained in:
Alexandru Vasile
2023-08-09 17:40:39 +03:00
committed by GitHub
parent d02afcdf07
commit b97acc5df6
4 changed files with 249 additions and 161 deletions
+25 -21
View File
@@ -39,7 +39,7 @@ use crate::{error::Error, utils::PhantomDataSendSync, Config, Metadata};
use super::{ use super::{
rpc_params, rpc_params,
types::{self, ChainHeadEvent, ChainHeadStorageEvent, FollowEvent, StorageQuery}, types::{self, FollowEvent, StorageQuery},
RpcClient, RpcClientT, Subscription, RpcClient, RpcClientT, Subscription,
}; };
@@ -499,7 +499,10 @@ impl<T: Config> Rpc<T> {
Ok(subscription) Ok(subscription)
} }
/// Subscribe to `chainHead_unstable_body` to obtain events regarding the block's body. /// Call the `chainHead_unstable_body` method and return an operation ID to obtain the block's body.
///
/// The response events are provided on the `chainHead_follow` subscription and identified by
/// the returned operation ID.
/// ///
/// # Note /// # Note
/// ///
@@ -509,17 +512,16 @@ impl<T: Config> Rpc<T> {
&self, &self,
subscription_id: String, subscription_id: String,
hash: T::Hash, hash: T::Hash,
) -> Result<Subscription<ChainHeadEvent<String>>, Error> { ) -> Result<types::MethodResponse, Error> {
let subscription = self let response = self
.client .client
.subscribe( .request(
"chainHead_unstable_body", "chainHead_unstable_body",
rpc_params![subscription_id, hash], rpc_params![subscription_id, hash],
"chainHead_unstable_stopBody",
) )
.await?; .await?;
Ok(subscription) Ok(response)
} }
/// Get the block's body using the `chainHead_unstable_header` method. /// Get the block's body using the `chainHead_unstable_header` method.
@@ -544,8 +546,10 @@ impl<T: Config> Rpc<T> {
Ok(header) Ok(header)
} }
/// Subscribe to `chainHead_storage` to obtain events regarding the /// Call the `chainhead_unstable_storage` method and return an operation ID to obtain the block's storage.
/// block's storage. ///
/// The response events are provided on the `chainHead_follow` subscription and identified by
/// the returned operation ID.
/// ///
/// # Note /// # Note
/// ///
@@ -557,7 +561,7 @@ impl<T: Config> Rpc<T> {
hash: T::Hash, hash: T::Hash,
items: Vec<StorageQuery<&[u8]>>, items: Vec<StorageQuery<&[u8]>>,
child_key: Option<&[u8]>, child_key: Option<&[u8]>,
) -> Result<Subscription<ChainHeadStorageEvent<Option<String>>>, Error> { ) -> Result<types::MethodResponse, Error> {
let items: Vec<StorageQuery<String>> = items let items: Vec<StorageQuery<String>> = items
.into_iter() .into_iter()
.map(|item| StorageQuery { .map(|item| StorageQuery {
@@ -566,20 +570,21 @@ impl<T: Config> Rpc<T> {
}) })
.collect(); .collect();
let subscription = self let response = self
.client .client
.subscribe( .request(
"chainHead_unstable_storage", "chainHead_unstable_storage",
rpc_params![subscription_id, hash, items, child_key.map(to_hex)], rpc_params![subscription_id, hash, items, child_key.map(to_hex)],
"chainHead_unstable_stopStorage",
) )
.await?; .await?;
Ok(subscription) Ok(response)
} }
/// Subscribe to `chainHead_call` to obtain events regarding the /// Call the `chainhead_unstable_storage` method and return an operation ID to obtain the runtime API result.
/// runtime API call. ///
/// The response events are provided on the `chainHead_follow` subscription and identified by
/// the returned operation ID.
/// ///
/// # Note /// # Note
/// ///
@@ -591,17 +596,16 @@ impl<T: Config> Rpc<T> {
hash: T::Hash, hash: T::Hash,
function: String, function: String,
call_parameters: &[u8], call_parameters: &[u8],
) -> Result<Subscription<ChainHeadEvent<String>>, Error> { ) -> Result<types::MethodResponse, Error> {
let subscription = self let response = self
.client .client
.subscribe( .request(
"chainHead_unstable_call", "chainHead_unstable_call",
rpc_params![subscription_id, hash, function, to_hex(call_parameters)], rpc_params![subscription_id, hash, function, to_hex(call_parameters)],
"chainHead_unstable_stopCall",
) )
.await?; .await?;
Ok(subscription) Ok(response)
} }
/// Unpin a block reported by the `chainHead_follow` subscription. /// Unpin a block reported by the `chainHead_follow` subscription.
+153 -110
View File
@@ -383,7 +383,7 @@ pub struct RuntimeVersionEvent {
} }
/// The runtime event generated if the `follow` subscription /// The runtime event generated if the `follow` subscription
/// has set the `runtime_updates` flag. /// has set the `with_runtime` flag.
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[serde(tag = "type")] #[serde(tag = "type")]
@@ -400,9 +400,6 @@ pub enum RuntimeEvent {
/// ///
/// This is the first event generated by the `follow` subscription /// This is the first event generated by the `follow` subscription
/// and is submitted only once. /// and is submitted only once.
///
/// If the `runtime_updates` flag is set, then this event contains
/// the `RuntimeEvent`, otherwise the `RuntimeEvent` is not present.
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Initialized<Hash> { pub struct Initialized<Hash> {
@@ -412,7 +409,7 @@ pub struct Initialized<Hash> {
/// ///
/// # Note /// # Note
/// ///
/// This is present only if the `runtime_updates` flag is set for /// This is present only if the `with_runtime` flag is set for
/// the `follow` subscription. /// the `follow` subscription.
pub finalized_block_runtime: Option<RuntimeEvent>, pub finalized_block_runtime: Option<RuntimeEvent>,
} }
@@ -429,7 +426,7 @@ pub struct NewBlock<Hash> {
/// ///
/// # Note /// # Note
/// ///
/// This is present only if the `runtime_updates` flag is set for /// This is present only if the `with_runtime` flag is set for
/// the `follow` subscription. /// the `follow` subscription.
pub new_runtime: Option<RuntimeEvent>, pub new_runtime: Option<RuntimeEvent>,
} }
@@ -452,16 +449,76 @@ pub struct Finalized<Hash> {
pub pruned_block_hashes: Vec<Hash>, pub pruned_block_hashes: Vec<Hash>,
} }
/// The event generated by the `chainHead_follow` method. /// Indicate the operation id of the event.
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OperationId {
/// The operation id of the event.
pub operation_id: String,
}
/// The response of the `chainHead_body` method.
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OperationBodyDone {
/// The operation id of the event.
pub operation_id: String,
/// Array of hexadecimal-encoded scale-encoded extrinsics found in the block.
pub value: Vec<String>,
}
/// The response of the `chainHead_call` method.
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OperationCallDone {
/// The operation id of the event.
pub operation_id: String,
/// Hexadecimal-encoded output of the runtime function call.
pub output: String,
}
/// The response of the `chainHead_call` method.
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OperationStorageItems {
/// The operation id of the event.
pub operation_id: String,
/// The resulting items.
pub items: Vec<StorageResult>,
}
/// Indicate a problem during the operation.
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OperationError {
/// The operation id of the event.
pub operation_id: String,
/// The reason of the error.
pub error: String,
}
/// The event generated by the `follow` method.
/// ///
/// The events are generated in the following order: /// The block events are generated in the following order:
/// 1. Initialized - generated only once to signal the /// 1. Initialized - generated only once to signal the latest finalized block
/// latest finalized block
/// 2. NewBlock - a new block was added. /// 2. NewBlock - a new block was added.
/// 3. BestBlockChanged - indicate that the best block /// 3. BestBlockChanged - indicate that the best block is now the one from this event. The block was
/// is now the one from this event. The block was /// announced priorly with the `NewBlock` event.
/// announced priorly with the `NewBlock` event.
/// 4. Finalized - State the finalized and pruned blocks. /// 4. Finalized - State the finalized and pruned blocks.
///
/// The following events are related to operations:
/// - OperationBodyDone: The response of the `chainHead_body`
/// - OperationCallDone: The response of the `chainHead_call`
/// - OperationStorageItems: Items produced by the `chianHead_storage`
/// - OperationWaitingForContinue: Generated after OperationStorageItems and requires the user to
/// call `chainHead_continue`
/// - OperationStorageDone: The `chainHead_storage` method has produced all the results
/// - OperationInaccessible: The server was unable to provide the result, retries might succeed in
/// the future
/// - OperationError: The server encountered an error, retries will not succeed
///
/// The stop event indicates that the JSON-RPC server was unable to provide a consistent list of
/// the blocks at the head of the chain.
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[serde(tag = "event")] #[serde(tag = "event")]
@@ -476,34 +533,99 @@ pub enum FollowEvent<Hash> {
BestBlockChanged(BestBlockChanged<Hash>), BestBlockChanged(BestBlockChanged<Hash>),
/// A list of finalized and pruned blocks. /// A list of finalized and pruned blocks.
Finalized(Finalized<Hash>), Finalized(Finalized<Hash>),
/// The response of the `chainHead_body` method.
OperationBodyDone(OperationBodyDone),
/// The response of the `chainHead_call` method.
OperationCallDone(OperationCallDone),
/// Yield one or more items found in the storage.
OperationStorageItems(OperationStorageItems),
/// Ask the user to call `chainHead_continue` to produce more events
/// regarding the operation id.
OperationWaitingForContinue(OperationId),
/// The responses of the `chainHead_storage` method have been produced.
OperationStorageDone(OperationId),
/// The RPC server was unable to provide the response of the following operation id.
///
/// Repeating the same operation in the future might succeed.
OperationInaccessible(OperationId),
/// The RPC server encountered an error while processing an operation id.
///
/// Repeating the same operation in the future will not succeed.
OperationError(OperationError),
/// The subscription is dropped and no further events /// The subscription is dropped and no further events
/// will be generated. /// will be generated.
Stop, Stop,
} }
/// The result of a chain head method. /// The storage item received as parameter.
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct ChainHeadResult<T> { pub struct StorageQuery<Key> {
/// Result of the method. /// The provided key.
pub result: T, pub key: Key,
/// The type of the storage query.
#[serde(rename = "type")]
pub query_type: StorageQueryType,
} }
/// The event generated by the body and call methods. /// The type of the storage query.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum StorageQueryType {
/// Fetch the value of the provided key.
Value,
/// Fetch the hash of the value of the provided key.
Hash,
/// Fetch the closest descendant merkle value.
ClosestDescendantMerkleValue,
/// Fetch the values of all descendants of they provided key.
DescendantsValues,
/// Fetch the hashes of the values of all descendants of they provided key.
DescendantsHashes,
}
/// The storage result.
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[serde(tag = "event")] pub struct StorageResult {
pub enum ChainHeadEvent<T> { /// The hex-encoded key of the result.
/// The request completed successfully. pub key: String,
Done(ChainHeadResult<T>), /// The result of the query.
/// The resources requested are inaccessible. #[serde(flatten)]
/// pub result: StorageResultType,
/// Resubmitting the request later might succeed. }
Inaccessible(ErrorEvent),
/// An error occurred. This is definitive. /// The type of the storage query.
Error(ErrorEvent), #[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
/// The provided subscription ID is stale or invalid. #[serde(rename_all = "camelCase")]
Disjoint, pub enum StorageResultType {
/// Fetch the value of the provided key.
Value(String),
/// Fetch the hash of the value of the provided key.
Hash(String),
/// Fetch the closest descendant merkle value.
ClosestDescendantMerkleValue(String),
}
/// The method respose of `chainHead_body`, `chainHead_call` and `chainHead_storage`.
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(tag = "result")]
pub enum MethodResponse {
/// The method has started.
Started(MethodResponseStarted),
/// The RPC server cannot handle the request at the moment.
LimitReached,
}
/// The `started` result of a method.
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MethodResponseStarted {
/// The operation id of the response.
pub operation_id: String,
/// The number of items from the back of the `chainHead_storage` that have been discarded.
pub discarded_items: Option<usize>,
} }
/// The transaction was broadcasted to a number of peers. /// The transaction was broadcasted to a number of peers.
@@ -728,85 +850,6 @@ mod as_string {
} }
} }
/// The storage item received as paramter.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StorageQuery<Key> {
/// The provided key.
pub key: Key,
/// The type of the storage query.
#[serde(rename = "type")]
pub query_type: StorageQueryType,
}
/// The type of the storage query.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum StorageQueryType {
/// Fetch the value of the provided key.
Value,
/// Fetch the hash of the value of the provided key.
Hash,
/// Fetch the closest descendant merkle value.
ClosestDescendantMerkleValue,
/// Fetch the values of all descendants of they provided key.
DescendantsValues,
/// Fetch the hashes of the values of all descendants of they provided key.
DescendantsHashes,
}
/// The storage result.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StorageResult<T> {
/// The hex-encoded key of the result.
pub key: String,
/// The result of the query.
#[serde(flatten)]
pub result: StorageResultType<T>,
}
/// The type of the storage query.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum StorageResultType<T> {
/// Fetch the value of the provided key.
Value(T),
/// Fetch the hash of the value of the provided key.
Hash(T),
/// Fetch the closest descendant merkle value.
ClosestDescendantMerkleValue(T),
}
/// The event generated by storage method.
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(rename_all = "kebab-case")]
#[serde(tag = "event")]
pub enum ChainHeadStorageEvent<T> {
/// The request produced multiple result items.
Items(ItemsEvent<T>),
/// The request produced multiple result items.
WaitForContinue,
/// The request completed successfully and all the results were provided.
Done,
/// The resources requested are inaccessible.
///
/// Resubmitting the request later might succeed.
Inaccessible,
/// An error occurred. This is definitive.
Error(ErrorEvent),
/// The provided subscription ID is stale or invalid.
Disjoint,
}
/// The request produced multiple result items.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ItemsEvent<T> {
/// The resulting items.
pub items: Vec<StorageResult<T>>,
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
@@ -9,18 +9,44 @@ use crate::{
use assert_matches::assert_matches; use assert_matches::assert_matches;
use codec::{Compact, Decode, Encode}; use codec::{Compact, Decode, Encode};
use sp_core::storage::well_known_keys; use sp_core::storage::well_known_keys;
use sp_runtime::DeserializeOwned;
use subxt::{ use subxt::{
error::{DispatchError, Error, TokenError}, error::{DispatchError, Error, TokenError},
rpc::types::{ rpc::{
ChainHeadEvent, ChainHeadStorageEvent, DryRunResult, DryRunResultBytes, FollowEvent, types::{
Initialized, RuntimeEvent, RuntimeVersionEvent, StorageQuery, StorageQueryType, DryRunResult, DryRunResultBytes, FollowEvent, Initialized, MethodResponse,
StorageResultType, RuntimeEvent, RuntimeVersionEvent, StorageQuery, StorageQueryType,
},
Subscription,
}, },
utils::AccountId32, utils::AccountId32,
}; };
use subxt_metadata::Metadata; use subxt_metadata::Metadata;
use subxt_signer::sr25519::dev; use subxt_signer::sr25519::dev;
/// Ignore block related events and obtain the next event related to an operation.
async fn next_operation_event<T: DeserializeOwned>(
sub: &mut Subscription<FollowEvent<T>>,
) -> FollowEvent<T> {
// At most 5 retries.
for _ in 0..5 {
let event = sub.next().await.unwrap().unwrap();
match event {
// Can also return the `Stop` event for better debugging.
FollowEvent::Initialized(_)
| FollowEvent::NewBlock(_)
| FollowEvent::BestBlockChanged(_)
| FollowEvent::Finalized(_) => continue,
_ => (),
};
return event;
}
panic!("Cannot find operation related event after 5 produced events");
}
#[tokio::test] #[tokio::test]
async fn insert_key() { async fn insert_key() {
let ctx = test_context_with("bob".to_string()).await; let ctx = test_context_with("bob".to_string()).await;
@@ -484,21 +510,22 @@ async fn chainhead_unstable_body() {
}; };
let sub_id = blocks.subscription_id().unwrap().clone(); let sub_id = blocks.subscription_id().unwrap().clone();
// Subscribe to fetch the block's body. // Fetch the block's body.
let mut sub = api let response = api
.rpc() .rpc()
.chainhead_unstable_body(sub_id, hash) .chainhead_unstable_body(sub_id, hash)
.await .await
.unwrap(); .unwrap();
let event = sub.next().await.unwrap().unwrap(); let operation_id = match response {
MethodResponse::Started(started) => started.operation_id,
MethodResponse::LimitReached => panic!("Expected started response"),
};
// Expected block's extrinsics scale encoded and hex encoded. // Response propagated to `chainHead_follow`.
let body = api.rpc().block(Some(hash)).await.unwrap().unwrap(); let event = next_operation_event(&mut blocks).await;
let extrinsics: Vec<Vec<u8>> = body.block.extrinsics.into_iter().map(|ext| ext.0).collect(); assert_matches!(
let expected = format!("0x{}", hex::encode(extrinsics.encode())); event,
FollowEvent::OperationBodyDone(done) if done.operation_id == operation_id
assert_matches!(event,
ChainHeadEvent::Done(done) if done.result == expected
); );
} }
@@ -549,24 +576,29 @@ async fn chainhead_unstable_storage() {
key: addr_bytes.as_slice(), key: addr_bytes.as_slice(),
query_type: StorageQueryType::Value, query_type: StorageQueryType::Value,
}]; }];
let mut sub = api
// Fetch storage.
let response = api
.rpc() .rpc()
.chainhead_unstable_storage(sub_id, hash, items, None) .chainhead_unstable_storage(sub_id, hash, items, None)
.await .await
.unwrap(); .unwrap();
let event = sub.next().await.unwrap().unwrap(); let operation_id = match response {
MethodResponse::Started(started) => started.operation_id,
match event { MethodResponse::LimitReached => panic!("Expected started response"),
ChainHeadStorageEvent::<Option<String>>::Items(event) => {
assert_eq!(event.items.len(), 1);
assert_eq!(event.items[0].key, format!("0x{}", hex::encode(addr_bytes)));
assert_matches!(&event.items[0].result, StorageResultType::Value(value) if value.is_some());
}
_ => panic!("unexpected ChainHeadStorageEvent"),
}; };
let event = sub.next().await.unwrap().unwrap(); // Response propagated to `chainHead_follow`.
assert_matches!(event, ChainHeadStorageEvent::<Option<String>>::Done); let event = next_operation_event(&mut blocks).await;
assert_matches!(
event,
FollowEvent::OperationStorageItems(res) if res.operation_id == operation_id &&
res.items.len() == 1 &&
res.items[0].key == format!("0x{}", hex::encode(addr_bytes))
);
let event = next_operation_event(&mut blocks).await;
assert_matches!(event, FollowEvent::OperationStorageDone(res) if res.operation_id == operation_id);
} }
#[tokio::test] #[tokio::test]
@@ -583,7 +615,8 @@ async fn chainhead_unstable_call() {
let sub_id = blocks.subscription_id().unwrap().clone(); let sub_id = blocks.subscription_id().unwrap().clone();
let alice_id = dev::alice().public_key().to_account_id(); let alice_id = dev::alice().public_key().to_account_id();
let mut sub = api // Runtime API call.
let response = api
.rpc() .rpc()
.chainhead_unstable_call( .chainhead_unstable_call(
sub_id, sub_id,
@@ -593,9 +626,17 @@ async fn chainhead_unstable_call() {
) )
.await .await
.unwrap(); .unwrap();
let event = sub.next().await.unwrap().unwrap(); let operation_id = match response {
MethodResponse::Started(started) => started.operation_id,
MethodResponse::LimitReached => panic!("Expected started response"),
};
assert_matches!(event, ChainHeadEvent::<String>::Done(_)); // Response propagated to `chainHead_follow`.
let event = next_operation_event(&mut blocks).await;
assert_matches!(
event,
FollowEvent::OperationCallDone(res) if res.operation_id == operation_id
);
} }
#[tokio::test] #[tokio::test]
@@ -31,7 +31,7 @@ use crate::utils::node_runtime;
use codec::{Compact, Encode}; use codec::{Compact, Encode};
use futures::StreamExt; use futures::StreamExt;
use subxt::{ use subxt::{
client::{LightClient, LightClientBuilder, OfflineClientT, OnlineClientT}, client::{LightClient, LightClientBuilder, OnlineClientT},
config::PolkadotConfig, config::PolkadotConfig,
rpc::types::FollowEvent, rpc::types::FollowEvent,
}; };