Merge remote-tracking branch 'origin/master' into lexnv/codegen-config

This commit is contained in:
Alexandru Vasile
2024-09-13 18:59:58 +03:00
111 changed files with 24479 additions and 7527 deletions
+27 -10
View File
@@ -26,9 +26,11 @@ default = ["jsonrpsee", "native"]
# Exactly 1 of "web" and "native" is expected.
native = [
"jsonrpsee?/async-client",
"jsonrpsee?/client-ws-transport-native-tls",
"jsonrpsee?/client-ws-transport-tls",
"jsonrpsee?/ws-client",
"subxt-lightclient?/native",
"tokio-util"
"tokio-util",
"tokio?/sync",
]
# Enable this for web/wasm builds.
@@ -36,14 +38,17 @@ native = [
web = [
"jsonrpsee?/async-wasm-client",
"jsonrpsee?/client-web-transport",
"jsonrpsee?/wasm-client",
"getrandom/js",
"subxt-lightclient?/web",
"subxt-macro/web",
"instant/wasm-bindgen"
"instant/wasm-bindgen",
"tokio?/sync",
"finito?/wasm-bindgen",
]
# Enable this to use the reconnecting rpc client
unstable-reconnecting-rpc-client = ["dep:reconnecting-jsonrpsee-ws-client"]
unstable-reconnecting-rpc-client = ["dep:finito", "dep:tokio", "jsonrpsee", "wasm-bindgen-futures"]
# Enable this to use jsonrpsee (allowing for example `OnlineClient::from_url`).
jsonrpsee = [
@@ -65,6 +70,9 @@ unstable-metadata = []
# Note that this feature is experimental and things may break or not work as expected.
unstable-light-client = ["subxt-lightclient"]
# Activate this to expose the ability to generate metadata from Wasm runtime files.
runtime-path = ["subxt-macro/runtime-path"]
[dependencies]
async-trait = { workspace = true }
codec = { package = "parity-scale-codec", workspace = true, features = ["derive"] }
@@ -98,9 +106,6 @@ subxt-core = { workspace = true, features = ["std"] }
subxt-metadata = { workspace = true, features = ["std"] }
subxt-lightclient = { workspace = true, optional = true, default-features = false }
# Reconnecting jsonrpc ws client
reconnecting-jsonrpsee-ws-client = { version = "0.4", optional = true }
# For parsing urls to disallow insecure schemes
url = { workspace = true }
@@ -110,11 +115,18 @@ getrandom = { workspace = true, optional = true }
# Included if "native" feature is enabled
tokio-util = { workspace = true, features = ["compat"], optional = true }
# Included if the reconnecting rpc client feature is enabled
# Only the `tokio/sync` is used in the reconnecting rpc client
# and that compiles both for native and web.
tokio = { workspace = true, optional = true }
finito = { workspace = true, optional = true }
wasm-bindgen-futures = { workspace = true, optional = true }
[dev-dependencies]
bitvec = { workspace = true }
codec = { workspace = true, features = ["derive", "bit-vec"] }
scale-info = { workspace = true, features = ["bit-vec"] }
tokio = { workspace = true, features = ["macros", "time", "rt-multi-thread"] }
tokio = { workspace = true, features = ["macros", "time", "rt-multi-thread", "sync"] }
sp-core = { workspace = true }
sp-keyring = { workspace = true }
sp-runtime = { workspace = true }
@@ -125,6 +137,11 @@ subxt-signer = { path = "../signer", features = ["unstable-eth"] }
# the light-client wlll emit INFO logs with
# `GrandPa warp sync finished` and `Finalized block runtime ready.`
tracing-subscriber = { workspace = true }
# These deps are needed to test the reconnecting rpc client
jsonrpsee = { workspace = true, features = ["server"] }
tower = "0.4"
hyper = "1"
http-body = "1"
[[example]]
name = "light_client_basic"
@@ -137,8 +154,8 @@ path = "examples/light_client_local_node.rs"
required-features = ["unstable-light-client", "jsonrpsee", "native"]
[[example]]
name = "reconnecting_rpc_client"
path = "examples/reconnecting_rpc_client.rs"
name = "setup_reconnecting_rpc_client"
path = "examples/setup_reconnecting_rpc_client.rs"
required-features = ["unstable-reconnecting-rpc-client"]
[package.metadata.docs.rs]
-73
View File
@@ -1,73 +0,0 @@
//! Example to utilize the `reconnecting rpc client` in subxt
//! which hidden behind behind `--feature unstable-reconnecting-rpc-client`
//!
//! To utilize full logs from the RPC client use:
//! `RUST_LOG="jsonrpsee=trace,reconnecting_jsonrpsee_ws_client=trace"`
#![allow(missing_docs)]
use std::time::Duration;
use subxt::backend::rpc::reconnecting_rpc_client::{Client, ExponentialBackoff, PingConfig};
use subxt::backend::rpc::RpcClient;
use subxt::error::{Error, RpcError};
use subxt::{OnlineClient, PolkadotConfig};
// Generate an interface that we can use from the node's metadata.
#[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata_small.scale")]
pub mod polkadot {}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
tracing_subscriber::fmt::init();
// Create a new client with with a reconnecting RPC client.
let rpc = Client::builder()
// Reconnect with exponential backoff
//
// This API is "iterator-like" so one could limit it to only
// reconnect x times and then quit.
.retry_policy(ExponentialBackoff::from_millis(100).max_delay(Duration::from_secs(10)))
// Send period WebSocket pings/pongs every 6th second and if it's not ACK:ed in 30 seconds
// then disconnect.
//
// This is just a way to ensure that the connection isn't idle if no message is sent that often
.enable_ws_ping(
PingConfig::new()
.ping_interval(Duration::from_secs(6))
.inactive_limit(Duration::from_secs(30)),
)
// There are other configurations as well that can be found here:
// <https://docs.rs/reconnecting-jsonrpsee-ws-client/latest/reconnecting_jsonrpsee_ws_client/struct.ClientBuilder.html>
.build("ws://localhost:9944".to_string())
.await?;
let api: OnlineClient<PolkadotConfig> =
OnlineClient::from_rpc_client(RpcClient::new(rpc.clone())).await?;
// Subscribe to all finalized blocks:
let mut blocks_sub = api.blocks().subscribe_finalized().await?;
// For each block, print a bunch of information about it:
while let Some(block) = blocks_sub.next().await {
let block = match block {
Ok(b) => b,
Err(Error::Rpc(RpcError::DisconnectedWillReconnect(err))) => {
println!("{err}");
continue;
}
Err(e) => {
return Err(e.into());
}
};
let block_number = block.header().number;
let block_hash = block.hash();
println!("Block #{block_number} ({block_hash})");
}
println!("RPC client reconnected `{}` times", rpc.reconnect_count());
Ok(())
}
@@ -37,6 +37,7 @@ impl Config for CustomConfig {
signed_extensions::CheckMortality<Self>,
signed_extensions::ChargeAssetTxPayment<Self>,
signed_extensions::ChargeTransactionPayment,
signed_extensions::CheckMetadataHash,
// And add a new one of our own:
CustomSignedExtension,
),
@@ -83,8 +84,8 @@ impl ExtrinsicParamsEncoder for CustomSignedExtension {
pub fn custom(
params: DefaultExtrinsicParamsBuilder<CustomConfig>,
) -> <<CustomConfig as Config>::ExtrinsicParams as ExtrinsicParams<CustomConfig>>::Params {
let (a, b, c, d, e, f, g) = params.build();
(a, b, c, d, e, f, g, ())
let (a, b, c, d, e, f, g, h) = params.build();
(a, b, c, d, e, f, g, h, ())
}
#[tokio::main]
@@ -0,0 +1,84 @@
//! Example to utilize the `reconnecting rpc client` in subxt
//! which hidden behind behind `--feature unstable-reconnecting-rpc-client`
//!
//! To utilize full logs from the RPC client use:
//! `RUST_LOG="jsonrpsee=trace,subxt-reconnecting-rpc-client=trace"`
#![allow(missing_docs)]
use std::time::Duration;
use futures::StreamExt;
use subxt::backend::rpc::reconnecting_rpc_client::{ExponentialBackoff, RpcClient};
use subxt::{OnlineClient, PolkadotConfig};
// Generate an interface that we can use from the node's metadata.
#[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata_small.scale")]
pub mod polkadot {}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
tracing_subscriber::fmt::init();
// Create a new client with with a reconnecting RPC client.
let rpc = RpcClient::builder()
// Reconnect with exponential backoff
//
// This API is "iterator-like" and we use `take` to limit the number of retries.
.retry_policy(
ExponentialBackoff::from_millis(100)
.max_delay(Duration::from_secs(10))
.take(3),
)
// There are other configurations as well that can be found at [`reconnecting_rpc_client::ClientBuilder`].
.build("ws://localhost:9944".to_string())
.await?;
// If you want to use the unstable backend with the reconnecting RPC client, you can do so like this:
//
// ```
// use subxt::backend::unstable::UnstableBackend;
// use subxt::OnlineClient;
//
// let (backend, mut driver) = UnstableBackend::builder().build(RpcClient::new(rpc.clone()));
// tokio::spawn(async move {
// while let Some(val) = driver.next().await {
// if let Err(e) = val {
// eprintln!("Error driving unstable backend: {e}; terminating client");
// }
// }
// });
// let api: OnlineClient<PolkadotConfig> = OnlineClient::from_backend(Arc::new(backend)).await?;
// ```
let api: OnlineClient<PolkadotConfig> = OnlineClient::from_rpc_client(rpc.clone()).await?;
// Run for at most 100 blocks and print a bunch of information about it.
//
// The subscription is automatically re-started when the RPC client has reconnected.
// You can test that by stopping the polkadot node and restarting it.
let mut blocks_sub = api.blocks().subscribe_finalized().await?.take(100);
while let Some(block) = blocks_sub.next().await {
let block = match block {
Ok(b) => b,
Err(e) => {
// This can only happen on the legacy backend and the unstable backend
// will handle this internally.
if e.is_disconnected_will_reconnect() {
println!("The RPC connection was lost and we may have missed a few blocks");
continue;
}
return Err(e.into());
}
};
let block_number = block.number();
let block_hash = block.hash();
println!("Block #{block_number} ({block_hash})");
}
Ok(())
}
+2 -2
View File
@@ -22,10 +22,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.storage()
.at_latest()
.await?
.fetch_raw(subxt_core::storage::get_address_bytes(&storage_query, &api.metadata()).unwrap())
.fetch(&storage_query)
.await?;
let v = hex::encode(result.unwrap());
let v = result.unwrap().data.free;
println!("Alice: {v}");
Ok(())
}
+4 -11
View File
@@ -6,7 +6,8 @@
#![allow(missing_docs)]
use subxt::OnlineClient;
use subxt_signer::eth::{dev, AccountId20, Signature};
use subxt_core::utils::AccountId20;
use subxt_signer::eth::{dev, Signature};
#[subxt::subxt(runtime_metadata_path = "../artifacts/frontier_metadata_small.scale")]
mod eth_runtime {}
@@ -25,28 +26,20 @@ impl subxt::Config for EthRuntimeConfig {
type AssetId = u32;
}
// This helper makes it easy to use our `AccountId20`'s with generated
// code that expects a generated `eth_runtime::runtime_types::fp_account:AccountId20` type.
impl From<AccountId20> for eth_runtime::runtime_types::fp_account::AccountId20 {
fn from(a: AccountId20) -> Self {
eth_runtime::runtime_types::fp_account::AccountId20(a.0)
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let api = OnlineClient::<EthRuntimeConfig>::from_insecure_url("ws://127.0.0.1:9944").await?;
let alith = dev::alith();
let baltathar = dev::baltathar();
let dest = baltathar.account_id();
let dest = baltathar.public_key().to_account_id();
println!("baltathar pub: {}", hex::encode(baltathar.public_key().0));
println!("baltathar addr: {}", hex::encode(dest));
let balance_transfer_tx = eth_runtime::tx()
.balances()
.transfer_allow_death(dest.into(), 10_001);
.transfer_allow_death(dest, 10_001);
let events = api
.tx()
+43
View File
@@ -0,0 +1,43 @@
#![allow(missing_docs)]
use subxt::{OnlineClient, PolkadotConfig};
use subxt_signer::sr25519::dev;
#[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata_small.scale")]
pub mod polkadot {}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let api = OnlineClient::<PolkadotConfig>::new().await?;
// Prepare some extrinsics. These are boxed so that they can live alongside each other.
let txs = [dynamic_remark(), balance_transfer(), remark()];
for tx in txs {
let from = dev::alice();
api.tx()
.sign_and_submit_then_watch_default(&tx, &from)
.await?
.wait_for_finalized_success()
.await?;
println!("Submitted tx");
}
Ok(())
}
fn balance_transfer() -> Box<dyn subxt::tx::Payload> {
let dest = dev::bob().public_key().into();
Box::new(polkadot::tx().balances().transfer_allow_death(dest, 10_000))
}
fn remark() -> Box<dyn subxt::tx::Payload> {
Box::new(polkadot::tx().system().remark(vec![1, 2, 3, 4, 5]))
}
fn dynamic_remark() -> Box<dyn subxt::tx::Payload> {
use subxt::dynamic::{tx, Value};
let tx_payload = tx("System", "remark", vec![Value::from_bytes("Hello")]);
Box::new(tx_payload)
}
+53
View File
@@ -0,0 +1,53 @@
#![allow(missing_docs)]
use subxt::{OnlineClient, PolkadotConfig};
use subxt_signer::sr25519::dev;
type BoxedError = Box<dyn std::error::Error + Send + Sync + 'static>;
#[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata_small.scale")]
pub mod polkadot {}
#[tokio::main]
async fn main() -> Result<(), BoxedError> {
// Spawned tasks require things held across await points to impl Send,
// so we use one to demonstrate that this is possible with `PartialExtrinsic`
tokio::spawn(signing_example()).await??;
Ok(())
}
async fn signing_example() -> Result<(), BoxedError> {
let api = OnlineClient::<PolkadotConfig>::new().await?;
// Build a balance transfer extrinsic.
let dest = dev::bob().public_key().into();
let balance_transfer_tx = polkadot::tx().balances().transfer_allow_death(dest, 10_000);
let alice = dev::alice();
// Create partial tx, ready to be signed.
let partial_tx = api
.tx()
.create_partial_signed(
&balance_transfer_tx,
&alice.public_key().to_account_id(),
Default::default(),
)
.await?;
// Simulate taking some time to get a signature back, in part to
// show that the `PartialExtrinsic` can be held across await points.
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
let signature = alice.sign(&partial_tx.signer_payload());
// Sign the transaction.
let tx = partial_tx
.sign_with_address_and_signature(&alice.public_key().to_address(), &signature.into());
// Submit it.
tx.submit_and_watch()
.await?
.wait_for_finalized_success()
.await?;
Ok(())
}
+158 -68
View File
@@ -8,10 +8,12 @@
pub mod rpc_methods;
use self::rpc_methods::TransactionStatus as RpcTransactionStatus;
use crate::backend::utils::{retry, retry_stream};
use crate::backend::{
rpc::RpcClient, Backend, BlockRef, RuntimeVersion, StorageResponse, StreamOf, StreamOfResults,
TransactionStatus,
};
use crate::error::RpcError;
use crate::{config::Header, Config, Error};
use async_trait::async_trait;
use futures::{future, future::Either, stream, Future, FutureExt, Stream, StreamExt};
@@ -62,12 +64,21 @@ impl<T: Config> LegacyBackendBuilder<T> {
}
/// The legacy backend.
#[derive(Debug, Clone)]
#[derive(Debug)]
pub struct LegacyBackend<T> {
storage_page_size: u32,
methods: LegacyRpcMethods<T>,
}
impl<T> Clone for LegacyBackend<T> {
fn clone(&self) -> LegacyBackend<T> {
LegacyBackend {
storage_page_size: self.storage_page_size,
methods: self.methods.clone(),
}
}
}
impl<T: Config> LegacyBackend<T> {
/// Configure and construct an [`LegacyBackend`].
pub fn builder() -> LegacyBackendBuilder<T> {
@@ -84,16 +95,28 @@ impl<T: Config + Send + Sync + 'static> Backend<T> for LegacyBackend<T> {
keys: Vec<Vec<u8>>,
at: T::Hash,
) -> Result<StreamOfResults<StorageResponse>, Error> {
fn get_entry<T: Config>(
key: Vec<u8>,
at: T::Hash,
methods: LegacyRpcMethods<T>,
) -> impl Future<Output = Result<Option<StorageResponse>, Error>> {
retry(move || {
let methods = methods.clone();
let key = key.clone();
async move {
let res = methods.state_get_storage(&key, Some(at)).await?;
Ok(res.map(move |value| StorageResponse { key, value }))
}
})
}
let keys = keys.clone();
let methods = self.methods.clone();
// For each key, return it + a future to get the result.
let iter = keys.into_iter().map(move |key| {
let methods = methods.clone();
async move {
let res = methods.state_get_storage(&key, Some(at)).await?;
Ok(res.map(|value| StorageResponse { key, value }))
}
});
let iter = keys
.into_iter()
.map(move |key| get_entry(key, at, methods.clone()));
let s = stream::iter(iter)
// Resolve the future
@@ -158,99 +181,159 @@ impl<T: Config + Send + Sync + 'static> Backend<T> for LegacyBackend<T> {
}
async fn genesis_hash(&self) -> Result<T::Hash, Error> {
self.methods.genesis_hash().await
retry(|| self.methods.genesis_hash()).await
}
async fn block_header(&self, at: T::Hash) -> Result<Option<T::Header>, Error> {
self.methods.chain_get_header(Some(at)).await
retry(|| self.methods.chain_get_header(Some(at))).await
}
async fn block_body(&self, at: T::Hash) -> Result<Option<Vec<Vec<u8>>>, Error> {
let Some(details) = self.methods.chain_get_block(Some(at)).await? else {
return Ok(None);
};
Ok(Some(
details.block.extrinsics.into_iter().map(|b| b.0).collect(),
))
retry(|| async {
let Some(details) = self.methods.chain_get_block(Some(at)).await? else {
return Ok(None);
};
Ok(Some(
details.block.extrinsics.into_iter().map(|b| b.0).collect(),
))
})
.await
}
async fn latest_finalized_block_ref(&self) -> Result<BlockRef<T::Hash>, Error> {
let hash = self.methods.chain_get_finalized_head().await?;
Ok(BlockRef::from_hash(hash))
retry(|| async {
let hash = self.methods.chain_get_finalized_head().await?;
Ok(BlockRef::from_hash(hash))
})
.await
}
async fn current_runtime_version(&self) -> Result<RuntimeVersion, Error> {
let details = self.methods.state_get_runtime_version(None).await?;
Ok(RuntimeVersion {
spec_version: details.spec_version,
transaction_version: details.transaction_version,
retry(|| async {
let details = self.methods.state_get_runtime_version(None).await?;
Ok(RuntimeVersion {
spec_version: details.spec_version,
transaction_version: details.transaction_version,
})
})
.await
}
async fn stream_runtime_version(&self) -> Result<StreamOfResults<RuntimeVersion>, Error> {
let sub = self.methods.state_subscribe_runtime_version().await?;
let sub = sub.map(|r| {
r.map(|v| RuntimeVersion {
spec_version: v.spec_version,
transaction_version: v.transaction_version,
let methods = self.methods.clone();
let retry_sub = retry_stream(move || {
let methods = methods.clone();
Box::pin(async move {
let sub = methods.state_subscribe_runtime_version().await?;
let sub = sub.map(|r| {
r.map(|v| RuntimeVersion {
spec_version: v.spec_version,
transaction_version: v.transaction_version,
})
});
Ok(StreamOf(Box::pin(sub)))
})
})
.await?;
// For runtime version subscriptions we omit the `DisconnectedWillReconnect` error
// because the once it resubscribes it will emit the latest runtime version.
//
// Thus, it's technically possible that a runtime version can be missed if
// two runtime upgrades happen in quick succession, but this is very unlikely.
let stream = retry_sub.filter(|r| {
let forward = !matches!(r, Err(Error::Rpc(RpcError::DisconnectedWillReconnect(_))));
async move { forward }
});
Ok(StreamOf(Box::pin(sub)))
Ok(StreamOf(Box::pin(stream)))
}
async fn stream_all_block_headers(
&self,
) -> Result<StreamOfResults<(T::Header, BlockRef<T::Hash>)>, Error> {
let sub = self.methods.chain_subscribe_all_heads().await?;
let sub = sub.map(|r| {
r.map(|h| {
let hash = h.hash();
(h, BlockRef::from_hash(hash))
let methods = self.methods.clone();
let retry_sub = retry_stream(move || {
let methods = methods.clone();
Box::pin(async move {
let sub = methods.chain_subscribe_all_heads().await?;
let sub = sub.map(|r| {
r.map(|h| {
let hash = h.hash();
(h, BlockRef::from_hash(hash))
})
});
Ok(StreamOf(Box::pin(sub)))
})
});
Ok(StreamOf(Box::pin(sub)))
})
.await?;
Ok(retry_sub)
}
async fn stream_best_block_headers(
&self,
) -> Result<StreamOfResults<(T::Header, BlockRef<T::Hash>)>, Error> {
let sub = self.methods.chain_subscribe_new_heads().await?;
let sub = sub.map(|r| {
r.map(|h| {
let hash = h.hash();
(h, BlockRef::from_hash(hash))
let methods = self.methods.clone();
let retry_sub = retry_stream(move || {
let methods = methods.clone();
Box::pin(async move {
let sub = methods.chain_subscribe_new_heads().await?;
let sub = sub.map(|r| {
r.map(|h| {
let hash = h.hash();
(h, BlockRef::from_hash(hash))
})
});
Ok(StreamOf(Box::pin(sub)))
})
});
Ok(StreamOf(Box::pin(sub)))
})
.await?;
Ok(retry_sub)
}
async fn stream_finalized_block_headers(
&self,
) -> Result<StreamOfResults<(T::Header, BlockRef<T::Hash>)>, Error> {
let sub: super::rpc::RpcSubscription<<T as Config>::Header> =
self.methods.chain_subscribe_finalized_heads().await?;
let this = self.clone();
// Get the last finalized block immediately so that the stream will emit every finalized block after this.
let last_finalized_block_ref = self.latest_finalized_block_ref().await?;
let last_finalized_block_num = self
.block_header(last_finalized_block_ref.hash())
.await?
.map(|h| h.number().into());
let retry_sub = retry_stream(move || {
let this = this.clone();
Box::pin(async move {
let sub = this.methods.chain_subscribe_finalized_heads().await?;
// Fill in any missing blocks, because the backend may not emit every finalized block; just the latest ones which
// are finalized each time.
let sub = subscribe_to_block_headers_filling_in_gaps(
self.methods.clone(),
sub,
last_finalized_block_num,
);
let sub = sub.map(|r| {
r.map(|h| {
let hash = h.hash();
(h, BlockRef::from_hash(hash))
// Get the last finalized block immediately so that the stream will emit every finalized block after this.
let last_finalized_block_ref = this.latest_finalized_block_ref().await?;
let last_finalized_block_num = this
.block_header(last_finalized_block_ref.hash())
.await?
.map(|h| h.number().into());
// Fill in any missing blocks, because the backend may not emit every finalized block; just the latest ones which
// are finalized each time.
let sub = subscribe_to_block_headers_filling_in_gaps(
this.methods.clone(),
sub,
last_finalized_block_num,
);
let sub = sub.map(|r| {
r.map(|h| {
let hash = h.hash();
(h, BlockRef::from_hash(hash))
})
});
Ok(StreamOf(Box::pin(sub)))
})
});
Ok(StreamOf(Box::pin(sub)))
})
.await?;
Ok(retry_sub)
}
async fn submit_transaction(
@@ -261,6 +344,7 @@ impl<T: Config + Send + Sync + 'static> Backend<T> for LegacyBackend<T> {
.methods
.author_submit_and_watch_extrinsic(extrinsic)
.await?;
let sub = sub.filter_map(|r| {
let mapped = r
.map(|tx| {
@@ -309,7 +393,8 @@ impl<T: Config + Send + Sync + 'static> Backend<T> for LegacyBackend<T> {
future::ready(mapped)
});
Ok(StreamOf(Box::pin(sub)))
Ok(StreamOf::new(Box::pin(sub)))
}
async fn call(
@@ -318,9 +403,7 @@ impl<T: Config + Send + Sync + 'static> Backend<T> for LegacyBackend<T> {
call_parameters: Option<&[u8]>,
at: T::Hash,
) -> Result<Vec<u8>, Error> {
self.methods
.state_call(method, call_parameters, Some(at))
.await
retry(|| self.methods.state_call(method, call_parameters, Some(at))).await
}
}
@@ -431,6 +514,11 @@ impl<T: Config> Stream for StorageFetchDescendantKeysStream<T> {
return Poll::Ready(Some(Ok(keys)));
}
Err(e) => {
if e.is_disconnected_will_reconnect() {
this.keys_fut = Some(keys_fut);
continue;
}
// Error getting keys? Return it.
return Poll::Ready(Some(Err(e)));
}
@@ -513,7 +601,9 @@ impl<T: Config> Stream for StorageFetchDescendantValuesStream<T> {
let at = this.keys.at;
let results_fut = async move {
let keys = keys.iter().map(|k| &**k);
let values = methods.state_query_storage_at(keys, Some(at)).await?;
let values =
retry(|| methods.state_query_storage_at(keys.clone(), Some(at)))
.await?;
let values: VecDeque<_> = values
.into_iter()
.flat_map(|v| {
+2 -2
View File
@@ -332,8 +332,7 @@ impl<T: Config> LegacyRpcMethods<T> {
public: Vec<u8>,
) -> Result<(), Error> {
let params = rpc_params![key_type, suri, Bytes(public)];
self.client.request("author_insertKey", params).await?;
Ok(())
self.client.request("author_insertKey", params).await
}
/// Generate new session keys and returns the corresponding public keys.
@@ -455,6 +454,7 @@ pub type EncodedJustification = Vec<u8>;
/// the RPC call `state_getRuntimeVersion`,
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(test, derive(serde::Serialize))]
pub struct RuntimeVersion {
/// Version of the runtime specification. A full-node will not attempt to use its native
/// runtime in substitute for the on-chain Wasm runtime unless all of `spec_name`,
+405
View File
@@ -9,6 +9,7 @@
pub mod legacy;
pub mod rpc;
pub mod unstable;
pub mod utils;
use subxt_core::client::RuntimeVersion;
@@ -324,9 +325,413 @@ pub enum TransactionStatus<Hash> {
/// A response from calls like [`Backend::storage_fetch_values`] or
/// [`Backend::storage_fetch_descendant_values`].
#[cfg_attr(test, derive(serde::Serialize, Clone, PartialEq, Debug))]
pub struct StorageResponse {
/// The key.
pub key: Vec<u8>,
/// The associated value.
pub value: Vec<u8>,
}
#[cfg(test)]
mod test {
use super::*;
mod legacy {
use super::rpc::{RpcClient, RpcClientT};
use crate::backend::rpc::RawRpcSubscription;
use crate::backend::BackendExt;
use crate::{
backend::{
legacy::rpc_methods::Bytes, legacy::rpc_methods::RuntimeVersion,
legacy::LegacyBackend, StorageResponse,
},
error::RpcError,
};
use futures::StreamExt;
use serde::Serialize;
use serde_json::value::RawValue;
use std::{
collections::{HashMap, VecDeque},
sync::Arc,
};
use subxt_core::{config::DefaultExtrinsicParams, Config};
use tokio::sync::{mpsc, Mutex};
type RpcResult<T> = Result<T, RpcError>;
type Item = RpcResult<String>;
struct MockDataTable {
items: HashMap<Vec<u8>, VecDeque<Item>>,
}
impl MockDataTable {
fn new() -> Self {
MockDataTable {
items: HashMap::new(),
}
}
fn from_iter<'a, T: Serialize, I: IntoIterator<Item = (&'a str, RpcResult<T>)>>(
item: I,
) -> Self {
let mut data = Self::new();
for (key, item) in item.into_iter() {
data.push(key.into(), item);
}
data
}
fn push<I: Serialize>(&mut self, key: Vec<u8>, item: RpcResult<I>) {
let item = item.map(|x| serde_json::to_string(&x).unwrap());
match self.items.entry(key) {
std::collections::hash_map::Entry::Occupied(v) => v.into_mut().push_back(item),
std::collections::hash_map::Entry::Vacant(e) => {
e.insert(VecDeque::from([item]));
}
}
}
fn pop(&mut self, key: Vec<u8>) -> Item {
self.items.get_mut(&key).unwrap().pop_front().unwrap()
}
}
struct Subscription {
sender: mpsc::Sender<RpcResult<Vec<Item>>>,
receiver: mpsc::Receiver<RpcResult<Vec<Item>>>,
}
impl Subscription {
fn new() -> Self {
let (sender, receiver) = mpsc::channel(32);
Self { sender, receiver }
}
async fn from_iter<
T: Serialize,
S: IntoIterator<Item = RpcResult<Vec<RpcResult<T>>>>,
>(
items: S,
) -> Self {
let sub = Self::new();
for i in items {
let i: RpcResult<Vec<Item>> = i.map(|items| {
items
.into_iter()
.map(|item| item.map(|i| serde_json::to_string(&i).unwrap()))
.collect()
});
sub.write(i).await
}
sub
}
async fn read(&mut self) -> RpcResult<Vec<Item>> {
self.receiver.recv().await.unwrap()
}
async fn write(&self, items: RpcResult<Vec<Item>>) {
self.sender.send(items).await.unwrap()
}
}
struct Data {
request: MockDataTable,
subscription: Subscription,
}
struct MockRpcClientStorage {
data: Arc<Mutex<Data>>,
}
impl RpcClientT for MockRpcClientStorage {
fn request_raw<'a>(
&'a self,
method: &'a str,
params: Option<Box<serde_json::value::RawValue>>,
) -> super::rpc::RawRpcFuture<'a, Box<serde_json::value::RawValue>> {
Box::pin(async move {
match method {
"state_getStorage" => {
let mut data = self.data.lock().await;
let params = params.map(|p| p.get().to_string());
let rpc_params = jsonrpsee::types::Params::new(params.as_deref());
let key: sp_core::Bytes = rpc_params.sequence().next().unwrap();
let value = data.request.pop(key.0);
value.map(|v| serde_json::value::RawValue::from_string(v).unwrap())
}
"chain_getBlockHash" => {
let mut data = self.data.lock().await;
let value = data.request.pop("chain_getBlockHash".into());
value.map(|v| serde_json::value::RawValue::from_string(v).unwrap())
}
_ => todo!(),
}
})
}
fn subscribe_raw<'a>(
&'a self,
_sub: &'a str,
_params: Option<Box<serde_json::value::RawValue>>,
_unsub: &'a str,
) -> super::rpc::RawRpcFuture<'a, super::rpc::RawRpcSubscription> {
Box::pin(async {
let mut data = self.data.lock().await;
let values: RpcResult<Vec<RpcResult<Box<RawValue>>>> =
data.subscription.read().await.map(|v| {
v.into_iter()
.map(|v| {
v.map(|v| serde_json::value::RawValue::from_string(v).unwrap())
})
.collect::<Vec<RpcResult<Box<RawValue>>>>()
});
values.map(|v| RawRpcSubscription {
stream: futures::stream::iter(v).boxed(),
id: Some("ID".to_string()),
})
})
}
}
// Define dummy config
enum Conf {}
impl Config for Conf {
type Hash = crate::utils::H256;
type AccountId = crate::utils::AccountId32;
type Address = crate::utils::MultiAddress<Self::AccountId, ()>;
type Signature = crate::utils::MultiSignature;
type Hasher = crate::config::substrate::BlakeTwo256;
type Header = crate::config::substrate::SubstrateHeader<u32, Self::Hasher>;
type ExtrinsicParams = DefaultExtrinsicParams<Self>;
type AssetId = u32;
}
use crate::backend::Backend;
fn client_runtime_version(num: u32) -> crate::client::RuntimeVersion {
crate::client::RuntimeVersion {
spec_version: num,
transaction_version: num,
}
}
fn runtime_version(num: u32) -> RuntimeVersion {
RuntimeVersion {
spec_version: num,
transaction_version: num,
other: HashMap::new(),
}
}
fn bytes(str: &str) -> RpcResult<Option<Bytes>> {
Ok(Some(Bytes(str.into())))
}
fn storage_response<K: Into<Vec<u8>>, V: Into<Vec<u8>>>(key: K, value: V) -> StorageResponse
where
Vec<u8>: From<K>,
{
StorageResponse {
key: key.into(),
value: value.into(),
}
}
async fn build_mock_client<
'a,
T: Serialize,
D: IntoIterator<Item = (&'a str, RpcResult<T>)>,
S: IntoIterator<Item = RpcResult<Vec<RpcResult<T>>>>,
>(
table_data: D,
subscription_data: S,
) -> RpcClient {
let data = Data {
request: MockDataTable::from_iter(table_data),
subscription: Subscription::from_iter(subscription_data).await,
};
RpcClient::new(MockRpcClientStorage {
data: Arc::new(Mutex::new(data)),
})
}
#[tokio::test]
async fn storage_fetch_values() {
let mock_data = vec![
("ID1", bytes("Data1")),
(
"ID2",
Err(RpcError::DisconnectedWillReconnect(
"Reconnecting".to_string(),
)),
),
("ID2", bytes("Data2")),
(
"ID3",
Err(RpcError::DisconnectedWillReconnect(
"Reconnecting".to_string(),
)),
),
("ID3", bytes("Data3")),
];
let rpc_client = build_mock_client(mock_data, vec![]).await;
let backend: LegacyBackend<Conf> = LegacyBackend::builder().build(rpc_client);
// Test
let response = backend
.storage_fetch_values(
["ID1".into(), "ID2".into(), "ID3".into()].into(),
crate::utils::H256::random(),
)
.await
.unwrap();
let response = response
.map(|x| x.unwrap())
.collect::<Vec<StorageResponse>>()
.await;
let expected = vec![
storage_response("ID1", "Data1"),
storage_response("ID2", "Data2"),
storage_response("ID3", "Data3"),
];
assert_eq!(expected, response)
}
#[tokio::test]
async fn storage_fetch_value() {
// Setup
let mock_data = [
(
"ID1",
Err(RpcError::DisconnectedWillReconnect(
"Reconnecting".to_string(),
)),
),
("ID1", bytes("Data1")),
];
let rpc_client = build_mock_client(mock_data, vec![]).await;
// Test
let backend: LegacyBackend<Conf> = LegacyBackend::builder().build(rpc_client);
let response = backend
.storage_fetch_value("ID1".into(), crate::utils::H256::random())
.await
.unwrap();
let response = response.unwrap();
assert_eq!("Data1".to_owned(), String::from_utf8(response).unwrap())
}
#[tokio::test]
/// This test should cover the logic of the following methods:
/// - `genesis_hash`
/// - `block_header`
/// - `block_body`
/// - `latest_finalized_block`
/// - `current_runtime_version`
/// - `current_runtime_version`
/// - `call`
/// The test covers them because they follow the simple pattern of:
/// ```no_run
/// async fn THE_THING(&self) -> Result<T::Hash, Error> {
/// retry(|| <DO THE THING> ).await
/// }
/// ```
async fn simple_fetch() {
let hash = crate::utils::H256::random();
let mock_data = vec![
(
"chain_getBlockHash",
Err(RpcError::DisconnectedWillReconnect(
"Reconnecting".to_string(),
)),
),
("chain_getBlockHash", Ok(Some(hash))),
];
let rpc_client = build_mock_client(mock_data, vec![]).await;
// Test
let backend: LegacyBackend<Conf> = LegacyBackend::builder().build(rpc_client);
let response = backend.genesis_hash().await.unwrap();
assert_eq!(hash, response)
}
#[tokio::test]
/// This test should cover the logic of the following methods:
/// - `stream_runtime_version`
/// - `stream_all_block_headers`
/// - `stream_best_block_headers`
/// The test covers them because they follow the simple pattern of:
/// ```no_run
/// async fn stream_the_thing(
/// &self,
/// ) -> Result<StreamOfResults<(T::Header, BlockRef<T::Hash>)>, Error> {
/// let methods = self.methods.clone();
/// let retry_sub = retry_stream(move || {
/// let methods = methods.clone();
/// Box::pin(async move {
/// methods.do_the_thing().await?
/// });
/// Ok(StreamOf(Box::pin(sub)))
/// })
/// })
/// .await?;
/// Ok(retry_sub)
/// }
/// ```
async fn stream_simple() {
let mock_subscription_data = vec![
Ok(vec![
Ok(runtime_version(0)),
Err(RpcError::DisconnectedWillReconnect(
"Reconnecting".to_string(),
)),
Ok(runtime_version(1)),
]),
Ok(vec![
Err(RpcError::DisconnectedWillReconnect(
"Reconnecting".to_string(),
)),
Ok(runtime_version(2)),
Ok(runtime_version(3)),
]),
Ok(vec![
Ok(runtime_version(4)),
Ok(runtime_version(5)),
Err(RpcError::RequestRejected("Reconnecting".to_string())),
]),
];
let rpc_client = build_mock_client(vec![], mock_subscription_data).await;
// Test
let backend: LegacyBackend<Conf> = LegacyBackend::builder().build(rpc_client);
let mut results = backend.stream_runtime_version().await.unwrap();
let mut expected = VecDeque::from(vec![
Ok::<crate::client::RuntimeVersion, crate::Error>(client_runtime_version(0)),
Ok(client_runtime_version(4)),
Ok(client_runtime_version(5)),
]);
while let Some(res) = results.next().await {
if res.is_ok() {
assert_eq!(expected.pop_front().unwrap().unwrap(), res.unwrap())
} else {
assert!(matches!(
res,
Err(crate::Error::Rpc(RpcError::RequestRejected(_)))
))
}
}
assert!(expected.is_empty());
assert!(results.next().await.is_none())
}
}
}
+2 -2
View File
@@ -65,8 +65,8 @@ crate::macros::cfg_unstable_light_client! {
}
crate::macros::cfg_reconnecting_rpc_client! {
mod reconnecting_jsonrpsee_impl;
pub use reconnecting_jsonrpsee_ws_client as reconnecting_rpc_client;
/// reconnecting rpc client.
pub mod reconnecting_rpc_client;
}
mod rpc_client;
@@ -1,52 +0,0 @@
// Copyright 2019-2023 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
use super::{RawRpcFuture, RawRpcSubscription, RpcClientT};
use crate::error::RpcError;
use futures::{FutureExt, StreamExt, TryStreamExt};
use reconnecting_jsonrpsee_ws_client::SubscriptionId;
use serde_json::value::RawValue;
impl RpcClientT for reconnecting_jsonrpsee_ws_client::Client {
fn request_raw<'a>(
&'a self,
method: &'a str,
params: Option<Box<RawValue>>,
) -> RawRpcFuture<'a, Box<RawValue>> {
async {
self.request_raw(method.to_string(), params)
.await
.map_err(|e| RpcError::ClientError(Box::new(e)))
}
.boxed()
}
fn subscribe_raw<'a>(
&'a self,
sub: &'a str,
params: Option<Box<RawValue>>,
unsub: &'a str,
) -> RawRpcFuture<'a, RawRpcSubscription> {
async {
let sub = self
.subscribe_raw(sub.to_string(), params, unsub.to_string())
.await
.map_err(|e| RpcError::ClientError(Box::new(e)))?;
let id = match sub.id() {
SubscriptionId::Num(n) => n.to_string(),
SubscriptionId::Str(s) => s.to_string(),
};
let stream = sub
.map_err(|e| RpcError::DisconnectedWillReconnect(e.to_string()))
.boxed();
Ok(RawRpcSubscription {
stream,
id: Some(id),
})
}
.boxed()
}
}
@@ -0,0 +1,640 @@
// Copyright 2019-2024 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
//! # reconnecting-jsonrpsee-ws-client
//!
//! A simple reconnecting JSON-RPC WebSocket client for subxt which
//! automatically reconnects when the connection is lost but
//! it doesn't retain subscriptions and pending method calls when it reconnects.
//!
//! The logic which action to take for individual calls and subscriptions are
//! handled by the subxt backend implementations.
//!
//! # Example
//!
//! ```no_run
//! use std::time::Duration;
//! use futures::StreamExt;
//! use subxt::backend::rpc::reconnecting_rpc_client::{RpcClient, ExponentialBackoff};
//! use subxt::{OnlineClient, PolkadotConfig};
//!
//! #[tokio::main]
//! async fn main() {
//! let rpc = RpcClient::builder()
//! .retry_policy(ExponentialBackoff::from_millis(100).max_delay(Duration::from_secs(10)))
//! .build("ws://localhost:9944".to_string())
//! .await
//! .unwrap();
//!
//! let subxt_client: OnlineClient<PolkadotConfig> = OnlineClient::from_rpc_client(rpc.clone()).await.unwrap();
//! let mut blocks_sub = subxt_client.blocks().subscribe_finalized().await.unwrap();
//!
//! while let Some(block) = blocks_sub.next().await {
//! let block = match block {
//! Ok(b) => b,
//! Err(e) => {
//! if e.is_disconnected_will_reconnect() {
//! println!("The RPC connection was lost and we may have missed a few blocks");
//! continue;
//! } else {
//! panic!("Error: {}", e);
//! }
//! }
//! };
//! println!("Block #{} ({})", block.number(), block.hash());
//! }
//! }
//! ```
mod platform;
#[cfg(test)]
mod tests;
mod utils;
use std::{
pin::Pin,
sync::Arc,
task::{self, Poll},
time::Duration,
};
use super::{RawRpcFuture, RawRpcSubscription, RpcClientT};
use crate::error::RpcError as SubxtRpcError;
use finito::Retry;
use futures::{FutureExt, Stream, StreamExt, TryStreamExt};
use jsonrpsee::core::{
client::{
Client as WsClient, ClientT, Subscription as RpcSubscription, SubscriptionClientT,
SubscriptionKind,
},
traits::ToRpcParams,
};
use platform::spawn;
use serde_json::value::RawValue;
use tokio::sync::{
mpsc::{self, UnboundedReceiver, UnboundedSender},
oneshot, Notify,
};
use utils::display_close_reason;
// re-exports
pub use finito::{ExponentialBackoff, FibonacciBackoff, FixedInterval};
pub use jsonrpsee::core::client::IdKind;
pub use jsonrpsee::{core::client::error::Error as RpcError, rpc_params, types::SubscriptionId};
#[cfg(feature = "native")]
pub use jsonrpsee::ws_client::{HeaderMap, PingConfig};
const LOG_TARGET: &str = "subxt-reconnecting-rpc-client";
/// Method result.
pub type MethodResult = Result<Box<RawValue>, Error>;
/// Subscription result.
pub type SubscriptionResult = Result<Box<RawValue>, DisconnectedWillReconnect>;
/// The connection was closed, reconnect initiated and the subscription was dropped.
#[derive(Debug, thiserror::Error)]
#[error("The connection was closed because of `{0:?}` and reconnect initiated")]
pub struct DisconnectedWillReconnect(String);
/// New-type pattern which implements [`ToRpcParams`] that is required by jsonrpsee.
#[derive(Debug, Clone)]
struct RpcParams(Option<Box<RawValue>>);
impl ToRpcParams for RpcParams {
fn to_rpc_params(self) -> Result<Option<Box<RawValue>>, serde_json::Error> {
Ok(self.0)
}
}
#[derive(Debug)]
enum Op {
Call {
method: String,
params: RpcParams,
send_back: oneshot::Sender<MethodResult>,
},
Subscription {
subscribe_method: String,
params: RpcParams,
unsubscribe_method: String,
send_back: oneshot::Sender<Result<Subscription, Error>>,
},
}
/// Error that can occur when for a RPC call or subscription.
#[derive(Debug, thiserror::Error)]
pub enum Error {
/// The client was dropped by the user.
#[error("The client was dropped")]
Dropped,
/// The connection was closed and reconnect initiated.
#[error(transparent)]
DisconnectedWillReconnect(#[from] DisconnectedWillReconnect),
/// Other rpc error.
#[error("{0}")]
RpcError(RpcError),
}
/// Represent a single subscription.
pub struct Subscription {
id: SubscriptionId<'static>,
stream: mpsc::UnboundedReceiver<SubscriptionResult>,
}
impl Subscription {
/// Returns the next notification from the stream.
/// This may return `None` if the subscription has been terminated,
/// which may happen if the channel becomes full or is dropped.
///
/// **Note:** This has an identical signature to the [`StreamExt::next`]
/// method (and delegates to that). Import [`StreamExt`] if you'd like
/// access to other stream combinator methods.
#[allow(clippy::should_implement_trait)]
pub async fn next(&mut self) -> Option<SubscriptionResult> {
StreamExt::next(self).await
}
/// Get the subscription ID.
pub fn id(&self) -> SubscriptionId<'static> {
self.id.clone()
}
}
impl Stream for Subscription {
type Item = SubscriptionResult;
fn poll_next(
mut self: Pin<&mut Self>,
cx: &mut task::Context<'_>,
) -> task::Poll<Option<Self::Item>> {
match self.stream.poll_recv(cx) {
Poll::Ready(Some(msg)) => Poll::Ready(Some(msg)),
Poll::Ready(None) => Poll::Ready(None),
Poll::Pending => Poll::Pending,
}
}
}
impl std::fmt::Debug for Subscription {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Subscription")
.field("id", &self.id)
.finish()
}
}
/// JSON-RPC client that reconnects automatically and may loose
/// subscription notifications when it reconnects.
#[derive(Clone, Debug)]
pub struct RpcClient {
tx: mpsc::UnboundedSender<Op>,
}
/// Builder for [`Client`].
#[derive(Clone, Debug)]
pub struct RpcClientBuilder<P> {
max_request_size: u32,
max_response_size: u32,
retry_policy: P,
#[cfg(feature = "native")]
ping_config: Option<PingConfig>,
#[cfg(feature = "native")]
// web doesn't support custom headers
// https://stackoverflow.com/a/4361358/6394734
headers: HeaderMap,
max_redirections: u32,
id_kind: IdKind,
max_log_len: u32,
max_concurrent_requests: u32,
request_timeout: Duration,
connection_timeout: Duration,
}
impl Default for RpcClientBuilder<ExponentialBackoff> {
fn default() -> Self {
Self {
max_request_size: 10 * 1024 * 1024,
max_response_size: 10 * 1024 * 1024,
retry_policy: ExponentialBackoff::from_millis(10).max_delay(Duration::from_secs(60)),
#[cfg(feature = "native")]
ping_config: Some(PingConfig::new()),
#[cfg(feature = "native")]
headers: HeaderMap::new(),
max_redirections: 5,
id_kind: IdKind::Number,
max_log_len: 1024,
max_concurrent_requests: 1024,
request_timeout: Duration::from_secs(60),
connection_timeout: Duration::from_secs(10),
}
}
}
impl RpcClientBuilder<ExponentialBackoff> {
/// Create a new builder.
pub fn new() -> Self {
Self::default()
}
}
impl<P> RpcClientBuilder<P>
where
P: Iterator<Item = Duration> + Send + Sync + 'static + Clone,
{
/// Configure the min response size a for websocket message.
///
/// Default: 10MB
pub fn max_request_size(mut self, max: u32) -> Self {
self.max_request_size = max;
self
}
/// Configure the max response size a for websocket message.
///
/// Default: 10MB
pub fn max_response_size(mut self, max: u32) -> Self {
self.max_response_size = max;
self
}
/// Set the max number of redirections to perform until a connection is regarded as failed.
///
/// Default: 5
pub fn max_redirections(mut self, redirect: u32) -> Self {
self.max_redirections = redirect;
self
}
/// Configure how many concurrent method calls are allowed.
///
/// Default: 1024
pub fn max_concurrent_requests(mut self, max: u32) -> Self {
self.max_concurrent_requests = max;
self
}
/// Configure how long until a method call is regarded as failed.
///
/// Default: 1 minute
pub fn request_timeout(mut self, timeout: Duration) -> Self {
self.request_timeout = timeout;
self
}
/// Set connection timeout for the WebSocket handshake
///
/// Default: 10 seconds
pub fn connection_timeout(mut self, timeout: Duration) -> Self {
self.connection_timeout = timeout;
self
}
/// Configure the data type of the request object ID
///
/// Default: number
pub fn id_format(mut self, kind: IdKind) -> Self {
self.id_kind = kind;
self
}
/// Set maximum length for logging calls and responses.
/// Logs bigger than this limit will be truncated.
///
/// Default: 1024
pub fn set_max_logging_length(mut self, max: u32) -> Self {
self.max_log_len = max;
self
}
#[cfg(feature = "native")]
#[cfg_attr(docsrs, doc(cfg(feature = "native")))]
/// Configure custom headers to use in the WebSocket handshake.
pub fn set_headers(mut self, headers: HeaderMap) -> Self {
self.headers = headers;
self
}
/// Configure which retry policy to use when a connection is lost.
///
/// Default: Exponential backoff 10ms
pub fn retry_policy<T>(self, retry_policy: T) -> RpcClientBuilder<T> {
RpcClientBuilder {
max_request_size: self.max_request_size,
max_response_size: self.max_response_size,
retry_policy,
#[cfg(feature = "native")]
ping_config: self.ping_config,
#[cfg(feature = "native")]
headers: self.headers,
max_redirections: self.max_redirections,
max_log_len: self.max_log_len,
id_kind: self.id_kind,
max_concurrent_requests: self.max_concurrent_requests,
request_timeout: self.request_timeout,
connection_timeout: self.connection_timeout,
}
}
#[cfg(feature = "native")]
#[cfg_attr(docsrs, doc(cfg(feature = "native")))]
/// Configure the WebSocket ping/pong interval.
///
/// Default: 30 seconds.
pub fn enable_ws_ping(mut self, ping_config: PingConfig) -> Self {
self.ping_config = Some(ping_config);
self
}
#[cfg(feature = "native")]
#[cfg_attr(docsrs, doc(cfg(feature = "native")))]
/// Disable WebSocket ping/pongs.
///
/// Default: 30 seconds.
pub fn disable_ws_ping(mut self) -> Self {
self.ping_config = None;
self
}
/// Build and connect to the target.
pub async fn build(self, url: String) -> Result<RpcClient, RpcError> {
let (tx, rx) = mpsc::unbounded_channel();
let client = Retry::new(self.retry_policy.clone(), || {
platform::ws_client(url.as_ref(), &self)
})
.await?;
platform::spawn(background_task(client, rx, url, self));
Ok(RpcClient { tx })
}
}
impl RpcClient {
/// Create a builder.
pub fn builder() -> RpcClientBuilder<ExponentialBackoff> {
RpcClientBuilder::new()
}
/// Perform a JSON-RPC method call.
pub async fn request(
&self,
method: String,
params: Option<Box<RawValue>>,
) -> Result<Box<RawValue>, Error> {
let (tx, rx) = oneshot::channel();
self.tx
.send(Op::Call {
method,
params: RpcParams(params),
send_back: tx,
})
.map_err(|_| Error::Dropped)?;
rx.await.map_err(|_| Error::Dropped)?
}
/// Perform a JSON-RPC subscription.
pub async fn subscribe(
&self,
subscribe_method: String,
params: Option<Box<RawValue>>,
unsubscribe_method: String,
) -> Result<Subscription, Error> {
let (tx, rx) = oneshot::channel();
self.tx
.send(Op::Subscription {
subscribe_method,
params: RpcParams(params),
unsubscribe_method,
send_back: tx,
})
.map_err(|_| Error::Dropped)?;
rx.await.map_err(|_| Error::Dropped)?
}
}
impl RpcClientT for RpcClient {
fn request_raw<'a>(
&'a self,
method: &'a str,
params: Option<Box<RawValue>>,
) -> RawRpcFuture<'a, Box<RawValue>> {
async {
self.request(method.to_string(), params)
.await
.map_err(|e| SubxtRpcError::DisconnectedWillReconnect(e.to_string()))
}
.boxed()
}
fn subscribe_raw<'a>(
&'a self,
sub: &'a str,
params: Option<Box<RawValue>>,
unsub: &'a str,
) -> RawRpcFuture<'a, RawRpcSubscription> {
async {
let sub = self
.subscribe(sub.to_string(), params, unsub.to_string())
.await
.map_err(|e| SubxtRpcError::ClientError(Box::new(e)))?;
let id = match sub.id() {
SubscriptionId::Num(n) => n.to_string(),
SubscriptionId::Str(s) => s.to_string(),
};
let stream = sub
.map_err(|e| SubxtRpcError::DisconnectedWillReconnect(e.to_string()))
.boxed();
Ok(RawRpcSubscription {
stream,
id: Some(id),
})
}
.boxed()
}
}
async fn background_task<P>(
mut client: Arc<WsClient>,
mut rx: UnboundedReceiver<Op>,
url: String,
client_builder: RpcClientBuilder<P>,
) where
P: Iterator<Item = Duration> + Send + 'static + Clone,
{
let disconnect = Arc::new(tokio::sync::Notify::new());
loop {
tokio::select! {
// An incoming JSON-RPC call to dispatch.
next_message = rx.recv() => {
match next_message {
None => break,
Some(op) => {
spawn(dispatch_call(client.clone(), op, disconnect.clone()));
}
};
}
// The connection was terminated and try to reconnect.
_ = client.on_disconnect() => {
let params = ReconnectParams {
url: &url,
client_builder: &client_builder,
close_reason: client.disconnect_reason().await,
};
client = match reconnect(params).await {
Ok(client) => client,
Err(e) => {
tracing::debug!(target: LOG_TARGET, "Failed to reconnect: {e}; terminating the connection");
break;
}
};
}
}
}
disconnect.notify_waiters();
}
async fn dispatch_call(client: Arc<WsClient>, op: Op, on_disconnect: Arc<tokio::sync::Notify>) {
match op {
Op::Call {
method,
params,
send_back,
} => {
match client.request::<Box<RawValue>, _>(&method, params).await {
Ok(rp) => {
// Fails only if the request is dropped by the client.
let _ = send_back.send(Ok(rp));
}
Err(RpcError::RestartNeeded(e)) => {
// Fails only if the request is dropped by the client.
let _ = send_back.send(Err(DisconnectedWillReconnect(e.to_string()).into()));
}
Err(e) => {
// Fails only if the request is dropped by the client.
let _ = send_back.send(Err(Error::RpcError(e)));
}
}
}
Op::Subscription {
subscribe_method,
params,
unsubscribe_method,
send_back,
} => {
match client
.subscribe::<Box<RawValue>, _>(
&subscribe_method,
params.clone(),
&unsubscribe_method,
)
.await
{
Ok(sub) => {
let (tx, rx) = mpsc::unbounded_channel();
let sub_id = match sub.kind() {
SubscriptionKind::Subscription(id) => id.clone().into_owned(),
_ => unreachable!("No method subscriptions possible in this crate; qed"),
};
platform::spawn(subscription_handler(
tx.clone(),
sub,
on_disconnect.clone(),
client.clone(),
));
let stream = Subscription {
id: sub_id,
stream: rx,
};
// Fails only if the request is dropped by the client.
let _ = send_back.send(Ok(stream));
}
Err(RpcError::RestartNeeded(e)) => {
// Fails only if the request is dropped by the client.
let _ = send_back.send(Err(DisconnectedWillReconnect(e.to_string()).into()));
}
Err(e) => {
// Fails only if the request is dropped.
let _ = send_back.send(Err(Error::RpcError(e)));
}
}
}
}
}
/// Handler for each individual subscription.
async fn subscription_handler(
sub_tx: UnboundedSender<SubscriptionResult>,
mut rpc_sub: RpcSubscription<Box<RawValue>>,
client_closed: Arc<Notify>,
client: Arc<WsClient>,
) {
loop {
tokio::select! {
next_msg = rpc_sub.next() => {
let Some(notif) = next_msg else {
let close = client.disconnect_reason().await;
_ = sub_tx.send(Err(DisconnectedWillReconnect(close.to_string())));
break;
};
let msg = notif.expect("RawValue is valid JSON; qed");
// Fails only if subscription was closed by the user.
if sub_tx.send(Ok(msg)).is_err() {
break;
}
}
// This channel indices whether the subscription was closed by user.
_ = sub_tx.closed() => {
break;
}
// This channel indicates whether the main task has been closed.
// at this point no further messages are processed.
_ = client_closed.notified() => {
break;
}
}
}
}
struct ReconnectParams<'a, P> {
url: &'a str,
client_builder: &'a RpcClientBuilder<P>,
close_reason: RpcError,
}
async fn reconnect<P>(params: ReconnectParams<'_, P>) -> Result<Arc<WsClient>, RpcError>
where
P: Iterator<Item = Duration> + Send + 'static + Clone,
{
let ReconnectParams {
url,
client_builder,
close_reason,
} = params;
let retry_policy = client_builder.retry_policy.clone();
tracing::debug!(target: LOG_TARGET, "Connection to {url} was closed: `{}`; starting to reconnect", display_close_reason(&close_reason));
let client = Retry::new(retry_policy.clone(), || {
platform::ws_client(url, client_builder)
})
.await?;
tracing::debug!(target: LOG_TARGET, "Connection to {url} was successfully re-established");
Ok(client)
}
@@ -0,0 +1,83 @@
// Copyright 2019-2024 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
use crate::backend::rpc::reconnecting_rpc_client::{RpcClientBuilder, RpcError};
use jsonrpsee::core::client::Client;
use std::sync::Arc;
#[cfg(feature = "native")]
pub use tokio::spawn;
#[cfg(feature = "web")]
pub use wasm_bindgen_futures::spawn_local as spawn;
#[cfg(feature = "native")]
pub async fn ws_client<P>(
url: &str,
builder: &RpcClientBuilder<P>,
) -> Result<Arc<Client>, RpcError> {
use jsonrpsee::ws_client::WsClientBuilder;
let RpcClientBuilder {
max_request_size,
max_response_size,
ping_config,
headers,
max_redirections,
id_kind,
max_concurrent_requests,
max_log_len,
request_timeout,
connection_timeout,
..
} = builder;
let mut ws_client_builder = WsClientBuilder::new()
.max_request_size(*max_request_size)
.max_response_size(*max_response_size)
.set_headers(headers.clone())
.max_redirections(*max_redirections as usize)
.max_buffer_capacity_per_subscription(tokio::sync::Semaphore::MAX_PERMITS)
.max_concurrent_requests(*max_concurrent_requests as usize)
.set_max_logging_length(*max_log_len)
.set_tcp_no_delay(true)
.request_timeout(*request_timeout)
.connection_timeout(*connection_timeout)
.id_format(*id_kind);
if let Some(ping) = ping_config {
ws_client_builder = ws_client_builder.enable_ws_ping(*ping);
}
let client = ws_client_builder.build(url).await?;
Ok(Arc::new(client))
}
#[cfg(feature = "web")]
pub async fn ws_client<P>(
url: &str,
builder: &RpcClientBuilder<P>,
) -> Result<Arc<Client>, RpcError> {
use jsonrpsee::wasm_client::WasmClientBuilder;
let RpcClientBuilder {
id_kind,
max_concurrent_requests,
max_log_len,
request_timeout,
..
} = builder;
let ws_client_builder = WasmClientBuilder::new()
.max_buffer_capacity_per_subscription(tokio::sync::Semaphore::MAX_PERMITS)
.max_concurrent_requests(*max_concurrent_requests as usize)
.set_max_logging_length(*max_log_len)
.request_timeout(*request_timeout)
.id_format(*id_kind);
let client = ws_client_builder.build(url).await?;
Ok(Arc::new(client))
}
@@ -0,0 +1,270 @@
// Copyright 2019-2024 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
use super::*;
use futures::{future::Either, FutureExt};
use jsonrpsee::core::BoxError;
use jsonrpsee::server::{
http, stop_channel, ws, ConnectionGuard, ConnectionState, HttpRequest, HttpResponse, RpcModule,
RpcServiceBuilder, ServerConfig, SubscriptionMessage,
};
#[tokio::test]
async fn call_works() {
let (_handle, addr) = run_server().await.unwrap();
let client = RpcClient::builder().build(addr).await.unwrap();
assert!(client.request("say_hello".to_string(), None).await.is_ok(),)
}
#[tokio::test]
async fn sub_works() {
let (_handle, addr) = run_server().await.unwrap();
let client = RpcClient::builder()
.retry_policy(ExponentialBackoff::from_millis(50))
.build(addr)
.await
.unwrap();
let mut sub = client
.subscribe(
"subscribe_lo".to_string(),
None,
"unsubscribe_lo".to_string(),
)
.await
.unwrap();
assert!(sub.next().await.is_some());
}
#[tokio::test]
async fn sub_with_reconnect() {
let (handle, addr) = run_server().await.unwrap();
let client = RpcClient::builder().build(addr.clone()).await.unwrap();
let mut sub = client
.subscribe(
"subscribe_lo".to_string(),
None,
"unsubscribe_lo".to_string(),
)
.await
.unwrap();
let _ = handle.send(());
// Hack to wait for the server to restart.
tokio::time::sleep(Duration::from_millis(100)).await;
assert!(matches!(sub.next().await, Some(Ok(_))));
assert!(matches!(
sub.next().await,
Some(Err(DisconnectedWillReconnect(_)))
));
// Restart the server.
let (_handle, _) = run_server_with_settings(Some(&addr), false).await.unwrap();
// Hack to wait for the server to restart.
tokio::time::sleep(Duration::from_millis(100)).await;
// Subscription should work after reconnect.
let mut sub = client
.subscribe(
"subscribe_lo".to_string(),
None,
"unsubscribe_lo".to_string(),
)
.await
.unwrap();
assert!(matches!(sub.next().await, Some(Ok(_))));
}
#[tokio::test]
async fn call_with_reconnect() {
let (handle, addr) = run_server_with_settings(None, true).await.unwrap();
let client = Arc::new(RpcClient::builder().build(addr.clone()).await.unwrap());
let req_fut = client.request("say_hello".to_string(), None).boxed();
let timeout_fut = tokio::time::sleep(Duration::from_secs(5));
// If the call isn't replied in 5 secs then it's regarded as it's still pending.
let req_fut = match futures::future::select(Box::pin(timeout_fut), req_fut).await {
Either::Left((_, f)) => f,
Either::Right(_) => panic!("RPC call finished"),
};
// Close the connection with a pending call.
let _ = handle.send(());
// Restart the server
let (_handle, _) = run_server_with_settings(Some(&addr), false).await.unwrap();
// Hack to wait for the server to restart.
tokio::time::sleep(Duration::from_millis(100)).await;
// This call should fail because reconnect.
assert!(req_fut.await.is_err());
// Future call should work after reconnect.
assert!(client.request("say_hello".to_string(), None).await.is_ok());
}
async fn run_server() -> Result<(tokio::sync::broadcast::Sender<()>, String), BoxError> {
run_server_with_settings(None, false).await
}
async fn run_server_with_settings(
url: Option<&str>,
dont_respond_to_method_calls: bool,
) -> Result<(tokio::sync::broadcast::Sender<()>, String), BoxError> {
use jsonrpsee::server::HttpRequest;
let sockaddr = match url {
Some(url) => url.strip_prefix("ws://").unwrap(),
None => "127.0.0.1:0",
};
let mut i = 0;
let listener = loop {
if let Ok(l) = tokio::net::TcpListener::bind(sockaddr).await {
break l;
}
tokio::time::sleep(Duration::from_millis(100)).await;
if i >= 10 {
panic!("Addr already in use");
}
i += 1;
};
let mut module = RpcModule::new(());
if dont_respond_to_method_calls {
module.register_async_method("say_hello", |_, _, _| async {
futures::future::pending::<()>().await;
"timeout"
})?;
} else {
module.register_async_method("say_hello", |_, _, _| async { "lo" })?;
}
module.register_subscription(
"subscribe_lo",
"subscribe_lo",
"unsubscribe_lo",
|_params, pending, _ctx, _| async move {
let sink = pending.accept().await.unwrap();
let i = 0;
loop {
if sink
.send(SubscriptionMessage::from_json(&i).unwrap())
.await
.is_err()
{
break;
}
tokio::time::sleep(std::time::Duration::from_secs(6)).await;
}
},
)?;
let (tx, mut rx) = tokio::sync::broadcast::channel(4);
let tx2 = tx.clone();
let (stop_handle, server_handle) = stop_channel();
let addr = listener.local_addr().expect("Could not find local addr");
tokio::spawn(async move {
loop {
let sock = tokio::select! {
res = listener.accept() => {
match res {
Ok((stream, _remote_addr)) => stream,
Err(e) => {
tracing::error!("Failed to accept connection: {:?}", e);
continue;
}
}
}
_ = rx.recv() => {
break
}
};
let module = module.clone();
let rx2 = tx2.subscribe();
let tx2 = tx2.clone();
let stop_handle2 = stop_handle.clone();
let svc = tower::service_fn(move |req: HttpRequest<hyper::body::Incoming>| {
let module = module.clone();
let tx = tx2.clone();
let stop_handle = stop_handle2.clone();
let conn_permit = ConnectionGuard::new(1).try_acquire().unwrap();
if ws::is_upgrade_request(&req) {
let rpc_service = RpcServiceBuilder::new();
let conn = ConnectionState::new(stop_handle, 1, conn_permit);
async move {
let mut rx = tx.subscribe();
let (rp, conn_fut) =
ws::connect(req, ServerConfig::default(), module, conn, rpc_service)
.await
.unwrap();
tokio::spawn(async move {
tokio::select! {
_ = conn_fut => (),
_ = rx.recv() => {},
}
});
Ok::<_, BoxError>(rp)
}
.boxed()
} else {
async { Ok(http::response::denied()) }.boxed()
}
});
tokio::spawn(serve_with_graceful_shutdown(sock, svc, rx2));
}
drop(server_handle);
});
Ok((tx, format!("ws://{}", addr)))
}
async fn serve_with_graceful_shutdown<S, B, I>(
io: I,
service: S,
mut rx: tokio::sync::broadcast::Receiver<()>,
) where
S: tower::Service<HttpRequest<hyper::body::Incoming>, Response = HttpResponse<B>>
+ Clone
+ Send
+ 'static,
S::Future: Send,
S::Response: Send,
S::Error: Into<BoxError>,
B: http_body::Body<Data = hyper::body::Bytes> + Send + 'static,
B::Error: Into<BoxError>,
I: tokio::io::AsyncRead + tokio::io::AsyncWrite + Send + Unpin + 'static,
{
if let Err(e) =
jsonrpsee::server::serve_with_graceful_shutdown(io, service, rx.recv().map(|_| ())).await
{
tracing::error!("Error while serving: {:?}", e);
}
}
@@ -0,0 +1,14 @@
// Copyright 2019-2024 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
//! Utils.
use crate::backend::rpc::reconnecting_rpc_client::RpcError;
pub fn display_close_reason(err: &RpcError) -> String {
match err {
RpcError::RestartNeeded(e) => e.to_string(),
other => other.to_string(),
}
}
@@ -148,6 +148,12 @@ impl<Hash> Stream for FollowStream<Hash> {
continue;
}
Poll::Ready(Err(e)) => {
// Re-start if a reconnecting backend was enabled.
if e.is_disconnected_will_reconnect() {
this.stream = InnerStreamState::Stopped;
continue;
}
// Finish forever if there's an error, passing it on.
this.stream = InnerStreamState::Finished;
return Poll::Ready(Some(Err(e)));
@@ -182,6 +188,12 @@ impl<Hash> Stream for FollowStream<Hash> {
return Poll::Ready(Some(Ok(FollowStreamMsg::Event(ev))));
}
Poll::Ready(Some(Err(e))) => {
// Re-start if a reconnecting backend was enabled.
if e.is_disconnected_will_reconnect() {
this.stream = InnerStreamState::Stopped;
continue;
}
// Finish forever if there's an error, passing it on.
this.stream = InnerStreamState::Finished;
return Poll::Ready(Some(Err(e)));
@@ -5,7 +5,7 @@
use super::follow_stream_unpin::{BlockRef, FollowStreamMsg, FollowStreamUnpin};
use crate::backend::unstable::rpc_methods::{FollowEvent, Initialized, RuntimeEvent};
use crate::config::BlockHash;
use crate::error::Error;
use crate::error::{Error, RpcError};
use futures::stream::{Stream, StreamExt};
use std::collections::{HashMap, HashSet, VecDeque};
use std::ops::DerefMut;
@@ -267,8 +267,9 @@ impl<Hash: BlockHash> Shared<Hash> {
shared.seen_runtime_events.clear();
init_message.finalized_block_hashes =
finalized_ev.finalized_block_hashes.clone();
init_message
.finalized_block_hashes
.clone_from(&finalized_ev.finalized_block_hashes);
if let Some(runtime_ev) = newest_runtime {
init_message.finalized_block_runtime = Some(runtime_ev);
@@ -379,6 +380,103 @@ struct SubscriberDetails<Hash: BlockHash> {
waker: Option<Waker>,
}
/// A stream that subscribes to finalized blocks
/// and indicates whether a block was missed if was restarted.
#[derive(Debug)]
pub struct FollowStreamFinalizedHeads<Hash: BlockHash, F> {
stream: FollowStreamDriverSubscription<Hash>,
sub_id: Option<String>,
last_seen_block: Option<BlockRef<Hash>>,
f: F,
is_done: bool,
}
impl<Hash: BlockHash, F> Unpin for FollowStreamFinalizedHeads<Hash, F> {}
impl<Hash, F> FollowStreamFinalizedHeads<Hash, F>
where
Hash: BlockHash,
F: Fn(FollowEvent<BlockRef<Hash>>) -> Vec<BlockRef<Hash>>,
{
pub fn new(stream: FollowStreamDriverSubscription<Hash>, f: F) -> Self {
Self {
stream,
sub_id: None,
last_seen_block: None,
f,
is_done: false,
}
}
}
impl<Hash, F> Stream for FollowStreamFinalizedHeads<Hash, F>
where
Hash: BlockHash,
F: Fn(FollowEvent<BlockRef<Hash>>) -> Vec<BlockRef<Hash>>,
{
type Item = Result<(String, Vec<BlockRef<Hash>>), Error>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
if self.is_done {
return Poll::Ready(None);
}
loop {
let Some(ev) = futures::ready!(self.stream.poll_next_unpin(cx)) else {
self.is_done = true;
return Poll::Ready(None);
};
let block_refs = match ev {
FollowStreamMsg::Ready(sub_id) => {
self.sub_id = Some(sub_id);
continue;
}
FollowStreamMsg::Event(FollowEvent::Finalized(finalized)) => {
self.last_seen_block = finalized.finalized_block_hashes.last().cloned();
(self.f)(FollowEvent::Finalized(finalized))
}
FollowStreamMsg::Event(FollowEvent::Initialized(mut init)) => {
let prev = self.last_seen_block.take();
self.last_seen_block = init.finalized_block_hashes.last().cloned();
if let Some(p) = prev {
let Some(pos) = init
.finalized_block_hashes
.iter()
.position(|b| b.hash() == p.hash())
else {
return Poll::Ready(Some(Err(RpcError::DisconnectedWillReconnect(
"Missed at least one block when the connection was lost".to_owned(),
)
.into())));
};
// If we got older blocks than `prev`, we need to remove them
// because they should already have been sent at this point.
init.finalized_block_hashes.drain(0..=pos);
}
(self.f)(FollowEvent::Initialized(init))
}
FollowStreamMsg::Event(ev) => (self.f)(ev),
};
if block_refs.is_empty() {
continue;
}
let sub_id = self
.sub_id
.clone()
.expect("Ready is always emitted before any other event");
return Poll::Ready(Some(Ok((sub_id, block_refs))));
}
}
}
#[cfg(test)]
mod test_utils {
use super::super::follow_stream_unpin::test_utils::test_unpin_stream_getter;
@@ -401,6 +499,9 @@ mod test_utils {
#[cfg(test)]
mod test {
use futures::TryStreamExt;
use sp_core::H256;
use super::super::follow_stream::test_utils::{
ev_best_block, ev_finalized, ev_initialized, ev_new_block,
};
@@ -544,4 +645,101 @@ mod test {
];
assert_eq!(evs, expected);
}
#[tokio::test]
async fn subscribe_finalized_blocks_restart_works() {
let mut driver = test_follow_stream_driver_getter(
|| {
[
Ok(ev_initialized(0)),
Ok(ev_new_block(0, 1)),
Ok(ev_best_block(1)),
Ok(ev_finalized([1], [])),
Ok(FollowEvent::Stop),
Ok(ev_initialized(1)),
Ok(ev_finalized([2], [])),
Err(Error::Other("ended".to_owned())),
]
},
10,
);
let handle = driver.handle();
tokio::spawn(async move { while driver.next().await.is_some() {} });
let f = |ev| match ev {
FollowEvent::Finalized(ev) => ev.finalized_block_hashes,
FollowEvent::Initialized(ev) => ev.finalized_block_hashes,
_ => vec![],
};
let stream = FollowStreamFinalizedHeads::new(handle.subscribe(), f);
let evs: Vec<_> = stream.try_collect().await.unwrap();
let expected = vec![
(
"sub_id_0".to_string(),
vec![BlockRef::new(H256::from_low_u64_le(0))],
),
(
"sub_id_0".to_string(),
vec![BlockRef::new(H256::from_low_u64_le(1))],
),
(
"sub_id_5".to_string(),
vec![BlockRef::new(H256::from_low_u64_le(2))],
),
];
assert_eq!(evs, expected);
}
#[tokio::test]
async fn subscribe_finalized_blocks_restart_with_missed_blocks() {
let mut driver = test_follow_stream_driver_getter(
|| {
[
Ok(ev_initialized(0)),
Ok(FollowEvent::Stop),
// Emulate that we missed some blocks.
Ok(ev_initialized(13)),
Ok(ev_finalized([14], [])),
Err(Error::Other("ended".to_owned())),
]
},
10,
);
let handle = driver.handle();
tokio::spawn(async move { while driver.next().await.is_some() {} });
let f = |ev| match ev {
FollowEvent::Finalized(ev) => ev.finalized_block_hashes,
FollowEvent::Initialized(ev) => ev.finalized_block_hashes,
_ => vec![],
};
let evs: Vec<_> = FollowStreamFinalizedHeads::new(handle.subscribe(), f)
.collect()
.await;
assert_eq!(
evs[0].as_ref().unwrap(),
&(
"sub_id_0".to_string(),
vec![BlockRef::new(H256::from_low_u64_le(0))]
)
);
assert!(
matches!(&evs[1], Err(Error::Rpc(RpcError::DisconnectedWillReconnect(e))) if e.contains("Missed at least one block when the connection was lost"))
);
assert_eq!(
evs[2].as_ref().unwrap(),
&(
"sub_id_2".to_string(),
vec![BlockRef::new(H256::from_low_u64_le(14))]
)
);
}
}
@@ -474,7 +474,7 @@ pub(super) mod test_utils {
pub type UnpinRx<Hash> = std::sync::mpsc::Receiver<(Hash, Arc<str>)>;
/// Get a `FolowStreamUnpin` from an iterator over events.
/// Get a [`FollowStreamUnpin`] from an iterator over events.
pub fn test_unpin_stream_getter<Hash, F, I>(
events: F,
max_life: usize,
+178 -144
View File
@@ -18,21 +18,22 @@ mod storage_items;
pub mod rpc_methods;
use self::follow_stream_driver::FollowStreamFinalizedHeads;
use self::rpc_methods::{
FollowEvent, MethodResponse, RuntimeEvent, StorageQuery, StorageQueryType, StorageResultType,
};
use crate::backend::{
rpc::RpcClient, Backend, BlockRef, BlockRefT, RuntimeVersion, StorageResponse, StreamOf,
StreamOfResults, TransactionStatus,
rpc::RpcClient, utils::retry, Backend, BlockRef, BlockRefT, RuntimeVersion, StorageResponse,
StreamOf, StreamOfResults, TransactionStatus,
};
use crate::config::BlockHash;
use crate::error::{Error, RpcError};
use crate::Config;
use async_trait::async_trait;
use follow_stream_driver::{FollowStreamDriver, FollowStreamDriverHandle};
use futures::future::Either;
use futures::{Stream, StreamExt};
use std::collections::HashMap;
use std::sync::Arc;
use std::task::Poll;
use storage_items::StorageItems;
@@ -136,43 +137,50 @@ impl<T: Config> UnstableBackend<T> {
}
/// Stream block headers based on the provided filter fn
async fn stream_headers<F, I>(
async fn stream_headers<F>(
&self,
f: F,
) -> Result<StreamOfResults<(T::Header, BlockRef<T::Hash>)>, Error>
where
F: Fn(FollowEvent<follow_stream_unpin::BlockRef<T::Hash>>) -> I + Copy + Send + 'static,
I: IntoIterator<Item = follow_stream_unpin::BlockRef<T::Hash>> + Send + 'static,
<I as IntoIterator>::IntoIter: Send,
F: Fn(
FollowEvent<follow_stream_unpin::BlockRef<T::Hash>>,
) -> Vec<follow_stream_unpin::BlockRef<T::Hash>>
+ Send
+ Sync
+ 'static,
{
let sub_id = get_subscription_id(&self.follow_handle).await?;
let sub_id = Arc::new(sub_id);
let methods = self.methods.clone();
let headers = self.follow_handle.subscribe().events().flat_map(move |ev| {
let sub_id = sub_id.clone();
let methods = methods.clone();
let block_refs = f(ev).into_iter();
futures::stream::iter(block_refs).filter_map(move |block_ref| {
let sub_id = sub_id.clone();
let headers =
FollowStreamFinalizedHeads::new(self.follow_handle.subscribe(), f).flat_map(move |r| {
let methods = methods.clone();
async move {
let res = methods
.chainhead_v1_header(&sub_id, block_ref.hash())
.await
.transpose()?;
let (sub_id, block_refs) = match r {
Ok(ev) => ev,
Err(e) => return Either::Left(futures::stream::once(async { Err(e) })),
};
let header = match res {
Ok(header) => header,
Err(e) => return Some(Err(e)),
};
Either::Right(
futures::stream::iter(block_refs).filter_map(move |block_ref| {
let methods = methods.clone();
let sub_id = sub_id.clone();
Some(Ok((header, block_ref.into())))
}
})
});
async move {
let res = methods
.chainhead_v1_header(&sub_id, block_ref.hash())
.await
.transpose()?;
let header = match res {
Ok(header) => header,
Err(e) => return Some(Err(e)),
};
Some(Ok((header, block_ref.into())))
}
}),
)
});
Ok(StreamOf(Box::pin(headers)))
}
@@ -194,31 +202,34 @@ impl<T: Config + Send + Sync + 'static> Backend<T> for UnstableBackend<T> {
keys: Vec<Vec<u8>>,
at: T::Hash,
) -> Result<StreamOfResults<StorageResponse>, Error> {
let queries = keys.iter().map(|key| StorageQuery {
key: &**key,
query_type: StorageQueryType::Value,
});
retry(|| async {
let queries = keys.iter().map(|key| StorageQuery {
key: &**key,
query_type: StorageQueryType::Value,
});
let storage_items =
StorageItems::from_methods(queries, at, &self.follow_handle, self.methods.clone())
.await?;
let storage_items =
StorageItems::from_methods(queries, at, &self.follow_handle, self.methods.clone())
.await?;
let storage_result_stream = storage_items.filter_map(|val| async move {
let val = match val {
Ok(val) => val,
Err(e) => return Some(Err(e)),
};
let stream = storage_items.filter_map(|val| async move {
let val = match val {
Ok(val) => val,
Err(e) => return Some(Err(e)),
};
let StorageResultType::Value(result) = val.result else {
return None;
};
Some(Ok(StorageResponse {
key: val.key.0,
value: result.0,
}))
});
let StorageResultType::Value(result) = val.result else {
return None;
};
Some(Ok(StorageResponse {
key: val.key.0,
value: result.0,
}))
});
Ok(StreamOf(Box::pin(storage_result_stream)))
Ok(StreamOf(Box::pin(stream)))
})
.await
}
async fn storage_fetch_descendant_keys(
@@ -226,22 +237,25 @@ impl<T: Config + Send + Sync + 'static> Backend<T> for UnstableBackend<T> {
key: Vec<u8>,
at: T::Hash,
) -> Result<StreamOfResults<Vec<u8>>, Error> {
// Ask for hashes, and then just ignore them and return the keys that come back.
let query = StorageQuery {
key: &*key,
query_type: StorageQueryType::DescendantsHashes,
};
retry(|| async {
// Ask for hashes, and then just ignore them and return the keys that come back.
let query = StorageQuery {
key: &*key,
query_type: StorageQueryType::DescendantsHashes,
};
let storage_items = StorageItems::from_methods(
std::iter::once(query),
at,
&self.follow_handle,
self.methods.clone(),
)
.await?;
let storage_items = StorageItems::from_methods(
std::iter::once(query),
at,
&self.follow_handle,
self.methods.clone(),
)
.await?;
let storage_result_stream = storage_items.map(|val| val.map(|v| v.key.0));
Ok(StreamOf(Box::pin(storage_result_stream)))
let storage_result_stream = storage_items.map(|val| val.map(|v| v.key.0));
Ok(StreamOf(Box::pin(storage_result_stream)))
})
.await
}
async fn storage_fetch_descendant_values(
@@ -249,72 +263,81 @@ impl<T: Config + Send + Sync + 'static> Backend<T> for UnstableBackend<T> {
key: Vec<u8>,
at: T::Hash,
) -> Result<StreamOfResults<StorageResponse>, Error> {
let query = StorageQuery {
key: &*key,
query_type: StorageQueryType::DescendantsValues,
};
let storage_items = StorageItems::from_methods(
std::iter::once(query),
at,
&self.follow_handle,
self.methods.clone(),
)
.await?;
let storage_result_stream = storage_items.filter_map(|val| async move {
let val = match val {
Ok(val) => val,
Err(e) => return Some(Err(e)),
retry(|| async {
let query = StorageQuery {
key: &*key,
query_type: StorageQueryType::DescendantsValues,
};
let StorageResultType::Value(result) = val.result else {
return None;
};
Some(Ok(StorageResponse {
key: val.key.0,
value: result.0,
}))
});
let storage_items = StorageItems::from_methods(
std::iter::once(query),
at,
&self.follow_handle,
self.methods.clone(),
)
.await?;
Ok(StreamOf(Box::pin(storage_result_stream)))
let storage_result_stream = storage_items.filter_map(|val| async move {
let val = match val {
Ok(val) => val,
Err(e) => return Some(Err(e)),
};
let StorageResultType::Value(result) = val.result else {
return None;
};
Some(Ok(StorageResponse {
key: val.key.0,
value: result.0,
}))
});
Ok(StreamOf(Box::pin(storage_result_stream)))
})
.await
}
async fn genesis_hash(&self) -> Result<T::Hash, Error> {
self.methods.chainspec_v1_genesis_hash().await
retry(|| self.methods.chainspec_v1_genesis_hash()).await
}
async fn block_header(&self, at: T::Hash) -> Result<Option<T::Header>, Error> {
let sub_id = get_subscription_id(&self.follow_handle).await?;
self.methods.chainhead_v1_header(&sub_id, at).await
retry(|| async {
let sub_id = get_subscription_id(&self.follow_handle).await?;
self.methods.chainhead_v1_header(&sub_id, at).await
})
.await
}
async fn block_body(&self, at: T::Hash) -> Result<Option<Vec<Vec<u8>>>, Error> {
let sub_id = get_subscription_id(&self.follow_handle).await?;
retry(|| async {
let sub_id = get_subscription_id(&self.follow_handle).await?;
// Subscribe to the body response and get our operationId back.
let follow_events = self.follow_handle.subscribe().events();
let status = self.methods.chainhead_v1_body(&sub_id, at).await?;
let operation_id = match status {
MethodResponse::LimitReached => {
return Err(RpcError::request_rejected("limit reached").into())
}
MethodResponse::Started(s) => s.operation_id,
};
// Wait for the response to come back with the correct operationId.
let mut exts_stream = follow_events.filter_map(|ev| {
let FollowEvent::OperationBodyDone(body) = ev else {
return std::future::ready(None);
// Subscribe to the body response and get our operationId back.
let follow_events = self.follow_handle.subscribe().events();
let status = self.methods.chainhead_v1_body(&sub_id, at).await?;
let operation_id = match status {
MethodResponse::LimitReached => {
return Err(RpcError::request_rejected("limit reached").into())
}
MethodResponse::Started(s) => s.operation_id,
};
if body.operation_id != operation_id {
return std::future::ready(None);
}
let exts: Vec<_> = body.value.into_iter().map(|ext| ext.0).collect();
std::future::ready(Some(exts))
});
Ok(exts_stream.next().await)
// Wait for the response to come back with the correct operationId.
let mut exts_stream = follow_events.filter_map(|ev| {
let FollowEvent::OperationBodyDone(body) = ev else {
return std::future::ready(None);
};
if body.operation_id != operation_id {
return std::future::ready(None);
}
let exts: Vec<_> = body.value.into_iter().map(|ext| ext.0).collect();
std::future::ready(Some(exts))
});
Ok(exts_stream.next().await)
})
.await
}
async fn latest_finalized_block_ref(&self) -> Result<BlockRef<T::Hash>, Error> {
@@ -423,12 +446,16 @@ impl<T: Config + Send + Sync + 'static> Backend<T> for UnstableBackend<T> {
std::future::ready(Some(Ok(runtime_version)))
});
Ok(StreamOf(Box::pin(runtime_stream)))
Ok(StreamOf::new(Box::pin(runtime_stream)))
}
async fn stream_all_block_headers(
&self,
) -> Result<StreamOfResults<(T::Header, BlockRef<T::Hash>)>, Error> {
// TODO: https://github.com/paritytech/subxt/issues/1568
//
// It's possible that blocks may be silently missed if
// a reconnection occurs because it's restarted by the unstable backend.
self.stream_headers(|ev| match ev {
FollowEvent::Initialized(init) => init.finalized_block_hashes,
FollowEvent::NewBlock(ev) => {
@@ -442,6 +469,10 @@ impl<T: Config + Send + Sync + 'static> Backend<T> for UnstableBackend<T> {
async fn stream_best_block_headers(
&self,
) -> Result<StreamOfResults<(T::Header, BlockRef<T::Hash>)>, Error> {
// TODO: https://github.com/paritytech/subxt/issues/1568
//
// It's possible that blocks may be silently missed if
// a reconnection occurs because it's restarted by the unstable backend.
self.stream_headers(|ev| match ev {
FollowEvent::Initialized(init) => init.finalized_block_hashes,
FollowEvent::BestBlockChanged(ev) => vec![ev.best_block_hash],
@@ -638,37 +669,40 @@ impl<T: Config + Send + Sync + 'static> Backend<T> for UnstableBackend<T> {
call_parameters: Option<&[u8]>,
at: T::Hash,
) -> Result<Vec<u8>, Error> {
let sub_id = get_subscription_id(&self.follow_handle).await?;
retry(|| async {
let sub_id = get_subscription_id(&self.follow_handle).await?;
// Subscribe to the body response and get our operationId back.
let follow_events = self.follow_handle.subscribe().events();
let call_parameters = call_parameters.unwrap_or(&[]);
let status = self
.methods
.chainhead_v1_call(&sub_id, at, method, call_parameters)
.await?;
let operation_id = match status {
MethodResponse::LimitReached => {
return Err(RpcError::request_rejected("limit reached").into())
}
MethodResponse::Started(s) => s.operation_id,
};
// Wait for the response to come back with the correct operationId.
let mut call_data_stream = follow_events.filter_map(|ev| {
let FollowEvent::OperationCallDone(body) = ev else {
return std::future::ready(None);
// Subscribe to the body response and get our operationId back.
let follow_events = self.follow_handle.subscribe().events();
let call_parameters = call_parameters.unwrap_or(&[]);
let status = self
.methods
.chainhead_v1_call(&sub_id, at, method, call_parameters)
.await?;
let operation_id = match status {
MethodResponse::LimitReached => {
return Err(RpcError::request_rejected("limit reached").into())
}
MethodResponse::Started(s) => s.operation_id,
};
if body.operation_id != operation_id {
return std::future::ready(None);
}
std::future::ready(Some(body.output.0))
});
call_data_stream
.next()
.await
.ok_or_else(|| RpcError::SubscriptionDropped.into())
// Wait for the response to come back with the correct operationId.
let mut call_data_stream = follow_events.filter_map(|ev| {
let FollowEvent::OperationCallDone(body) = ev else {
return std::future::ready(None);
};
if body.operation_id != operation_id {
return std::future::ready(None);
}
std::future::ready(Some(body.output.0))
});
call_data_stream
.next()
.await
.ok_or_else(|| RpcError::SubscriptionDropped.into())
})
.await
}
}
+3 -9
View File
@@ -77,9 +77,7 @@ impl<T: Config> UnstableRpcMethods<T> {
"chainHead_v1_continue",
rpc_params![follow_subscription, operation_id],
)
.await?;
Ok(())
.await
}
/// Stops an operation started with `chainHead_v1_body`, `chainHead_v1_call`, or
@@ -97,9 +95,7 @@ impl<T: Config> UnstableRpcMethods<T> {
"chainHead_v1_stopOperation",
rpc_params![follow_subscription, operation_id],
)
.await?;
Ok(())
.await
}
/// Call the `chainHead_v1_body` method and return an operation ID to obtain the block's body.
@@ -222,9 +218,7 @@ impl<T: Config> UnstableRpcMethods<T> {
) -> Result<(), Error> {
self.client
.request("chainHead_v1_unpin", rpc_params![subscription_id, hash])
.await?;
Ok(())
.await
}
/// Return the genesis hash.
@@ -111,6 +111,11 @@ impl<T: Config> Stream for StorageItems<T> {
return Poll::Pending;
}
Poll::Ready(Err(e)) => {
if e.is_disconnected_will_reconnect() {
self.continue_fut = Some((self.continue_call)());
continue;
}
self.done = true;
return Poll::Ready(Some(Err(e)));
}
+271
View File
@@ -0,0 +1,271 @@
//! RPC utils.
use super::{StreamOf, StreamOfResults};
use crate::error::Error;
use futures::future::BoxFuture;
use futures::{FutureExt, Stream, StreamExt};
use std::{future::Future, pin::Pin, task::Poll};
/// Resubscribe callback.
type ResubscribeGetter<T> = Box<dyn FnMut() -> ResubscribeFuture<T> + Send>;
/// Future that resolves to a subscription stream.
type ResubscribeFuture<T> = Pin<Box<dyn Future<Output = Result<StreamOfResults<T>, Error>> + Send>>;
pub(crate) enum PendingOrStream<T> {
Pending(BoxFuture<'static, Result<StreamOfResults<T>, Error>>),
Stream(StreamOfResults<T>),
}
impl<T> std::fmt::Debug for PendingOrStream<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PendingOrStream::Pending(_) => write!(f, "Pending"),
PendingOrStream::Stream(_) => write!(f, "Stream"),
}
}
}
/// Retry subscription.
struct RetrySubscription<T> {
resubscribe: ResubscribeGetter<T>,
state: Option<PendingOrStream<T>>,
}
impl<T> std::marker::Unpin for RetrySubscription<T> {}
impl<T> Stream for RetrySubscription<T> {
type Item = Result<T, Error>;
fn poll_next(
mut self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> Poll<Option<Self::Item>> {
loop {
let Some(mut this) = self.state.take() else {
return Poll::Ready(None);
};
match this {
PendingOrStream::Stream(ref mut s) => match s.poll_next_unpin(cx) {
Poll::Ready(Some(Err(err))) => {
if err.is_disconnected_will_reconnect() {
self.state = Some(PendingOrStream::Pending((self.resubscribe)()));
}
return Poll::Ready(Some(Err(err)));
}
Poll::Ready(None) => return Poll::Ready(None),
Poll::Ready(Some(Ok(val))) => {
self.state = Some(this);
return Poll::Ready(Some(Ok(val)));
}
Poll::Pending => {
self.state = Some(this);
return Poll::Pending;
}
},
PendingOrStream::Pending(mut fut) => match fut.poll_unpin(cx) {
Poll::Ready(Ok(stream)) => {
self.state = Some(PendingOrStream::Stream(stream));
continue;
}
Poll::Ready(Err(err)) => {
if err.is_disconnected_will_reconnect() {
self.state = Some(PendingOrStream::Pending((self.resubscribe)()));
}
return Poll::Ready(Some(Err(err)));
}
Poll::Pending => {
self.state = Some(PendingOrStream::Pending(fut));
return Poll::Pending;
}
},
};
}
}
}
/// Retry a future until it doesn't return a disconnected error.
///
/// # Example
///
/// ```no_run
/// use subxt::backend::utils::retry;
///
/// async fn some_future() -> Result<(), subxt::error::Error> {
/// Ok(())
/// }
///
/// #[tokio::main]
/// async fn main() {
/// let result = retry(|| some_future()).await;
/// }
/// ```
pub async fn retry<T, F, R>(mut retry_future: F) -> Result<R, Error>
where
F: FnMut() -> T,
T: Future<Output = Result<R, Error>>,
{
const REJECTED_MAX_RETRIES: usize = 10;
let mut rejected_retries = 0;
loop {
match retry_future().await {
Ok(v) => return Ok(v),
Err(e) => {
if e.is_disconnected_will_reconnect() {
continue;
}
// TODO: https://github.com/paritytech/subxt/issues/1567
// This is a hack because if a reconnection occurs
// the order of pending calls is not guaranteed.
//
// Such that it's possible the a pending future completes
// before `chainHead_follow` is established with fresh
// subscription id.
//
if e.is_rejected() && rejected_retries < REJECTED_MAX_RETRIES {
rejected_retries += 1;
continue;
}
return Err(e);
}
}
}
}
/// Create a retry stream that will resubscribe on disconnect.
///
/// It's important to note that this function is intended to work only for stateless subscriptions.
/// If the subscription takes input or modifies state, this function should not be used.
///
/// # Example
///
/// ```no_run
/// use subxt::backend::{utils::retry_stream, StreamOf};
/// use futures::future::FutureExt;
///
/// #[tokio::main]
/// async fn main() {
/// retry_stream(|| {
/// // This needs to return a stream of results but if you are using
/// // the subxt backend already it will return StreamOf so you can just
/// // return it directly in the async block below.
/// async move { Ok(StreamOf::new(Box::pin(futures::stream::iter([Ok(2)])))) }.boxed()
/// }).await;
/// }
/// ```
pub async fn retry_stream<F, R>(sub_stream: F) -> Result<StreamOfResults<R>, Error>
where
F: FnMut() -> ResubscribeFuture<R> + Send + 'static + Clone,
R: Send + 'static,
{
let stream = retry(sub_stream.clone()).await?;
let resubscribe = Box::new(move || {
let sub_stream = sub_stream.clone();
async move { retry(sub_stream).await }.boxed()
});
// The extra Box is to encapsulate the retry subscription type
Ok(StreamOf::new(Box::pin(RetrySubscription {
state: Some(PendingOrStream::Stream(stream)),
resubscribe,
})))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::backend::StreamOf;
fn disconnect_err() -> Error {
Error::Rpc(crate::error::RpcError::DisconnectedWillReconnect(
String::new(),
))
}
fn custom_err() -> Error {
Error::Other(String::new())
}
#[tokio::test]
async fn retry_stream_works() {
let retry_stream = retry_stream(|| {
async {
Ok(StreamOf::new(Box::pin(futures::stream::iter([
Ok(1),
Ok(2),
Ok(3),
Err(disconnect_err()),
]))))
}
.boxed()
})
.await
.unwrap();
let result = retry_stream
.take(5)
.collect::<Vec<Result<usize, Error>>>()
.await;
assert!(matches!(result[0], Ok(r) if r == 1));
assert!(matches!(result[1], Ok(r) if r == 2));
assert!(matches!(result[2], Ok(r) if r == 3));
assert!(matches!(result[3], Err(ref e) if e.is_disconnected_will_reconnect()));
assert!(matches!(result[4], Ok(r) if r == 1));
}
#[tokio::test]
async fn retry_sub_works() {
let stream = futures::stream::iter([Ok(1), Err(disconnect_err())]);
let resubscribe = Box::new(move || {
async move { Ok(StreamOf::new(Box::pin(futures::stream::iter([Ok(2)])))) }.boxed()
});
let retry_stream = RetrySubscription {
state: Some(PendingOrStream::Stream(StreamOf::new(Box::pin(stream)))),
resubscribe,
};
let result: Vec<_> = retry_stream.collect().await;
assert!(matches!(result[0], Ok(r) if r == 1));
assert!(matches!(result[1], Err(ref e) if e.is_disconnected_will_reconnect()));
assert!(matches!(result[2], Ok(r) if r == 2));
}
#[tokio::test]
async fn retry_sub_err_terminates_stream() {
let stream = futures::stream::iter([Ok(1)]);
let resubscribe = Box::new(move || async move { Err(custom_err()) }.boxed());
let retry_stream = RetrySubscription {
state: Some(PendingOrStream::Stream(StreamOf::new(Box::pin(stream)))),
resubscribe,
};
assert_eq!(retry_stream.count().await, 1);
}
#[tokio::test]
async fn retry_sub_resubscribe_err() {
let stream = futures::stream::iter([Ok(1), Err(disconnect_err())]);
let resubscribe = Box::new(move || async move { Err(custom_err()) }.boxed());
let retry_stream = RetrySubscription {
state: Some(PendingOrStream::Stream(StreamOf::new(Box::pin(stream)))),
resubscribe,
};
let result: Vec<_> = retry_stream.collect().await;
assert!(matches!(result[0], Ok(r) if r == 1));
assert!(matches!(result[1], Err(ref e) if e.is_disconnected_will_reconnect()));
assert!(matches!(result[2], Err(ref e) if matches!(e, Error::Other(_))));
}
}
+6 -6
View File
@@ -95,8 +95,8 @@ where
{
let client = self.client.clone();
header_sub_fut_to_block_sub(self.clone(), async move {
let sub = client.backend().stream_all_block_headers().await?;
BlockStreamRes::Ok(sub)
let stream = client.backend().stream_all_block_headers().await?;
BlockStreamRes::Ok(stream)
})
}
@@ -112,8 +112,8 @@ where
{
let client = self.client.clone();
header_sub_fut_to_block_sub(self.clone(), async move {
let sub = client.backend().stream_best_block_headers().await?;
BlockStreamRes::Ok(sub)
let stream = client.backend().stream_best_block_headers().await?;
BlockStreamRes::Ok(stream)
})
}
@@ -126,8 +126,8 @@ where
{
let client = self.client.clone();
header_sub_fut_to_block_sub(self.clone(), async move {
let sub = client.backend().stream_finalized_block_headers().await?;
BlockStreamRes::Ok(sub)
let stream = client.backend().stream_finalized_block_headers().await?;
BlockStreamRes::Ok(stream)
})
}
}
+5
View File
@@ -159,6 +159,11 @@ where
}
}
/// See [`subxt_core::blocks::ExtrinsicDetails::hash()`].
pub fn hash(&self) -> T::Hash {
self.inner.hash()
}
/// See [`subxt_core::blocks::ExtrinsicDetails::is_signed()`].
pub fn is_signed(&self) -> bool {
self.inner.is_signed()
+1 -1
View File
@@ -46,7 +46,7 @@
//! Using the [`#[subxt]`](crate::subxt) macro carries some downsides:
//!
//! - Using it to generate an interface will have a small impact on compile times (though much less of
//! one if you only need a few pallets).
//! one if you only need a few pallets).
//! - IDE support for autocompletion and documentation when using the macro interface can be poor.
//! - It's impossible to manually look at the generated code to understand and debug things.
//!
+17
View File
@@ -196,6 +196,14 @@
//! This example doesn't wait for the transaction to be included in a block; it just submits it and
//! hopes for the best!
//!
//! ### Boxing transaction payloads
//!
//! Transaction payloads can be boxed so that they all share a common type and can be stored together.
//!
//! ```rust,ignore
#![doc = include_str!("../../../examples/tx_boxed.rs")]
//! ```
//!
//! ### Custom handling of transaction status updates
//!
//! If you'd like more control or visibility over exactly which status updates are being emitted for
@@ -205,6 +213,15 @@
#![doc = include_str!("../../../examples/tx_status_stream.rs")]
//! ```
//!
//! ### Signing transactions externally
//!
//! Subxt also allows you to get hold of the signer payload and hand that off to something else to be
//! signed. The signature can then be provided back to Subxt to build the final transaction to submit:
//!
//! ```rust,ignore
#![doc = include_str!("../../../examples/tx_partial.rs")]
//! ```
//!
//! Take a look at the API docs for [`crate::tx::TxProgress`], [`crate::tx::TxStatus`] and
//! [`crate::tx::TxInBlock`] for more options.
//!
+1 -2
View File
@@ -432,9 +432,8 @@ impl<T: Config> ClientRuntimeUpdater<T> {
/// Instead that's up to the user of this API to decide when to update and
/// to perform the actual updating.
pub async fn runtime_updates(&self) -> Result<RuntimeUpdaterStream<T>, Error> {
let stream = self.0.backend().stream_runtime_version().await?;
Ok(RuntimeUpdaterStream {
stream,
stream: self.0.backend().stream_runtime_version().await?,
client: self.0.clone(),
})
}
+5
View File
@@ -125,6 +125,11 @@ impl Error {
pub fn is_disconnected_will_reconnect(&self) -> bool {
matches!(self, Error::Rpc(RpcError::DisconnectedWillReconnect(_)))
}
/// Checks whether the error was caused by a RPC request being rejected.
pub fn is_rejected(&self) -> bool {
matches!(self, Error::Rpc(RpcError::RequestRejected(_)))
}
}
/// An RPC error. Since we are generic over the RPC client that is used,
+1 -1
View File
@@ -56,7 +56,7 @@ macro_rules! cfg_jsonrpsee_web {
macro_rules! cfg_reconnecting_rpc_client {
($($item:item)*) => {
$(
#[cfg(feature = "unstable-reconnecting-rpc-client")]
#[cfg(all(feature = "unstable-reconnecting-rpc-client", any(feature = "native", feature = "web")))]
#[cfg_attr(docsrs, doc(cfg(feature = "unstable-reconnecting-rpc-client")))]
$item
)*
+3 -4
View File
@@ -599,11 +599,10 @@ pub enum TransactionInvalid {
///
/// # Possible causes
///
/// For `FRAME`-based runtimes this would be caused by `current block number
/// For `FRAME`-based runtimes this would be caused by `current block number`
/// - Era::birth block number > BlockHashCount`. (e.g. in Polkadot `BlockHashCount` = 2400, so
/// a
/// transaction with birth block number 1337 would be valid up until block number 1337 + 2400,
/// after which point the transaction would be considered to have an ancient birth block.)
/// a transaction with birth block number 1337 would be valid up until block number 1337 + 2400,
/// after which point the transaction would be considered to have an ancient birth block.)
AncientBirthBlock,
/// The transaction would exhaust the resources of current block.
///
+1 -1
View File
@@ -11,11 +11,11 @@ use crate::{
client::OnlineClientT,
error::{DispatchError, Error, RpcError, TransactionError},
events::EventsClient,
utils::strip_compact_prefix,
Config,
};
use derive_where::derive_where;
use futures::{Stream, StreamExt};
use subxt_core::utils::strip_compact_prefix;
/// This struct represents a subscription to the progress of some transaction.
pub struct TxProgress<T: Config, C> {