fix: Convert vendor/pezkuwi-subxt from submodule to regular directory

This commit is contained in:
2025-12-19 16:45:24 +03:00
parent 9a52edf0df
commit fdd023c499
393 changed files with 154124 additions and 1 deletions
@@ -0,0 +1,336 @@
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
use crate::config::{Config, HashFor};
use crate::error::BackendError;
use futures::{FutureExt, Stream, StreamExt, TryStreamExt};
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use pezkuwi_subxt_rpcs::methods::chain_head::{ChainHeadRpcMethods, FollowEvent};
/// A `Stream` whose goal is to remain subscribed to `chainHead_follow`. It will re-subscribe if the subscription
/// is ended for any reason, and it will return the current `subscription_id` as an event, along with the other
/// follow events.
pub struct FollowStream<Hash> {
// Using this and not just keeping a copy of the RPC methods
// around means that we can test this in isolation with dummy streams.
stream_getter: FollowEventStreamGetter<Hash>,
stream: InnerStreamState<Hash>,
}
impl<Hash> std::fmt::Debug for FollowStream<Hash> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FollowStream")
.field("stream_getter", &"..")
.field("stream", &self.stream)
.finish()
}
}
/// A getter function that returns an [`FollowEventStreamFut<Hash>`].
pub type FollowEventStreamGetter<Hash> = Box<dyn FnMut() -> FollowEventStreamFut<Hash> + Send>;
/// The future which will return a stream of follow events and the subscription ID for it.
pub type FollowEventStreamFut<Hash> = Pin<
Box<
dyn Future<Output = Result<(FollowEventStream<Hash>, String), BackendError>>
+ Send
+ 'static,
>,
>;
/// The stream of follow events.
pub type FollowEventStream<Hash> =
Pin<Box<dyn Stream<Item = Result<FollowEvent<Hash>, BackendError>> + Send + 'static>>;
/// Either a ready message with the current subscription ID, or
/// an event from the stream itself.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FollowStreamMsg<Hash> {
/// The stream is ready (and has a subscription ID)
Ready(String),
/// An event from the stream.
Event(FollowEvent<Hash>),
}
impl<Hash> FollowStreamMsg<Hash> {
/// Return an event, or none if the message is a "ready" one.
pub fn into_event(self) -> Option<FollowEvent<Hash>> {
match self {
FollowStreamMsg::Ready(_) => None,
FollowStreamMsg::Event(e) => Some(e),
}
}
}
enum InnerStreamState<Hash> {
/// We've just created the stream; we'll start Initializing it
New,
/// We're fetching the inner subscription. Move to Ready when we have one.
Initializing(FollowEventStreamFut<Hash>),
/// Report back the subscription ID here, and then start ReceivingEvents.
Ready(Option<(FollowEventStream<Hash>, String)>),
/// We are polling for, and receiving events from the stream.
ReceivingEvents(FollowEventStream<Hash>),
/// We received a stop event. We'll send one on and restart the stream.
Stopped,
/// The stream is finished and will not restart (likely due to an error).
Finished,
}
impl<Hash> std::fmt::Debug for InnerStreamState<Hash> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::New => write!(f, "New"),
Self::Initializing(_) => write!(f, "Initializing(..)"),
Self::Ready(_) => write!(f, "Ready(..)"),
Self::ReceivingEvents(_) => write!(f, "ReceivingEvents(..)"),
Self::Stopped => write!(f, "Stopped"),
Self::Finished => write!(f, "Finished"),
}
}
}
impl<Hash> FollowStream<Hash> {
/// Create a new [`FollowStream`] given a function which returns the stream.
pub fn new(stream_getter: FollowEventStreamGetter<Hash>) -> Self {
Self {
stream_getter,
stream: InnerStreamState::New,
}
}
/// Create a new [`FollowStream`] given the RPC methods.
pub fn from_methods<T: Config>(methods: ChainHeadRpcMethods<T>) -> FollowStream<HashFor<T>> {
FollowStream {
stream_getter: Box::new(move || {
let methods = methods.clone();
Box::pin(async move {
// Make the RPC call:
let stream = methods.chainhead_v1_follow(true).await?;
// Extract the subscription ID:
let Some(sub_id) = stream.subscription_id().map(ToOwned::to_owned) else {
return Err(BackendError::Other(
"Subscription ID expected for chainHead_follow response, but not given"
.to_owned(),
));
};
// Map stream errors into the higher level subxt one:
let stream = stream.map_err(|e| e.into());
let stream: FollowEventStream<HashFor<T>> = Box::pin(stream);
// Return both:
Ok((stream, sub_id))
})
}),
stream: InnerStreamState::New,
}
}
}
impl<Hash> std::marker::Unpin for FollowStream<Hash> {}
impl<Hash> Stream for FollowStream<Hash> {
type Item = Result<FollowStreamMsg<Hash>, BackendError>;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let this = self.get_mut();
loop {
match &mut this.stream {
InnerStreamState::New => {
let fut = (this.stream_getter)();
this.stream = InnerStreamState::Initializing(fut);
continue;
}
InnerStreamState::Initializing(fut) => {
match fut.poll_unpin(cx) {
Poll::Pending => {
return Poll::Pending;
}
Poll::Ready(Ok(sub_with_id)) => {
this.stream = InnerStreamState::Ready(Some(sub_with_id));
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)));
}
}
}
InnerStreamState::Ready(stream) => {
// We never set the Option to `None`; we just have an Option so
// that we can take ownership of the contents easily here.
let (sub, sub_id) = stream.take().expect("should always be Some");
this.stream = InnerStreamState::ReceivingEvents(sub);
return Poll::Ready(Some(Ok(FollowStreamMsg::Ready(sub_id))));
}
InnerStreamState::ReceivingEvents(stream) => {
match stream.poll_next_unpin(cx) {
Poll::Pending => {
return Poll::Pending;
}
Poll::Ready(None) => {
// No error happened but the stream ended; restart and
// pass on a Stop message anyway.
this.stream = InnerStreamState::Stopped;
continue;
}
Poll::Ready(Some(Ok(ev))) => {
if let FollowEvent::Stop = ev {
// A stop event means the stream has ended, so start
// over after passing on the stop message.
this.stream = InnerStreamState::Stopped;
continue;
}
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)));
}
}
}
InnerStreamState::Stopped => {
this.stream = InnerStreamState::New;
return Poll::Ready(Some(Ok(FollowStreamMsg::Event(FollowEvent::Stop))));
}
InnerStreamState::Finished => {
return Poll::Ready(None);
}
}
}
}
}
#[cfg(test)]
pub(super) mod test_utils {
use super::*;
use crate::config::substrate::H256;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use pezkuwi_subxt_rpcs::methods::chain_head::{BestBlockChanged, Finalized, Initialized, NewBlock};
/// Given some events, returns a follow stream getter that we can use in
/// place of the usual RPC method.
pub fn test_stream_getter<Hash, F, I>(events: F) -> FollowEventStreamGetter<Hash>
where
Hash: Send + 'static,
F: Fn() -> I + Send + 'static,
I: IntoIterator<Item = Result<FollowEvent<Hash>, BackendError>>,
{
let start_idx = Arc::new(AtomicUsize::new(0));
Box::new(move || {
// Start the events from where we left off last time.
let start_idx = start_idx.clone();
let this_idx = start_idx.load(Ordering::Relaxed);
let events: Vec<_> = events().into_iter().skip(this_idx).collect();
Box::pin(async move {
// Increment start_idx for each event we see, so that if we get
// the stream again, we get only the remaining events for it.
let stream = futures::stream::iter(events).map(move |ev| {
start_idx.fetch_add(1, Ordering::Relaxed);
ev
});
let stream: FollowEventStream<Hash> = Box::pin(stream);
Ok((stream, format!("sub_id_{this_idx}")))
})
})
}
/// An initialized event
pub fn ev_initialized(n: u64) -> FollowEvent<H256> {
FollowEvent::Initialized(Initialized {
finalized_block_hashes: vec![H256::from_low_u64_le(n)],
finalized_block_runtime: None,
})
}
/// A new block event
pub fn ev_new_block(parent_n: u64, n: u64) -> FollowEvent<H256> {
FollowEvent::NewBlock(NewBlock {
parent_block_hash: H256::from_low_u64_le(parent_n),
block_hash: H256::from_low_u64_le(n),
new_runtime: None,
})
}
/// A best block event
pub fn ev_best_block(n: u64) -> FollowEvent<H256> {
FollowEvent::BestBlockChanged(BestBlockChanged {
best_block_hash: H256::from_low_u64_le(n),
})
}
/// A finalized event
pub fn ev_finalized(
finalized_ns: impl IntoIterator<Item = u64>,
pruned_ns: impl IntoIterator<Item = u64>,
) -> FollowEvent<H256> {
FollowEvent::Finalized(Finalized {
finalized_block_hashes: finalized_ns
.into_iter()
.map(H256::from_low_u64_le)
.collect(),
pruned_block_hashes: pruned_ns.into_iter().map(H256::from_low_u64_le).collect(),
})
}
}
#[cfg(test)]
pub mod test {
use super::*;
use test_utils::{ev_initialized, ev_new_block, test_stream_getter};
#[tokio::test]
async fn follow_stream_provides_messages_until_error() {
// The events we'll get back on the stream.
let stream_getter = test_stream_getter(|| {
[
Ok(ev_initialized(1)),
// Stop should lead to a drop and resubscribe:
Ok(FollowEvent::Stop),
Ok(FollowEvent::Stop),
Ok(ev_new_block(1, 2)),
// Nothing should be emitted after an error:
Err(BackendError::Other("ended".to_owned())),
Ok(ev_new_block(2, 3)),
]
});
let s = FollowStream::new(stream_getter);
let out: Vec<_> = s.filter_map(async |e| e.ok()).collect().await;
// The expected response, given the above.
assert_eq!(
out,
vec![
FollowStreamMsg::Ready("sub_id_0".to_owned()),
FollowStreamMsg::Event(ev_initialized(1)),
FollowStreamMsg::Event(FollowEvent::Stop),
FollowStreamMsg::Ready("sub_id_2".to_owned()),
FollowStreamMsg::Event(FollowEvent::Stop),
FollowStreamMsg::Ready("sub_id_3".to_owned()),
FollowStreamMsg::Event(ev_new_block(1, 2)),
]
);
}
}
@@ -0,0 +1,755 @@
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
use super::follow_stream_unpin::{BlockRef, FollowStreamMsg, FollowStreamUnpin};
use crate::config::Hash;
use crate::error::{BackendError, RpcError};
use futures::stream::{Stream, StreamExt};
use std::collections::{HashMap, HashSet, VecDeque};
use std::ops::DerefMut;
use std::pin::Pin;
use std::sync::{Arc, Mutex};
use std::task::{Context, Poll, Waker};
use pezkuwi_subxt_rpcs::methods::chain_head::{FollowEvent, Initialized, RuntimeEvent};
/// A `Stream` which builds on `FollowStreamDriver`, and allows multiple subscribers to obtain events
/// from the single underlying subscription (each being provided an `Initialized` message and all new
/// blocks since then, as if they were each creating a unique `chainHead_follow` subscription). This
/// is the "top" layer of our follow stream subscriptions, and the one that's interacted with elsewhere.
#[derive(Debug)]
pub struct FollowStreamDriver<H: Hash> {
inner: FollowStreamUnpin<H>,
shared: Shared<H>,
}
impl<H: Hash> FollowStreamDriver<H> {
/// Create a new [`FollowStreamDriver`]. This must be polled by some executor
/// in order for any progress to be made. Things can subscribe to events.
pub fn new(follow_unpin: FollowStreamUnpin<H>) -> Self {
Self {
inner: follow_unpin,
shared: Shared::default(),
}
}
/// Return a handle from which we can create new subscriptions to follow events.
pub fn handle(&self) -> FollowStreamDriverHandle<H> {
FollowStreamDriverHandle {
shared: self.shared.clone(),
}
}
}
impl<H: Hash> Stream for FollowStreamDriver<H> {
type Item = Result<(), BackendError>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
match self.inner.poll_next_unpin(cx) {
Poll::Pending => Poll::Pending,
Poll::Ready(None) => {
// Mark ourselves as done so that everything can end.
self.shared.done();
Poll::Ready(None)
}
Poll::Ready(Some(Err(e))) => Poll::Ready(Some(Err(e))),
Poll::Ready(Some(Ok(item))) => {
// Push item to any subscribers.
self.shared.push_item(item);
Poll::Ready(Some(Ok(())))
}
}
}
}
/// A handle that can be used to create subscribers, but that doesn't
/// itself subscribe to events.
#[derive(Debug, Clone)]
pub struct FollowStreamDriverHandle<H: Hash> {
shared: Shared<H>,
}
impl<H: Hash> FollowStreamDriverHandle<H> {
/// Subscribe to follow events.
pub fn subscribe(&self) -> FollowStreamDriverSubscription<H> {
self.shared.subscribe()
}
}
/// A subscription to events from the [`FollowStreamDriver`]. All subscriptions
/// begin first with a `Ready` event containing the current subscription ID, and
/// then with an `Initialized` event containing the latest finalized block and latest
/// runtime information, and then any new/best block events and so on received since
/// the latest finalized block.
#[derive(Debug)]
pub struct FollowStreamDriverSubscription<H: Hash> {
id: usize,
done: bool,
shared: Shared<H>,
local_items: VecDeque<FollowStreamMsg<BlockRef<H>>>,
}
impl<H: Hash> Stream for FollowStreamDriverSubscription<H> {
type Item = FollowStreamMsg<BlockRef<H>>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
if self.done {
return Poll::Ready(None);
}
loop {
if let Some(item) = self.local_items.pop_front() {
return Poll::Ready(Some(item));
}
let items = self.shared.take_items_and_save_waker(self.id, cx.waker());
// If no items left, mark locally as done (to avoid further locking)
// and return None to signal done-ness.
let Some(items) = items else {
self.done = true;
return Poll::Ready(None);
};
// No items? We've saved the waker so we'll be told when more come.
// Else, save the items locally and loop around to pop from them.
if items.is_empty() {
return Poll::Pending;
} else {
self.local_items = items;
}
}
}
}
impl<H: Hash> FollowStreamDriverSubscription<H> {
/// Return the current subscription ID. If the subscription has stopped, then this will
/// wait until a new subscription has started with a new ID.
pub async fn subscription_id(self) -> Option<String> {
let ready_event = self
.skip_while(|ev| std::future::ready(!matches!(ev, FollowStreamMsg::Ready(_))))
.next()
.await?;
match ready_event {
FollowStreamMsg::Ready(sub_id) => Some(sub_id),
_ => None,
}
}
/// Subscribe to the follow events, ignoring any other messages.
pub fn events(self) -> impl Stream<Item = FollowEvent<BlockRef<H>>> + Send + Sync {
self.filter_map(|ev| std::future::ready(ev.into_event()))
}
}
impl<H: Hash> Clone for FollowStreamDriverSubscription<H> {
fn clone(&self) -> Self {
self.shared.subscribe()
}
}
impl<H: Hash> Drop for FollowStreamDriverSubscription<H> {
fn drop(&mut self) {
self.shared.remove_sub(self.id);
}
}
/// Locked shared state. The driver stream will access this state to push
/// events to any subscribers, and subscribers will access it to pull the
/// events destined for themselves.
#[derive(Debug, Clone)]
struct Shared<H: Hash>(Arc<Mutex<SharedState<H>>>);
#[derive(Debug)]
struct SharedState<H: Hash> {
done: bool,
next_id: usize,
subscribers: HashMap<usize, SubscriberDetails<H>>,
/// Keep a buffer of all events that should be handed to a new subscription.
block_events_for_new_subscriptions: VecDeque<FollowEvent<BlockRef<H>>>,
// Keep track of the subscription ID we send out on new subs.
current_subscription_id: Option<String>,
// Keep track of the init message we send out on new subs.
current_init_message: Option<Initialized<BlockRef<H>>>,
// Runtime events by block hash; we need to track these to know
// whether the runtime has changed when we see a finalized block notification.
seen_runtime_events: HashMap<H, RuntimeEvent>,
}
impl<H: Hash> Default for Shared<H> {
fn default() -> Self {
Shared(Arc::new(Mutex::new(SharedState {
next_id: 1,
done: false,
subscribers: HashMap::new(),
current_init_message: None,
current_subscription_id: None,
seen_runtime_events: HashMap::new(),
block_events_for_new_subscriptions: VecDeque::new(),
})))
}
}
impl<H: Hash> Shared<H> {
/// Set the shared state to "done"; no more items will be handed to it.
pub fn done(&self) {
let mut shared = self.0.lock().unwrap();
shared.done = true;
// Wake up all subscribers so they get notified that the backend was closed
for details in shared.subscribers.values_mut() {
if let Some(waker) = details.waker.take() {
waker.wake();
}
}
}
/// Cleanup a subscription.
pub fn remove_sub(&self, sub_id: usize) {
let mut shared = self.0.lock().unwrap();
shared.subscribers.remove(&sub_id);
}
/// Take items for some subscription ID and save the waker.
pub fn take_items_and_save_waker(
&self,
sub_id: usize,
waker: &Waker,
) -> Option<VecDeque<FollowStreamMsg<BlockRef<H>>>> {
let mut shared = self.0.lock().unwrap();
let is_done = shared.done;
let details = shared.subscribers.get_mut(&sub_id)?;
// no more items to pull, and stream closed, so return None.
if details.items.is_empty() && is_done {
return None;
}
// else, take whatever items, and save the waker if not done yet.
let items = std::mem::take(&mut details.items);
if !is_done {
details.waker = Some(waker.clone());
}
Some(items)
}
/// Push a new item out to subscribers.
pub fn push_item(&self, item: FollowStreamMsg<BlockRef<H>>) {
let mut shared = self.0.lock().unwrap();
let shared = shared.deref_mut();
// broadcast item to subscribers:
for details in shared.subscribers.values_mut() {
details.items.push_back(item.clone());
if let Some(waker) = details.waker.take() {
waker.wake();
}
}
// Keep our buffer of ready/block events up-to-date:
match item {
FollowStreamMsg::Ready(sub_id) => {
// Set new subscription ID when it comes in.
shared.current_subscription_id = Some(sub_id);
}
FollowStreamMsg::Event(FollowEvent::Initialized(ev)) => {
// New subscriptions will be given this init message:
shared.current_init_message = Some(ev.clone());
// Clear block cache (since a new finalized block hash is seen):
shared.block_events_for_new_subscriptions.clear();
}
FollowStreamMsg::Event(FollowEvent::Finalized(finalized_ev)) => {
// Update the init message that we'll hand out to new subscriptions. If the init message
// is `None` for some reason, we just ignore this step.
if let Some(init_message) = &mut shared.current_init_message {
// Find the latest runtime update that's been finalized.
let newest_runtime = finalized_ev
.finalized_block_hashes
.iter()
.rev()
.filter_map(|h| shared.seen_runtime_events.get(&h.hash()).cloned())
.next();
shared.seen_runtime_events.clear();
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);
}
}
// The last finalized block will be reported as Initialized by our driver,
// therefore there is no need to report NewBlock and BestBlock events for it.
// If the Finalized event reported multiple finalized hashes, we only care about
// the state at the head of the chain, therefore it is correct to remove those as well.
// Idem for the pruned hashes; they will never be reported again and we remove
// them from the window of events.
let to_remove: HashSet<H> = finalized_ev
.finalized_block_hashes
.iter()
.chain(finalized_ev.pruned_block_hashes.iter())
.map(|h| h.hash())
.collect();
shared
.block_events_for_new_subscriptions
.retain(|ev| match ev {
FollowEvent::NewBlock(new_block_ev) => {
!to_remove.contains(&new_block_ev.block_hash.hash())
}
FollowEvent::BestBlockChanged(best_block_ev) => {
!to_remove.contains(&best_block_ev.best_block_hash.hash())
}
_ => true,
});
}
FollowStreamMsg::Event(FollowEvent::NewBlock(new_block_ev)) => {
// If a new runtime is seen, note it so that when a block is finalized, we
// can associate that with a runtime update having happened.
if let Some(runtime_event) = &new_block_ev.new_runtime {
shared
.seen_runtime_events
.insert(new_block_ev.block_hash.hash(), runtime_event.clone());
}
shared
.block_events_for_new_subscriptions
.push_back(FollowEvent::NewBlock(new_block_ev));
}
FollowStreamMsg::Event(ev @ FollowEvent::BestBlockChanged(_)) => {
shared.block_events_for_new_subscriptions.push_back(ev);
}
FollowStreamMsg::Event(FollowEvent::Stop) => {
// On a stop event, clear everything. Wait for resubscription and new ready/initialised events.
shared.block_events_for_new_subscriptions.clear();
shared.current_subscription_id = None;
shared.current_init_message = None;
}
_ => {
// We don't buffer any other events.
}
}
}
/// Create a new subscription.
pub fn subscribe(&self) -> FollowStreamDriverSubscription<H> {
let mut shared = self.0.lock().unwrap();
let id = shared.next_id;
shared.next_id += 1;
shared.subscribers.insert(
id,
SubscriberDetails {
items: VecDeque::new(),
waker: None,
},
);
// Any new subscription should start with a "Ready" message and then an "Initialized"
// message, and then any non-finalized block events since that. If these don't exist,
// it means the subscription is currently stopped, and we should expect new Ready/Init
// messages anyway once it restarts.
let mut local_items = VecDeque::new();
if let Some(sub_id) = &shared.current_subscription_id {
local_items.push_back(FollowStreamMsg::Ready(sub_id.clone()));
}
if let Some(init_msg) = &shared.current_init_message {
local_items.push_back(FollowStreamMsg::Event(FollowEvent::Initialized(
init_msg.clone(),
)));
}
for ev in &shared.block_events_for_new_subscriptions {
local_items.push_back(FollowStreamMsg::Event(ev.clone()));
}
drop(shared);
FollowStreamDriverSubscription {
id,
done: false,
shared: self.clone(),
local_items,
}
}
}
/// Details for a given subscriber: any items it's not yet claimed,
/// and a way to wake it up when there are more items for it.
#[derive(Debug)]
struct SubscriberDetails<H: Hash> {
items: VecDeque<FollowStreamMsg<BlockRef<H>>>,
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<H: Hash, F> {
stream: FollowStreamDriverSubscription<H>,
sub_id: Option<String>,
last_seen_block: Option<BlockRef<H>>,
f: F,
is_done: bool,
}
impl<H: Hash, F> Unpin for FollowStreamFinalizedHeads<H, F> {}
impl<H, F> FollowStreamFinalizedHeads<H, F>
where
H: Hash,
F: Fn(FollowEvent<BlockRef<H>>) -> Vec<BlockRef<H>>,
{
pub fn new(stream: FollowStreamDriverSubscription<H>, f: F) -> Self {
Self {
stream,
sub_id: None,
last_seen_block: None,
f,
is_done: false,
}
}
}
impl<H, F> Stream for FollowStreamFinalizedHeads<H, F>
where
H: Hash,
F: Fn(FollowEvent<BlockRef<H>>) -> Vec<BlockRef<H>>,
{
type Item = Result<(String, Vec<BlockRef<H>>), BackendError>;
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::ClientError(
pezkuwi_subxt_rpcs::Error::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;
use super::*;
/// Return a `FollowStreamDriver`
pub fn test_follow_stream_driver_getter<H, F, I>(
events: F,
max_life: usize,
) -> FollowStreamDriver<H>
where
H: Hash + 'static,
F: Fn() -> I + Send + 'static,
I: IntoIterator<Item = Result<FollowEvent<H>, BackendError>>,
{
let (stream, _) = test_unpin_stream_getter(events, max_life);
FollowStreamDriver::new(stream)
}
}
#[cfg(test)]
mod test {
use futures::TryStreamExt;
use primitive_types::H256;
use super::super::follow_stream::test_utils::{
ev_best_block, ev_finalized, ev_initialized, ev_new_block,
};
use super::super::follow_stream_unpin::test_utils::{
ev_best_block_ref, ev_finalized_ref, ev_initialized_ref, ev_new_block_ref,
};
use super::test_utils::test_follow_stream_driver_getter;
use super::*;
#[test]
fn follow_stream_driver_is_sendable() {
fn assert_send<T: Send + 'static>(_: T) {}
let stream_getter = test_follow_stream_driver_getter(|| [Ok(ev_initialized(1))], 10);
assert_send(stream_getter);
}
#[tokio::test]
async fn subscribers_all_receive_events_and_finish_gracefully_on_error() {
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], [])),
Err(BackendError::Other("ended".to_owned())),
]
},
10,
);
let handle = driver.handle();
let a = handle.subscribe();
let b = handle.subscribe();
let c = handle.subscribe();
// Drive to completion (the sort of real life usage I'd expect):
tokio::spawn(async move { while driver.next().await.is_some() {} });
let a_vec: Vec<_> = a.collect().await;
let b_vec: Vec<_> = b.collect().await;
let c_vec: Vec<_> = c.collect().await;
let expected = vec![
FollowStreamMsg::Ready("sub_id_0".into()),
FollowStreamMsg::Event(ev_initialized_ref(0)),
FollowStreamMsg::Event(ev_new_block_ref(0, 1)),
FollowStreamMsg::Event(ev_best_block_ref(1)),
FollowStreamMsg::Event(ev_finalized_ref([1])),
];
assert_eq!(a_vec, expected);
assert_eq!(b_vec, expected);
assert_eq!(c_vec, expected);
}
#[tokio::test]
async fn subscribers_receive_block_events_from_last_finalised() {
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(ev_new_block(1, 2)),
Ok(ev_new_block(2, 3)),
Err(BackendError::Other("ended".to_owned())),
]
},
10,
);
// Skip past ready, init, new, best events.
let _r = driver.next().await.unwrap();
let _i0 = driver.next().await.unwrap();
let _n1 = driver.next().await.unwrap();
let _b1 = driver.next().await.unwrap();
// THEN subscribe; subscription should still receive them:
let evs: Vec<_> = driver.handle().subscribe().take(4).collect().await;
let expected = vec![
FollowStreamMsg::Ready("sub_id_0".into()),
FollowStreamMsg::Event(ev_initialized_ref(0)),
FollowStreamMsg::Event(ev_new_block_ref(0, 1)),
FollowStreamMsg::Event(ev_best_block_ref(1)),
];
assert_eq!(evs, expected);
// Skip past finalized 1, new 2, new 3 events
let _f1 = driver.next().await.unwrap();
let _n2 = driver.next().await.unwrap();
let _n3 = driver.next().await.unwrap();
// THEN subscribe again; new subs will see an updated initialized message
// with the latest finalized block hash.
let evs: Vec<_> = driver.handle().subscribe().take(4).collect().await;
let expected = vec![
FollowStreamMsg::Ready("sub_id_0".into()),
FollowStreamMsg::Event(ev_initialized_ref(1)),
FollowStreamMsg::Event(ev_new_block_ref(1, 2)),
FollowStreamMsg::Event(ev_new_block_ref(2, 3)),
];
assert_eq!(evs, expected);
}
#[tokio::test]
async fn subscribers_receive_new_blocks_before_subscribing() {
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_new_block(1, 2)),
Ok(ev_new_block(2, 3)),
Ok(ev_finalized([1], [])),
Err(BackendError::Other("ended".to_owned())),
]
},
10,
);
// Skip to the first finalized block F1.
let _r = driver.next().await.unwrap();
let _i0 = driver.next().await.unwrap();
let _n1 = driver.next().await.unwrap();
let _b1 = driver.next().await.unwrap();
let _n2 = driver.next().await.unwrap();
let _n3 = driver.next().await.unwrap();
let _f1 = driver.next().await.unwrap();
// THEN subscribe; and make sure new block 1 and 2 are received.
let evs: Vec<_> = driver.handle().subscribe().take(4).collect().await;
let expected = vec![
FollowStreamMsg::Ready("sub_id_0".into()),
FollowStreamMsg::Event(ev_initialized_ref(1)),
FollowStreamMsg::Event(ev_new_block_ref(1, 2)),
FollowStreamMsg::Event(ev_new_block_ref(2, 3)),
];
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(BackendError::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(BackendError::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(BackendError::Rpc(RpcError::ClientError(pezkuwi_subxt_rpcs::Error::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))]
)
);
}
}
@@ -0,0 +1,813 @@
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
use super::ChainHeadRpcMethods;
use super::follow_stream::FollowStream;
use crate::config::{Config, Hash, HashFor};
use crate::error::BackendError;
use futures::stream::{FuturesUnordered, Stream, StreamExt};
use pezkuwi_subxt_rpcs::methods::chain_head::{
BestBlockChanged, Finalized, FollowEvent, Initialized, NewBlock,
};
use std::collections::{HashMap, HashSet};
use std::future::Future;
use std::pin::Pin;
use std::sync::{Arc, Mutex};
use std::task::{Context, Poll, Waker};
/// The type of stream item.
pub use super::follow_stream::FollowStreamMsg;
/// A `Stream` which builds on `FollowStream`, and handles pinning. It replaces any block hash seen in
/// the follow events with a `BlockRef` which, when all clones are dropped, will lead to an "unpin" call
/// for that block hash being queued. It will also automatically unpin any blocks that exceed a given max
/// age, to try and prevent the underlying stream from ending (and _all_ blocks from being unpinned as a
/// result). Put simply, it tries to keep every block pinned as long as possible until the block is no longer
/// used anywhere.
#[derive(Debug)]
pub struct FollowStreamUnpin<H: Hash> {
// The underlying stream of events.
inner: FollowStream<H>,
// A method to call to unpin a block, given a block hash and a subscription ID.
unpin_method: UnpinMethodHolder<H>,
// Futures for sending unpin events that we'll poll to completion as
// part of polling the stream as a whole.
unpin_futs: FuturesUnordered<UnpinFut>,
// Each time a new finalized block is seen, we give it an age of `next_rel_block_age`,
// and then increment this ready for the next finalized block. So, the first finalized
// block will have an age of 0, the next 1, 2, 3 and so on. We can then use `max_block_life`
// to say "unpin all blocks with an age < (next_rel_block_age-1) - max_block_life".
next_rel_block_age: usize,
// The latest ID of the FollowStream subscription, which we can use
// to unpin blocks.
subscription_id: Option<Arc<str>>,
// The longest period a block can be pinned for.
max_block_life: usize,
// The currently seen and pinned blocks.
pinned: HashMap<H, PinnedDetails<H>>,
// Shared state about blocks we've flagged to unpin from elsewhere
unpin_flags: UnpinFlags<H>,
}
// Just a wrapper to make implementing debug on the whole thing easier.
struct UnpinMethodHolder<H>(UnpinMethod<H>);
impl<H> std::fmt::Debug for UnpinMethodHolder<H> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"UnpinMethodHolder(Box<dyn FnMut(Hash, Arc<str>) -> UnpinFut>)"
)
}
}
/// The type of the unpin method that we need to provide.
pub type UnpinMethod<H> = Box<dyn FnMut(H, Arc<str>) -> UnpinFut + Send>;
/// The future returned from [`UnpinMethod`].
pub type UnpinFut = Pin<Box<dyn Future<Output = ()> + Send + 'static>>;
impl<H: Hash> std::marker::Unpin for FollowStreamUnpin<H> {}
impl<H: Hash> Stream for FollowStreamUnpin<H> {
type Item = Result<FollowStreamMsg<BlockRef<H>>, BackendError>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let mut this = self.as_mut();
loop {
// Poll any queued unpin tasks.
let unpin_futs_are_pending = match this.unpin_futs.poll_next_unpin(cx) {
Poll::Ready(Some(())) => continue,
Poll::Ready(None) => false,
Poll::Pending => true,
};
// Poll the inner stream for the next event.
let Poll::Ready(ev) = this.inner.poll_next_unpin(cx) else {
return Poll::Pending;
};
let Some(ev) = ev else {
// if the stream is done, but `unpin_futs` are still pending, then
// return pending here so that they are still driven to completion.
// Else, return `Ready(None)` to signal nothing left to do.
return match unpin_futs_are_pending {
true => Poll::Pending,
false => Poll::Ready(None),
};
};
// Error? just return it and do nothing further.
let ev = match ev {
Ok(ev) => ev,
Err(e) => {
return Poll::Ready(Some(Err(e)));
}
};
// React to any actual FollowEvent we get back.
let ev = match ev {
FollowStreamMsg::Ready(subscription_id) => {
// update the subscription ID we'll use to unpin things.
this.subscription_id = Some(subscription_id.clone().into());
FollowStreamMsg::Ready(subscription_id)
}
FollowStreamMsg::Event(FollowEvent::Initialized(details)) => {
let mut finalized_block_hashes =
Vec::with_capacity(details.finalized_block_hashes.len());
// Pin each of the finalized blocks. None of them will show up again (except as a
// parent block), and so they can all be unpinned immediately at any time. Increment
// the block age for each one, so that older finalized blocks are pruned first.
for finalized_block in &details.finalized_block_hashes {
let rel_block_age = this.next_rel_block_age;
let block_ref =
this.pin_unpinnable_block_at(rel_block_age, *finalized_block);
finalized_block_hashes.push(block_ref);
this.next_rel_block_age += 1;
}
FollowStreamMsg::Event(FollowEvent::Initialized(Initialized {
finalized_block_hashes,
finalized_block_runtime: details.finalized_block_runtime,
}))
}
FollowStreamMsg::Event(FollowEvent::NewBlock(details)) => {
// One bigger than our parent, and if no parent seen (maybe it was
// unpinned already), then one bigger than the last finalized block num
// as a best guess.
let parent_rel_block_age = this
.pinned
.get(&details.parent_block_hash)
.map(|p| p.rel_block_age)
.unwrap_or(this.next_rel_block_age.saturating_sub(1));
let block_ref = this.pin_block_at(parent_rel_block_age + 1, details.block_hash);
let parent_block_ref =
this.pin_block_at(parent_rel_block_age, details.parent_block_hash);
FollowStreamMsg::Event(FollowEvent::NewBlock(NewBlock {
block_hash: block_ref,
parent_block_hash: parent_block_ref,
new_runtime: details.new_runtime,
}))
}
FollowStreamMsg::Event(FollowEvent::BestBlockChanged(details)) => {
// We expect this block to already exist, so it'll keep its existing block_num,
// but worst case it'll just get the current finalized block_num + 1.
let rel_block_age = this.next_rel_block_age;
let block_ref = this.pin_block_at(rel_block_age, details.best_block_hash);
FollowStreamMsg::Event(FollowEvent::BestBlockChanged(BestBlockChanged {
best_block_hash: block_ref,
}))
}
FollowStreamMsg::Event(FollowEvent::Finalized(details)) => {
let finalized_block_refs: Vec<_> = details
.finalized_block_hashes
.into_iter()
.enumerate()
.map(|(idx, hash)| {
// These blocks _should_ exist already and so will have a known block num,
// but if they don't, we just increment the num from the last finalized block
// we saw, which should be accurate.
//
// `pin_unpinnable_block_at` indicates that the block will not show up in future events
// (They will show up as a parent block, but we don't care about that right now).
let rel_block_age = this.next_rel_block_age + idx;
this.pin_unpinnable_block_at(rel_block_age, hash)
})
.collect();
// Our relative block height is increased by however many finalized
// blocks we've seen.
this.next_rel_block_age += finalized_block_refs.len();
let pruned_block_refs: Vec<_> = details
.pruned_block_hashes
.into_iter()
.map(|hash| {
// We should know about these, too, and if not we set their age to last_finalized + 1.
//
// `pin_unpinnable_block_at` indicates that the block will not show up in future events.
let rel_block_age = this.next_rel_block_age;
this.pin_unpinnable_block_at(rel_block_age, hash)
})
.collect();
// At this point, we also check to see which blocks we should submit unpin events
// for. We will unpin:
// - Any block that's older than the max age.
// - Any block that has no references left (ie has been dropped) that _also_ has
// showed up in the pruned list in a finalized event (so it will never be in another event).
this.unpin_blocks(cx.waker());
FollowStreamMsg::Event(FollowEvent::Finalized(Finalized {
finalized_block_hashes: finalized_block_refs,
pruned_block_hashes: pruned_block_refs,
}))
}
FollowStreamMsg::Event(FollowEvent::Stop) => {
// clear out "old" things that are no longer applicable since
// the subscription has ended (a new one will be created under the hood, at
// which point we'll get given a new subscription ID.
this.subscription_id = None;
this.pinned.clear();
this.unpin_futs.clear();
this.unpin_flags.lock().unwrap().clear();
this.next_rel_block_age = 0;
FollowStreamMsg::Event(FollowEvent::Stop)
}
// These events aren't interesting; we just forward them on:
FollowStreamMsg::Event(FollowEvent::OperationBodyDone(details)) => {
FollowStreamMsg::Event(FollowEvent::OperationBodyDone(details))
}
FollowStreamMsg::Event(FollowEvent::OperationCallDone(details)) => {
FollowStreamMsg::Event(FollowEvent::OperationCallDone(details))
}
FollowStreamMsg::Event(FollowEvent::OperationStorageItems(details)) => {
FollowStreamMsg::Event(FollowEvent::OperationStorageItems(details))
}
FollowStreamMsg::Event(FollowEvent::OperationWaitingForContinue(details)) => {
FollowStreamMsg::Event(FollowEvent::OperationWaitingForContinue(details))
}
FollowStreamMsg::Event(FollowEvent::OperationStorageDone(details)) => {
FollowStreamMsg::Event(FollowEvent::OperationStorageDone(details))
}
FollowStreamMsg::Event(FollowEvent::OperationInaccessible(details)) => {
FollowStreamMsg::Event(FollowEvent::OperationInaccessible(details))
}
FollowStreamMsg::Event(FollowEvent::OperationError(details)) => {
FollowStreamMsg::Event(FollowEvent::OperationError(details))
}
};
// Return our event.
return Poll::Ready(Some(Ok(ev)));
}
}
}
impl<H: Hash> FollowStreamUnpin<H> {
/// Create a new [`FollowStreamUnpin`].
pub fn new(
follow_stream: FollowStream<H>,
unpin_method: UnpinMethod<H>,
max_block_life: usize,
) -> Self {
Self {
inner: follow_stream,
unpin_method: UnpinMethodHolder(unpin_method),
max_block_life,
pinned: Default::default(),
subscription_id: None,
next_rel_block_age: 0,
unpin_flags: Default::default(),
unpin_futs: Default::default(),
}
}
/// Create a new [`FollowStreamUnpin`] given the RPC methods.
pub fn from_methods<T: Config>(
follow_stream: FollowStream<HashFor<T>>,
methods: ChainHeadRpcMethods<T>,
max_block_life: usize,
) -> FollowStreamUnpin<HashFor<T>> {
let unpin_method = Box::new(move |hash: HashFor<T>, sub_id: Arc<str>| {
let methods = methods.clone();
let fut: UnpinFut = Box::pin(async move {
// We ignore any errors trying to unpin at the moment.
let _ = methods.chainhead_v1_unpin(&sub_id, hash).await;
});
fut
});
FollowStreamUnpin::new(follow_stream, unpin_method, max_block_life)
}
/// Is the block hash currently pinned.
pub fn is_pinned(&self, hash: &H) -> bool {
self.pinned.contains_key(hash)
}
/// Pin a block, or return the reference to an already-pinned block. If the block has been registered to
/// be unpinned, we'll clear those flags, so that it won't be unpinned. If the unpin request has already
/// been sent though, then the block will be unpinned.
fn pin_block_at(&mut self, rel_block_age: usize, hash: H) -> BlockRef<H> {
self.pin_block_at_setting_unpinnable_flag(rel_block_age, hash, false)
}
/// Pin a block, or return the reference to an already-pinned block.
///
/// This is the same as [`Self::pin_block_at`], except that it also marks the block as being unpinnable now,
/// which should be done for any block that will no longer be seen in future events.
fn pin_unpinnable_block_at(&mut self, rel_block_age: usize, hash: H) -> BlockRef<H> {
self.pin_block_at_setting_unpinnable_flag(rel_block_age, hash, true)
}
fn pin_block_at_setting_unpinnable_flag(
&mut self,
rel_block_age: usize,
hash: H,
can_be_unpinned: bool,
) -> BlockRef<H> {
let entry = self
.pinned
.entry(hash)
// If there's already an entry, then clear any unpin_flags and update the
// can_be_unpinned status (this can become true but cannot become false again
// once true).
.and_modify(|entry| {
entry.can_be_unpinned = entry.can_be_unpinned || can_be_unpinned;
self.unpin_flags.lock().unwrap().remove(&hash);
})
// If there's not an entry already, make one and return it.
.or_insert_with(|| PinnedDetails {
rel_block_age,
block_ref: BlockRef {
inner: Arc::new(BlockRefInner {
hash,
unpin_flags: self.unpin_flags.clone(),
}),
},
can_be_unpinned,
});
entry.block_ref.clone()
}
/// Unpin any blocks that are either too old, or have the unpin flag set and are old enough.
fn unpin_blocks(&mut self, waker: &Waker) {
let mut unpin_flags = self.unpin_flags.lock().unwrap();
// This gets the age of the last finalized block.
let rel_block_age = self.next_rel_block_age.saturating_sub(1);
// If we asked to unpin and there was no subscription_id, then there's nothing we can do,
// and nothing will need unpinning now anyway.
let Some(sub_id) = &self.subscription_id else {
return;
};
let mut blocks_to_unpin = vec![];
for (hash, details) in &self.pinned {
if rel_block_age.saturating_sub(details.rel_block_age) >= self.max_block_life
|| (unpin_flags.contains(hash) && details.can_be_unpinned)
{
// The block is too old, or it's been flagged to be unpinned and won't be in a future
// backend event, so we can unpin it for real now.
blocks_to_unpin.push(*hash);
// Clear it from our unpin flags if present so that we don't try to unpin it again.
unpin_flags.remove(hash);
}
}
// Release our lock on unpin_flags ASAP.
drop(unpin_flags);
// No need to call the waker etc if nothing to do:
if blocks_to_unpin.is_empty() {
return;
}
for hash in blocks_to_unpin {
self.pinned.remove(&hash);
let fut = (self.unpin_method.0)(hash, sub_id.clone());
self.unpin_futs.push(fut);
}
// Any new futures pushed above need polling to start. We could
// just wait for the next stream event, but let's wake the task to
// have it polled sooner, just in case it's slow to receive things.
waker.wake_by_ref();
}
}
// The set of block hashes that can be unpinned when ready.
// BlockRefs write to this when they are dropped.
type UnpinFlags<H> = Arc<Mutex<HashSet<H>>>;
#[derive(Debug)]
struct PinnedDetails<H: Hash> {
/// Relatively speaking, how old is the block? When we start following
/// blocks, the first finalized block gets an age of 0, the second an age
/// of 1 and so on.
rel_block_age: usize,
/// A block ref we can hand out to keep blocks pinned.
/// Because we store one here until it's unpinned, the live count
/// will only drop to 1 when no external refs are left.
block_ref: BlockRef<H>,
/// Has this block showed up in the list of pruned blocks, or has it
/// been finalized? In this case, it can now been pinned as it won't
/// show up again in future events (except as a "parent block" of some
/// new block, which we're currently ignoring).
can_be_unpinned: bool,
}
/// All blocks reported will be wrapped in this.
#[derive(Debug, Clone)]
pub struct BlockRef<H: Hash> {
inner: Arc<BlockRefInner<H>>,
}
#[derive(Debug)]
struct BlockRefInner<H> {
hash: H,
unpin_flags: UnpinFlags<H>,
}
impl<H: Hash> BlockRef<H> {
/// For testing purposes only, create a BlockRef from a hash
/// that isn't pinned.
#[cfg(test)]
pub fn new(hash: H) -> Self {
BlockRef {
inner: Arc::new(BlockRefInner {
hash,
unpin_flags: Default::default(),
}),
}
}
/// Return the hash for this block.
pub fn hash(&self) -> H {
self.inner.hash
}
}
impl<H: Hash> PartialEq for BlockRef<H> {
fn eq(&self, other: &Self) -> bool {
self.inner.hash == other.inner.hash
}
}
impl<H: Hash> PartialEq<H> for BlockRef<H> {
fn eq(&self, other: &H) -> bool {
&self.inner.hash == other
}
}
impl<H: Hash> Drop for BlockRef<H> {
fn drop(&mut self) {
// PinnedDetails keeps one ref, so if this is the second ref, it's the
// only "external" one left and we should ask to unpin it now. if it's
// the only ref remaining, it means that it's already been unpinned, so
// nothing to do here anyway.
if Arc::strong_count(&self.inner) == 2 {
if let Ok(mut unpin_flags) = self.inner.unpin_flags.lock() {
unpin_flags.insert(self.inner.hash);
}
}
}
}
#[cfg(test)]
pub(super) mod test_utils {
use super::super::follow_stream::{FollowStream, test_utils::test_stream_getter};
use super::*;
use crate::config::substrate::H256;
pub type UnpinRx<H> = std::sync::mpsc::Receiver<(H, Arc<str>)>;
/// Get a [`FollowStreamUnpin`] from an iterator over events.
pub fn test_unpin_stream_getter<H, F, I>(
events: F,
max_life: usize,
) -> (FollowStreamUnpin<H>, UnpinRx<H>)
where
H: Hash + 'static,
F: Fn() -> I + Send + 'static,
I: IntoIterator<Item = Result<FollowEvent<H>, BackendError>>,
{
// Unpin requests will come here so that we can look out for them.
let (unpin_tx, unpin_rx) = std::sync::mpsc::channel();
let follow_stream = FollowStream::new(test_stream_getter(events));
let unpin_method: UnpinMethod<H> = Box::new(move |hash, sub_id| {
unpin_tx.send((hash, sub_id)).unwrap();
Box::pin(std::future::ready(()))
});
let follow_unpin = FollowStreamUnpin::new(follow_stream, unpin_method, max_life);
(follow_unpin, unpin_rx)
}
/// Assert that the unpinned blocks sent from the `UnpinRx` channel match the items given.
pub fn assert_from_unpin_rx<H: Hash + 'static>(
unpin_rx: &UnpinRx<H>,
items: impl IntoIterator<Item = H>,
) {
let expected_hashes = HashSet::<H>::from_iter(items);
for i in 0..expected_hashes.len() {
let Ok((hash, _)) = unpin_rx.try_recv() else {
panic!("Another unpin event is expected, but failed to pull item {i} from channel");
};
assert!(
expected_hashes.contains(&hash),
"Hash {hash:?} was unpinned, but is not expected to have been"
);
}
}
/// An initialized event containing a BlockRef (useful for comparisons)
pub fn ev_initialized_ref(n: u64) -> FollowEvent<BlockRef<H256>> {
FollowEvent::Initialized(Initialized {
finalized_block_hashes: vec![BlockRef::new(H256::from_low_u64_le(n))],
finalized_block_runtime: None,
})
}
/// A new block event containing a BlockRef (useful for comparisons)
pub fn ev_new_block_ref(parent: u64, n: u64) -> FollowEvent<BlockRef<H256>> {
FollowEvent::NewBlock(NewBlock {
parent_block_hash: BlockRef::new(H256::from_low_u64_le(parent)),
block_hash: BlockRef::new(H256::from_low_u64_le(n)),
new_runtime: None,
})
}
/// A best block event containing a BlockRef (useful for comparisons)
pub fn ev_best_block_ref(n: u64) -> FollowEvent<BlockRef<H256>> {
FollowEvent::BestBlockChanged(BestBlockChanged {
best_block_hash: BlockRef::new(H256::from_low_u64_le(n)),
})
}
/// A finalized event containing a BlockRef (useful for comparisons)
pub fn ev_finalized_ref(ns: impl IntoIterator<Item = u64>) -> FollowEvent<BlockRef<H256>> {
FollowEvent::Finalized(Finalized {
finalized_block_hashes: ns
.into_iter()
.map(|h| BlockRef::new(H256::from_low_u64_le(h)))
.collect(),
pruned_block_hashes: vec![],
})
}
}
#[cfg(test)]
mod test {
use super::super::follow_stream::test_utils::{
ev_best_block, ev_finalized, ev_initialized, ev_new_block,
};
use super::test_utils::{assert_from_unpin_rx, ev_new_block_ref, test_unpin_stream_getter};
use super::*;
use crate::config::substrate::H256;
#[tokio::test]
async fn hands_back_blocks() {
let (follow_unpin, _) = test_unpin_stream_getter(
|| {
[
Ok(ev_new_block(0, 1)),
Ok(ev_new_block(1, 2)),
Ok(ev_new_block(2, 3)),
Err(BackendError::Other("ended".to_owned())),
]
},
10,
);
let out: Vec<_> = follow_unpin.filter_map(async |e| e.ok()).collect().await;
assert_eq!(
out,
vec![
FollowStreamMsg::Ready("sub_id_0".into()),
FollowStreamMsg::Event(ev_new_block_ref(0, 1)),
FollowStreamMsg::Event(ev_new_block_ref(1, 2)),
FollowStreamMsg::Event(ev_new_block_ref(2, 3)),
]
);
}
#[tokio::test]
async fn unpins_initialized_block() {
let (mut follow_unpin, unpin_rx) = test_unpin_stream_getter(
|| {
[
Ok(ev_initialized(0)),
Ok(ev_finalized([1], [])),
Err(BackendError::Other("ended".to_owned())),
]
},
3,
);
let _r = follow_unpin.next().await.unwrap().unwrap();
// Drop the initialized block:
let i0 = follow_unpin.next().await.unwrap().unwrap();
drop(i0);
// Let a finalization event occur.
let _f1 = follow_unpin.next().await.unwrap().unwrap();
// Now, initialized block should be unpinned.
assert_from_unpin_rx(&unpin_rx, [H256::from_low_u64_le(0)]);
assert!(!follow_unpin.is_pinned(&H256::from_low_u64_le(0)));
}
#[tokio::test]
async fn unpins_old_blocks() {
let (mut follow_unpin, unpin_rx) = test_unpin_stream_getter(
|| {
[
Ok(ev_initialized(0)),
Ok(ev_finalized([1], [])),
Ok(ev_finalized([2], [])),
Ok(ev_finalized([3], [])),
Ok(ev_finalized([4], [])),
Ok(ev_finalized([5], [])),
Err(BackendError::Other("ended".to_owned())),
]
},
3,
);
let _r = follow_unpin.next().await.unwrap().unwrap();
let _i0 = follow_unpin.next().await.unwrap().unwrap();
unpin_rx.try_recv().expect_err("nothing unpinned yet");
let _f1 = follow_unpin.next().await.unwrap().unwrap();
unpin_rx.try_recv().expect_err("nothing unpinned yet");
let _f2 = follow_unpin.next().await.unwrap().unwrap();
unpin_rx.try_recv().expect_err("nothing unpinned yet");
let _f3 = follow_unpin.next().await.unwrap().unwrap();
// Max age is 3, so after block 3 finalized, block 0 becomes too old and is unpinned.
assert_from_unpin_rx(&unpin_rx, [H256::from_low_u64_le(0)]);
let _f4 = follow_unpin.next().await.unwrap().unwrap();
// Block 1 is now too old and is unpinned.
assert_from_unpin_rx(&unpin_rx, [H256::from_low_u64_le(1)]);
let _f5 = follow_unpin.next().await.unwrap().unwrap();
// Block 2 is now too old and is unpinned.
assert_from_unpin_rx(&unpin_rx, [H256::from_low_u64_le(2)]);
}
#[tokio::test]
async fn dropped_new_blocks_should_not_get_unpinned_until_finalization() {
let (mut follow_unpin, unpin_rx) = test_unpin_stream_getter(
|| {
[
Ok(ev_initialized(0)),
Ok(ev_new_block(0, 1)),
Ok(ev_new_block(1, 2)),
Ok(ev_finalized([1], [])),
Ok(ev_finalized([2], [])),
Err(BackendError::Other("ended".to_owned())),
]
},
10,
);
let _r = follow_unpin.next().await.unwrap().unwrap();
let _i0 = follow_unpin.next().await.unwrap().unwrap();
let n1 = follow_unpin.next().await.unwrap().unwrap();
drop(n1);
let n2 = follow_unpin.next().await.unwrap().unwrap();
drop(n2);
// New blocks dropped but still pinned:
assert!(follow_unpin.is_pinned(&H256::from_low_u64_le(1)));
assert!(follow_unpin.is_pinned(&H256::from_low_u64_le(2)));
let f1 = follow_unpin.next().await.unwrap().unwrap();
drop(f1);
// After block 1 finalized, both blocks are still pinned because:
// - block 1 was handed back in the finalized event, so will be unpinned next time.
// - block 2 wasn't mentioned in the finalized event, so should not have been unpinned yet.
assert!(follow_unpin.is_pinned(&H256::from_low_u64_le(1)));
assert!(follow_unpin.is_pinned(&H256::from_low_u64_le(2)));
let f2 = follow_unpin.next().await.unwrap().unwrap();
drop(f2);
// After block 2 finalized, block 1 can be unpinned finally, but block 2 needs to wait one more event.
assert!(!follow_unpin.is_pinned(&H256::from_low_u64_le(1)));
assert!(follow_unpin.is_pinned(&H256::from_low_u64_le(2)));
assert_from_unpin_rx(&unpin_rx, [H256::from_low_u64_le(1)]);
}
#[tokio::test]
async fn dropped_new_blocks_should_not_get_unpinned_until_pruned() {
let (mut follow_unpin, unpin_rx) = test_unpin_stream_getter(
|| {
[
Ok(ev_initialized(0)),
Ok(ev_new_block(0, 1)),
Ok(ev_new_block(1, 2)),
Ok(ev_new_block(1, 3)),
Ok(ev_finalized([1], [])),
Ok(ev_finalized([2], [3])),
Ok(ev_finalized([4], [])),
Err(BackendError::Other("ended".to_owned())),
]
},
10,
);
let _r = follow_unpin.next().await.unwrap().unwrap();
let _i0 = follow_unpin.next().await.unwrap().unwrap();
let n1 = follow_unpin.next().await.unwrap().unwrap();
drop(n1);
let n2 = follow_unpin.next().await.unwrap().unwrap();
drop(n2);
let n3 = follow_unpin.next().await.unwrap().unwrap();
drop(n3);
let f1 = follow_unpin.next().await.unwrap().unwrap();
drop(f1);
// After block 1 is finalized, everything is still pinned because the finalization event
// itself returns 1, and 2/3 aren't finalized or pruned yet.
assert!(follow_unpin.is_pinned(&H256::from_low_u64_le(1)));
assert!(follow_unpin.is_pinned(&H256::from_low_u64_le(2)));
assert!(follow_unpin.is_pinned(&H256::from_low_u64_le(3)));
let f2 = follow_unpin.next().await.unwrap().unwrap();
drop(f2);
// After the next finalization event, block 1 can finally be unpinned since it was Finalized
// last event _and_ is no longer handed back anywhere. 2 and 3 should still be pinned.
assert!(!follow_unpin.is_pinned(&H256::from_low_u64_le(1)));
assert!(follow_unpin.is_pinned(&H256::from_low_u64_le(2)));
assert!(follow_unpin.is_pinned(&H256::from_low_u64_le(3)));
assert_from_unpin_rx(&unpin_rx, [H256::from_low_u64_le(1)]);
let f4 = follow_unpin.next().await.unwrap().unwrap();
drop(f4);
// After some other finalized event, we are now allowed to ditch the previously pruned and
// finalized blocks 2 and 3.
assert!(!follow_unpin.is_pinned(&H256::from_low_u64_le(2)));
assert!(!follow_unpin.is_pinned(&H256::from_low_u64_le(3)));
assert_from_unpin_rx(
&unpin_rx,
[H256::from_low_u64_le(2), H256::from_low_u64_le(3)],
);
}
#[tokio::test]
async fn never_unpin_new_block_before_finalized() {
// Ensure that if we drop a new block; the pinning is still active until the block is finalized.
let (mut follow_unpin, unpin_rx) = test_unpin_stream_getter(
|| {
[
Ok(ev_initialized(0)),
Ok(ev_new_block(0, 1)),
Ok(ev_new_block(1, 2)),
Ok(ev_best_block(1)),
Ok(ev_finalized([1], [])),
Ok(ev_finalized([2], [])),
Err(BackendError::Other("ended".to_owned())),
]
},
10,
);
let _r = follow_unpin.next().await.unwrap().unwrap();
// drop initialised block 0 and new block 1 and new block 2.
let i0 = follow_unpin.next().await.unwrap().unwrap();
drop(i0);
let n1 = follow_unpin.next().await.unwrap().unwrap();
drop(n1);
let n2 = follow_unpin.next().await.unwrap().unwrap();
drop(n2);
let b1 = follow_unpin.next().await.unwrap().unwrap();
drop(b1);
// Nothing unpinned yet!
unpin_rx.try_recv().expect_err("nothing unpinned yet");
let f1 = follow_unpin.next().await.unwrap().unwrap();
drop(f1);
// After finalization, block 1 is now ready to be unpinned (it won't be seen again),
// but isn't actually unpinned yet (because it was just handed back in f1). Block 0
// however has now been unpinned.
assert!(!follow_unpin.is_pinned(&H256::from_low_u64_le(0)));
assert_from_unpin_rx(&unpin_rx, [H256::from_low_u64_le(0)]);
unpin_rx.try_recv().expect_err("nothing unpinned yet");
let f2 = follow_unpin.next().await.unwrap().unwrap();
drop(f2);
// After f2, we can get rid of block 1 now, which was finalized last time.
assert!(!follow_unpin.is_pinned(&H256::from_low_u64_le(1)));
assert_from_unpin_rx(&unpin_rx, [H256::from_low_u64_le(1)]);
unpin_rx.try_recv().expect_err("nothing unpinned yet");
}
}
+878
View File
@@ -0,0 +1,878 @@
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
//! This module will expose a backend implementation based on the new APIs
//! described at <https://github.com/paritytech/json-rpc-interface-spec/>. See
//! [`rpc_methods`] for the raw API calls.
//!
//! # Warning
//!
//! Everything in this module is **unstable**, meaning that it could change without
//! warning at any time.
mod follow_stream;
mod follow_stream_driver;
mod follow_stream_unpin;
mod storage_items;
use self::follow_stream_driver::FollowStreamFinalizedHeads;
use crate::backend::{
Backend, BlockRef, BlockRefT, RuntimeVersion, StorageResponse, StreamOf, StreamOfResults,
TransactionStatus, utils::retry,
};
use crate::config::{Config, Hash, HashFor};
use crate::error::{BackendError, RpcError};
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::task::Poll;
use storage_items::StorageItems;
use pezkuwi_subxt_rpcs::RpcClient;
use pezkuwi_subxt_rpcs::methods::chain_head::{
FollowEvent, MethodResponse, RuntimeEvent, StorageQuery, StorageQueryType, StorageResultType,
};
/// Re-export RPC types and methods from [`pezkuwi_subxt_rpcs::methods::chain_head`].
pub mod rpc_methods {
pub use pezkuwi_subxt_rpcs::methods::legacy::*;
}
// Expose the RPC methods.
pub use pezkuwi_subxt_rpcs::methods::chain_head::ChainHeadRpcMethods;
/// Configure and build an [`ChainHeadBackend`].
pub struct ChainHeadBackendBuilder<T> {
max_block_life: usize,
transaction_timeout_secs: usize,
submit_transactions_ignoring_follow_events: bool,
_marker: std::marker::PhantomData<T>,
}
impl<T: Config> Default for ChainHeadBackendBuilder<T> {
fn default() -> Self {
Self::new()
}
}
impl<T: Config> ChainHeadBackendBuilder<T> {
/// Create a new [`ChainHeadBackendBuilder`].
pub fn new() -> Self {
Self {
max_block_life: usize::MAX,
transaction_timeout_secs: 240,
submit_transactions_ignoring_follow_events: false,
_marker: std::marker::PhantomData,
}
}
/// The age of a block is defined here as the difference between the current finalized block number
/// and the block number of a given block. Once the difference equals or exceeds the number given
/// here, the block is unpinned.
///
/// By default, we will never automatically unpin blocks, but if the number of pinned blocks that we
/// keep hold of exceeds the number that the server can tolerate, then a `stop` event is generated and
/// we are forced to resubscribe, losing any pinned blocks.
pub fn max_block_life(mut self, max_block_life: usize) -> Self {
self.max_block_life = max_block_life;
self
}
/// When a transaction is submitted, we wait for events indicating it's successfully made it into a finalized
/// block. If it takes too long for this to happen, we assume that something went wrong and that we should
/// give up waiting.
///
/// Provide a value here to denote how long, in seconds, to wait before giving up. Defaults to 240 seconds.
///
/// If [`Self::submit_transactions_ignoring_follow_events()`] is called, this timeout is ignored.
pub fn transaction_timeout(mut self, timeout_secs: usize) -> Self {
self.transaction_timeout_secs = timeout_secs;
self
}
/// When a transaction is submitted, we normally synchronize the events that we get back with events from
/// our background `chainHead_follow` subscription, to ensure that any blocks hashes that we see can be
/// immediately queried (for example to get events or state at that block), and are kept around unless they
/// are no longer needed.
///
/// The main downside of this synchronization is that there may be a delay in being handed back a
/// [`TransactionStatus::InFinalizedBlock`] event while we wait to see the same block hash emitted from
/// our background `chainHead_follow` subscription in order to ensure it's available for querying.
///
/// Calling this method turns off this synchronization, speeding up the response and removing any reliance
/// on the `chainHead_follow` subscription continuing to run without stopping throughout submitting a transaction.
///
/// # Warning
///
/// This can lead to errors when calling APIs like `wait_for_finalized_success`, which will try to retrieve events
/// at the finalized block, because there will be a race and the finalized block may not be available for querying
/// yet.
pub fn submit_transactions_ignoring_follow_events(mut self) -> Self {
self.submit_transactions_ignoring_follow_events = true;
self
}
/// A low-level API to build the backend and driver which requires polling the driver for the backend
/// to make progress.
///
/// This is useful if you want to manage the driver yourself, for example if you want to run it in on
/// a specific runtime.
///
/// If you just want to run the driver in the background until completion in on the default runtime,
/// use [`ChainHeadBackendBuilder::build_with_background_driver`] instead.
pub fn build(
self,
client: impl Into<RpcClient>,
) -> (ChainHeadBackend<T>, ChainHeadBackendDriver<T>) {
// Construct the underlying follow_stream layers:
let rpc_methods = ChainHeadRpcMethods::new(client.into());
let follow_stream =
follow_stream::FollowStream::<HashFor<T>>::from_methods(rpc_methods.clone());
let follow_stream_unpin =
follow_stream_unpin::FollowStreamUnpin::<HashFor<T>>::from_methods(
follow_stream,
rpc_methods.clone(),
self.max_block_life,
);
let follow_stream_driver = FollowStreamDriver::new(follow_stream_unpin);
// Wrap these into the backend and driver that we'll expose.
let backend = ChainHeadBackend {
methods: rpc_methods,
follow_handle: follow_stream_driver.handle(),
transaction_timeout_secs: self.transaction_timeout_secs,
submit_transactions_ignoring_follow_events: self
.submit_transactions_ignoring_follow_events,
};
let driver = ChainHeadBackendDriver {
driver: follow_stream_driver,
};
(backend, driver)
}
/// An API to build the backend and driver which will run in the background until completion
/// on the default runtime.
///
/// - On non-wasm targets, this will spawn the driver on `tokio`.
/// - On wasm targets, this will spawn the driver on `wasm-bindgen-futures`.
#[cfg(feature = "runtime")]
#[cfg_attr(docsrs, doc(cfg(feature = "runtime")))]
pub fn build_with_background_driver(self, client: impl Into<RpcClient>) -> ChainHeadBackend<T> {
fn spawn<F: std::future::Future + Send + 'static>(future: F) {
#[cfg(not(target_family = "wasm"))]
tokio::spawn(async move {
future.await;
});
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
wasm_bindgen_futures::spawn_local(async move {
future.await;
});
}
let (backend, mut driver) = self.build(client);
spawn(async move {
// NOTE: we need to poll the driver until it's done i.e returns None
// to ensure that the backend is shutdown properly.
while let Some(res) = driver.next().await {
if let Err(err) = res {
tracing::debug!(target: "subxt", "chainHead backend error={err}");
}
}
tracing::debug!(target: "subxt", "chainHead backend was closed");
});
backend
}
}
/// Driver for the [`ChainHeadBackend`]. This must be polled in order for the
/// backend to make progress.
#[derive(Debug)]
pub struct ChainHeadBackendDriver<T: Config> {
driver: FollowStreamDriver<HashFor<T>>,
}
impl<T: Config> Stream for ChainHeadBackendDriver<T> {
type Item = <FollowStreamDriver<HashFor<T>> as Stream>::Item;
fn poll_next(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Self::Item>> {
self.driver.poll_next_unpin(cx)
}
}
/// The chainHead backend.
#[derive(Debug, Clone)]
pub struct ChainHeadBackend<T: Config> {
// RPC methods we'll want to call:
methods: ChainHeadRpcMethods<T>,
// A handle to the chainHead_follow subscription:
follow_handle: FollowStreamDriverHandle<HashFor<T>>,
// How long to wait until giving up on transactions:
transaction_timeout_secs: usize,
// Don't synchronise blocks with chainHead_follow when submitting txs:
submit_transactions_ignoring_follow_events: bool,
}
impl<T: Config> ChainHeadBackend<T> {
/// Configure and construct an [`ChainHeadBackend`] and the associated [`ChainHeadBackendDriver`].
pub fn builder() -> ChainHeadBackendBuilder<T> {
ChainHeadBackendBuilder::new()
}
/// Stream block headers based on the provided filter fn
async fn stream_headers<F>(
&self,
f: F,
) -> Result<StreamOfResults<(T::Header, BlockRef<HashFor<T>>)>, BackendError>
where
F: Fn(
FollowEvent<follow_stream_unpin::BlockRef<HashFor<T>>>,
) -> Vec<follow_stream_unpin::BlockRef<HashFor<T>>>
+ Send
+ Sync
+ 'static,
{
let methods = self.methods.clone();
let headers =
FollowStreamFinalizedHeads::new(self.follow_handle.subscribe(), f).flat_map(move |r| {
let methods = methods.clone();
let (sub_id, block_refs) = match r {
Ok(ev) => ev,
Err(e) => return Either::Left(futures::stream::once(async { Err(e) })),
};
Either::Right(
futures::stream::iter(block_refs).filter_map(move |block_ref| {
let methods = methods.clone();
let sub_id = sub_id.clone();
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.into())),
};
Some(Ok((header, block_ref.into())))
}
}),
)
});
Ok(StreamOf(Box::pin(headers)))
}
}
impl<H: Hash + 'static> BlockRefT for follow_stream_unpin::BlockRef<H> {}
impl<H: Hash + 'static> From<follow_stream_unpin::BlockRef<H>> for BlockRef<H> {
fn from(b: follow_stream_unpin::BlockRef<H>) -> Self {
BlockRef::new(b.hash(), b)
}
}
impl<T: Config> super::sealed::Sealed for ChainHeadBackend<T> {}
#[async_trait]
impl<T: Config + Send + Sync + 'static> Backend<T> for ChainHeadBackend<T> {
async fn storage_fetch_values(
&self,
keys: Vec<Vec<u8>>,
at: HashFor<T>,
) -> Result<StreamOfResults<StorageResponse>, BackendError> {
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 stream = storage_items.filter_map(async |val| {
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(stream)))
})
.await
}
async fn storage_fetch_descendant_keys(
&self,
key: Vec<u8>,
at: HashFor<T>,
) -> Result<StreamOfResults<Vec<u8>>, BackendError> {
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_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(
&self,
key: Vec<u8>,
at: HashFor<T>,
) -> Result<StreamOfResults<StorageResponse>, BackendError> {
retry(|| async {
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(async |val| {
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<HashFor<T>, BackendError> {
retry(|| async {
let genesis_hash = self.methods.chainspec_v1_genesis_hash().await?;
Ok(genesis_hash)
})
.await
}
async fn block_header(&self, at: HashFor<T>) -> Result<Option<T::Header>, BackendError> {
retry(|| async {
let sub_id = get_subscription_id(&self.follow_handle).await?;
let header = self.methods.chainhead_v1_header(&sub_id, at).await?;
Ok(header)
})
.await
}
async fn block_body(&self, at: HashFor<T>) -> Result<Option<Vec<Vec<u8>>>, BackendError> {
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::LimitReached.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);
};
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<HashFor<T>>, BackendError> {
let next_ref: Option<BlockRef<HashFor<T>>> = self
.follow_handle
.subscribe()
.events()
.filter_map(|ev| {
let out = match ev {
FollowEvent::Initialized(init) => {
init.finalized_block_hashes.last().map(|b| b.clone().into())
}
_ => None,
};
std::future::ready(out)
})
.next()
.await;
next_ref.ok_or_else(|| RpcError::SubscriptionDropped.into())
}
async fn current_runtime_version(&self) -> Result<RuntimeVersion, BackendError> {
// Just start a stream of version infos, and return the first value we get from it.
let runtime_version = self.stream_runtime_version().await?.next().await;
match runtime_version {
None => Err(BackendError::Rpc(RpcError::SubscriptionDropped)),
Some(Err(e)) => Err(e),
Some(Ok(version)) => Ok(version),
}
}
async fn stream_runtime_version(
&self,
) -> Result<StreamOfResults<RuntimeVersion>, BackendError> {
// Keep track of runtime details announced in new blocks, and then when blocks
// are finalized, find the latest of these that has runtime details, and clear the rest.
let mut runtimes = HashMap::new();
let runtime_stream = self
.follow_handle
.subscribe()
.events()
.filter_map(move |ev| {
let output = match ev {
FollowEvent::Initialized(ev) => {
for finalized_block in ev.finalized_block_hashes {
runtimes.remove(&finalized_block.hash());
}
ev.finalized_block_runtime
}
FollowEvent::NewBlock(ev) => {
if let Some(runtime) = ev.new_runtime {
runtimes.insert(ev.block_hash.hash(), runtime);
}
None
}
FollowEvent::Finalized(ev) => {
let next_runtime = {
let mut it = ev
.finalized_block_hashes
.iter()
.rev()
.filter_map(|h| runtimes.get(&h.hash()).cloned())
.peekable();
let next = it.next();
if it.peek().is_some() {
tracing::warn!(
target: "subxt",
"Several runtime upgrades in the finalized blocks but only the latest runtime upgrade is returned"
);
}
next
};
// Remove finalized and pruned blocks as valid runtime upgrades.
for block in ev
.finalized_block_hashes
.iter()
.chain(ev.pruned_block_hashes.iter())
{
runtimes.remove(&block.hash());
}
next_runtime
}
_ => None,
};
let runtime_event = match output {
None => return std::future::ready(None),
Some(ev) => ev,
};
let runtime_details = match runtime_event {
RuntimeEvent::Invalid(err) => {
return std::future::ready(Some(Err(BackendError::Other(format!("Invalid runtime error using chainHead RPCs: {}", err.error)))))
}
RuntimeEvent::Valid(ev) => ev,
};
let runtime_version = RuntimeVersion {
spec_version: runtime_details.spec.spec_version,
transaction_version: runtime_details.spec.transaction_version
};
std::future::ready(Some(Ok(runtime_version)))
});
Ok(StreamOf::new(Box::pin(runtime_stream)))
}
async fn stream_all_block_headers(
&self,
_hasher: T::Hasher,
) -> Result<StreamOfResults<(T::Header, BlockRef<HashFor<T>>)>, BackendError> {
// 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) => {
vec![ev.block_hash]
}
_ => vec![],
})
.await
}
async fn stream_best_block_headers(
&self,
_hasher: T::Hasher,
) -> Result<StreamOfResults<(T::Header, BlockRef<HashFor<T>>)>, BackendError> {
// 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],
_ => vec![],
})
.await
}
async fn stream_finalized_block_headers(
&self,
_hasher: T::Hasher,
) -> Result<StreamOfResults<(T::Header, BlockRef<HashFor<T>>)>, BackendError> {
self.stream_headers(|ev| match ev {
FollowEvent::Initialized(init) => init.finalized_block_hashes,
FollowEvent::Finalized(ev) => ev.finalized_block_hashes,
_ => vec![],
})
.await
}
async fn submit_transaction(
&self,
extrinsic: &[u8],
) -> Result<StreamOfResults<TransactionStatus<HashFor<T>>>, BackendError> {
// Submit a transaction. This makes no attempt to sync with follow events,
async fn submit_transaction_ignoring_follow_events<T: Config>(
extrinsic: &[u8],
methods: &ChainHeadRpcMethods<T>,
) -> Result<StreamOfResults<TransactionStatus<HashFor<T>>>, BackendError> {
let tx_progress = methods
.transactionwatch_v1_submit_and_watch(extrinsic)
.await?
.map(|ev| {
ev.map(|tx_status| {
use pezkuwi_subxt_rpcs::methods::chain_head::TransactionStatus as RpcTransactionStatus;
match tx_status {
RpcTransactionStatus::Validated => TransactionStatus::Validated,
RpcTransactionStatus::Broadcasted => TransactionStatus::Broadcasted,
RpcTransactionStatus::BestChainBlockIncluded { block: None } => {
TransactionStatus::NoLongerInBestBlock
},
RpcTransactionStatus::BestChainBlockIncluded { block: Some(block) } => {
TransactionStatus::InBestBlock { hash: BlockRef::from_hash(block.hash) }
},
RpcTransactionStatus::Finalized { block } => {
TransactionStatus::InFinalizedBlock { hash: BlockRef::from_hash(block.hash) }
},
RpcTransactionStatus::Error { error } => {
TransactionStatus::Error { message: error }
},
RpcTransactionStatus::Invalid { error } => {
TransactionStatus::Invalid { message: error }
},
RpcTransactionStatus::Dropped { error } => {
TransactionStatus::Dropped { message: error }
},
}
}).map_err(Into::into)
});
Ok(StreamOf(Box::pin(tx_progress)))
}
// Submit a transaction. This synchronizes with chainHead_follow events to ensure
// that block hashes returned are ready to be queried.
async fn submit_transaction_tracking_follow_events<T: Config>(
extrinsic: &[u8],
transaction_timeout_secs: u64,
methods: &ChainHeadRpcMethods<T>,
follow_handle: &FollowStreamDriverHandle<HashFor<T>>,
) -> Result<StreamOfResults<TransactionStatus<HashFor<T>>>, BackendError> {
// We care about new and finalized block hashes.
enum SeenBlockMarker {
New,
Finalized,
}
// First, subscribe to new blocks.
let mut seen_blocks_sub = follow_handle.subscribe().events();
// Then, submit the transaction.
let mut tx_progress = methods
.transactionwatch_v1_submit_and_watch(extrinsic)
.await?;
let mut seen_blocks = HashMap::new();
let mut done = false;
// If we see the finalized event, we start waiting until we find a finalized block that
// matches, so we can guarantee to return a pinned block hash and be properly in sync
// with chainHead_follow.
let mut finalized_hash: Option<HashFor<T>> = None;
// Record the start time so that we can time out if things appear to take too long.
let start_instant = web_time::Instant::now();
// A quick helper to return a generic error.
let err_other = |s: &str| Some(Err(BackendError::Other(s.into())));
// Now we can attempt to associate tx events with pinned blocks.
let tx_stream = futures::stream::poll_fn(move |cx| {
loop {
// Bail early if we're finished; nothing else to do.
if done {
return Poll::Ready(None);
}
// Bail if we exceed 4 mins; something very likely went wrong.
if start_instant.elapsed().as_secs() > transaction_timeout_secs {
return Poll::Ready(err_other(
"Timeout waiting for the transaction to be finalized",
));
}
// Poll for a follow event, and error if the stream has unexpectedly ended.
let follow_ev_poll = match seen_blocks_sub.poll_next_unpin(cx) {
Poll::Ready(None) => {
return Poll::Ready(err_other(
"chainHead_follow stream ended unexpectedly",
));
}
Poll::Ready(Some(follow_ev)) => Poll::Ready(follow_ev),
Poll::Pending => Poll::Pending,
};
let follow_ev_is_pending = follow_ev_poll.is_pending();
// If there was a follow event, then handle it and loop around to see if there are more.
// We want to buffer follow events until we hit Pending, so that we are as up-to-date as possible
// for when we see a BestBlockChanged event, so that we have the best change of already having
// seen the block that it mentions and returning a proper pinned block.
if let Poll::Ready(follow_ev) = follow_ev_poll {
match follow_ev {
FollowEvent::NewBlock(ev) => {
// Optimization: once we have a `finalized_hash`, we only care about finalized
// block refs now and can avoid bothering to save new blocks.
if finalized_hash.is_none() {
seen_blocks.insert(
ev.block_hash.hash(),
(SeenBlockMarker::New, ev.block_hash),
);
}
}
FollowEvent::Finalized(ev) => {
for block_ref in ev.finalized_block_hashes {
seen_blocks.insert(
block_ref.hash(),
(SeenBlockMarker::Finalized, block_ref),
);
}
}
FollowEvent::Stop => {
// If we get this event, we'll lose all of our existing pinned blocks and have a gap
// in which we may lose the finalized block that the TX is in. For now, just error if
// this happens, to prevent the case in which we never see a finalized block and wait
// forever.
return Poll::Ready(err_other(
"chainHead_follow emitted 'stop' event during transaction submission",
));
}
_ => {}
}
continue;
}
// If we have a finalized hash, we are done looking for tx events and we are just waiting
// for a pinned block with a matching hash (which must appear eventually given it's finalized).
if let Some(hash) = &finalized_hash {
if let Some((SeenBlockMarker::Finalized, block_ref)) =
seen_blocks.remove(hash)
{
// Found it! Hand back the event with a pinned block. We're done.
done = true;
let ev = TransactionStatus::InFinalizedBlock {
hash: block_ref.into(),
};
return Poll::Ready(Some(Ok(ev)));
} else {
// Not found it! If follow ev is pending, then return pending here and wait for
// a new one to come in, else loop around and see if we get another one immediately.
seen_blocks.clear();
if follow_ev_is_pending {
return Poll::Pending;
} else {
continue;
}
}
}
// If we don't have a finalized block yet, we keep polling for tx progress events.
let tx_progress_ev = match tx_progress.poll_next_unpin(cx) {
Poll::Pending => return Poll::Pending,
Poll::Ready(None) => {
return Poll::Ready(err_other(
"No more transaction progress events, but we haven't seen a Finalized one yet",
));
}
Poll::Ready(Some(Err(e))) => return Poll::Ready(Some(Err(e.into()))),
Poll::Ready(Some(Ok(ev))) => ev,
};
// When we get one, map it to the correct format (or for finalized ev, wait for the pinned block):
use pezkuwi_subxt_rpcs::methods::chain_head::TransactionStatus as RpcTransactionStatus;
let tx_progress_ev = match tx_progress_ev {
RpcTransactionStatus::Finalized { block } => {
// We'll wait until we have seen this hash, to try to guarantee
// that when we return this event, the corresponding block is
// pinned and accessible.
finalized_hash = Some(block.hash);
continue;
}
RpcTransactionStatus::BestChainBlockIncluded { block: Some(block) } => {
// Look up a pinned block ref if we can, else return a non-pinned
// block that likely isn't accessible. We have no guarantee that a best
// block on the node a tx was sent to will ever be known about on the
// chainHead_follow subscription.
let block_ref = match seen_blocks.get(&block.hash) {
Some((_, block_ref)) => block_ref.clone().into(),
None => BlockRef::from_hash(block.hash),
};
TransactionStatus::InBestBlock { hash: block_ref }
}
RpcTransactionStatus::BestChainBlockIncluded { block: None } => {
TransactionStatus::NoLongerInBestBlock
}
RpcTransactionStatus::Broadcasted => TransactionStatus::Broadcasted,
RpcTransactionStatus::Dropped { error, .. } => {
TransactionStatus::Dropped { message: error }
}
RpcTransactionStatus::Error { error } => {
TransactionStatus::Error { message: error }
}
RpcTransactionStatus::Invalid { error } => {
TransactionStatus::Invalid { message: error }
}
RpcTransactionStatus::Validated => TransactionStatus::Validated,
};
return Poll::Ready(Some(Ok(tx_progress_ev)));
}
});
Ok(StreamOf(Box::pin(tx_stream)))
}
if self.submit_transactions_ignoring_follow_events {
submit_transaction_ignoring_follow_events(extrinsic, &self.methods).await
} else {
submit_transaction_tracking_follow_events::<T>(
extrinsic,
self.transaction_timeout_secs as u64,
&self.methods,
&self.follow_handle,
)
.await
}
}
async fn call(
&self,
method: &str,
call_parameters: Option<&[u8]>,
at: HashFor<T>,
) -> Result<Vec<u8>, BackendError> {
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::LimitReached.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);
};
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
}
}
/// A helper to obtain a subscription ID.
async fn get_subscription_id<H: Hash>(
follow_handle: &FollowStreamDriverHandle<H>,
) -> Result<String, BackendError> {
let Some(sub_id) = follow_handle.subscribe().subscription_id().await else {
return Err(RpcError::SubscriptionDropped.into());
};
Ok(sub_id)
}
@@ -0,0 +1,169 @@
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
use super::follow_stream_driver::FollowStreamDriverHandle;
use super::follow_stream_unpin::BlockRef;
use crate::config::{Config, HashFor};
use crate::error::{BackendError, RpcError};
use futures::{FutureExt, Stream, StreamExt};
use std::collections::VecDeque;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};
use pezkuwi_subxt_rpcs::methods::chain_head::{
ChainHeadRpcMethods, FollowEvent, MethodResponse, StorageQuery, StorageResult,
};
/// Obtain a stream of storage items given some query. this handles continuing
/// and stopping under the hood, and returns a stream of `StorageResult`s.
pub struct StorageItems<T: Config> {
done: bool,
operation_id: Arc<str>,
buffered_responses: VecDeque<StorageResult>,
continue_call: ContinueFutGetter,
continue_fut: Option<ContinueFut>,
follow_event_stream: FollowEventStream<HashFor<T>>,
}
impl<T: Config> StorageItems<T> {
// Subscribe to follow events, and return a stream of storage results
// given some storage queries. The stream will automatically resume as
// needed, and stop when done.
pub async fn from_methods(
queries: impl Iterator<Item = StorageQuery<&[u8]>>,
at: HashFor<T>,
follow_handle: &FollowStreamDriverHandle<HashFor<T>>,
methods: ChainHeadRpcMethods<T>,
) -> Result<Self, BackendError> {
let sub_id = super::get_subscription_id(follow_handle).await?;
// Subscribe to events and make the initial request to get an operation ID.
let follow_events = follow_handle.subscribe().events();
let status = methods
.chainhead_v1_storage(&sub_id, at, queries, None)
.await?;
let operation_id: Arc<str> = match status {
MethodResponse::LimitReached => return Err(RpcError::LimitReached.into()),
MethodResponse::Started(s) => s.operation_id.into(),
};
// A function which returns the call to continue the subscription:
let continue_call: ContinueFutGetter = {
let operation_id = operation_id.clone();
Box::new(move || {
let sub_id = sub_id.clone();
let operation_id = operation_id.clone();
let methods = methods.clone();
Box::pin(async move {
methods
.chainhead_v1_continue(&sub_id, &operation_id)
.await?;
Ok(())
})
})
};
Ok(StorageItems::new(
operation_id,
continue_call,
Box::pin(follow_events),
))
}
fn new(
operation_id: Arc<str>,
continue_call: ContinueFutGetter,
follow_event_stream: FollowEventStream<HashFor<T>>,
) -> Self {
Self {
done: false,
buffered_responses: VecDeque::new(),
operation_id,
continue_call,
continue_fut: None,
follow_event_stream,
}
}
}
pub type FollowEventStream<Hash> =
Pin<Box<dyn Stream<Item = FollowEvent<BlockRef<Hash>>> + Send + 'static>>;
pub type ContinueFutGetter = Box<dyn Fn() -> ContinueFut + Send + 'static>;
pub type ContinueFut = Pin<Box<dyn Future<Output = Result<(), BackendError>> + Send + 'static>>;
impl<T: Config> Stream for StorageItems<T> {
type Item = Result<StorageResult, BackendError>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
loop {
if self.done {
return Poll::Ready(None);
}
if let Some(item) = self.buffered_responses.pop_front() {
return Poll::Ready(Some(Ok(item)));
}
if let Some(mut fut) = self.continue_fut.take() {
match fut.poll_unpin(cx) {
Poll::Pending => {
self.continue_fut = Some(fut);
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)));
}
Poll::Ready(Ok(())) => {
// Finished; carry on.
}
}
}
let ev = match self.follow_event_stream.poll_next_unpin(cx) {
Poll::Pending => return Poll::Pending,
Poll::Ready(None) => return Poll::Ready(None),
Poll::Ready(Some(ev)) => ev,
};
match ev {
FollowEvent::OperationWaitingForContinue(id)
if id.operation_id == *self.operation_id =>
{
// Start a call to ask for more events
self.continue_fut = Some((self.continue_call)());
continue;
}
FollowEvent::OperationStorageDone(id) if id.operation_id == *self.operation_id => {
// We're finished!
self.done = true;
return Poll::Ready(None);
}
FollowEvent::OperationStorageItems(items)
if items.operation_id == *self.operation_id =>
{
// We have items; buffer them to emit next loops.
self.buffered_responses = items.items;
continue;
}
FollowEvent::OperationError(err) if err.operation_id == *self.operation_id => {
// Something went wrong obtaining storage items; mark as done and return the error.
self.done = true;
return Poll::Ready(Some(Err(BackendError::Other(err.error))));
}
_ => {
// We don't care about this event; wait for the next.
continue;
}
}
}
}
}
+662
View File
@@ -0,0 +1,662 @@
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
//! This module exposes a legacy backend implementation, which relies
//! on the legacy RPC API methods.
use self::rpc_methods::TransactionStatus as RpcTransactionStatus;
use crate::backend::utils::{retry, retry_stream};
use crate::backend::{
Backend, BlockRef, RuntimeVersion, StorageResponse, StreamOf, StreamOfResults,
TransactionStatus,
};
use crate::config::{Config, HashFor, Header};
use crate::error::BackendError;
use async_trait::async_trait;
use futures::TryStreamExt;
use futures::{Future, FutureExt, Stream, StreamExt, future, future::Either, stream};
use std::collections::VecDeque;
use std::pin::Pin;
use std::task::{Context, Poll};
use pezkuwi_subxt_rpcs::RpcClient;
/// Re-export legacy RPC types and methods from [`pezkuwi_subxt_rpcs::methods::legacy`].
pub mod rpc_methods {
pub use pezkuwi_subxt_rpcs::methods::legacy::*;
}
// Expose the RPC methods.
pub use rpc_methods::LegacyRpcMethods;
/// Configure and build an [`LegacyBackend`].
pub struct LegacyBackendBuilder<T> {
storage_page_size: u32,
_marker: std::marker::PhantomData<T>,
}
impl<T: Config> Default for LegacyBackendBuilder<T> {
fn default() -> Self {
Self::new()
}
}
impl<T: Config> LegacyBackendBuilder<T> {
/// Create a new [`LegacyBackendBuilder`].
pub fn new() -> Self {
Self {
storage_page_size: 64,
_marker: std::marker::PhantomData,
}
}
/// Iterating over storage entries using the [`LegacyBackend`] requires
/// fetching entries in batches. This configures the number of entries that
/// we'll try to obtain in each batch (default: 64).
pub fn storage_page_size(mut self, storage_page_size: u32) -> Self {
self.storage_page_size = storage_page_size;
self
}
/// Given an [`RpcClient`] to use to make requests, this returns a [`LegacyBackend`],
/// which implements the [`Backend`] trait.
pub fn build(self, client: impl Into<RpcClient>) -> LegacyBackend<T> {
LegacyBackend {
storage_page_size: self.storage_page_size,
methods: LegacyRpcMethods::new(client.into()),
}
}
}
/// The legacy backend.
#[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> {
LegacyBackendBuilder::new()
}
}
impl<T: Config> super::sealed::Sealed for LegacyBackend<T> {}
#[async_trait]
impl<T: Config + Send + Sync + 'static> Backend<T> for LegacyBackend<T> {
async fn storage_fetch_values(
&self,
keys: Vec<Vec<u8>>,
at: HashFor<T>,
) -> Result<StreamOfResults<StorageResponse>, BackendError> {
fn get_entry<T: Config>(
key: Vec<u8>,
at: HashFor<T>,
methods: LegacyRpcMethods<T>,
) -> impl Future<Output = Result<Option<StorageResponse>, BackendError>> {
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| get_entry(key, at, methods.clone()));
let s = stream::iter(iter)
// Resolve the future
.then(|fut| fut)
// Filter any Options out (ie if we didn't find a value at some key we return nothing for it).
.filter_map(|r| future::ready(r.transpose()));
Ok(StreamOf(Box::pin(s)))
}
async fn storage_fetch_descendant_keys(
&self,
key: Vec<u8>,
at: HashFor<T>,
) -> Result<StreamOfResults<Vec<u8>>, BackendError> {
let keys = StorageFetchDescendantKeysStream {
at,
key,
storage_page_size: self.storage_page_size,
methods: self.methods.clone(),
done: Default::default(),
keys_fut: Default::default(),
pagination_start_key: None,
};
let keys = keys.flat_map(|keys| {
match keys {
Err(e) => {
// If there's an error, return that next:
Either::Left(stream::iter(std::iter::once(Err(e))))
}
Ok(keys) => {
// Or, stream each "ok" value:
Either::Right(stream::iter(keys.into_iter().map(Ok)))
}
}
});
Ok(StreamOf(Box::pin(keys)))
}
async fn storage_fetch_descendant_values(
&self,
key: Vec<u8>,
at: HashFor<T>,
) -> Result<StreamOfResults<StorageResponse>, BackendError> {
let keys_stream = StorageFetchDescendantKeysStream {
at,
key,
storage_page_size: self.storage_page_size,
methods: self.methods.clone(),
done: Default::default(),
keys_fut: Default::default(),
pagination_start_key: None,
};
Ok(StreamOf(Box::pin(StorageFetchDescendantValuesStream {
keys: keys_stream,
results_fut: None,
results: Default::default(),
})))
}
async fn genesis_hash(&self) -> Result<HashFor<T>, BackendError> {
retry(|| async {
let hash = self.methods.genesis_hash().await?;
Ok(hash)
})
.await
}
async fn block_header(&self, at: HashFor<T>) -> Result<Option<T::Header>, BackendError> {
retry(|| async {
let header = self.methods.chain_get_header(Some(at)).await?;
Ok(header)
})
.await
}
async fn block_body(&self, at: HashFor<T>) -> Result<Option<Vec<Vec<u8>>>, BackendError> {
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<HashFor<T>>, BackendError> {
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, BackendError> {
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>, BackendError> {
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_err(|e| e.into()).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 mut keep = true;
if let Err(e) = r {
if e.is_disconnected_will_reconnect() {
keep = false;
}
}
async move { keep }
});
Ok(StreamOf(Box::pin(stream)))
}
async fn stream_all_block_headers(
&self,
hasher: T::Hasher,
) -> Result<StreamOfResults<(T::Header, BlockRef<HashFor<T>>)>, BackendError> {
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_err(|e| e.into()).map(move |r| {
r.map(|h| {
let hash = h.hash_with(hasher);
(h, BlockRef::from_hash(hash))
})
});
Ok(StreamOf(Box::pin(sub)))
})
})
.await?;
Ok(retry_sub)
}
async fn stream_best_block_headers(
&self,
hasher: T::Hasher,
) -> Result<StreamOfResults<(T::Header, BlockRef<HashFor<T>>)>, BackendError> {
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_err(|e| e.into()).map(move |r| {
r.map(|h| {
let hash = h.hash_with(hasher);
(h, BlockRef::from_hash(hash))
})
});
Ok(StreamOf(Box::pin(sub)))
})
})
.await?;
Ok(retry_sub)
}
async fn stream_finalized_block_headers(
&self,
hasher: T::Hasher,
) -> Result<StreamOfResults<(T::Header, BlockRef<HashFor<T>>)>, BackendError> {
let this = self.clone();
let retry_sub = retry_stream(move || {
let this = this.clone();
Box::pin(async move {
let sub = this.methods.chain_subscribe_finalized_heads().await?;
// 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(move |r| {
r.map(|h| {
let hash = h.hash_with(hasher);
(h, BlockRef::from_hash(hash))
})
});
Ok(StreamOf(Box::pin(sub)))
})
})
.await?;
Ok(retry_sub)
}
async fn submit_transaction(
&self,
extrinsic: &[u8],
) -> Result<StreamOfResults<TransactionStatus<HashFor<T>>>, BackendError> {
let sub = self
.methods
.author_submit_and_watch_extrinsic(extrinsic)
.await?;
let sub = sub.filter_map(|r| {
let mapped = r
.map_err(|e| e.into())
.map(|tx| {
match tx {
// We ignore these because they don't map nicely to the new API. They don't signal "end states" so this should be fine.
RpcTransactionStatus::Future => None,
RpcTransactionStatus::Retracted(_) => None,
// These roughly map across:
RpcTransactionStatus::Ready => Some(TransactionStatus::Validated),
RpcTransactionStatus::Broadcast(_peers) => {
Some(TransactionStatus::Broadcasted)
}
RpcTransactionStatus::InBlock(hash) => {
Some(TransactionStatus::InBestBlock {
hash: BlockRef::from_hash(hash),
})
}
// These 5 mean that the stream will very likely end:
RpcTransactionStatus::FinalityTimeout(_) => {
Some(TransactionStatus::Dropped {
message: "Finality timeout".into(),
})
}
RpcTransactionStatus::Finalized(hash) => {
Some(TransactionStatus::InFinalizedBlock {
hash: BlockRef::from_hash(hash),
})
}
RpcTransactionStatus::Usurped(_) => Some(TransactionStatus::Invalid {
message: "Transaction was usurped by another with the same nonce"
.into(),
}),
RpcTransactionStatus::Dropped => Some(TransactionStatus::Dropped {
message: "Transaction was dropped".into(),
}),
RpcTransactionStatus::Invalid => Some(TransactionStatus::Invalid {
message:
"Transaction is invalid (eg because of a bad nonce, signature etc)"
.into(),
}),
}
})
.transpose();
future::ready(mapped)
});
Ok(StreamOf::new(Box::pin(sub)))
}
async fn call(
&self,
method: &str,
call_parameters: Option<&[u8]>,
at: HashFor<T>,
) -> Result<Vec<u8>, BackendError> {
retry(|| async {
let res = self
.methods
.state_call(method, call_parameters, Some(at))
.await?;
Ok(res)
})
.await
}
}
/// Note: This is exposed for testing but is not considered stable and may change
/// without notice in a patch release.
#[doc(hidden)]
pub fn subscribe_to_block_headers_filling_in_gaps<T, S, E>(
methods: LegacyRpcMethods<T>,
sub: S,
mut last_block_num: Option<u64>,
) -> impl Stream<Item = Result<T::Header, BackendError>> + Send
where
T: Config,
S: Stream<Item = Result<T::Header, E>> + Send,
E: Into<BackendError> + Send + 'static,
{
sub.flat_map(move |s| {
// Get the header, or return a stream containing just the error.
let header = match s {
Ok(header) => header,
Err(e) => return Either::Left(stream::once(async { Err(e.into()) })),
};
// We want all previous details up to, but not including this current block num.
let end_block_num = header.number().into();
// This is one after the last block we returned details for last time.
let start_block_num = last_block_num.map(|n| n + 1).unwrap_or(end_block_num);
// Iterate over all of the previous blocks we need headers for, ignoring the current block
// (which we already have the header info for):
let methods = methods.clone();
let previous_headers = stream::iter(start_block_num..end_block_num)
.then(move |n| {
let methods = methods.clone();
async move {
let hash = methods.chain_get_block_hash(Some(n.into())).await?;
let header = methods.chain_get_header(hash).await?;
Ok::<_, BackendError>(header)
}
})
.filter_map(async |h| h.transpose());
// On the next iteration, we'll get details starting just after this end block.
last_block_num = Some(end_block_num);
// Return a combination of any previous headers plus the new header.
Either::Right(previous_headers.chain(stream::once(async { Ok(header) })))
})
}
/// This provides a stream of values given some prefix `key`. It
/// internally manages pagination and such.
#[allow(clippy::type_complexity)]
pub struct StorageFetchDescendantKeysStream<T: Config> {
methods: LegacyRpcMethods<T>,
key: Vec<u8>,
at: HashFor<T>,
// How many entries to ask for each time.
storage_page_size: u32,
// What key do we start paginating from? None = from the beginning.
pagination_start_key: Option<Vec<u8>>,
// Keys, future and cached:
keys_fut:
Option<Pin<Box<dyn Future<Output = Result<Vec<Vec<u8>>, BackendError>> + Send + 'static>>>,
// Set to true when we're done:
done: bool,
}
impl<T: Config> std::marker::Unpin for StorageFetchDescendantKeysStream<T> {}
impl<T: Config> Stream for StorageFetchDescendantKeysStream<T> {
type Item = Result<Vec<Vec<u8>>, BackendError>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let mut this = self.as_mut();
loop {
// We're already done.
if this.done {
return Poll::Ready(None);
}
// Poll future to fetch next keys.
if let Some(mut keys_fut) = this.keys_fut.take() {
let Poll::Ready(keys) = keys_fut.poll_unpin(cx) else {
this.keys_fut = Some(keys_fut);
return Poll::Pending;
};
match keys {
Ok(mut keys) => {
if this.pagination_start_key.is_some()
&& keys.first() == this.pagination_start_key.as_ref()
{
// Currently, Smoldot returns the "start key" as the first key in the input
// (see https://github.com/smol-dot/smoldot/issues/1692), whereas Substrate doesn't.
// We don't expect the start key to be returned either (since it was the last key of prev
// iteration), so remove it if we see it. This `remove()` method isn't very efficient but
// this will be a non issue with the RPC V2 APIs or if Smoldot aligns with Substrate anyway.
keys.remove(0);
}
if keys.is_empty() {
// No keys left; we're done!
this.done = true;
return Poll::Ready(None);
}
// The last key is where we want to paginate from next time.
this.pagination_start_key = keys.last().cloned();
// return all of the keys from this run.
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)));
}
}
}
// Else, we don't have a fut to get keys yet so start one going.
let methods = this.methods.clone();
let key = this.key.clone();
let at = this.at;
let storage_page_size = this.storage_page_size;
let pagination_start_key = this.pagination_start_key.clone();
let keys_fut = async move {
let keys = methods
.state_get_keys_paged(
&key,
storage_page_size,
pagination_start_key.as_deref(),
Some(at),
)
.await?;
Ok(keys)
};
this.keys_fut = Some(Box::pin(keys_fut));
}
}
}
/// This provides a stream of values given some stream of keys.
#[allow(clippy::type_complexity)]
pub struct StorageFetchDescendantValuesStream<T: Config> {
// Stream of keys.
keys: StorageFetchDescendantKeysStream<T>,
// Then we track the future to get the values back for each key:
results_fut: Option<
Pin<
Box<
dyn Future<Output = Result<Option<VecDeque<(Vec<u8>, Vec<u8>)>>, BackendError>>
+ Send
+ 'static,
>,
>,
>,
// And finally we return each result back one at a time:
results: VecDeque<(Vec<u8>, Vec<u8>)>,
}
impl<T: Config> Stream for StorageFetchDescendantValuesStream<T> {
type Item = Result<StorageResponse, BackendError>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let mut this = self.as_mut();
loop {
// If we have results back, return them one by one
if let Some((key, value)) = this.results.pop_front() {
let res = StorageResponse { key, value };
return Poll::Ready(Some(Ok(res)));
}
// If we're waiting on the next results then poll that future:
if let Some(mut results_fut) = this.results_fut.take() {
match results_fut.poll_unpin(cx) {
Poll::Ready(Ok(Some(results))) => {
this.results = results;
continue;
}
Poll::Ready(Ok(None)) => {
// No values back for some keys? Skip.
continue;
}
Poll::Ready(Err(e)) => return Poll::Ready(Some(Err(e))),
Poll::Pending => {
this.results_fut = Some(results_fut);
return Poll::Pending;
}
}
}
match this.keys.poll_next_unpin(cx) {
Poll::Ready(Some(Ok(keys))) => {
let methods = this.keys.methods.clone();
let at = this.keys.at;
let results_fut = async move {
let keys = keys.iter().map(|k| &**k);
let values = retry(|| async {
let res = methods
.state_query_storage_at(keys.clone(), Some(at))
.await?;
Ok(res)
})
.await?;
let values: VecDeque<_> = values
.into_iter()
.flat_map(|v| {
v.changes.into_iter().filter_map(|(k, v)| {
let v = v?;
Some((k.0, v.0))
})
})
.collect();
Ok(Some(values))
};
this.results_fut = Some(Box::pin(results_fut));
continue;
}
Poll::Ready(Some(Err(e))) => return Poll::Ready(Some(Err(e))),
Poll::Ready(None) => return Poll::Ready(None),
Poll::Pending => return Poll::Pending,
}
}
}
}
File diff suppressed because it is too large Load Diff
+276
View File
@@ -0,0 +1,276 @@
//! RPC utils.
use super::{StreamOf, StreamOfResults};
use crate::error::BackendError;
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>, BackendError>> + Send>>;
pub(crate) enum PendingOrStream<T> {
Pending(BoxFuture<'static, Result<StreamOfResults<T>, BackendError>>),
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, BackendError>;
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
///
/// ```rust,no_run,standalone_crate
/// use subxt::backend::utils::retry;
///
/// async fn some_future() -> Result<(), subxt::error::BackendError> {
/// 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, BackendError>
where
F: FnMut() -> T,
T: Future<Output = Result<R, BackendError>>,
{
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, in the event of a disconnection,
// we may not get the correct subscription ID back on reconnecting.
//
// This is because we have a race between this future and the
// separate chainHead subscription, which runs in a different task.
// if this future is too quick, it'll be given back an old
// subscription ID from the chainHead subscription which has yet
// to reconnect and establish a new subscription ID.
//
// In the event of a wrong subscription Id being used, we happen to
// hand back an `RpcError::LimitReached`, and so can retry when we
// specifically hit that error to see if we get a new subscription ID
// eventually.
if e.is_rpc_limit_reached() && 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
///
/// ```rust,no_run,standalone_crate
/// 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>, BackendError>
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() -> BackendError {
BackendError::Rpc(pezkuwi_subxt_rpcs::Error::DisconnectedWillReconnect(String::new()).into())
}
fn custom_err() -> BackendError {
BackendError::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, BackendError>>>()
.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(|| 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(|| 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, BackendError::Other(_))));
}
}
+195
View File
@@ -0,0 +1,195 @@
// Copyright 2019-2025 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::BlockRef,
blocks::Extrinsics,
client::{OfflineClientT, OnlineClientT},
config::{Config, HashFor, Header},
error::{AccountNonceError, BlockError, EventsError, ExtrinsicError},
events,
runtime_api::RuntimeApi,
storage::StorageClientAt,
};
use codec::{Decode, Encode};
use futures::lock::Mutex as AsyncMutex;
use std::sync::Arc;
/// A representation of a block.
pub struct Block<T: Config, C> {
header: T::Header,
block_ref: BlockRef<HashFor<T>>,
client: C,
// Since we obtain the same events for every extrinsic, let's
// cache them so that we only ever do that once:
cached_events: CachedEvents<T>,
}
impl<T: Config, C: Clone> Clone for Block<T, C> {
fn clone(&self) -> Self {
Self {
header: self.header.clone(),
block_ref: self.block_ref.clone(),
client: self.client.clone(),
cached_events: self.cached_events.clone(),
}
}
}
// A cache for our events so we don't fetch them more than once when
// iterating over events for extrinsics.
pub(crate) type CachedEvents<T> = Arc<AsyncMutex<Option<events::Events<T>>>>;
impl<T, C> Block<T, C>
where
T: Config,
C: OfflineClientT<T>,
{
pub(crate) fn new(header: T::Header, block_ref: BlockRef<HashFor<T>>, client: C) -> Self {
Block {
header,
block_ref,
client,
cached_events: Default::default(),
}
}
/// Return a reference to the given block. While this reference is kept alive,
/// the backend will (if possible) endeavour to keep hold of the block.
pub fn reference(&self) -> BlockRef<HashFor<T>> {
self.block_ref.clone()
}
/// Return the block hash.
pub fn hash(&self) -> HashFor<T> {
self.block_ref.hash()
}
/// Return the block number.
pub fn number(&self) -> <T::Header as crate::config::Header>::Number {
self.header().number()
}
/// Return the entire block header.
pub fn header(&self) -> &T::Header {
&self.header
}
}
impl<T, C> Block<T, C>
where
T: Config,
C: OnlineClientT<T>,
{
/// Return the events associated with the block, fetching them from the node if necessary.
pub async fn events(&self) -> Result<events::Events<T>, EventsError> {
get_events(&self.client, self.hash(), &self.cached_events).await
}
/// Fetch and return the extrinsics in the block body.
pub async fn extrinsics(&self) -> Result<Extrinsics<T, C>, ExtrinsicError> {
let block_hash = self.hash();
let extrinsics = self
.client
.backend()
.block_body(block_hash)
.await
.map_err(ExtrinsicError::CannotGetBlockBody)?
.ok_or_else(|| ExtrinsicError::BlockNotFound(block_hash.into()))?;
let extrinsics = Extrinsics::new(
self.client.clone(),
extrinsics,
self.cached_events.clone(),
block_hash,
)?;
Ok(extrinsics)
}
/// Work with storage.
pub fn storage(&self) -> StorageClientAt<T, C> {
StorageClientAt::new(self.client.clone(), self.block_ref.clone())
}
/// Execute a runtime API call at this block.
pub async fn runtime_api(&self) -> RuntimeApi<T, C> {
RuntimeApi::new(self.client.clone(), self.block_ref.clone())
}
/// Get the account nonce for a given account ID at this block.
pub async fn account_nonce(&self, account_id: &T::AccountId) -> Result<u64, BlockError> {
get_account_nonce(&self.client, account_id, self.hash())
.await
.map_err(|e| BlockError::AccountNonceError {
block_hash: self.hash().into(),
account_id: account_id.encode().into(),
reason: e,
})
}
}
// Return Events from the cache, or fetch from the node if needed.
pub(crate) async fn get_events<C, T>(
client: &C,
block_hash: HashFor<T>,
cached_events: &AsyncMutex<Option<events::Events<T>>>,
) -> Result<events::Events<T>, EventsError>
where
T: Config,
C: OnlineClientT<T>,
{
// Acquire lock on the events cache. We either get back our events or we fetch and set them
// before unlocking, so only one fetch call should ever be made. We do this because the
// same events can be shared across all extrinsics in the block.
let mut lock = cached_events.lock().await;
let events = match &*lock {
Some(events) => events.clone(),
None => {
let events = events::EventsClient::new(client.clone())
.at(block_hash)
.await?;
lock.replace(events.clone());
events
}
};
Ok(events)
}
// Return the account nonce at some block hash for an account ID.
pub(crate) async fn get_account_nonce<C, T>(
client: &C,
account_id: &T::AccountId,
block_hash: HashFor<T>,
) -> Result<u64, AccountNonceError>
where
C: OnlineClientT<T>,
T: Config,
{
let account_nonce_bytes = client
.backend()
.call(
"AccountNonceApi_account_nonce",
Some(&account_id.encode()),
block_hash,
)
.await?;
// custom decoding from a u16/u32/u64 into a u64, based on the number of bytes we got back.
let cursor = &mut &account_nonce_bytes[..];
let account_nonce: u64 = match account_nonce_bytes.len() {
2 => u16::decode(cursor)?.into(),
4 => u32::decode(cursor)?.into(),
8 => u64::decode(cursor)?,
_ => {
return Err(AccountNonceError::WrongNumberOfBytes(
account_nonce_bytes.len(),
));
}
};
Ok(account_nonce)
}
+192
View File
@@ -0,0 +1,192 @@
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
use super::Block;
use crate::{
backend::{BlockRef, StreamOfResults},
client::OnlineClientT,
config::{Config, HashFor},
error::BlockError,
utils::PhantomDataSendSync,
};
use derive_where::derive_where;
use futures::StreamExt;
use std::future::Future;
type BlockStream<T> = StreamOfResults<T>;
type BlockStreamRes<T> = Result<BlockStream<T>, BlockError>;
/// A client for working with blocks.
#[derive_where(Clone; Client)]
pub struct BlocksClient<T, Client> {
client: Client,
_marker: PhantomDataSendSync<T>,
}
impl<T, Client> BlocksClient<T, Client> {
/// Create a new [`BlocksClient`].
pub fn new(client: Client) -> Self {
Self {
client,
_marker: PhantomDataSendSync::new(),
}
}
}
impl<T, Client> BlocksClient<T, Client>
where
T: Config,
Client: OnlineClientT<T>,
{
/// Obtain block details given the provided block hash.
///
/// # Warning
///
/// This call only supports blocks produced since the most recent
/// runtime upgrade. You can attempt to retrieve older blocks,
/// but may run into errors attempting to work with them.
pub fn at(
&self,
block_ref: impl Into<BlockRef<HashFor<T>>>,
) -> impl Future<Output = Result<Block<T, Client>, BlockError>> + Send + 'static {
self.at_or_latest(Some(block_ref.into()))
}
/// Obtain block details of the latest finalized block.
pub fn at_latest(
&self,
) -> impl Future<Output = Result<Block<T, Client>, BlockError>> + Send + 'static {
self.at_or_latest(None)
}
/// Obtain block details given the provided block hash, or the latest block if `None` is
/// provided.
fn at_or_latest(
&self,
block_ref: Option<BlockRef<HashFor<T>>>,
) -> impl Future<Output = Result<Block<T, Client>, BlockError>> + Send + 'static {
let client = self.client.clone();
async move {
// If a block ref isn't provided, we'll get the latest finalized ref to use.
let block_ref = match block_ref {
Some(r) => r,
None => client
.backend()
.latest_finalized_block_ref()
.await
.map_err(BlockError::CouldNotGetLatestBlock)?,
};
let maybe_block_header = client
.backend()
.block_header(block_ref.hash())
.await
.map_err(|e| BlockError::CouldNotGetBlockHeader {
block_hash: block_ref.hash().into(),
reason: e,
})?;
let block_header = match maybe_block_header {
Some(header) => header,
None => {
return Err(BlockError::BlockNotFound {
block_hash: block_ref.hash().into(),
});
}
};
Ok(Block::new(block_header, block_ref, client))
}
}
/// Subscribe to all new blocks imported by the node.
///
/// **Note:** You probably want to use [`Self::subscribe_finalized()`] most of
/// the time.
pub fn subscribe_all(
&self,
) -> impl Future<Output = Result<BlockStream<Block<T, Client>>, BlockError>> + Send + 'static
where
Client: Send + Sync + 'static,
{
let client = self.client.clone();
let hasher = client.hasher();
header_sub_fut_to_block_sub(self.clone(), async move {
let stream = client
.backend()
.stream_all_block_headers(hasher)
.await
.map_err(BlockError::CouldNotSubscribeToAllBlocks)?;
BlockStreamRes::Ok(stream)
})
}
/// Subscribe to all new blocks imported by the node onto the current best fork.
///
/// **Note:** You probably want to use [`Self::subscribe_finalized()`] most of
/// the time.
pub fn subscribe_best(
&self,
) -> impl Future<Output = Result<BlockStream<Block<T, Client>>, BlockError>> + Send + 'static
where
Client: Send + Sync + 'static,
{
let client = self.client.clone();
let hasher = client.hasher();
header_sub_fut_to_block_sub(self.clone(), async move {
let stream = client
.backend()
.stream_best_block_headers(hasher)
.await
.map_err(BlockError::CouldNotSubscribeToBestBlocks)?;
BlockStreamRes::Ok(stream)
})
}
/// Subscribe to finalized blocks.
pub fn subscribe_finalized(
&self,
) -> impl Future<Output = Result<BlockStream<Block<T, Client>>, BlockError>> + Send + 'static
where
Client: Send + Sync + 'static,
{
let client = self.client.clone();
let hasher = client.hasher();
header_sub_fut_to_block_sub(self.clone(), async move {
let stream = client
.backend()
.stream_finalized_block_headers(hasher)
.await
.map_err(BlockError::CouldNotSubscribeToFinalizedBlocks)?;
BlockStreamRes::Ok(stream)
})
}
}
/// Take a promise that will return a subscription to some block headers,
/// and return a subscription to some blocks based on this.
async fn header_sub_fut_to_block_sub<T, Client, S>(
blocks_client: BlocksClient<T, Client>,
sub: S,
) -> Result<BlockStream<Block<T, Client>>, BlockError>
where
T: Config,
S: Future<Output = Result<BlockStream<(T::Header, BlockRef<HashFor<T>>)>, BlockError>>
+ Send
+ 'static,
Client: OnlineClientT<T> + Send + Sync + 'static,
{
let sub = sub.await?.then(move |header_and_ref| {
let client = blocks_client.client.clone();
async move {
let (header, block_ref) = match header_and_ref {
Ok(header_and_ref) => header_and_ref,
Err(e) => return Err(e),
};
Ok(Block::new(header, block_ref, client))
}
});
BlockStreamRes::Ok(StreamOfResults::new(Box::pin(sub)))
}
+350
View File
@@ -0,0 +1,350 @@
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
use crate::{
blocks::block_types::{CachedEvents, get_events},
client::{OfflineClientT, OnlineClientT},
config::{Config, HashFor},
error::{EventsError, ExtrinsicDecodeErrorAt, ExtrinsicError},
events,
};
use derive_where::derive_where;
use scale_decode::{DecodeAsFields, DecodeAsType};
use pezkuwi_subxt_core::blocks::{ExtrinsicDetails as CoreExtrinsicDetails, Extrinsics as CoreExtrinsics};
// Re-export anything that's directly returned/used in the APIs below.
pub use pezkuwi_subxt_core::blocks::{
ExtrinsicTransactionExtension, ExtrinsicTransactionExtensions, StaticExtrinsic,
};
/// The body of a block.
pub struct Extrinsics<T: Config, C> {
inner: CoreExtrinsics<T>,
client: C,
cached_events: CachedEvents<T>,
hash: HashFor<T>,
}
impl<T, C> Extrinsics<T, C>
where
T: Config,
C: OfflineClientT<T>,
{
pub(crate) fn new(
client: C,
extrinsics: Vec<Vec<u8>>,
cached_events: CachedEvents<T>,
hash: HashFor<T>,
) -> Result<Self, ExtrinsicDecodeErrorAt> {
let inner = CoreExtrinsics::decode_from(extrinsics, client.metadata())?;
Ok(Self {
inner,
client,
cached_events,
hash,
})
}
/// See [`pezkuwi_subxt_core::blocks::Extrinsics::len()`].
pub fn len(&self) -> usize {
self.inner.len()
}
/// See [`pezkuwi_subxt_core::blocks::Extrinsics::is_empty()`].
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
/// Return the block hash that these extrinsics are from.
pub fn block_hash(&self) -> HashFor<T> {
self.hash
}
/// Returns an iterator over the extrinsics in the block body.
// Dev note: The returned iterator is 'static + Send so that we can box it up and make
// use of it with our `FilterExtrinsic` stuff.
pub fn iter(&self) -> impl Iterator<Item = ExtrinsicDetails<T, C>> + Send + Sync + 'static {
let client = self.client.clone();
let cached_events = self.cached_events.clone();
let block_hash = self.hash;
self.inner.iter().map(move |inner| {
ExtrinsicDetails::new(inner, client.clone(), block_hash, cached_events.clone())
})
}
/// Iterate through the extrinsics using metadata to dynamically decode and skip
/// them, and return only those which should decode to the provided `E` type.
/// If an error occurs, all subsequent iterations return `None`.
pub fn find<E: StaticExtrinsic>(
&self,
) -> impl Iterator<Item = Result<FoundExtrinsic<T, C, E>, ExtrinsicError>> {
self.inner.find::<E>().map(|res| {
match res {
Err(e) => Err(ExtrinsicError::from(e)),
Ok(ext) => {
// Wrap details from subxt-core into what we want here:
let details = ExtrinsicDetails::new(
ext.details,
self.client.clone(),
self.hash,
self.cached_events.clone(),
);
Ok(FoundExtrinsic {
details,
value: ext.value,
})
}
}
})
}
/// Iterate through the extrinsics using metadata to dynamically decode and skip
/// them, and return the first extrinsic found which decodes to the provided `E` type.
pub fn find_first<E: StaticExtrinsic>(
&self,
) -> Result<Option<FoundExtrinsic<T, C, E>>, ExtrinsicError> {
self.find::<E>().next().transpose()
}
/// Iterate through the extrinsics using metadata to dynamically decode and skip
/// them, and return the last extrinsic found which decodes to the provided `Ev` type.
pub fn find_last<E: StaticExtrinsic>(
&self,
) -> Result<Option<FoundExtrinsic<T, C, E>>, ExtrinsicError> {
self.find::<E>().last().transpose()
}
/// Find an extrinsics that decodes to the type provided. Returns true if it was found.
pub fn has<E: StaticExtrinsic>(&self) -> Result<bool, ExtrinsicError> {
Ok(self.find::<E>().next().transpose()?.is_some())
}
}
/// A single extrinsic in a block.
pub struct ExtrinsicDetails<T: Config, C> {
inner: CoreExtrinsicDetails<T>,
/// The block hash of this extrinsic (needed to fetch events).
block_hash: HashFor<T>,
/// Subxt client.
client: C,
/// Cached events.
cached_events: CachedEvents<T>,
}
impl<T, C> ExtrinsicDetails<T, C>
where
T: Config,
C: OfflineClientT<T>,
{
// Attempt to dynamically decode a single extrinsic from the given input.
pub(crate) fn new(
inner: CoreExtrinsicDetails<T>,
client: C,
block_hash: HashFor<T>,
cached_events: CachedEvents<T>,
) -> ExtrinsicDetails<T, C> {
ExtrinsicDetails {
inner,
client,
block_hash,
cached_events,
}
}
/// See [`pezkuwi_subxt_core::blocks::ExtrinsicDetails::hash()`].
pub fn hash(&self) -> HashFor<T> {
self.inner.hash()
}
/// See [`pezkuwi_subxt_core::blocks::ExtrinsicDetails::is_signed()`].
pub fn is_signed(&self) -> bool {
self.inner.is_signed()
}
/// See [`pezkuwi_subxt_core::blocks::ExtrinsicDetails::index()`].
pub fn index(&self) -> u32 {
self.inner.index()
}
/// See [`pezkuwi_subxt_core::blocks::ExtrinsicDetails::bytes()`].
pub fn bytes(&self) -> &[u8] {
self.inner.bytes()
}
/// See [`pezkuwi_subxt_core::blocks::ExtrinsicDetails::call_bytes()`].
pub fn call_bytes(&self) -> &[u8] {
self.inner.call_bytes()
}
/// See [`pezkuwi_subxt_core::blocks::ExtrinsicDetails::field_bytes()`].
pub fn field_bytes(&self) -> &[u8] {
self.inner.field_bytes()
}
/// See [`pezkuwi_subxt_core::blocks::ExtrinsicDetails::address_bytes()`].
pub fn address_bytes(&self) -> Option<&[u8]> {
self.inner.address_bytes()
}
/// See [`pezkuwi_subxt_core::blocks::ExtrinsicDetails::signature_bytes()`].
pub fn signature_bytes(&self) -> Option<&[u8]> {
self.inner.signature_bytes()
}
/// See [`pezkuwi_subxt_core::blocks::ExtrinsicDetails::transaction_extensions_bytes()`].
pub fn transaction_extensions_bytes(&self) -> Option<&[u8]> {
self.inner.transaction_extensions_bytes()
}
/// See [`pezkuwi_subxt_core::blocks::ExtrinsicDetails::transaction_extensions()`].
pub fn transaction_extensions(&self) -> Option<ExtrinsicTransactionExtensions<'_, T>> {
self.inner.transaction_extensions()
}
/// See [`pezkuwi_subxt_core::blocks::ExtrinsicDetails::pallet_index()`].
pub fn pallet_index(&self) -> u8 {
self.inner.pallet_index()
}
/// See [`pezkuwi_subxt_core::blocks::ExtrinsicDetails::call_index()`].
pub fn call_index(&self) -> u8 {
self.inner.call_index()
}
/// See [`pezkuwi_subxt_core::blocks::ExtrinsicDetails::pallet_name()`].
pub fn pallet_name(&self) -> &str {
self.inner.pallet_name()
}
/// See [`pezkuwi_subxt_core::blocks::ExtrinsicDetails::call_name()`].
pub fn call_name(&self) -> &str {
self.inner.call_name()
}
/// See [`pezkuwi_subxt_core::blocks::ExtrinsicDetails::decode_as_fields()`].
pub fn decode_as_fields<E: DecodeAsFields>(&self) -> Result<E, ExtrinsicError> {
self.inner.decode_as_fields().map_err(Into::into)
}
/// See [`pezkuwi_subxt_core::blocks::ExtrinsicDetails::as_extrinsic()`].
pub fn as_extrinsic<E: StaticExtrinsic>(&self) -> Result<Option<E>, ExtrinsicError> {
self.inner.as_extrinsic::<E>().map_err(Into::into)
}
/// See [`pezkuwi_subxt_core::blocks::ExtrinsicDetails::as_root_extrinsic()`].
pub fn as_root_extrinsic<E: DecodeAsType>(&self) -> Result<E, ExtrinsicError> {
self.inner.as_root_extrinsic::<E>().map_err(Into::into)
}
}
impl<T, C> ExtrinsicDetails<T, C>
where
T: Config,
C: OnlineClientT<T>,
{
/// The events associated with the extrinsic.
pub async fn events(&self) -> Result<ExtrinsicEvents<T>, EventsError> {
let events = get_events(&self.client, self.block_hash, &self.cached_events).await?;
let ext_hash = self.inner.hash();
Ok(ExtrinsicEvents::new(ext_hash, self.index(), events))
}
}
/// A Static Extrinsic found in a block coupled with it's details.
pub struct FoundExtrinsic<T: Config, C, E> {
/// Details for the extrinsic.
pub details: ExtrinsicDetails<T, C>,
/// The decoded extrinsic value.
pub value: E,
}
/// The events associated with a given extrinsic.
#[derive_where(Debug)]
pub struct ExtrinsicEvents<T: Config> {
// The hash of the extrinsic (handy to expose here because
// this type is returned from TxProgress things in the most
// basic flows, so it's the only place people can access it
// without complicating things for themselves).
ext_hash: HashFor<T>,
// The index of the extrinsic:
idx: u32,
// All of the events in the block:
events: events::Events<T>,
}
impl<T: Config> ExtrinsicEvents<T> {
/// Creates a new instance of `ExtrinsicEvents`.
#[doc(hidden)]
pub fn new(ext_hash: HashFor<T>, idx: u32, events: events::Events<T>) -> Self {
Self {
ext_hash,
idx,
events,
}
}
/// The index of the extrinsic that these events are produced from.
pub fn extrinsic_index(&self) -> u32 {
self.idx
}
/// Return the hash of the extrinsic.
pub fn extrinsic_hash(&self) -> HashFor<T> {
self.ext_hash
}
/// Return all of the events in the block that the extrinsic is in.
pub fn all_events_in_block(&self) -> &events::Events<T> {
&self.events
}
/// Iterate over all of the raw events associated with this transaction.
///
/// This works in the same way that [`events::Events::iter()`] does, with the
/// exception that it filters out events not related to the submitted extrinsic.
pub fn iter(&self) -> impl Iterator<Item = Result<events::EventDetails<T>, EventsError>> {
self.events.iter().filter(|ev| {
ev.as_ref()
.map(|ev| ev.phase() == events::Phase::ApplyExtrinsic(self.idx))
.unwrap_or(true) // Keep any errors.
})
}
/// Find all of the transaction events matching the event type provided as a generic parameter.
///
/// This works in the same way that [`events::Events::find()`] does, with the
/// exception that it filters out events not related to the submitted extrinsic.
pub fn find<Ev: events::StaticEvent>(&self) -> impl Iterator<Item = Result<Ev, EventsError>> {
self.iter()
.filter_map(|ev| ev.and_then(|ev| ev.as_event::<Ev>()).transpose())
}
/// Iterate through the transaction events using metadata to dynamically decode and skip
/// them, and return the first event found which decodes to the provided `Ev` type.
///
/// This works in the same way that [`events::Events::find_first()`] does, with the
/// exception that it ignores events not related to the submitted extrinsic.
pub fn find_first<Ev: events::StaticEvent>(&self) -> Result<Option<Ev>, EventsError> {
self.find::<Ev>().next().transpose()
}
/// Iterate through the transaction events using metadata to dynamically decode and skip
/// them, and return the last event found which decodes to the provided `Ev` type.
///
/// This works in the same way that [`events::Events::find_last()`] does, with the
/// exception that it ignores events not related to the submitted extrinsic.
pub fn find_last<Ev: events::StaticEvent>(&self) -> Result<Option<Ev>, EventsError> {
self.find::<Ev>().last().transpose()
}
/// Find an event in those associated with this transaction. Returns true if it was found.
///
/// This works in the same way that [`events::Events::has()`] does, with the
/// exception that it ignores events not related to the submitted extrinsic.
pub fn has<Ev: events::StaticEvent>(&self) -> Result<bool, EventsError> {
Ok(self.find::<Ev>().next().transpose()?.is_some())
}
}
+22
View File
@@ -0,0 +1,22 @@
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
//! This module exposes the necessary functionality for working with events.
mod block_types;
mod blocks_client;
mod extrinsic_types;
/// A reference to a block.
pub use crate::backend::BlockRef;
pub use block_types::Block;
pub use blocks_client::BlocksClient;
pub use extrinsic_types::{
ExtrinsicDetails, ExtrinsicEvents, ExtrinsicTransactionExtension,
ExtrinsicTransactionExtensions, Extrinsics, FoundExtrinsic, StaticExtrinsic,
};
// We get account nonce info in tx_client, too, so re-use the logic:
pub(crate) use block_types::get_account_nonce;
+108
View File
@@ -0,0 +1,108 @@
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
// Dev note; I used the following command to normalize and wrap comments:
// rustfmt +nightly --config wrap_comments=true,comment_width=100,normalize_comments=true subxt/src/book/custom_values
// It messed up comments in code blocks though, so be prepared to go and fix those.
//! # The Subxt Guide
//!
//! Subxt is a library for interacting with Substrate based nodes. It has a focus on **sub**mitting
//! e**xt**rinsics, hence the name, however it's also capable of reading blocks, storage, events and
//! constants from a node. The aim of this guide is to explain key concepts and get you started with
//! using Subxt.
//!
//! 1. [Features](#features-at-a-glance)
//! 2. [Limitations](#limitations)
//! 3. [Quick start](#quick-start)
//! 4. [Usage](#usage)
//!
//! ## Features at a glance
//!
//! Here's a quick overview of the features that Subxt has to offer:
//!
//! - Subxt allows you to generate a static, type safe interface to a node given some metadata; this
//! allows you to catch many errors at compile time rather than runtime.
//! - Subxt also makes heavy use of node metadata to encode/decode the data sent to/from it. This
//! allows it to target almost any node which can output the correct metadata, and allows it some
//! flexibility in encoding and decoding things to account for cross-node differences.
//! - Subxt has a pallet-oriented interface, meaning that code you write to talk to some pallet on
//! one node will often "Just Work" when pointed at different nodes that use the same pallet.
//! - Subxt can work offline; you can generate and sign transactions, access constants from node
//! metadata and more, without a network connection. This is all checked at compile time, so you
//! can be certain it won't try to establish a network connection if you don't want it to.
//! - Subxt can forego the statically generated interface and build transactions, storage queries
//! and constant queries using data provided at runtime, rather than queries constructed
//! statically.
//! - Subxt can be compiled to WASM to run in the browser, allowing it to back Rust based browser
//! apps, or even bind to JS apps.
//!
//! ## Limitations
//!
//! In various places, you can provide a block hash to access data at a particular block, for
//! instance:
//!
//! - [`crate::storage::StorageClient::at`]
//! - [`crate::events::EventsClient::at`]
//! - [`crate::blocks::BlocksClient::at`]
//! - [`crate::runtime_api::RuntimeApiClient::at`]
//!
//! However, Subxt is (by default) only capable of properly working with blocks that were produced
//! after the most recent runtime update. This is because it uses the most recent metadata given
//! back by a node to encode and decode things. It's possible to decode older blocks produced by a
//! runtime that emits compatible (currently, V14) metadata by manually setting the metadata used by
//! the client using [`crate::client::OnlineClient::set_metadata()`].
//!
//! Subxt does not support working with blocks produced prior to the runtime update that introduces
//! V14 metadata. It may have some success decoding older blocks using newer metadata, but may also
//! completely fail to do so.
//!
//! ## Quick start
//!
//! Here is a simple but complete example of using Subxt to transfer some tokens from the example
//! accounts, Alice to Bob:
//!
//! ```rust,ignore
#![doc = include_str!("../../examples/tx_basic.rs")]
//! ```
//!
//! This example assumes that a Polkadot node is running locally (Subxt endeavors to support all
//! recent releases). Typically, to use Subxt to talk to some custom Substrate node (for example a
//! parachain node), you'll want to:
//!
//! 1. [Generate an interface](setup::codegen)
//! 2. [Create a config](setup::config)
//! 3. [Use the config to instantiate the client](setup::client)
//!
//! Follow the above links to learn more about each step.
//!
//! ## Usage
//!
//! Once Subxt is configured, the next step is interacting with a node. Follow the links
//! below to learn more about how to use Subxt for each of the following things:
//!
//! - [Transactions](usage::transactions): Subxt can build and submit transactions, wait until they are in
//! blocks, and retrieve the associated events.
//! - [Storage](usage::storage): Subxt can query the node storage.
//! - [Events](usage::events): Subxt can read the events emitted for recent blocks.
//! - [Constants](usage::constants): Subxt can access the constant values stored in a node, which
//! remain the same for a given runtime version.
//! - [Blocks](usage::blocks): Subxt can load recent blocks or subscribe to new/finalized blocks,
//! reading the extrinsics, events and storage at these blocks.
//! - [Runtime APIs](usage::runtime_apis): Subxt can make calls into pallet runtime APIs to retrieve
//! data.
//! - [Custom values](usage::custom_values): Subxt can access "custom values" stored in the metadata.
//! - [Raw RPC calls](usage::rpc): Subxt can be used to make raw RPC requests to compatible nodes.
//!
//! ## Examples
//!
//! Some complete, self contained examples which are not a part of this guide:
//!
//! - [`parachain-example`](https://github.com/paritytech/subxt/tree/master/examples/parachain-example) is an example
//! which uses Zombienet to spawn a parachain locally, and then connects to it using Subxt.
//! - [`wasm-example`](https://github.com/paritytech/subxt/tree/master/examples/wasm-example) is an example of writing
//! a Rust app that contains a Yew based UI, uses Subxt to interact with a chain, and compiles to WASM in order to
//! run entirely in the browser.
pub mod setup;
pub mod usage;
+52
View File
@@ -0,0 +1,52 @@
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
//! # The Subxt client.
//!
//! The client forms the entry point to all of the Subxt APIs. Every client implements one or
//! both of [`crate::client::OfflineClientT`] and [`crate::client::OnlineClientT`].
//!
//! Subxt ships with three clients which implement one or both of traits:
//! - An [online client](crate::client::OnlineClient).
//! - An [offline client](crate::client::OfflineClient).
//! - A light client (which is currently still unstable).
//!
//! In theory it's possible for users to implement their own clients, although this isn't generally
//! expected.
//!
//! The provided clients are all generic over the [`crate::config::Config`] that they accept, which
//! determines how they will interact with the chain.
//!
//! In the case of the [`crate::OnlineClient`], we have various ways to instantiate it:
//!
//! - [`crate::OnlineClient::new()`] to connect to a node running locally. This uses the default Subxt
//! backend, and the default RPC client.
//! - [`crate::OnlineClient::from_url()`] to connect to a node at a specific URL. This uses the default Subxt
//! backend, and the default RPC client.
//! - [`crate::OnlineClient::from_rpc_client()`] to instantiate the client with a [`crate::backend::rpc::RpcClient`].
//! - [`crate::OnlineClient::from_backend()`] to instantiate Subxt using a custom backend. Currently there
//! is just one backend, [`crate::backend::legacy::LegacyBackend`]. This backend can be instantiated from
//! a [`crate::backend::rpc::RpcClient`].
//!
//! [`crate::backend::rpc::RpcClient`] can itself be instantiated from anything that implements the low level
//! [`crate::backend::rpc::RpcClientT`] trait; this allows you to decide how Subxt will attempt to talk to a node
//! if you'd prefer something other default client. We use this approach under the hood to implement the light client.
//!
//! ## Examples
//!
//! Most of the other examples will instantiate a client. Here are a couple of examples for less common
//! cases.
//!
//! ### Writing a custom [`crate::backend::rpc::RpcClientT`] implementation:
//!
//! ```rust,ignore
#![doc = include_str!("../../../examples/setup_client_custom_rpc.rs")]
//! ```
//!
//! ### Creating an [`crate::OfflineClient`]:
//!
//! ```rust,ignore
#![doc = include_str!("../../../examples/setup_client_offline.rs")]
//! ```
//!
+66
View File
@@ -0,0 +1,66 @@
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
//! # Generating an interface
//!
//! The simplest way to use Subxt is to generate an interface to a chain that you'd like to interact
//! with. This generated interface allows you to build transactions and construct queries to access
//! data while leveraging the full type safety of the Rust compiler.
//!
//! ## The `#[subxt]` macro
//!
//! The most common way to generate the interface is to use the [`#[subxt]`](crate::subxt) macro.
//! Using this macro looks something like:
//!
//! ```rust,no_run,standalone_crate
//! #[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata_tiny.scale")]
//! pub mod polkadot {}
//! ```
//!
//! The macro takes a path to some node metadata, and uses that to generate the interface you'll use
//! to talk to it. [Go here](crate::subxt) to learn more about the options available to the macro.
//!
//! To obtain this metadata you'll need for the above, you can use the `subxt` CLI tool to download it
//! from a node. The tool can be installed via `cargo`:
//!
//! ```shell
//! cargo install subxt-cli
//! ```
//!
//! And then it can be used to fetch metadata and save it to a file:
//!
//! ```shell
//! # Download and save all of the metadata:
//! subxt metadata > metadata.scale
//! # Download and save only the pallets you want to generate an interface for:
//! subxt metadata --pallets Balances,System > metadata.scale
//! ```
//!
//! Explicitly specifying pallets will cause the tool to strip out all unnecessary metadata and type
//! information, making the bundle much smaller in the event that you only need to generate an
//! interface for a subset of the available pallets on the node.
//!
//! ## The CLI tool
//!
//! 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).
//! - 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.
//!
//! If these are an issue, you can manually generate the same code that the macro generates under the hood
//! by using the `subxt codegen` command:
//!
//! ```shell
//! # Install the CLI tool if you haven't already:
//! cargo install subxt-cli
//! # Generate and format rust code, saving it to `interface.rs`:
//! subxt codegen | rustfmt > interface.rs
//! ```
//!
//! Use `subxt codegen --help` for more options; many of the options available via the macro are
//! also available via the CLI tool, such as the ability to substitute generated types for others,
//! or strip out docs from the generated code.
//!
+166
View File
@@ -0,0 +1,166 @@
//! # Creating a Config
//!
//! Subxt requires you to provide a type implementing [`crate::config::Config`] in order to connect to a node.
//! The [`crate::config::Config`] trait for the most part mimics the `frame_system::Config` trait.
//! For most use cases, you can just use one of the following Configs shipped with Subxt:
//!
//! - [`PolkadotConfig`](crate::config::PolkadotConfig) for talking to Polkadot nodes, and
//! - [`SubstrateConfig`](crate::config::SubstrateConfig) for talking to generic nodes built with Substrate.
//!
//! # How to create a Config for a custom chain?
//!
//! Some chains may use config that is not compatible with our [`PolkadotConfig`](crate::config::PolkadotConfig) or
//! [`SubstrateConfig`](crate::config::SubstrateConfig).
//!
//! We now walk through creating a custom [`crate::config::Config`] for a parachain, using the
//! ["Statemint"](https://parachains.info/details/statemint) parachain, also known as "Asset Hub", as an example. It
//! is currently (as of 2023-06-26) deployed on Polkadot and [Kusama (as "Statemine")](https://parachains.info/details/statemine).
//!
//! To construct a valid [`crate::config::Config`] implementation, we need to find out which types to use for `AccountId`, `Hasher`, etc.
//! For this, we need to take a look at the source code of Statemint, which is currently a part of the [Cumulus Github repository](https://github.com/paritytech/cumulus).
//! The crate defining the asset hub runtime can be found [here](https://github.com/paritytech/cumulus/tree/master/parachains/runtimes/assets/asset-hub-polkadot).
//!
//! ## `AccountId`, `Hash`, `Hasher` and `Header`
//!
//! For these config types, we need to find out where the parachain runtime implements the `frame_system::Config` trait.
//! Look for a code fragment like `impl frame_system::Config for Runtime { ... }` In the source code.
//! For Statemint it looks like [this](https://github.com/paritytech/cumulus/blob/e2b7ad2061824f490c08df27a922c64f50accd6b/parachains/runtimes/assets/asset-hub-polkadot/src/lib.rs#L179)
//! at the time of writing. The `AccountId`, `Hash` and `Header` types of the [frame_system::pallet::Config](https://docs.rs/frame-system/latest/frame_system/pallet/trait.Config.html)
//! correspond to the ones we want to use in our Subxt [crate::Config]. In the Case of Statemint (Asset Hub) they are:
//!
//! - AccountId: `sp_core::crypto::AccountId32`
//! - Hash: `sp_core::H256`
//! - Hasher (type `Hashing` in [frame_system::pallet::Config](https://docs.rs/frame-system/latest/frame_system/pallet/trait.Config.html)): `sp_runtime::traits::BlakeTwo256`
//! - Header: `sp_runtime::generic::Header<u32, sp_runtime::traits::BlakeTwo256>`
//!
//! Subxt has its own versions of some of these types in order to avoid needing to pull in Substrate dependencies:
//!
//! - `sp_core::crypto::AccountId32` can be swapped with [`crate::utils::AccountId32`].
//! - `sp_core::H256` is a re-export which subxt also provides as [`crate::config::substrate::H256`].
//! - `sp_runtime::traits::BlakeTwo256` can be swapped with [`crate::config::substrate::BlakeTwo256`].
//! - `sp_runtime::generic::Header` can be swapped with [`crate::config::substrate::SubstrateHeader`].
//!
//! Having a look at how those types are implemented can give some clues as to how to implement other custom types that
//! you may need to use as part of your config.
//!
//! ## `Address`, `Signature`
//!
//! A Substrate runtime is typically constructed by using the [frame_support::construct_runtime](https://docs.rs/frame-support/latest/frame_support/macro.construct_runtime.html) macro.
//! In this macro, we need to specify the type of an `UncheckedExtrinsic`. Most of the time, the `UncheckedExtrinsic` will be of the type
//! `sp_runtime::generic::UncheckedExtrinsic<Address, RuntimeCall, Signature, SignedExtra>`.
//! The generic parameters `Address` and `Signature` specified when declaring the `UncheckedExtrinsic` type
//! are the types for `Address` and `Signature` we should use with our [crate::Config] implementation. This information can
//! also be obtained from the metadata (see [`frame_metadata::v15::ExtrinsicMetadata`]). In case of Statemint (Polkadot Asset Hub)
//! we see the following types being used in `UncheckedExtrinsic`:
//!
//! - Address: `sp_runtime::MultiAddress<Self::AccountId, ()>`
//! - Signature: `sp_runtime::MultiSignature`
//!
//! As above, Subxt has its own versions of these types that can be used instead to avoid pulling in Substrate dependencies.
//! Using the Subxt versions also makes interacting with generated code (which uses them in some places) a little nicer:
//!
//! - `sp_runtime::MultiAddress` can be swapped with [`crate::utils::MultiAddress`].
//! - `sp_runtime::MultiSignature` can be swapped with [`crate::utils::MultiSignature`].
//!
//! ## ExtrinsicParams
//!
//! Chains each have a set of "transaction extensions" (formally called "signed extensions") configured. Transaction extensions provide
//! a means to extend how transactions work. Each transaction extension can potentially encode some "extra" data which is sent along with a transaction, as well as some
//! "additional" data which is included in the transaction signer payload, but not transmitted along with the transaction. On
//! a node, transaction extensions can then perform additional checks on the submitted transactions to ensure their validity.
//!
//! The `ExtrinsicParams` config type expects to be given an implementation of the [`crate::config::ExtrinsicParams`] trait.
//! Implementations of the [`crate::config::ExtrinsicParams`] trait are handed some parameters from Subxt itself, and can
//! accept arbitrary other `Params` from users, and are then expected to provide this "extra" and "additional" data when asked
//! via the required [`crate::config::ExtrinsicParamsEncoder`] impl.
//!
//! **In most cases, the default [crate::config::DefaultExtrinsicParams] type will work**: it understands the "standard"
//! transaction extensions that are in use, and allows the user to provide things like a tip, and set the extrinsic mortality via
//! [`crate::config::DefaultExtrinsicParamsBuilder`]. It will use the chain metadata to decide which transaction extensions to use
//! and in which order. It will return an error if the chain uses a transaction extension which it doesn't know how to handle.
//!
//! If the chain uses novel transaction extensions (or if you just wish to provide a different interface for users to configure
//! transactions), you can either:
//!
//! 1. Implement a new transaction extension and add it to the list.
//! 2. Implement [`crate::config::DefaultExtrinsicParams`] from scratch.
//!
//! See below for examples of each.
//!
//! ### Finding out which transaction extensions a chain is using.
//!
//! In either case, you'll want to find out which transaction extensions a chain is using. This information can be obtained from
//! the `SignedExtra` parameter of the `UncheckedExtrinsic` of your parachain, which will be a tuple of transaction extensions.
//! It can also be obtained from the metadata (see [`frame_metadata::v15::SignedExtensionMetadata`]).
//!
//! For statemint, the transaction extensions look like
//! [this](https://github.com/paritytech/cumulus/blob/d4bb2215bb28ee05159c4c7df1b3435177b5bf4e/parachains/runtimes/assets/asset-hub-polkadot/src/lib.rs#L786):
//!
//! ```rust,ignore
//! pub type SignedExtra = (
//! frame_system::CheckNonZeroSender<Runtime>,
//! frame_system::CheckSpecVersion<Runtime>,
//! frame_system::CheckTxVersion<Runtime>,
//! frame_system::CheckGenesis<Runtime>,
//! frame_system::CheckEra<Runtime>,
//! frame_system::CheckNonce<Runtime>,
//! frame_system::CheckWeight<Runtime>,
//! pallet_asset_tx_payment::ChargeAssetTxPayment<Runtime>,
//! );
//! ```
//!
//! Each element of the `SignedExtra` tuple implements [codec::Encode] and `sp_runtime::traits::SignedExtension`
//! which has an associated type `AdditionalSigned` that also implements [codec::Encode]. Let's look at the underlying types
//! for each tuple element. All zero-sized types have been replaced by `()` for simplicity.
//!
//! | tuple element | struct type | `AdditionalSigned` type |
//! | ------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- |
//! | [`frame_system::CheckNonZeroSender`](https://docs.rs/frame-system/latest/frame_system/struct.CheckNonZeroSender.html) | () | () |
//! | [`frame_system::CheckSpecVersion`](https://docs.rs/frame-system/latest/frame_system/struct.CheckSpecVersion.html) | () | [u32] |
//! | [`frame_system::CheckTxVersion`](https://docs.rs/frame-system/latest/frame_system/struct.CheckTxVersion.html) | () | [u32] |
//! | [`frame_system::CheckGenesis`](https://docs.rs/frame-system/latest/frame_system/struct.CheckGenesis.html) | () | `Config::Hash` = `sp_core::H256` |
//! | [`frame_system::CheckMortality`](https://docs.rs/frame-system/latest/frame_system/struct.CheckMortality.html) | `sp_runtime::generic::Era` | `Config::Hash` = `sp_core::H256` |
//! | [`frame_system::CheckNonce`](https://docs.rs/frame-system/latest/frame_system/struct.CheckNonce.html) | `frame_system::pallet::Config::Index` = u32 | () |
//! | [`frame_system::CheckWeight`](https://docs.rs/frame-system/latest/frame_system/struct.CheckWeight.html) | () | () |
//! | [`frame_system::ChargeAssetTxPayment`](https://docs.rs/frame-system/latest/frame_system/struct.ChargeAssetTxPayment.html) | [pallet_asset_tx_payment::ChargeAssetTxPayment](https://docs.rs/pallet-asset-tx-payment/latest/pallet_asset_tx_payment/struct.ChargeAssetTxPayment.html) | () |
//!
//! All types in the `struct type` column make up the "extra" data that we're expected to provide. All types in the
//! `AdditionalSigned` column make up the "additional" data that we're expected to provide. This information will be useful
//! whether we want to implement [`crate::config::TransactionExtension`] for a transaction extension, or implement
//! [`crate::config::ExtrinsicParams`] from scratch.
//!
//! As it happens, all of the transaction extensions in the table are either already exported in [`crate::config::transaction_extensions`],
//! or they hand back no "additional" or "extra" data. In both of these cases, the default `ExtrinsicParams` configuration will
//! work out of the box.
//!
//! ### Implementing and adding new transaction extensions to the config
//!
//! If you do need to implement a novel transaction extension, then you can implement [`crate::config::transaction_extensions::TransactionExtension`]
//! on a custom type and place it into a new set of transaction extensions, like so:
//!
//! ```rust,ignore
#![doc = include_str ! ("../../../examples/setup_config_transaction_extension.rs")]
//! ```
//!
//! ### Implementing [`crate::config::ExtrinsicParams`] from scratch
//!
//! Alternately, you are free to implement [`crate::config::ExtrinsicParams`] entirely from scratch if you know exactly what "extra" and
//! "additional" data your node needs and would prefer to craft your own interface.
//!
//! Let's see what this looks like (this config won't work on any real node):
//!
//! ```rust,ignore
#![doc = include_str ! ("../../../examples/setup_config_custom.rs")]
//! ```
//!
//! ### Using a type from the metadata as a config parameter
//!
//! You can also use types that are generated from chain metadata as type parameters of the Config trait.
//! Just make sure all trait bounds are satisfied. This can often be achieved by using custom derives with the subxt macro.
//! For example, the AssetHub Parachain expects tips to include a `MultiLocation`, which is a type we can draw from the metadata.
//!
//! This example shows what using the `MultiLocation` struct as part of your config would look like in subxt:
//!
//! ```rust,ignore
#![doc = include_str ! ("../../../examples/setup_config_assethub.rs")]
//! ```
+14
View File
@@ -0,0 +1,14 @@
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
//! This modules contains details on setting up Subxt:
//!
//! - [Codegen](codegen)
//! - [Client](client)
//!
//! Alternately, [go back](super).
pub mod client;
pub mod codegen;
pub mod config;
+103
View File
@@ -0,0 +1,103 @@
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
//! # Blocks
//!
//! The [blocks API](crate::blocks::BlocksClient) in Subxt unifies many of the other interfaces, and
//! allows you to:
//!
//! - Access information about specific blocks (see [`crate::blocks::BlocksClient::at()`] and
//! [`crate::blocks::BlocksClient::at_latest()`]).
//! - Subscribe to [all](crate::blocks::BlocksClient::subscribe_all()),
//! [best](crate::blocks::BlocksClient::subscribe_best()) or
//! [finalized](crate::blocks::BlocksClient::subscribe_finalized()) blocks as they are produced.
//! **Prefer to subscribe to finalized blocks unless you know what you're doing.**
//!
//! In either case, you'll end up with [`crate::blocks::Block`]'s, from which you can access various
//! information about the block, such a the [header](crate::blocks::Block::header()),
//! [block number](crate::blocks::Block::number()) and [body (the extrinsics)](crate::blocks::Block::extrinsics()).
//! [`crate::blocks::Block`]'s also provide shortcuts to other Subxt APIs that will operate at the
//! given block:
//!
//! - [storage](crate::blocks::Block::storage()),
//! - [events](crate::blocks::Block::events())
//! - [runtime APIs](crate::blocks::Block::runtime_api())
//!
//! Aside from these links to other Subxt APIs, the main thing that we can do here is iterate over and
//! decode the extrinsics in a block body.
//!
//! ## Decoding Extrinsics
//!
//! Given a block, you can [download the block body](crate::blocks::Block::extrinsics()) and
//! [iterate over the extrinsics](crate::blocks::Extrinsics::iter) stored within it. The extrinsics yielded are of type
//! [ExtrinsicDetails](crate::blocks::ExtrinsicDetails), which is just a blob of bytes that also stores which
//! pallet and call in that pallet it belongs to. It also contains information about signed extensions that
//! have been used for submitting this extrinsic.
//!
//! To use the extrinsic, you probably want to decode it into a concrete Rust type. These Rust types representing
//! extrinsics from different pallets can be generated from metadata using the subxt macro or the CLI tool.
//!
//! When decoding the extrinsic into a static type you have two options:
//!
//! ### Statically decode the extrinsics into [the root extrinsic type](crate::blocks::ExtrinsicDetails::as_root_extrinsic())
//!
//! The root extrinsic type generated by subxt is a Rust enum with one variant for each pallet. Each of these
//! variants has a field that is another enum whose variants cover all calls of the respective pallet.
//! If the extrinsic bytes are valid and your metadata matches the chain's metadata, decoding the bytes of an extrinsic into
//! this root extrinsic type should always succeed.
//!
//! This example shows how to subscribe to blocks and decode the extrinsics in each block into the root extrinsic type.
//! Once we get hold of the [ExtrinsicDetails](crate::blocks::ExtrinsicDetails), we can decode it statically or dynamically.
//! We can also access details about the extrinsic, including the associated events and transaction extensions.
//!
//! ```rust,ignore
#![doc = include_str!("../../../examples/blocks_subscribing.rs")]
//! ```
//!
//! ### Statically decode the extrinsic into [a specific pallet call](crate::blocks::ExtrinsicDetails::as_extrinsic())
//!
//! This is useful if you are expecting a specific extrinsic to be part of some block. If the extrinsic you try to decode
//! is a different extrinsic, an `Ok(None)` value is returned from [`as_extrinsic::<T>()`](crate::blocks::ExtrinsicDetails::as_extrinsic());
//!
//! If you are only interested in finding specific extrinsics in a block, you can also [iterate over all of them](crate::blocks::Extrinsics::find),
//! get only [the first one](crate::blocks::Extrinsics::find_first), or [the last one](crate::blocks::Extrinsics::find_last).
//!
//! The following example monitors `TransferKeepAlive` extrinsics on the Polkadot network.
//! We statically decode them and access the [tip](crate::blocks::ExtrinsicTransactionExtensions::tip()) and
//! [account nonce](crate::blocks::ExtrinsicTransactionExtensions::nonce()) transaction extensions.
//!
//! ```rust,ignore
#![doc = include_str!("../../../examples/block_decoding_static.rs")]
//! ```
//!
//! ### Dynamically decode the extrinsic
//!
//! Sometimes you might use subxt with metadata that is not known at compile time. In this case, you do not
//! have access to a statically generated interface module that contains the relevant Rust types. You can
//! [decode ExtrinsicDetails dynamically](crate::blocks::ExtrinsicDetails::decode_as_fields()), which gives
//! you access to it's fields as a [scale value composite](scale_value::Composite). The following example
//! looks for signed extrinsics on the Polkadot network and retrieves their pallet name, variant name, data
//! fields and transaction extensions dynamically. Notice how we do not need to use code generation via the
//! subxt macro. The only fixed component we provide is the [PolkadotConfig](crate::config::PolkadotConfig).
//! Other than that it works in a chain-agnostic way:
//!
//! ```rust,ignore
#![doc = include_str!("../../../examples/block_decoding_dynamic.rs")]
//! ```
//!
//! ## Decoding transaction extensions
//!
//! Extrinsics can contain transaction extensions. The transaction extensions can be different across chains.
//! The [Config](crate::Config) implementation for your chain defines which transaction extensions you expect.
//! Once you get hold of the [ExtrinsicDetails](crate::blocks::ExtrinsicDetails) for an extrinsic you are interested in,
//! you can try to [get its transaction extensions](crate::blocks::ExtrinsicDetails::transaction_extensions()).
//! These are only available on V4 signed extrinsics or V5 general extrinsics. You can try to
//! [find a specific transaction extension](crate::blocks::ExtrinsicTransactionExtensions::find), in the returned
//! [transaction extensions](crate::blocks::ExtrinsicTransactionExtensions).
//!
//! Subxt also provides utility functions to get the [tip](crate::blocks::ExtrinsicTransactionExtensions::tip()) and
//! the [account nonce](crate::blocks::ExtrinsicTransactionExtensions::nonce()) associated with an extrinsic, given
//! its transaction extensions. If you prefer to do things dynamically you can get the data of the transaction extension
//! as a [scale value](crate::blocks::ExtrinsicTransactionExtension::value()).
//!
+55
View File
@@ -0,0 +1,55 @@
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
//! # Constants
//!
//! There are various constants stored in a node; the types and values of these are defined in a
//! runtime, and can only change when the runtime is updated. Much like [`super::storage`], we can
//! query these using Subxt by taking the following steps:
//!
//! 1. [Constructing a constant query](#constructing-a-query).
//! 2. [Submitting the query to get back the associated value](#submitting-it).
//!
//! ## Constructing a constant query
//!
//! We can use the statically generated interface to build constant queries:
//!
//! ```rust,no_run,standalone_crate
//! #[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata_full.scale")]
//! pub mod polkadot {}
//!
//! let constant_query = polkadot::constants().system().block_length();
//! ```
//!
//! Alternately, we can dynamically construct a constant query. A dynamic query needs the return
//! type to be specified, where we can use [`crate::dynamic::Value`] if unsure:
//!
//! ```rust,no_run,standalone_crate
//! use subxt::dynamic::Value;
//!
//! let storage_query = subxt::dynamic::constant::<Value>("System", "BlockLength");
//! ```
//!
//! ## Submitting it
//!
//! Call [`crate::constants::ConstantsClient::at()`] to return and decode the constant into the
//! type given by the address, or [`crate::constants::ConstantsClient::bytes_at()`] to return the
//! raw bytes for some constant.
//!
//! Constant values are pulled directly out of the node metadata which Subxt has
//! already acquired, and so this function requires no network access and is available from a
//! [`crate::OfflineClient`].
//!
//! Here's an example using a static query:
//!
//! ```rust,ignore
#![doc = include_str!("../../../examples/constants_static.rs")]
//! ```
//!
//! And here's one using a dynamic query:
//!
//! ```rust,ignore
#![doc = include_str!("../../../examples/constants_dynamic.rs")]
//! ```
//!
@@ -0,0 +1,69 @@
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
//! # Custom Values
//!
//! Substrate-based chains can expose custom values in their metadata.
//! Each of these values:
//!
//! - can be accessed by a unique __name__.
//! - refers to a concrete __type__ stored in the metadata.
//! - contains a scale encoded __value__ of that type.
//!
//! ## Getting a custom value
//!
//! First, you must construct an address to access a custom value. This can be either:
//! - a raw [`str`] which assumes the return type to be the dynamic [`crate::dynamic::Value`] type,
//! - created via [`dynamic`](crate::custom_values::dynamic) function whereby you set the return type
//! that you want back,
//! - created via statically generated addresses as part of the `#[subxt]` macro which define the return type.
//!
//! With an address, use [`at`](crate::custom_values::CustomValuesClient::at) to access and decode specific values, and
//! [`bytes_at`](crate::custom_values::CustomValuesClient::bytes_at) to access the raw bytes.
//!
//! ## Examples
//!
//! Dynamically accessing a custom value using a [`str`] to select which one:
//!
//! ```rust,ignore
//! use subxt::{OnlineClient, PolkadotConfig, ext::scale_decode::DecodeAsType};
//! use subxt::dynamic::Value;
//!
//! let api = OnlineClient::<PolkadotConfig>::new().await?;
//! let custom_value_client = api.custom_values();
//! let foo: Value = custom_value_client.at("foo")?;
//! ```
//!
//! Use the [`dynamic`](crate::custom_values::dynamic) function to select the return type:
//!
//! ```rust,ignore
//! use subxt::{OnlineClient, PolkadotConfig, ext::scale_decode::DecodeAsType};
//!
//! #[derive(Decode, DecodeAsType, Debug)]
//! struct Foo {
//! n: u8,
//! b: bool,
//! }
//!
//! let api = OnlineClient::<PolkadotConfig>::new().await?;
//! let custom_value_client = api.custom_values();
//! let custom_value_addr = subxt::custom_values::dynamic::<Foo>("foo");
//! let foo: Foo = custom_value_client.at(&custom_value_addr)?;
//! ```
//!
//! Alternatively we also provide a statically generated api for custom values:
//!
//! ```rust,ignore
//! #[subxt::subxt(runtime_metadata_path = "some_metadata.scale")]
//! pub mod interface {}
//!
//! let static_address = interface::custom().foo();
//!
//! let api = OnlineClient::<PolkadotConfig>::new().await?;
//! let custom_value_client = api.custom_values();
//!
//! // Now the `at()` function already decodes the value into the Foo type:
//! let foo = custom_value_client.at(&static_address)?;
//! ```
//!
+49
View File
@@ -0,0 +1,49 @@
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
//! # Events
//!
//! In the process of adding extrinsics to a block, they are executed. When extrinsics are executed,
//! they normally produce events describing what's happening (at the very least, an event dictating whether
//! the extrinsic has succeeded or failed). The node may also emit some events of its own as the block is
//! processed.
//!
//! Events live in a single location in node storage which is overwritten at each block. Normal nodes tend to
//! keep a snapshot of the state at a small number of previous blocks, so you can sometimes access
//! older events by using [`crate::events::EventsClient::at()`] and providing an older block hash.
//!
//! When we submit transactions using Subxt, methods like [`crate::tx::TxProgress::wait_for_finalized_success()`]
//! return [`crate::blocks::ExtrinsicEvents`], which can be used to iterate and inspect the events produced
//! by that transaction being executed. We can also access _all_ of the events produced in a single block using one
//! of these two interfaces:
//!
//! ```rust,no_run,standalone_crate
//! # #[tokio::main]
//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
//! use subxt::client::OnlineClient;
//! use subxt::config::PolkadotConfig;
//!
//! // Create client:
//! let client = OnlineClient::<PolkadotConfig>::new().await?;
//!
//! // Get events from the latest block (use .at() to specify a block hash):
//! let events = client.blocks().at_latest().await?.events().await?;
//! // We can use this shorthand too:
//! let events = client.events().at_latest().await?;
//! # Ok(())
//! # }
//! ```
//!
//! Once we've loaded our events, we can iterate all events or search for specific events via
//! methods like [`crate::events::Events::iter()`] and [`crate::events::Events::find()`]. See
//! [`crate::events::Events`] and [`crate::events::EventDetails`] for more information.
//!
//! ## Example
//!
//! Here's an example which puts this all together:
//!
//! ```rust,ignore
#![doc = include_str!("../../../examples/events.rs")]
//! ```
//!
@@ -0,0 +1,51 @@
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
//! # Light Client
//!
//! The light client based interface uses _Smoldot_ to connect to a _chain_, rather than an individual
//! node. This means that you don't have to trust a specific node when interacting with some chain.
//!
//! This feature is currently unstable. Use the `unstable-light-client` feature flag to enable it.
//! To use this in WASM environments, enable the `web` feature flag and disable the "native" one.
//!
//! To connect to a blockchain network, the Light Client requires a trusted sync state of the network,
//! known as a _chain spec_. One way to obtain this is by making a `sync_state_genSyncSpec` RPC call to a
//! trusted node belonging to the chain that you wish to interact with.
//!
//! Subxt exposes a utility method to obtain the chain spec: [`crate::utils::fetch_chainspec_from_rpc_node()`].
//! Alternately, you can manually make an RPC call to `sync_state_genSyncSpec` like do (assuming a node running
//! locally on port 9933):
//!
//! ```bash
//! curl -H "Content-Type: application/json" -d '{"id":1, "jsonrpc":"2.0", "method": "sync_state_genSyncSpec", "params":[true]}' http://localhost:9933/ | jq .result > chain_spec.json
//! ```
//!
//! ## Examples
//!
//! ### Basic Example
//!
//! This basic example uses some already-known chain specs to connect to a relay chain and parachain
//! and stream information about their finalized blocks:
//!
//! ```rust,ignore
#![doc = include_str!("../../../examples/light_client_basic.rs")]
//! ```
//!
//! ### Connecting to a local node
//!
//! This example connects to a local chain and submits a transaction. To run this, you first need
//! to have a local polkadot node running using the following command:
//!
//! ```text
//! polkadot --dev --node-key 0000000000000000000000000000000000000000000000000000000000000001
//! ```
//!
//! Then, the following code will download a chain spec from this local node, alter the bootnodes
//! to point only to the local node, and then submit a transaction through it.
//!
//! ```rust,ignore
#![doc = include_str!("../../../examples/light_client_local_node.rs")]
//! ```
//!
+27
View File
@@ -0,0 +1,27 @@
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
//! This modules contains examples of using Subxt; follow the links for more:
//!
//! - [Transactions](transactions)
//! - [Storage](storage)
//! - [Events](events)
//! - [Constants](constants)
//! - [Blocks](blocks)
//! - [Runtime APIs](runtime_apis)
//! - [Unstable Light Client](light_client)
//! - [Custom Values](custom_values)
//! - [RPC calls](rpc)
//!
//! Alternately, [go back](super).
pub mod blocks;
pub mod constants;
pub mod custom_values;
pub mod events;
pub mod light_client;
pub mod rpc;
pub mod runtime_apis;
pub mod storage;
pub mod transactions;
+22
View File
@@ -0,0 +1,22 @@
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
//! # RPC calls
//!
//! The RPC interface is provided by the [`pezkuwi_subxt_rpcs`] crate but re-exposed here. We have:
//!
//! - [`crate::backend::rpc::RpcClient`] and [`crate::backend::rpc::RpcClientT`]: the underlying type and trait
//! which provides a basic RPC client.
//! - [`crate::backend::legacy::rpc_methods`] and [`crate::backend::chain_head::rpc_methods`]: RPc methods that
//! can be instantiated with an RPC client.
//!
//! See [`pezkuwi_subxt_rpcs`] or [`crate::ext::pezkuwi_subxt_rpcs`] for more.
//!
//! # Example
//!
//! Here's an example which calls some legacy JSON-RPC methods, and reuses the same connection to run a full Subxt client
//!
//! ```rust,ignore
#![doc = include_str!("../../../examples/rpc_legacy.rs")]
//! ```
@@ -0,0 +1,80 @@
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
//! # Runtime API interface
//!
//! The Runtime API interface allows Subxt to call runtime APIs exposed by certain pallets in order
//! to obtain information. Much like [`super::storage`] and [`super::transactions`], Making a runtime
//! call to a node and getting the response back takes the following steps:
//!
//! 1. [Constructing a runtime call](#constructing-a-runtime-call)
//! 2. [Submitting it to get back the response](#submitting-it)
//!
//! **Note:** Runtime APIs are only available when using V15 metadata, which is currently unstable.
//! You'll need to use `subxt metadata --version unstable` command to download the unstable V15 metadata,
//! and activate the `unstable-metadata` feature in Subxt for it to also use this metadata from a node. The
//! metadata format is unstable because it may change and break compatibility with Subxt at any moment, so
//! use at your own risk.
//!
//! ## Constructing a runtime call
//!
//! We can use the statically generated interface to build runtime calls:
//!
//! ```rust,no_run,standalone_crate
//! #[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata_small.scale")]
//! pub mod polkadot {}
//!
//! let runtime_call = polkadot::apis().metadata().metadata_versions();
//! ```
//!
//! Alternately, we can dynamically construct a runtime call. The input type can be a tuple or
//! vec or valid types implementing [`scale_encode::EncodeAsType`], and the output can be anything
//! implementing [`scale_decode::DecodeAsType`]:
//!
//! ```rust,no_run
//! use subxt::dynamic::Value;
//!
//! let runtime_call = subxt::dynamic::runtime_api_call::<(), Vec<u32>>(
//! "Metadata",
//! "metadata_versions",
//! ()
//! );
//! ```
//!
//! All valid runtime calls implement [`crate::runtime_api::Payload`], a trait which
//! describes how to encode the runtime call arguments and what return type to decode from the
//! response.
//!
//! ## Submitting it
//!
//! Runtime calls can be handed to [`crate::runtime_api::RuntimeApi::call()`], which will submit
//! them and hand back the associated response.
//!
//! ### Making a static Runtime API call
//!
//! The easiest way to make a runtime API call is to use the statically generated interface.
//!
//! ```rust,ignore
#![doc = include_str!("../../../examples/runtime_apis_static.rs")]
//! ```
//!
//! ### Making a dynamic Runtime API call
//!
//! If you'd prefer to construct the call at runtime, you can do this using the
//! [`crate::dynamic::runtime_api_call`] method.
//!
//! ```rust,ignore
#![doc = include_str!("../../../examples/runtime_apis_dynamic.rs")]
//! ```
//!
//! ### Making a raw call
//!
//! This is generally discouraged in favour of one of the above, but may be necessary (especially if
//! the node you're talking to does not yet serve V15 metadata). Here, you must manually encode
//! the argument bytes and manually provide a type for the response bytes to be decoded into.
//!
//! ```rust,ignore
#![doc = include_str!("../../../examples/runtime_apis_raw.rs")]
//! ```
//!
+82
View File
@@ -0,0 +1,82 @@
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
//! # Storage
//!
//! A Substrate based chain can be seen as a key/value database which starts off at some initial
//! state, and is modified by the extrinsics in each block. This database is referred to as the
//! node storage. With Subxt, you can query this key/value storage with the following steps:
//!
//! 1. [Constructing a storage query](#constructing-a-storage-query).
//! 2. [Submitting the query to get back the associated entry](#submitting-it).
//! 3. [Fetching](#fetching-storage-entries) or [iterating](#iterating-storage-entries) over that
//! entry to retrieve the value or values within it.
//!
//! ## Constructing a storage query
//!
//! We can use the statically generated interface to build storage queries:
//!
//! ```rust,no_run,standalone_crate
//! #[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata_small.scale")]
//! pub mod polkadot {}
//!
//! let storage_query = polkadot::storage().system().account();
//! ```
//!
//! Alternately, we can dynamically construct a storage query. A dynamic query needs the input
//! and return value types to be specified, where we can use [`crate::dynamic::Value`] if unsure.
//!
//! ```rust,no_run,standalone_crate
//! use subxt::dynamic::Value;
//!
//! let storage_query = subxt::dynamic::storage::<(Value,), Value>("System", "Account");
//! ```
//!
//! ## Submitting it
//!
//! Storage queries can be handed to various functions in [`crate::storage::StorageClientAt`] in order to
//! obtain the associated values (also referred to as storage entries) back.
//!
//! The core API here is [`crate::storage::StorageClientAt::entry()`], which takes a query and looks up the
//! corresponding storage entry, from which you can then fetch or iterate over the values contained within.
//! [`crate::storage::StorageClientAt::fetch()`] and [`crate::storage::StorageClientAt::iter()`] are shorthand
//! for this.
//!
//! When you wish to manually query some entry, [`crate::storage::StorageClientAt::fetch_raw()`] exists to take
//! in raw bytes pointing at some storage value, and return the value bytes if possible. [`crate::storage::StorageClientAt::storage_version()`]
//! and [`crate::storage::StorageClientAt::runtime_wasm_code()`] use this to retrieve the version of some storage API
//! and the current Runtime WASM blob respectively.
//!
//! ### Fetching storage entries
//!
//! The simplest way to access storage entries is to construct a query and then call either
//! [`crate::storage::StorageClientAt::fetch()`]:
//!
//! ```rust,ignore
#![doc = include_str!("../../../examples/storage_fetch.rs")]
//! ```
//!
//! For completeness, below is an example using a dynamic query instead. Dynamic queries can define the types that
//! they wish to accept inputs and decode the return value into ([`crate::dynamic::Value`] can be used here anywhere we
//! are not sure of the specific types).
//!
//! ```rust,ignore
#![doc = include_str!("../../../examples/storage_fetch_dynamic.rs")]
//! ```
//!
//! ### Iterating storage entries
//!
//! Many storage entries are maps of values; as well as fetching individual values, it's possible to
//! iterate over all of the values stored at that location:
//!
//! ```rust,ignore
#![doc = include_str!("../../../examples/storage_iterating.rs")]
//! ```
//!
//! Here's the same logic but using dynamically constructed values instead:
//!
//! ```rust,ignore
#![doc = include_str!("../../../examples/storage_iterating_dynamic.rs")]
//! ```
//!
@@ -0,0 +1,202 @@
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
//! # Transactions
//!
//! A transaction is an extrinsic that's signed (ie it originates from a given address). The purpose
//! of extrinsics is to modify the node storage in a deterministic way, and so being able to submit
//! transactions to a node is one of the core features of Subxt.
//!
//! > Note: the documentation tends to use the terms _extrinsic_ and _transaction_ interchangeably;
//! > An extrinsic is some data that can be added to a block, and is either signed (a _transaction_)
//! > or unsigned (an _inherent_). Subxt can construct either, but overwhelmingly you'll need to
//! > sign the payload you'd like to submit.
//!
//! Submitting a transaction to a node consists of the following steps:
//!
//! 1. [Constructing a transaction payload to submit](#constructing-a-transaction-payload).
//! 2. [Signing it](#signing-it).
//! 3. [Submitting it (optionally with some additional parameters)](#submitting-it).
//!
//! We'll look at each of these steps in turn.
//!
//! ## Constructing a transaction payload
//!
//! We can use the statically generated interface to build transaction payloads:
//!
//! ```rust,no_run,standalone_crate
//! #[subxt::subxt(runtime_metadata_path = "../artifacts/polkadot_metadata_small.scale")]
//! pub mod polkadot {}
//!
//! let remark = "Hello there".as_bytes().to_vec();
//! let tx_payload = polkadot::tx().system().remark(remark);
//! ```
//!
//! > If you're not sure what types to import and use to build a given payload, you can use the
//! > `subxt` CLI tool to generate the interface by using something like `subxt codegen | rustfmt >
//! > interface.rs`, to see what types and things are available (or even just to use directly
//! > instead of the [`#[subxt]`](crate::subxt) macro).
//!
//! Alternately, we can dynamically construct a transaction payload. This will not be type checked or
//! validated until it's submitted:
//!
//! ```rust,no_run,standalone_crate
//! use subxt::dynamic::Value;
//!
//! let tx_payload = subxt::dynamic::tx("System", "remark", vec![
//! Value::from_bytes("Hello there")
//! ]);
//! ```
//!
//! The [`crate::dynamic::Value`] type is a dynamic type much like a `serde_json::Value` but instead
//! represents any type of data that can be SCALE encoded or decoded. It can be serialized,
//! deserialized and parsed from/to strings.
//!
//! A valid transaction payload is just something that implements the [`crate::tx::Payload`] trait;
//! you can implement this trait on your own custom types if the built-in ones are not suitable for
//! your needs.
//!
//! ## Signing it
//!
//! You'll normally need to sign an extrinsic to prove that it originated from an account that you
//! control. To do this, you will typically first create a [`crate::tx::Signer`] instance, which tells
//! Subxt who the extrinsic is from, and takes care of signing the relevant details to prove this.
//!
//! There are two main ways to create a compatible signer instance:
//! 1. The `pezkuwi_subxt_signer` crate provides a WASM compatible implementation of [`crate::tx::Signer`]
//! for chains which require sr25519 or ecdsa signatures (requires the `subxt` feature to be enabled).
//! 2. Alternately, implement your own [`crate::tx::Signer`] instance by wrapping it in a new type pattern.
//!
//! Going for 1 leads to fewer dependencies being imported and WASM compatibility out of the box via
//! the `web` feature flag. Going for 2 is useful if you're already using the Substrate dependencies or
//! need additional signing algorithms that `pezkuwi_subxt_signer` doesn't support, and don't care about WASM
//! compatibility.
//!
//! Because 2 is more complex and require more code, we'll focus on 1 here.
//! For 2, see the example in `subxt/examples/substrate_compat_signer.rs` how
//! you can integrate things like sp_core's signer in subxt.
//!
//! Let's go through how to create a signer using the `pezkuwi_subxt_signer` crate:
//!
//! ```rust,standalone_crate
//! use subxt::config::PolkadotConfig;
//! use std::str::FromStr;
//!
//! use pezkuwi_subxt_signer::{SecretUri, sr25519};
//!
//! // Get hold of a `Signer` for a test account:
//! let alice = sr25519::dev::alice();
//!
//! // Or generate a keypair, here from an SURI:
//! let uri = SecretUri::from_str("vessel ladder alter error federal sibling chat ability sun glass valve picture/0/1///Password")
//! .expect("valid URI");
//! let keypair = sr25519::Keypair::from_uri(&uri)
//! .expect("valid keypair");
//!```
//!
//! After initializing the signer, let's also go through how to create a transaction and sign it:
//!
//! ```rust,no_run,standalone_crate
//! # #[tokio::main]
//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
//! use subxt::client::OnlineClient;
//! use subxt::config::PolkadotConfig;
//! use subxt::dynamic::Value;
//!
//! // Create client:
//! let client = OnlineClient::<PolkadotConfig>::new().await?;
//!
//! // Create a dummy tx payload to sign:
//! let payload = subxt::dynamic::tx("System", "remark", vec![
//! Value::from_bytes("Hello there")
//! ]);
//!
//! // Construct the tx but don't sign it. The account nonce here defaults to 0.
//! // You can use `create_partial` to fetch the correct nonce.
//! let mut partial_tx = client.tx().create_partial_offline(
//! &payload,
//! Default::default()
//! )?;
//!
//! // Fetch the payload that needs to be signed:
//! let signer_payload = partial_tx.signer_payload();
//!
//! // ... At this point, we can hand off the `signer_payload` to be signed externally.
//! // Ultimately we need to be given back a `signature` (or really, anything
//! // that can be SCALE encoded) and an `address`:
//! let signature;
//! let account_id;
//! # use subxt::tx::Signer;
//! # let signer = pezkuwi_subxt_signer::sr25519::dev::alice();
//! # signature = signer.sign(&signer_payload).into();
//! # account_id = signer.public_key().to_account_id();
//!
//! // Now we can build an tx, which one can call `submit` or `submit_and_watch`
//! // on to submit to a node and optionally watch the status.
//! let tx = partial_tx.sign_with_account_and_signature(
//! &account_id,
//! &signature
//! );
//! # Ok(())
//! # }
//! ```
//!
//! ## Submitting it
//!
//! Once we have signed the transaction, we need to submit it.
//!
//! ### The high level API
//!
//! The highest level approach to doing this is to call
//! [`crate::tx::TxClient::sign_and_submit_then_watch_default`]. This hands back a
//! [`crate::tx::TxProgress`] struct which will monitor the transaction status. We can then call
//! [`crate::tx::TxProgress::wait_for_finalized_success()`] to wait for this transaction to make it
//! into a finalized block, check for an `ExtrinsicSuccess` event, and then hand back the events for
//! inspection. This looks like:
//!
//! ```rust,ignore
#![doc = include_str!("../../../examples/tx_basic.rs")]
//! ```
//!
//! ### Providing transaction parameters
//!
//! If you'd like to provide parameters (such as mortality) to the transaction, you can use
//! [`crate::tx::TxClient::sign_and_submit_then_watch`] instead:
//!
//! ```rust,ignore
#![doc = include_str!("../../../examples/tx_with_params.rs")]
//! ```
//!
//! 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
//! the transaction, you can monitor them as they are emitted and react however you choose:
//!
//! ```rust,ignore
#![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.
//!
+18
View File
@@ -0,0 +1,18 @@
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
//! This module provides two clients that can be used to work with
//! transactions, storage and events. The [`OfflineClient`] works
//! entirely offline and can be passed to any function that doesn't
//! require network access. The [`OnlineClient`] requires network
//! access.
mod offline_client;
mod online_client;
pub use offline_client::{OfflineClient, OfflineClientT};
pub use online_client::{
ClientRuntimeUpdater, OnlineClient, OnlineClientT, RuntimeUpdaterStream, Update,
};
pub use pezkuwi_subxt_core::client::{ClientState, RuntimeVersion};
+203
View File
@@ -0,0 +1,203 @@
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
use crate::custom_values::CustomValuesClient;
use crate::{
Metadata,
blocks::BlocksClient,
config::{Config, HashFor},
constants::ConstantsClient,
events::EventsClient,
runtime_api::RuntimeApiClient,
storage::StorageClient,
tx::TxClient,
view_functions::ViewFunctionsClient,
};
use derive_where::derive_where;
use std::sync::Arc;
use pezkuwi_subxt_core::client::{ClientState, RuntimeVersion};
/// A trait representing a client that can perform
/// offline-only actions.
pub trait OfflineClientT<T: Config>: Clone + Send + Sync + 'static {
/// Return the provided [`Metadata`].
fn metadata(&self) -> Metadata;
/// Return the provided genesis hash.
fn genesis_hash(&self) -> HashFor<T>;
/// Return the provided [`RuntimeVersion`].
fn runtime_version(&self) -> RuntimeVersion;
/// Return the hasher used on the chain.
fn hasher(&self) -> T::Hasher;
/// Return the [pezkuwi_subxt_core::client::ClientState] (metadata, runtime version and genesis hash).
fn client_state(&self) -> ClientState<T> {
ClientState {
genesis_hash: self.genesis_hash(),
runtime_version: self.runtime_version(),
metadata: self.metadata(),
}
}
/// Work with transactions.
fn tx(&self) -> TxClient<T, Self> {
TxClient::new(self.clone())
}
/// Work with events.
fn events(&self) -> EventsClient<T, Self> {
EventsClient::new(self.clone())
}
/// Work with storage.
fn storage(&self) -> StorageClient<T, Self> {
StorageClient::new(self.clone())
}
/// Access constants.
fn constants(&self) -> ConstantsClient<T, Self> {
ConstantsClient::new(self.clone())
}
/// Work with blocks.
fn blocks(&self) -> BlocksClient<T, Self> {
BlocksClient::new(self.clone())
}
/// Work with runtime APIs.
fn runtime_api(&self) -> RuntimeApiClient<T, Self> {
RuntimeApiClient::new(self.clone())
}
/// Work with View Functions.
fn view_functions(&self) -> ViewFunctionsClient<T, Self> {
ViewFunctionsClient::new(self.clone())
}
/// Work this custom types.
fn custom_values(&self) -> CustomValuesClient<T, Self> {
CustomValuesClient::new(self.clone())
}
}
/// A client that is capable of performing offline-only operations.
/// Can be constructed as long as you can populate the required fields.
#[derive_where(Debug, Clone)]
pub struct OfflineClient<T: Config> {
inner: Arc<ClientState<T>>,
hasher: T::Hasher,
}
impl<T: Config> OfflineClient<T> {
/// Construct a new [`OfflineClient`], providing
/// the necessary runtime and compile-time arguments.
pub fn new(
genesis_hash: HashFor<T>,
runtime_version: RuntimeVersion,
metadata: impl Into<Metadata>,
) -> OfflineClient<T> {
let metadata = metadata.into();
let hasher = <T::Hasher as pezkuwi_subxt_core::config::Hasher>::new(&metadata);
OfflineClient {
hasher,
inner: Arc::new(ClientState {
genesis_hash,
runtime_version,
metadata,
}),
}
}
/// Return the genesis hash.
pub fn genesis_hash(&self) -> HashFor<T> {
self.inner.genesis_hash
}
/// Return the runtime version.
pub fn runtime_version(&self) -> RuntimeVersion {
self.inner.runtime_version
}
/// Return the [`Metadata`] used in this client.
pub fn metadata(&self) -> Metadata {
self.inner.metadata.clone()
}
/// Return the hasher used for the chain.
pub fn hasher(&self) -> T::Hasher {
self.hasher
}
// Just a copy of the most important trait methods so that people
// don't need to import the trait for most things:
/// Work with transactions.
pub fn tx(&self) -> TxClient<T, Self> {
<Self as OfflineClientT<T>>::tx(self)
}
/// Work with events.
pub fn events(&self) -> EventsClient<T, Self> {
<Self as OfflineClientT<T>>::events(self)
}
/// Work with storage.
pub fn storage(&self) -> StorageClient<T, Self> {
<Self as OfflineClientT<T>>::storage(self)
}
/// Access constants.
pub fn constants(&self) -> ConstantsClient<T, Self> {
<Self as OfflineClientT<T>>::constants(self)
}
/// Work with blocks.
pub fn blocks(&self) -> BlocksClient<T, Self> {
<Self as OfflineClientT<T>>::blocks(self)
}
/// Work with runtime APIs.
pub fn runtime_api(&self) -> RuntimeApiClient<T, Self> {
<Self as OfflineClientT<T>>::runtime_api(self)
}
/// Work with View Functions.
pub fn view_functions(&self) -> ViewFunctionsClient<T, Self> {
<Self as OfflineClientT<T>>::view_functions(self)
}
/// Access custom types
pub fn custom_values(&self) -> CustomValuesClient<T, Self> {
<Self as OfflineClientT<T>>::custom_values(self)
}
}
impl<T: Config> OfflineClientT<T> for OfflineClient<T> {
fn genesis_hash(&self) -> HashFor<T> {
self.genesis_hash()
}
fn runtime_version(&self) -> RuntimeVersion {
self.runtime_version()
}
fn metadata(&self) -> Metadata {
self.metadata()
}
fn hasher(&self) -> T::Hasher {
self.hasher()
}
}
// For ergonomics; cloning a client is deliberately fairly cheap (via Arc),
// so this allows users to pass references to a client rather than explicitly
// cloning. This is partly for consistency with OnlineClient, which can be
// easily converted into an OfflineClient for ergonomics.
impl<'a, T: Config> From<&'a OfflineClient<T>> for OfflineClient<T> {
fn from(c: &'a OfflineClient<T>) -> Self {
c.clone()
}
}
+580
View File
@@ -0,0 +1,580 @@
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
use super::{OfflineClient, OfflineClientT};
use crate::custom_values::CustomValuesClient;
use crate::{
Metadata,
backend::{Backend, BackendExt, StreamOfResults, legacy::LegacyBackend, rpc::RpcClient},
blocks::{BlockRef, BlocksClient},
config::{Config, HashFor},
constants::ConstantsClient,
error::{BackendError, OnlineClientError, RuntimeUpdateeApplyError, RuntimeUpdaterError},
events::EventsClient,
runtime_api::RuntimeApiClient,
storage::StorageClient,
tx::TxClient,
view_functions::ViewFunctionsClient,
};
use derive_where::derive_where;
use futures::TryFutureExt;
use futures::future;
use std::sync::{Arc, RwLock};
use pezkuwi_subxt_core::client::{ClientState, RuntimeVersion};
/// A trait representing a client that can perform
/// online actions.
pub trait OnlineClientT<T: Config>: OfflineClientT<T> {
/// Return a backend that can be used to communicate with a node.
fn backend(&self) -> &dyn Backend<T>;
}
/// A client that can be used to perform API calls (that is, either those
/// requiring an [`OfflineClientT`] or those requiring an [`OnlineClientT`]).
#[derive_where(Clone)]
pub struct OnlineClient<T: Config> {
inner: Arc<RwLock<Inner<T>>>,
backend: Arc<dyn Backend<T>>,
}
#[derive_where(Debug)]
struct Inner<T: Config> {
genesis_hash: HashFor<T>,
runtime_version: RuntimeVersion,
metadata: Metadata,
hasher: T::Hasher,
}
impl<T: Config> std::fmt::Debug for OnlineClient<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Client")
.field("rpc", &"RpcClient")
.field("inner", &self.inner)
.finish()
}
}
// The default constructors assume Jsonrpsee.
#[cfg(feature = "jsonrpsee")]
#[cfg_attr(docsrs, doc(cfg(feature = "jsonrpsee")))]
impl<T: Config> OnlineClient<T> {
/// Construct a new [`OnlineClient`] using default settings which
/// point to a locally running node on `ws://127.0.0.1:9944`.
pub async fn new() -> Result<OnlineClient<T>, OnlineClientError> {
let url = "ws://127.0.0.1:9944";
OnlineClient::from_url(url).await
}
/// Construct a new [`OnlineClient`], providing a URL to connect to.
pub async fn from_url(url: impl AsRef<str>) -> Result<OnlineClient<T>, OnlineClientError> {
pezkuwi_subxt_rpcs::utils::validate_url_is_secure(url.as_ref())?;
OnlineClient::from_insecure_url(url).await
}
/// Construct a new [`OnlineClient`], providing a URL to connect to.
///
/// Allows insecure URLs without SSL encryption, e.g. (http:// and ws:// URLs).
pub async fn from_insecure_url(
url: impl AsRef<str>,
) -> Result<OnlineClient<T>, OnlineClientError> {
let client = RpcClient::from_insecure_url(url).await?;
let backend = LegacyBackend::builder().build(client);
OnlineClient::from_backend(Arc::new(backend)).await
}
}
impl<T: Config> OnlineClient<T> {
/// Construct a new [`OnlineClient`] by providing an [`RpcClient`] to drive the connection.
/// This will use the current default [`Backend`], which may change in future releases.
pub async fn from_rpc_client(
rpc_client: impl Into<RpcClient>,
) -> Result<OnlineClient<T>, OnlineClientError> {
let rpc_client = rpc_client.into();
let backend = Arc::new(LegacyBackend::builder().build(rpc_client));
OnlineClient::from_backend(backend).await
}
/// Construct a new [`OnlineClient`] by providing an RPC client along with the other
/// necessary details. This will use the current default [`Backend`], which may change
/// in future releases.
///
/// # Warning
///
/// This is considered the most primitive and also error prone way to
/// instantiate a client; the genesis hash, metadata and runtime version provided will
/// entirely determine which node and blocks this client will be able to interact with,
/// and whether it will be able to successfully do things like submit transactions.
///
/// If you're unsure what you're doing, prefer one of the alternate methods to instantiate
/// a client.
pub fn from_rpc_client_with(
genesis_hash: HashFor<T>,
runtime_version: RuntimeVersion,
metadata: impl Into<Metadata>,
rpc_client: impl Into<RpcClient>,
) -> Result<OnlineClient<T>, OnlineClientError> {
let rpc_client = rpc_client.into();
let backend = Arc::new(LegacyBackend::builder().build(rpc_client));
OnlineClient::from_backend_with(genesis_hash, runtime_version, metadata, backend)
}
/// Construct a new [`OnlineClient`] by providing an underlying [`Backend`]
/// implementation to power it. Other details will be obtained from the chain.
pub async fn from_backend<B: Backend<T>>(
backend: Arc<B>,
) -> Result<OnlineClient<T>, OnlineClientError> {
let latest_block = backend
.latest_finalized_block_ref()
.await
.map_err(OnlineClientError::CannotGetLatestFinalizedBlock)?;
let (genesis_hash, runtime_version, metadata) = future::join3(
backend
.genesis_hash()
.map_err(OnlineClientError::CannotGetGenesisHash),
backend
.current_runtime_version()
.map_err(OnlineClientError::CannotGetCurrentRuntimeVersion),
OnlineClient::fetch_metadata(&*backend, latest_block.hash())
.map_err(OnlineClientError::CannotFetchMetadata),
)
.await;
OnlineClient::from_backend_with(genesis_hash?, runtime_version?, metadata?, backend)
}
/// Construct a new [`OnlineClient`] by providing all of the underlying details needed
/// to make it work.
///
/// # Warning
///
/// This is considered the most primitive and also error prone way to
/// instantiate a client; the genesis hash, metadata and runtime version provided will
/// entirely determine which node and blocks this client will be able to interact with,
/// and whether it will be able to successfully do things like submit transactions.
///
/// If you're unsure what you're doing, prefer one of the alternate methods to instantiate
/// a client.
pub fn from_backend_with<B: Backend<T>>(
genesis_hash: HashFor<T>,
runtime_version: RuntimeVersion,
metadata: impl Into<Metadata>,
backend: Arc<B>,
) -> Result<OnlineClient<T>, OnlineClientError> {
use pezkuwi_subxt_core::config::Hasher;
let metadata = metadata.into();
let hasher = T::Hasher::new(&metadata);
Ok(OnlineClient {
inner: Arc::new(RwLock::new(Inner {
genesis_hash,
runtime_version,
metadata,
hasher,
})),
backend,
})
}
/// Fetch the metadata from substrate using the runtime API.
async fn fetch_metadata(
backend: &dyn Backend<T>,
block_hash: HashFor<T>,
) -> Result<Metadata, BackendError> {
#[cfg(feature = "unstable-metadata")]
{
/// The unstable metadata version number.
const UNSTABLE_METADATA_VERSION: u32 = u32::MAX;
// Try to fetch the latest unstable metadata, if that fails fall back to
// fetching the latest stable metadata.
match backend
.metadata_at_version(UNSTABLE_METADATA_VERSION, block_hash)
.await
{
Ok(bytes) => Ok(bytes),
Err(_) => OnlineClient::fetch_latest_stable_metadata(backend, block_hash).await,
}
}
#[cfg(not(feature = "unstable-metadata"))]
OnlineClient::fetch_latest_stable_metadata(backend, block_hash).await
}
/// Fetch the latest stable metadata from the node.
async fn fetch_latest_stable_metadata(
backend: &dyn Backend<T>,
block_hash: HashFor<T>,
) -> Result<Metadata, BackendError> {
// The metadata versions we support in Subxt, from newest to oldest.
use pezkuwi_subxt_metadata::SUPPORTED_METADATA_VERSIONS;
// Try to fetch each version that we support in order from newest to oldest.
for version in SUPPORTED_METADATA_VERSIONS {
if let Ok(bytes) = backend.metadata_at_version(version, block_hash).await {
return Ok(bytes);
}
}
// If that fails, fetch the metadata V14 using the old API.
backend.legacy_metadata(block_hash).await
}
/// Create an object which can be used to keep the runtime up to date
/// in a separate thread.
///
/// # Example
///
/// ```rust,no_run,standalone_crate
/// # #[tokio::main]
/// # async fn main() {
/// use subxt::{ OnlineClient, PolkadotConfig };
///
/// let client = OnlineClient::<PolkadotConfig>::new().await.unwrap();
///
/// // high level API.
///
/// let update_task = client.updater();
/// tokio::spawn(async move {
/// update_task.perform_runtime_updates().await;
/// });
///
///
/// // low level API.
///
/// let updater = client.updater();
/// tokio::spawn(async move {
/// let mut update_stream = updater.runtime_updates().await.unwrap();
///
/// while let Ok(update) = update_stream.next().await {
/// let version = update.runtime_version().spec_version;
///
/// match updater.apply_update(update) {
/// Ok(()) => {
/// println!("Upgrade to version: {} successful", version)
/// }
/// Err(e) => {
/// println!("Upgrade to version {} failed {:?}", version, e);
/// }
/// };
/// }
/// });
/// # }
/// ```
pub fn updater(&self) -> ClientRuntimeUpdater<T> {
ClientRuntimeUpdater(self.clone())
}
/// Return the hasher configured for hashing blocks and extrinsics.
pub fn hasher(&self) -> T::Hasher {
self.inner.read().expect("shouldn't be poisoned").hasher
}
/// Return the [`Metadata`] used in this client.
pub fn metadata(&self) -> Metadata {
let inner = self.inner.read().expect("shouldn't be poisoned");
inner.metadata.clone()
}
/// Change the [`Metadata`] used in this client.
///
/// # Warning
///
/// Setting custom metadata may leave Subxt unable to work with certain blocks,
/// subscribe to latest blocks or submit valid transactions.
pub fn set_metadata(&self, metadata: impl Into<Metadata>) {
let mut inner = self.inner.write().expect("shouldn't be poisoned");
inner.metadata = metadata.into();
}
/// Return the genesis hash.
pub fn genesis_hash(&self) -> HashFor<T> {
let inner = self.inner.read().expect("shouldn't be poisoned");
inner.genesis_hash
}
/// Change the genesis hash used in this client.
///
/// # Warning
///
/// Setting a custom genesis hash may leave Subxt unable to
/// submit valid transactions.
pub fn set_genesis_hash(&self, genesis_hash: HashFor<T>) {
let mut inner = self.inner.write().expect("shouldn't be poisoned");
inner.genesis_hash = genesis_hash;
}
/// Return the runtime version.
pub fn runtime_version(&self) -> RuntimeVersion {
let inner = self.inner.read().expect("shouldn't be poisoned");
inner.runtime_version
}
/// Change the [`RuntimeVersion`] used in this client.
///
/// # Warning
///
/// Setting a custom runtime version may leave Subxt unable to
/// submit valid transactions.
pub fn set_runtime_version(&self, runtime_version: RuntimeVersion) {
let mut inner = self.inner.write().expect("shouldn't be poisoned");
inner.runtime_version = runtime_version;
}
/// Return an RPC client to make raw requests with.
pub fn backend(&self) -> &dyn Backend<T> {
&*self.backend
}
/// Return an offline client with the same configuration as this.
pub fn offline(&self) -> OfflineClient<T> {
let inner = self.inner.read().expect("shouldn't be poisoned");
OfflineClient::new(
inner.genesis_hash,
inner.runtime_version,
inner.metadata.clone(),
)
}
// Just a copy of the most important trait methods so that people
// don't need to import the trait for most things:
/// Work with transactions.
pub fn tx(&self) -> TxClient<T, Self> {
<Self as OfflineClientT<T>>::tx(self)
}
/// Work with events.
pub fn events(&self) -> EventsClient<T, Self> {
<Self as OfflineClientT<T>>::events(self)
}
/// Work with storage.
pub fn storage(&self) -> StorageClient<T, Self> {
<Self as OfflineClientT<T>>::storage(self)
}
/// Access constants.
pub fn constants(&self) -> ConstantsClient<T, Self> {
<Self as OfflineClientT<T>>::constants(self)
}
/// Work with blocks.
pub fn blocks(&self) -> BlocksClient<T, Self> {
<Self as OfflineClientT<T>>::blocks(self)
}
/// Work with runtime API.
pub fn runtime_api(&self) -> RuntimeApiClient<T, Self> {
<Self as OfflineClientT<T>>::runtime_api(self)
}
/// Work with View Functions.
pub fn view_functions(&self) -> ViewFunctionsClient<T, Self> {
<Self as OfflineClientT<T>>::view_functions(self)
}
/// Access custom types.
pub fn custom_values(&self) -> CustomValuesClient<T, Self> {
<Self as OfflineClientT<T>>::custom_values(self)
}
}
impl<T: Config> OfflineClientT<T> for OnlineClient<T> {
fn metadata(&self) -> Metadata {
self.metadata()
}
fn genesis_hash(&self) -> HashFor<T> {
self.genesis_hash()
}
fn runtime_version(&self) -> RuntimeVersion {
self.runtime_version()
}
fn hasher(&self) -> T::Hasher {
self.hasher()
}
// This is provided by default, but we can optimise here and only lock once:
fn client_state(&self) -> ClientState<T> {
let inner = self.inner.read().expect("shouldn't be poisoned");
ClientState {
genesis_hash: inner.genesis_hash,
runtime_version: inner.runtime_version,
metadata: inner.metadata.clone(),
}
}
}
impl<T: Config> OnlineClientT<T> for OnlineClient<T> {
fn backend(&self) -> &dyn Backend<T> {
&*self.backend
}
}
/// Client wrapper for performing runtime updates. See [`OnlineClient::updater()`]
/// for example usage.
pub struct ClientRuntimeUpdater<T: Config>(OnlineClient<T>);
impl<T: Config> ClientRuntimeUpdater<T> {
fn is_runtime_version_different(&self, new: &RuntimeVersion) -> bool {
let curr = self.0.inner.read().expect("shouldn't be poisoned");
&curr.runtime_version != new
}
fn do_update(&self, update: Update) {
let mut writable = self.0.inner.write().expect("shouldn't be poisoned");
writable.metadata = update.metadata;
writable.runtime_version = update.runtime_version;
}
/// Tries to apply a new update.
pub fn apply_update(&self, update: Update) -> Result<(), RuntimeUpdateeApplyError> {
if !self.is_runtime_version_different(&update.runtime_version) {
return Err(RuntimeUpdateeApplyError::SameVersion);
}
self.do_update(update);
Ok(())
}
/// Performs runtime updates indefinitely unless encountering an error.
///
/// *Note:* This will run indefinitely until it errors, so the typical usage
/// would be to run it in a separate background task.
pub async fn perform_runtime_updates(&self) -> Result<(), RuntimeUpdaterError> {
// Obtain an update subscription to further detect changes in the runtime version of the node.
let mut runtime_version_stream = self.runtime_updates().await?;
loop {
let update = runtime_version_stream.next().await?;
// This only fails if received the runtime version is the same the current runtime version
// which might occur because that runtime subscriptions in substrate sends out the initial
// value when they created and not only when runtime upgrades occurs.
// Thus, fine to ignore here as it strictly speaking isn't really an error
let _ = self.apply_update(update);
}
}
/// Low-level API to get runtime updates as a stream but it's doesn't check if the
/// runtime version is newer or updates the runtime.
///
/// 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>, RuntimeUpdaterError> {
let stream = self
.0
.backend()
.stream_runtime_version()
.await
.map_err(RuntimeUpdaterError::CannotStreamRuntimeVersion)?;
Ok(RuntimeUpdaterStream {
stream,
client: self.0.clone(),
})
}
}
/// Stream to perform runtime upgrades.
pub struct RuntimeUpdaterStream<T: Config> {
stream: StreamOfResults<RuntimeVersion>,
client: OnlineClient<T>,
}
impl<T: Config> RuntimeUpdaterStream<T> {
/// Wait for the next runtime update.
pub async fn next(&mut self) -> Result<Update, RuntimeUpdaterError> {
let runtime_version = self
.stream
.next()
.await
.ok_or(RuntimeUpdaterError::UnexpectedEndOfUpdateStream)?
.map_err(RuntimeUpdaterError::CannotGetNextRuntimeVersion)?;
let at = wait_runtime_upgrade_in_finalized_block(&self.client, &runtime_version).await?;
let metadata = OnlineClient::fetch_metadata(self.client.backend(), at.hash())
.await
.map_err(RuntimeUpdaterError::CannotFetchNewMetadata)?;
Ok(Update {
metadata,
runtime_version,
})
}
}
/// Represents the state when a runtime upgrade occurred.
pub struct Update {
runtime_version: RuntimeVersion,
metadata: Metadata,
}
impl Update {
/// Get the runtime version.
pub fn runtime_version(&self) -> &RuntimeVersion {
&self.runtime_version
}
/// Get the metadata.
pub fn metadata(&self) -> &Metadata {
&self.metadata
}
}
/// Helper to wait until the runtime upgrade is applied on at finalized block.
async fn wait_runtime_upgrade_in_finalized_block<T: Config>(
client: &OnlineClient<T>,
runtime_version: &RuntimeVersion,
) -> Result<BlockRef<HashFor<T>>, RuntimeUpdaterError> {
let hasher = client
.inner
.read()
.expect("Lock shouldn't be poisoned")
.hasher;
let mut block_sub = client
.backend()
.stream_finalized_block_headers(hasher)
.await
.map_err(RuntimeUpdaterError::CannotStreamFinalizedBlocks)?;
let block_ref = loop {
let (_, block_ref) = block_sub
.next()
.await
.ok_or(RuntimeUpdaterError::UnexpectedEndOfBlockStream)?
.map_err(RuntimeUpdaterError::CannotGetNextFinalizedBlock)?;
let addr =
crate::dynamic::storage::<(), scale_value::Value>("System", "LastRuntimeUpgrade");
let client_at = client.storage().at(block_ref.hash());
let value = client_at
.entry(addr)
// The storage `system::lastRuntimeUpgrade` should always exist.
// <https://github.com/paritytech/polkadot-sdk/blob/master/substrate/frame/system/src/lib.rs#L958>
.map_err(|_| RuntimeUpdaterError::CantFindSystemLastRuntimeUpgrade)?
.fetch(())
.await
.map_err(RuntimeUpdaterError::CantFetchLastRuntimeUpgrade)?
.decode_as::<LastRuntimeUpgrade>()
.map_err(RuntimeUpdaterError::CannotDecodeLastRuntimeUpgrade)?;
#[derive(scale_decode::DecodeAsType)]
struct LastRuntimeUpgrade {
spec_version: u32,
}
// We are waiting for the chain to have the same spec version
// as sent out via the runtime subscription.
if value.spec_version == runtime_version.spec_version {
break block_ref;
}
};
Ok(block_ref)
}
@@ -0,0 +1,49 @@
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
use crate::{Config, client::OfflineClientT, error::ConstantError};
use derive_where::derive_where;
use pezkuwi_subxt_core::constants::address::Address;
/// A client for accessing constants.
#[derive_where(Clone; Client)]
pub struct ConstantsClient<T, Client> {
client: Client,
_marker: std::marker::PhantomData<T>,
}
impl<T, Client> ConstantsClient<T, Client> {
/// Create a new [`ConstantsClient`].
pub fn new(client: Client) -> Self {
Self {
client,
_marker: std::marker::PhantomData,
}
}
}
impl<T: Config, Client: OfflineClientT<T>> ConstantsClient<T, Client> {
/// Run the validation logic against some constant address you'd like to access. Returns `Ok(())`
/// if the address is valid (or if it's not possible to check since the address has no validation hash).
/// Return an error if the address was not valid or something went wrong trying to validate it (ie
/// the pallet or constant in question do not exist at all).
pub fn validate<Addr: Address>(&self, address: Addr) -> Result<(), ConstantError> {
let metadata = self.client.metadata();
pezkuwi_subxt_core::constants::validate(address, &metadata)
}
/// Access the constant at the address given, returning the type defined by this address.
/// This is probably used with addresses given from static codegen, although you can manually
/// construct your own, too.
pub fn at<Addr: Address>(&self, address: Addr) -> Result<Addr::Target, ConstantError> {
let metadata = self.client.metadata();
pezkuwi_subxt_core::constants::get(address, &metadata)
}
/// Access the bytes of a constant by the address it is registered under.
pub fn bytes_at<Addr: Address>(&self, address: Addr) -> Result<Vec<u8>, ConstantError> {
let metadata = self.client.metadata();
pezkuwi_subxt_core::constants::get_bytes(address, &metadata)
}
}
+10
View File
@@ -0,0 +1,10 @@
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
//! Types associated with accessing constants.
mod constants_client;
pub use constants_client::ConstantsClient;
pub use pezkuwi_subxt_core::constants::address::{Address, DynamicAddress, StaticAddress, dynamic};
@@ -0,0 +1,134 @@
use crate::client::OfflineClientT;
use crate::{Config, error::CustomValueError};
use derive_where::derive_where;
use pezkuwi_subxt_core::custom_values::address::{Address, Maybe};
/// A client for accessing custom values stored in the metadata.
#[derive_where(Clone; Client)]
pub struct CustomValuesClient<T, Client> {
client: Client,
_marker: std::marker::PhantomData<T>,
}
impl<T, Client> CustomValuesClient<T, Client> {
/// Create a new [`CustomValuesClient`].
pub fn new(client: Client) -> Self {
Self {
client,
_marker: std::marker::PhantomData,
}
}
}
impl<T: Config, Client: OfflineClientT<T>> CustomValuesClient<T, Client> {
/// Access a custom value by the address it is registered under. This can be just a [str] to get back a dynamic value,
/// or a static address from the generated static interface to get a value of a static type returned.
pub fn at<Addr: Address<IsDecodable = Maybe>>(
&self,
address: Addr,
) -> Result<Addr::Target, CustomValueError> {
pezkuwi_subxt_core::custom_values::get(address, &self.client.metadata())
}
/// Access the bytes of a custom value by the address it is registered under.
pub fn bytes_at<Addr: Address>(&self, address: Addr) -> Result<Vec<u8>, CustomValueError> {
pezkuwi_subxt_core::custom_values::get_bytes(address, &self.client.metadata())
}
/// Run the validation logic against some custom value address you'd like to access. Returns `Ok(())`
/// if the address is valid (or if it's not possible to check since the address has no validation hash).
/// Returns an error if the address was not valid (wrong name, type or raw bytes)
pub fn validate<Addr: Address>(&self, address: Addr) -> Result<(), CustomValueError> {
pezkuwi_subxt_core::custom_values::validate(address, &self.client.metadata())
}
}
#[cfg(test)]
mod tests {
use crate::custom_values::{self, CustomValuesClient};
use crate::{Metadata, OfflineClient, SubstrateConfig};
use codec::Encode;
use scale_decode::DecodeAsType;
use scale_info::TypeInfo;
use scale_info::form::PortableForm;
use std::collections::BTreeMap;
use pezkuwi_subxt_core::client::RuntimeVersion;
#[derive(Debug, Clone, PartialEq, Eq, Encode, TypeInfo, DecodeAsType)]
pub struct Person {
age: u16,
name: String,
}
fn mock_metadata() -> Metadata {
let person_ty = scale_info::MetaType::new::<Person>();
let unit = scale_info::MetaType::new::<()>();
let mut types = scale_info::Registry::new();
let person_ty_id = types.register_type(&person_ty);
let unit_id = types.register_type(&unit);
let types: scale_info::PortableRegistry = types.into();
let person = Person {
age: 42,
name: "Neo".into(),
};
let person_value_metadata: frame_metadata::v15::CustomValueMetadata<PortableForm> =
frame_metadata::v15::CustomValueMetadata {
ty: person_ty_id,
value: person.encode(),
};
let frame_metadata = frame_metadata::v15::RuntimeMetadataV15 {
types,
pallets: vec![],
extrinsic: frame_metadata::v15::ExtrinsicMetadata {
version: 0,
address_ty: unit_id,
call_ty: unit_id,
signature_ty: unit_id,
extra_ty: unit_id,
signed_extensions: vec![],
},
ty: unit_id,
apis: vec![],
outer_enums: frame_metadata::v15::OuterEnums {
call_enum_ty: unit_id,
event_enum_ty: unit_id,
error_enum_ty: unit_id,
},
custom: frame_metadata::v15::CustomMetadata {
map: BTreeMap::from_iter([("Person".to_string(), person_value_metadata)]),
},
};
let metadata: pezkuwi_subxt_metadata::Metadata = frame_metadata.try_into().unwrap();
metadata
}
#[test]
fn test_decoding() {
let client = OfflineClient::<SubstrateConfig>::new(
Default::default(),
RuntimeVersion {
spec_version: 0,
transaction_version: 0,
},
mock_metadata(),
);
let custom_value_client = CustomValuesClient::new(client);
assert!(custom_value_client.at("No one").is_err());
let person_addr = custom_values::dynamic::<Person>("Person");
let person = custom_value_client.at(&person_addr).unwrap();
assert_eq!(
person,
Person {
age: 42,
name: "Neo".into()
}
)
}
}
+10
View File
@@ -0,0 +1,10 @@
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
//! Types associated with accessing custom types
mod custom_values_client;
pub use custom_values_client::CustomValuesClient;
pub use pezkuwi_subxt_core::custom_values::address::{Address, DynamicAddress, StaticAddress, dynamic};
+358
View File
@@ -0,0 +1,358 @@
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
//! A representation of the dispatch error; an error returned when
//! something fails in trying to submit/execute a transaction.
use super::{DispatchErrorDecodeError, ModuleErrorDecodeError, ModuleErrorDetailsError};
use crate::metadata::Metadata;
use core::fmt::Debug;
use scale_decode::{DecodeAsType, TypeResolver, visitor::DecodeAsTypeResult};
use std::{borrow::Cow, marker::PhantomData};
/// An error dispatching a transaction.
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
#[allow(clippy::large_enum_variant)]
#[non_exhaustive]
pub enum DispatchError {
/// Some error occurred.
#[error("Some unknown error occurred.")]
Other,
/// Failed to lookup some data.
#[error("Failed to lookup some data.")]
CannotLookup,
/// A bad origin.
#[error("Bad origin.")]
BadOrigin,
/// A custom error in a module.
#[error("Pallet error: {0}")]
Module(ModuleError),
/// At least one consumer is remaining so the account cannot be destroyed.
#[error("At least one consumer is remaining so the account cannot be destroyed.")]
ConsumerRemaining,
/// There are no providers so the account cannot be created.
#[error("There are no providers so the account cannot be created.")]
NoProviders,
/// There are too many consumers so the account cannot be created.
#[error("There are too many consumers so the account cannot be created.")]
TooManyConsumers,
/// An error to do with tokens.
#[error("Token error: {0}")]
Token(TokenError),
/// An arithmetic error.
#[error("Arithmetic error: {0}")]
Arithmetic(ArithmeticError),
/// The number of transactional layers has been reached, or we are not in a transactional layer.
#[error("Transactional error: {0}")]
Transactional(TransactionalError),
/// Resources exhausted, e.g. attempt to read/write data which is too large to manipulate.
#[error(
"Resources exhausted, e.g. attempt to read/write data which is too large to manipulate."
)]
Exhausted,
/// The state is corrupt; this is generally not going to fix itself.
#[error("The state is corrupt; this is generally not going to fix itself.")]
Corruption,
/// Some resource (e.g. a preimage) is unavailable right now. This might fix itself later.
#[error(
"Some resource (e.g. a preimage) is unavailable right now. This might fix itself later."
)]
Unavailable,
/// Root origin is not allowed.
#[error("Root origin is not allowed.")]
RootNotAllowed,
}
/// An error relating to tokens when dispatching a transaction.
#[derive(scale_decode::DecodeAsType, Debug, thiserror::Error, PartialEq, Eq)]
#[non_exhaustive]
pub enum TokenError {
/// Funds are unavailable.
#[error("Funds are unavailable.")]
FundsUnavailable,
/// Some part of the balance gives the only provider reference to the account and thus cannot be (re)moved.
#[error(
"Some part of the balance gives the only provider reference to the account and thus cannot be (re)moved."
)]
OnlyProvider,
/// Account cannot exist with the funds that would be given.
#[error("Account cannot exist with the funds that would be given.")]
BelowMinimum,
/// Account cannot be created.
#[error("Account cannot be created.")]
CannotCreate,
/// The asset in question is unknown.
#[error("The asset in question is unknown.")]
UnknownAsset,
/// Funds exist but are frozen.
#[error("Funds exist but are frozen.")]
Frozen,
/// Operation is not supported by the asset.
#[error("Operation is not supported by the asset.")]
Unsupported,
/// Account cannot be created for a held balance.
#[error("Account cannot be created for a held balance.")]
CannotCreateHold,
/// Withdrawal would cause unwanted loss of account.
#[error("Withdrawal would cause unwanted loss of account.")]
NotExpendable,
/// Account cannot receive the assets.
#[error("Account cannot receive the assets.")]
Blocked,
}
/// An error relating to arithmetic when dispatching a transaction.
#[derive(scale_decode::DecodeAsType, Debug, thiserror::Error, PartialEq, Eq)]
#[non_exhaustive]
pub enum ArithmeticError {
/// Underflow.
#[error("Underflow.")]
Underflow,
/// Overflow.
#[error("Overflow.")]
Overflow,
/// Division by zero.
#[error("Division by zero.")]
DivisionByZero,
}
/// An error relating to the transactional layers when dispatching a transaction.
#[derive(scale_decode::DecodeAsType, Debug, thiserror::Error, PartialEq, Eq)]
#[non_exhaustive]
pub enum TransactionalError {
/// Too many transactional layers have been spawned.
#[error("Too many transactional layers have been spawned.")]
LimitReached,
/// A transactional layer was expected, but does not exist.
#[error("A transactional layer was expected, but does not exist.")]
NoLayer,
}
/// Details about a module error that has occurred.
#[derive(Clone, thiserror::Error)]
#[non_exhaustive]
pub struct ModuleError {
metadata: Metadata,
/// Bytes representation:
/// - `bytes[0]`: pallet index
/// - `bytes[1]`: error index
/// - `bytes[2..]`: 3 bytes specific for the module error
bytes: [u8; 5],
}
impl PartialEq for ModuleError {
fn eq(&self, other: &Self) -> bool {
// A module error is the same if the raw underlying details are the same.
self.bytes == other.bytes
}
}
impl Eq for ModuleError {}
/// Custom `Debug` implementation, ignores the very large `metadata` field, using it instead (as
/// intended) to resolve the actual pallet and error names. This is much more useful for debugging.
impl Debug for ModuleError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let details = self.details_string();
write!(f, "ModuleError(<{details}>)")
}
}
impl std::fmt::Display for ModuleError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let details = self.details_string();
write!(f, "{details}")
}
}
impl ModuleError {
/// Return more details about this error.
pub fn details(&self) -> Result<ModuleErrorDetails<'_>, ModuleErrorDetailsError> {
let pallet = self
.metadata
.pallet_by_error_index(self.pallet_index())
.ok_or(ModuleErrorDetailsError::PalletNotFound {
pallet_index: self.pallet_index(),
})?;
let variant = pallet
.error_variant_by_index(self.error_index())
.ok_or_else(|| ModuleErrorDetailsError::ErrorVariantNotFound {
pallet_name: pallet.name().into(),
error_index: self.error_index(),
})?;
Ok(ModuleErrorDetails { pallet, variant })
}
/// Return a formatted string of the resolved error details for debugging/display purposes.
pub fn details_string(&self) -> String {
match self.details() {
Ok(details) => format!(
"{pallet_name}::{variant_name}",
pallet_name = details.pallet.name(),
variant_name = details.variant.name,
),
Err(_) => format!(
"Unknown pallet error '{bytes:?}' (pallet and error details cannot be retrieved)",
bytes = self.bytes
),
}
}
/// Return the underlying module error data that was decoded.
pub fn bytes(&self) -> [u8; 5] {
self.bytes
}
/// Obtain the pallet index from the underlying byte data.
pub fn pallet_index(&self) -> u8 {
self.bytes[0]
}
/// Obtain the error index from the underlying byte data.
pub fn error_index(&self) -> u8 {
self.bytes[1]
}
/// Attempts to decode the ModuleError into the top outer Error enum.
pub fn as_root_error<E: DecodeAsType>(&self) -> Result<E, ModuleErrorDecodeError> {
let decoded = E::decode_as_type(
&mut &self.bytes[..],
self.metadata.outer_enums().error_enum_ty(),
self.metadata.types(),
)
.map_err(ModuleErrorDecodeError)?;
Ok(decoded)
}
}
/// Details about the module error.
pub struct ModuleErrorDetails<'a> {
/// The pallet that the error is in
pub pallet: pezkuwi_subxt_metadata::PalletMetadata<'a>,
/// The variant representing the error
pub variant: &'a scale_info::Variant<scale_info::form::PortableForm>,
}
impl DispatchError {
/// Attempt to decode a runtime [`DispatchError`].
#[doc(hidden)]
pub fn decode_from<'a>(
bytes: impl Into<Cow<'a, [u8]>>,
metadata: Metadata,
) -> Result<Self, DispatchErrorDecodeError> {
let bytes = bytes.into();
let dispatch_error_ty_id = metadata
.dispatch_error_ty()
.ok_or(DispatchErrorDecodeError::DispatchErrorTypeIdNotFound)?;
// The aim is to decode our bytes into roughly this shape. This is copied from
// `sp_runtime::DispatchError`; we need the variant names and any inner variant
// names/shapes to line up in order for decoding to be successful.
#[derive(scale_decode::DecodeAsType)]
enum DecodedDispatchError {
Other,
CannotLookup,
BadOrigin,
Module(DecodedModuleErrorBytes),
ConsumerRemaining,
NoProviders,
TooManyConsumers,
Token(TokenError),
Arithmetic(ArithmeticError),
Transactional(TransactionalError),
Exhausted,
Corruption,
Unavailable,
RootNotAllowed,
}
// ModuleError is a bit special; we want to support being decoded from either
// a legacy format of 2 bytes, or a newer format of 5 bytes. So, just grab the bytes
// out when decoding to manually work with them.
struct DecodedModuleErrorBytes(Vec<u8>);
struct DecodedModuleErrorBytesVisitor<R: TypeResolver>(PhantomData<R>);
impl<R: TypeResolver> scale_decode::Visitor for DecodedModuleErrorBytesVisitor<R> {
type Error = scale_decode::Error;
type Value<'scale, 'info> = DecodedModuleErrorBytes;
type TypeResolver = R;
fn unchecked_decode_as_type<'scale, 'info>(
self,
input: &mut &'scale [u8],
_type_id: R::TypeId,
_types: &'info R,
) -> DecodeAsTypeResult<Self, Result<Self::Value<'scale, 'info>, Self::Error>>
{
DecodeAsTypeResult::Decoded(Ok(DecodedModuleErrorBytes(input.to_vec())))
}
}
impl scale_decode::IntoVisitor for DecodedModuleErrorBytes {
type AnyVisitor<R: TypeResolver> = DecodedModuleErrorBytesVisitor<R>;
fn into_visitor<R: TypeResolver>() -> DecodedModuleErrorBytesVisitor<R> {
DecodedModuleErrorBytesVisitor(PhantomData)
}
}
// Decode into our temporary error:
let decoded_dispatch_err = DecodedDispatchError::decode_as_type(
&mut &*bytes,
dispatch_error_ty_id,
metadata.types(),
)
.map_err(DispatchErrorDecodeError::CouldNotDecodeDispatchError)?;
// Convert into the outward-facing error, mainly by handling the Module variant.
let dispatch_error = match decoded_dispatch_err {
// Mostly we don't change anything from our decoded to our outward-facing error:
DecodedDispatchError::Other => DispatchError::Other,
DecodedDispatchError::CannotLookup => DispatchError::CannotLookup,
DecodedDispatchError::BadOrigin => DispatchError::BadOrigin,
DecodedDispatchError::ConsumerRemaining => DispatchError::ConsumerRemaining,
DecodedDispatchError::NoProviders => DispatchError::NoProviders,
DecodedDispatchError::TooManyConsumers => DispatchError::TooManyConsumers,
DecodedDispatchError::Token(val) => DispatchError::Token(val),
DecodedDispatchError::Arithmetic(val) => DispatchError::Arithmetic(val),
DecodedDispatchError::Transactional(val) => DispatchError::Transactional(val),
DecodedDispatchError::Exhausted => DispatchError::Exhausted,
DecodedDispatchError::Corruption => DispatchError::Corruption,
DecodedDispatchError::Unavailable => DispatchError::Unavailable,
DecodedDispatchError::RootNotAllowed => DispatchError::RootNotAllowed,
// But we apply custom logic to transform the module error into the outward facing version:
DecodedDispatchError::Module(module_bytes) => {
let module_bytes = module_bytes.0;
// The old version is 2 bytes; a pallet and error index.
// The new version is 5 bytes; a pallet and error index and then 3 extra bytes.
let bytes = if module_bytes.len() == 2 {
[module_bytes[0], module_bytes[1], 0, 0, 0]
} else if module_bytes.len() == 5 {
[
module_bytes[0],
module_bytes[1],
module_bytes[2],
module_bytes[3],
module_bytes[4],
]
} else {
tracing::warn!(
"Can't decode error sp_runtime::DispatchError: bytes do not match known shapes"
);
// Return _all_ of the bytes; every "unknown" return should be consistent.
return Err(DispatchErrorDecodeError::CouldNotDecodeModuleError {
bytes: bytes.to_vec(),
});
};
// And return our outward-facing version:
DispatchError::Module(ModuleError { metadata, bytes })
}
};
Ok(dispatch_error)
}
}
+15
View File
@@ -0,0 +1,15 @@
/// Display hex strings.
#[derive(PartialEq, Eq, Clone, Debug, PartialOrd, Ord)]
pub struct Hex(String);
impl<T: AsRef<[u8]>> From<T> for Hex {
fn from(value: T) -> Self {
Hex(hex::encode(value.as_ref()))
}
}
impl std::fmt::Display for Hex {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
+702
View File
@@ -0,0 +1,702 @@
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
//! Types representing the errors that can be returned.
mod dispatch_error;
mod hex;
crate::macros::cfg_unstable_light_client! {
pub use pezkuwi_subxt_lightclient::LightClientError;
}
// Re-export dispatch error types:
pub use dispatch_error::{
ArithmeticError, DispatchError, ModuleError, TokenError, TransactionalError,
};
// Re-expose the errors we use from other crates here:
pub use crate::Metadata;
pub use hex::Hex;
pub use scale_decode::Error as DecodeError;
pub use scale_encode::Error as EncodeError;
pub use pezkuwi_subxt_metadata::TryFromError as MetadataTryFromError;
// Re-export core error types we're just reusing.
pub use pezkuwi_subxt_core::error::{
ConstantError,
CustomValueError,
EventsError as CoreEventsError,
// These errors are exposed as-is:
ExtrinsicDecodeErrorAt,
// These errors are wrapped:
ExtrinsicError as CoreExtrinsicError,
RuntimeApiError as CoreRuntimeApiError,
StorageError as CoreStorageError,
StorageKeyError,
StorageValueError,
ViewFunctionError as CoreViewFunctionError,
};
/// A global error type. Any of the errors exposed here can convert into this
/// error via `.into()`, but this error isn't itself exposed from anything.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
#[allow(missing_docs)]
pub enum Error {
#[error(transparent)]
ExtrinsicDecodeErrorAt(#[from] ExtrinsicDecodeErrorAt),
#[error(transparent)]
ConstantError(#[from] ConstantError),
#[error(transparent)]
CustomValueError(#[from] CustomValueError),
#[error(transparent)]
StorageKeyError(#[from] StorageKeyError),
#[error(transparent)]
StorageValueError(#[from] StorageValueError),
#[error(transparent)]
BackendError(#[from] BackendError),
#[error(transparent)]
BlockError(#[from] BlockError),
#[error(transparent)]
AccountNonceError(#[from] AccountNonceError),
#[error(transparent)]
OnlineClientError(#[from] OnlineClientError),
#[error(transparent)]
RuntimeUpdaterError(#[from] RuntimeUpdaterError),
#[error(transparent)]
RuntimeUpdateeApplyError(#[from] RuntimeUpdateeApplyError),
#[error(transparent)]
RuntimeApiError(#[from] RuntimeApiError),
#[error(transparent)]
EventsError(#[from] EventsError),
#[error(transparent)]
ExtrinsicError(#[from] ExtrinsicError),
#[error(transparent)]
ViewFunctionError(#[from] ViewFunctionError),
#[error(transparent)]
TransactionProgressError(#[from] TransactionProgressError),
#[error(transparent)]
TransactionStatusError(#[from] TransactionStatusError),
#[error(transparent)]
TransactionEventsError(#[from] TransactionEventsError),
#[error(transparent)]
TransactionFinalizedSuccessError(#[from] TransactionFinalizedSuccessError),
#[error(transparent)]
ModuleErrorDetailsError(#[from] ModuleErrorDetailsError),
#[error(transparent)]
ModuleErrorDecodeError(#[from] ModuleErrorDecodeError),
#[error(transparent)]
DispatchErrorDecodeError(#[from] DispatchErrorDecodeError),
#[error(transparent)]
StorageError(#[from] StorageError),
// Dev note: Subxt doesn't directly return Raw* errors. These exist so that when
// users use common crates (like parity-scale-codec and subxt-rpcs), errors returned
// there can be handled automatically using ? when the expected error is subxt::Error.
#[error("Other RPC client error: {0}")]
OtherRpcClientError(#[from] pezkuwi_subxt_rpcs::Error),
#[error("Other codec error: {0}")]
OtherCodecError(#[from] codec::Error),
#[cfg(feature = "unstable-light-client")]
#[error("Other light client error: {0}")]
OtherLightClientError(#[from] pezkuwi_subxt_lightclient::LightClientError),
#[cfg(feature = "unstable-light-client")]
#[error("Other light client RPC error: {0}")]
OtherLightClientRpcError(#[from] pezkuwi_subxt_lightclient::LightClientRpcError),
// Dev note: Nothing in subxt should ever emit this error. It can instead be used
// to easily map other errors into a subxt::Error for convenience. Some From impls
// make this automatic for common "other" error types.
#[error("Other error: {0}")]
Other(Box<dyn std::error::Error + Send + Sync + 'static>),
}
impl From<std::convert::Infallible> for Error {
fn from(value: std::convert::Infallible) -> Self {
match value {}
}
}
impl Error {
/// Create a generic error. This is a quick workaround when you are using
/// [`Error`] and have a non-Subxt error to return.
pub fn other<E: std::error::Error + Send + Sync + 'static>(error: E) -> Error {
Error::Other(Box::new(error))
}
/// Create a generic error from a string. This is a quick workaround when you are using
/// [`Error`] and have a non-Subxt error to return.
pub fn other_str(error: impl Into<String>) -> Error {
#[derive(thiserror::Error, Debug, Clone)]
#[error("{0}")]
struct StrError(String);
Error::Other(Box::new(StrError(error.into())))
}
/// Checks whether the error was caused by a RPC re-connection.
pub fn is_disconnected_will_reconnect(&self) -> bool {
matches!(
self.backend_error(),
Some(BackendError::Rpc(RpcError::ClientError(
pezkuwi_subxt_rpcs::Error::DisconnectedWillReconnect(_)
)))
)
}
/// Checks whether the error was caused by a RPC request being rejected.
pub fn is_rpc_limit_reached(&self) -> bool {
matches!(
self.backend_error(),
Some(BackendError::Rpc(RpcError::LimitReached))
)
}
fn backend_error(&self) -> Option<&BackendError> {
match self {
Error::BlockError(e) => e.backend_error(),
Error::AccountNonceError(e) => e.backend_error(),
Error::OnlineClientError(e) => e.backend_error(),
Error::RuntimeUpdaterError(e) => e.backend_error(),
Error::RuntimeApiError(e) => e.backend_error(),
Error::EventsError(e) => e.backend_error(),
Error::ExtrinsicError(e) => e.backend_error(),
Error::ViewFunctionError(e) => e.backend_error(),
Error::TransactionProgressError(e) => e.backend_error(),
Error::TransactionEventsError(e) => e.backend_error(),
Error::TransactionFinalizedSuccessError(e) => e.backend_error(),
Error::StorageError(e) => e.backend_error(),
// Any errors that **don't** return a BackendError anywhere will return None:
_ => None,
}
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
#[allow(missing_docs)]
pub enum BackendError {
#[error("Backend error: RPC error: {0}")]
Rpc(#[from] RpcError),
#[error("Backend error: Could not find metadata version {0}")]
MetadataVersionNotFound(u32),
#[error("Backend error: Could not codec::Decode Runtime API response: {0}")]
CouldNotScaleDecodeRuntimeResponse(codec::Error),
#[error("Backend error: Could not codec::Decode metadata bytes into subxt::Metadata: {0}")]
CouldNotDecodeMetadata(codec::Error),
// This is for errors in `Backend` implementations which aren't any of the "pre-defined" set above:
#[error("Custom backend error: {0}")]
Other(String),
}
impl BackendError {
/// Checks whether the error was caused by a RPC re-connection.
pub fn is_disconnected_will_reconnect(&self) -> bool {
matches!(
self,
BackendError::Rpc(RpcError::ClientError(
pezkuwi_subxt_rpcs::Error::DisconnectedWillReconnect(_)
))
)
}
/// Checks whether the error was caused by a RPC request being rejected.
pub fn is_rpc_limit_reached(&self) -> bool {
matches!(self, BackendError::Rpc(RpcError::LimitReached))
}
}
impl From<pezkuwi_subxt_rpcs::Error> for BackendError {
fn from(value: pezkuwi_subxt_rpcs::Error) -> Self {
BackendError::Rpc(RpcError::ClientError(value))
}
}
/// An RPC error. Since we are generic over the RPC client that is used,
/// the error is boxed and could be casted.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum RpcError {
/// Error related to the RPC client.
#[error("RPC error: {0}")]
ClientError(#[from] pezkuwi_subxt_rpcs::Error),
/// This error signals that we got back a [`pezkuwi_subxt_rpcs::methods::chain_head::MethodResponse::LimitReached`],
/// which is not technically an RPC error but is treated as an error in our own APIs.
#[error("RPC error: limit reached")]
LimitReached,
/// The RPC subscription was dropped.
#[error("RPC error: subscription dropped.")]
SubscriptionDropped,
}
/// Block error
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
#[allow(missing_docs)]
pub enum BlockError {
#[error(
"Could not find the block body with hash {block_hash} (perhaps it was on a non-finalized fork?)"
)]
BlockNotFound { block_hash: Hex },
#[error("Could not download the block header with hash {block_hash}: {reason}")]
CouldNotGetBlockHeader {
block_hash: Hex,
reason: BackendError,
},
#[error("Could not download the latest block header: {0}")]
CouldNotGetLatestBlock(BackendError),
#[error("Could not subscribe to all blocks: {0}")]
CouldNotSubscribeToAllBlocks(BackendError),
#[error("Could not subscribe to best blocks: {0}")]
CouldNotSubscribeToBestBlocks(BackendError),
#[error("Could not subscribe to finalized blocks: {0}")]
CouldNotSubscribeToFinalizedBlocks(BackendError),
#[error("Error getting account nonce at block {block_hash}")]
AccountNonceError {
block_hash: Hex,
account_id: Hex,
reason: AccountNonceError,
},
}
impl BlockError {
fn backend_error(&self) -> Option<&BackendError> {
match self {
BlockError::CouldNotGetBlockHeader { reason: e, .. }
| BlockError::CouldNotGetLatestBlock(e)
| BlockError::CouldNotSubscribeToAllBlocks(e)
| BlockError::CouldNotSubscribeToBestBlocks(e)
| BlockError::CouldNotSubscribeToFinalizedBlocks(e) => Some(e),
_ => None,
}
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
#[allow(missing_docs)]
pub enum AccountNonceError {
#[error("Could not retrieve account nonce: {0}")]
CouldNotRetrieve(#[from] BackendError),
#[error("Could not decode account nonce: {0}")]
CouldNotDecode(#[from] codec::Error),
#[error("Wrong number of account nonce bytes returned: {0} (expected 2, 4 or 8)")]
WrongNumberOfBytes(usize),
}
impl AccountNonceError {
fn backend_error(&self) -> Option<&BackendError> {
match self {
AccountNonceError::CouldNotRetrieve(e) => Some(e),
_ => None,
}
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
#[allow(missing_docs)]
pub enum OnlineClientError {
#[error("Cannot construct OnlineClient: {0}")]
RpcError(#[from] pezkuwi_subxt_rpcs::Error),
#[error(
"Cannot construct OnlineClient: Cannot fetch latest finalized block to obtain init details from: {0}"
)]
CannotGetLatestFinalizedBlock(BackendError),
#[error("Cannot construct OnlineClient: Cannot fetch genesis hash: {0}")]
CannotGetGenesisHash(BackendError),
#[error("Cannot construct OnlineClient: Cannot fetch current runtime version: {0}")]
CannotGetCurrentRuntimeVersion(BackendError),
#[error("Cannot construct OnlineClient: Cannot fetch metadata: {0}")]
CannotFetchMetadata(BackendError),
}
impl OnlineClientError {
fn backend_error(&self) -> Option<&BackendError> {
match self {
OnlineClientError::CannotGetLatestFinalizedBlock(e)
| OnlineClientError::CannotGetGenesisHash(e)
| OnlineClientError::CannotGetCurrentRuntimeVersion(e)
| OnlineClientError::CannotFetchMetadata(e) => Some(e),
_ => None,
}
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
#[allow(missing_docs)]
pub enum RuntimeUpdaterError {
#[error("Error subscribing to runtime updates: The update stream ended unexpectedly")]
UnexpectedEndOfUpdateStream,
#[error("Error subscribing to runtime updates: The finalized block stream ended unexpectedly")]
UnexpectedEndOfBlockStream,
#[error("Error subscribing to runtime updates: Can't stream runtime version: {0}")]
CannotStreamRuntimeVersion(BackendError),
#[error("Error subscribing to runtime updates: Can't get next runtime version in stream: {0}")]
CannotGetNextRuntimeVersion(BackendError),
#[error("Error subscribing to runtime updates: Cannot stream finalized blocks: {0}")]
CannotStreamFinalizedBlocks(BackendError),
#[error("Error subscribing to runtime updates: Cannot get next finalized block in stream: {0}")]
CannotGetNextFinalizedBlock(BackendError),
#[error("Cannot fetch new metadata for runtime update: {0}")]
CannotFetchNewMetadata(BackendError),
#[error(
"Error subscribing to runtime updates: Cannot find the System.LastRuntimeUpgrade storage entry"
)]
CantFindSystemLastRuntimeUpgrade,
#[error("Error subscribing to runtime updates: Cannot fetch last runtime upgrade: {0}")]
CantFetchLastRuntimeUpgrade(StorageError),
#[error("Error subscribing to runtime updates: Cannot decode last runtime upgrade: {0}")]
CannotDecodeLastRuntimeUpgrade(StorageValueError),
}
impl RuntimeUpdaterError {
fn backend_error(&self) -> Option<&BackendError> {
match self {
RuntimeUpdaterError::CannotStreamRuntimeVersion(e)
| RuntimeUpdaterError::CannotGetNextRuntimeVersion(e)
| RuntimeUpdaterError::CannotStreamFinalizedBlocks(e)
| RuntimeUpdaterError::CannotGetNextFinalizedBlock(e)
| RuntimeUpdaterError::CannotFetchNewMetadata(e) => Some(e),
_ => None,
}
}
}
/// Error that can occur during upgrade.
#[non_exhaustive]
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum RuntimeUpdateeApplyError {
#[error("The proposed runtime update is the same as the current version")]
SameVersion,
}
/// Error working with Runtime APIs
#[non_exhaustive]
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum RuntimeApiError {
#[error("Cannot access Runtime APIs at latest block: Cannot fetch latest finalized block: {0}")]
CannotGetLatestFinalizedBlock(BackendError),
#[error("{0}")]
OfflineError(#[from] CoreRuntimeApiError),
#[error("Cannot call the Runtime API: {0}")]
CannotCallApi(BackendError),
}
impl RuntimeApiError {
fn backend_error(&self) -> Option<&BackendError> {
match self {
RuntimeApiError::CannotGetLatestFinalizedBlock(e)
| RuntimeApiError::CannotCallApi(e) => Some(e),
_ => None,
}
}
}
/// Error working with events.
#[non_exhaustive]
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum EventsError {
#[error("{0}")]
OfflineError(#[from] CoreEventsError),
#[error("Cannot access events at latest block: Cannot fetch latest finalized block: {0}")]
CannotGetLatestFinalizedBlock(BackendError),
#[error("Cannot fetch event bytes: {0}")]
CannotFetchEventBytes(BackendError),
}
impl EventsError {
fn backend_error(&self) -> Option<&BackendError> {
match self {
EventsError::CannotGetLatestFinalizedBlock(e)
| EventsError::CannotFetchEventBytes(e) => Some(e),
_ => None,
}
}
}
/// Error working with extrinsics.
#[non_exhaustive]
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum ExtrinsicError {
#[error("{0}")]
OfflineError(#[from] CoreExtrinsicError),
#[error("Could not download block body to extract extrinsics from: {0}")]
CannotGetBlockBody(BackendError),
#[error("Block not found: {0}")]
BlockNotFound(Hex),
#[error("{0}")]
CouldNotDecodeExtrinsics(#[from] ExtrinsicDecodeErrorAt),
#[error(
"Extrinsic submission error: Cannot get latest finalized block to grab account nonce at: {0}"
)]
CannotGetLatestFinalizedBlock(BackendError),
#[error("Cannot find block header for block {block_hash}")]
CannotFindBlockHeader { block_hash: Hex },
#[error("Error getting account nonce at block {block_hash}")]
AccountNonceError {
block_hash: Hex,
account_id: Hex,
reason: AccountNonceError,
},
#[error("Cannot submit extrinsic: {0}")]
ErrorSubmittingTransaction(BackendError),
#[error("A transaction status error was returned while submitting the extrinsic: {0}")]
TransactionStatusError(TransactionStatusError),
#[error(
"The transaction status stream encountered an error while submitting the extrinsic: {0}"
)]
TransactionStatusStreamError(BackendError),
#[error(
"The transaction status stream unexpectedly ended, so we don't know the status of the submitted extrinsic"
)]
UnexpectedEndOfTransactionStatusStream,
#[error("Cannot get fee info from Runtime API: {0}")]
CannotGetFeeInfo(BackendError),
#[error("Cannot get validation info from Runtime API: {0}")]
CannotGetValidationInfo(BackendError),
#[error("Cannot decode ValidationResult bytes: {0}")]
CannotDecodeValidationResult(codec::Error),
#[error("ValidationResult bytes could not be decoded")]
UnexpectedValidationResultBytes(Vec<u8>),
}
impl ExtrinsicError {
fn backend_error(&self) -> Option<&BackendError> {
match self {
ExtrinsicError::CannotGetBlockBody(e)
| ExtrinsicError::CannotGetLatestFinalizedBlock(e)
| ExtrinsicError::ErrorSubmittingTransaction(e)
| ExtrinsicError::TransactionStatusStreamError(e)
| ExtrinsicError::CannotGetFeeInfo(e)
| ExtrinsicError::CannotGetValidationInfo(e) => Some(e),
ExtrinsicError::AccountNonceError { reason, .. } => reason.backend_error(),
_ => None,
}
}
}
/// Error working with View Functions.
#[non_exhaustive]
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum ViewFunctionError {
#[error("{0}")]
OfflineError(#[from] CoreViewFunctionError),
#[error(
"Cannot access View Functions at latest block: Cannot fetch latest finalized block: {0}"
)]
CannotGetLatestFinalizedBlock(BackendError),
#[error("Cannot call the View Function Runtime API: {0}")]
CannotCallApi(BackendError),
}
impl ViewFunctionError {
fn backend_error(&self) -> Option<&BackendError> {
match self {
ViewFunctionError::CannotGetLatestFinalizedBlock(e)
| ViewFunctionError::CannotCallApi(e) => Some(e),
_ => None,
}
}
}
/// Error during the transaction progress.
#[non_exhaustive]
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum TransactionProgressError {
#[error("Cannot get the next transaction progress update: {0}")]
CannotGetNextProgressUpdate(BackendError),
#[error("Error during transaction progress: {0}")]
TransactionStatusError(#[from] TransactionStatusError),
#[error(
"The transaction status stream unexpectedly ended, so we have no further transaction progress updates"
)]
UnexpectedEndOfTransactionStatusStream,
}
impl TransactionProgressError {
fn backend_error(&self) -> Option<&BackendError> {
match self {
TransactionProgressError::CannotGetNextProgressUpdate(e) => Some(e),
TransactionProgressError::TransactionStatusError(_) => None,
TransactionProgressError::UnexpectedEndOfTransactionStatusStream => None,
}
}
}
/// An error emitted as the result of a transaction progress update.
#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
#[non_exhaustive]
#[allow(missing_docs)]
pub enum TransactionStatusError {
/// An error happened on the node that the transaction was submitted to.
#[error("Error handling transaction: {0}")]
Error(String),
/// The transaction was deemed invalid.
#[error("The transaction is not valid: {0}")]
Invalid(String),
/// The transaction was dropped.
#[error("The transaction was dropped: {0}")]
Dropped(String),
}
/// Error fetching events for a just-submitted transaction
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
#[allow(missing_docs)]
pub enum TransactionEventsError {
#[error(
"The block containing the submitted transaction ({block_hash}) could not be downloaded: {error}"
)]
CannotFetchBlockBody {
block_hash: Hex,
error: BackendError,
},
#[error(
"Cannot find the the submitted transaction (hash: {transaction_hash}) in the block (hash: {block_hash}) it is supposed to be in."
)]
CannotFindTransactionInBlock {
block_hash: Hex,
transaction_hash: Hex,
},
#[error("The block containing the submitted transaction ({block_hash}) could not be found")]
BlockNotFound { block_hash: Hex },
#[error(
"Could not decode event at index {event_index} for the submitted transaction at block {block_hash}: {error}"
)]
CannotDecodeEventInBlock {
event_index: usize,
block_hash: Hex,
error: EventsError,
},
#[error("Could not fetch events for the submitted transaction: {error}")]
CannotFetchEventsForTransaction {
block_hash: Hex,
transaction_hash: Hex,
error: EventsError,
},
#[error("The transaction led to a DispatchError, but we failed to decode it: {error}")]
CannotDecodeDispatchError {
error: DispatchErrorDecodeError,
bytes: Vec<u8>,
},
#[error("The transaction failed with the following dispatch error: {0}")]
ExtrinsicFailed(#[from] DispatchError),
}
impl TransactionEventsError {
fn backend_error(&self) -> Option<&BackendError> {
match self {
TransactionEventsError::CannotFetchBlockBody { error, .. } => Some(error),
TransactionEventsError::CannotDecodeEventInBlock { error, .. }
| TransactionEventsError::CannotFetchEventsForTransaction { error, .. } => {
error.backend_error()
}
_ => None,
}
}
}
/// Error waiting for the transaction to be finalized and successful.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
#[allow(missing_docs, clippy::large_enum_variant)]
pub enum TransactionFinalizedSuccessError {
#[error("Could not finalize the transaction: {0}")]
FinalizationError(#[from] TransactionProgressError),
#[error("The transaction did not succeed: {0}")]
SuccessError(#[from] TransactionEventsError),
}
impl TransactionFinalizedSuccessError {
fn backend_error(&self) -> Option<&BackendError> {
match self {
TransactionFinalizedSuccessError::FinalizationError(e) => e.backend_error(),
TransactionFinalizedSuccessError::SuccessError(e) => e.backend_error(),
}
}
}
/// Error decoding the [`DispatchError`]
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
#[allow(missing_docs)]
pub enum ModuleErrorDetailsError {
#[error(
"Could not get details for the DispatchError: could not find pallet index {pallet_index}"
)]
PalletNotFound { pallet_index: u8 },
#[error(
"Could not get details for the DispatchError: could not find error index {error_index} in pallet {pallet_name}"
)]
ErrorVariantNotFound {
pallet_name: String,
error_index: u8,
},
}
/// Error decoding the [`ModuleError`]
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
#[allow(missing_docs)]
#[error("Could not decode the DispatchError::Module payload into the given type: {0}")]
pub struct ModuleErrorDecodeError(scale_decode::Error);
/// Error decoding the [`DispatchError`]
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
#[allow(missing_docs)]
pub enum DispatchErrorDecodeError {
#[error(
"Could not decode the DispatchError: could not find the corresponding type ID in the metadata"
)]
DispatchErrorTypeIdNotFound,
#[error("Could not decode the DispatchError: {0}")]
CouldNotDecodeDispatchError(scale_decode::Error),
#[error("Could not decode the DispatchError::Module variant")]
CouldNotDecodeModuleError {
/// The bytes corresponding to the Module variant we were unable to decode:
bytes: Vec<u8>,
},
}
/// Error working with storage.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
#[allow(missing_docs)]
pub enum StorageError {
#[error("{0}")]
Offline(#[from] CoreStorageError),
#[error("Cannot access storage at latest block: Cannot fetch latest finalized block: {0}")]
CannotGetLatestFinalizedBlock(BackendError),
#[error(
"No storage value found at the given address, and no default value to fall back to using."
)]
NoValueFound,
#[error("Cannot fetch the storage value: {0}")]
CannotFetchValue(BackendError),
#[error("Cannot iterate storage values: {0}")]
CannotIterateValues(BackendError),
#[error("Encountered an error iterating over storage values: {0}")]
StreamFailure(BackendError),
#[error("Cannot decode the storage version for a given entry: {0}")]
CannotDecodeStorageVersion(codec::Error),
}
impl StorageError {
fn backend_error(&self) -> Option<&BackendError> {
match self {
StorageError::CannotGetLatestFinalizedBlock(e)
| StorageError::CannotFetchValue(e)
| StorageError::CannotIterateValues(e)
| StorageError::StreamFailure(e) => Some(e),
_ => None,
}
}
}
+104
View File
@@ -0,0 +1,104 @@
// Copyright 2019-2025 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::{Backend, BackendExt, BlockRef};
use crate::{
client::OnlineClientT,
config::{Config, HashFor},
error::EventsError,
events::Events,
};
use derive_where::derive_where;
use std::future::Future;
/// A client for working with events.
#[derive_where(Clone; Client)]
pub struct EventsClient<T, Client> {
client: Client,
_marker: std::marker::PhantomData<T>,
}
impl<T, Client> EventsClient<T, Client> {
/// Create a new [`EventsClient`].
pub fn new(client: Client) -> Self {
Self {
client,
_marker: std::marker::PhantomData,
}
}
}
impl<T, Client> EventsClient<T, Client>
where
T: Config,
Client: OnlineClientT<T>,
{
/// Obtain events at some block hash.
///
/// # Warning
///
/// This call only supports blocks produced since the most recent
/// runtime upgrade. You can attempt to retrieve events from older blocks,
/// but may run into errors attempting to work with them.
pub fn at(
&self,
block_ref: impl Into<BlockRef<HashFor<T>>>,
) -> impl Future<Output = Result<Events<T>, EventsError>> + Send + 'static {
self.at_or_latest(Some(block_ref.into()))
}
/// Obtain events for the latest finalized block.
pub fn at_latest(
&self,
) -> impl Future<Output = Result<Events<T>, EventsError>> + Send + 'static {
self.at_or_latest(None)
}
/// Obtain events at some block hash.
fn at_or_latest(
&self,
block_ref: Option<BlockRef<HashFor<T>>>,
) -> impl Future<Output = Result<Events<T>, EventsError>> + Send + 'static {
// Clone and pass the client in like this so that we can explicitly
// return a Future that's Send + 'static, rather than tied to &self.
let client = self.client.clone();
async move {
// If a block ref isn't provided, we'll get the latest finalized block to use.
let block_ref = match block_ref {
Some(r) => r,
None => client
.backend()
.latest_finalized_block_ref()
.await
.map_err(EventsError::CannotGetLatestFinalizedBlock)?,
};
let event_bytes = get_event_bytes(client.backend(), block_ref.hash()).await?;
Ok(Events::decode_from(event_bytes, client.metadata()))
}
}
}
// The storage key needed to access events.
fn system_events_key() -> [u8; 32] {
let a = pezsp_crypto_hashing::twox_128(b"System");
let b = pezsp_crypto_hashing::twox_128(b"Events");
let mut res = [0; 32];
res[0..16].clone_from_slice(&a);
res[16..32].clone_from_slice(&b);
res
}
// Get the event bytes from the provided client, at the provided block hash.
pub(crate) async fn get_event_bytes<T: Config>(
backend: &dyn Backend<T>,
block_hash: HashFor<T>,
) -> Result<Vec<u8>, EventsError> {
let bytes = backend
.storage_fetch_value(system_events_key().to_vec(), block_hash)
.await
.map_err(EventsError::CannotFetchEventBytes)?
.unwrap_or_default();
Ok(bytes)
}
+163
View File
@@ -0,0 +1,163 @@
use crate::{
Metadata,
config::{Config, HashFor},
error::EventsError,
};
use derive_where::derive_where;
use scale_decode::{DecodeAsFields, DecodeAsType};
use pezkuwi_subxt_core::events::{EventDetails as CoreEventDetails, Events as CoreEvents};
pub use pezkuwi_subxt_core::events::{EventMetadataDetails, Phase, StaticEvent};
/// A collection of events obtained from a block, bundled with the necessary
/// information needed to decode and iterate over them.
// Dev note: we are just wrapping the pezkuwi_subxt_core types here to avoid leaking them
// in Subxt and map any errors into Subxt errors so that we don't have this part of the
// API returning a different error type (ie the pezkuwi_subxt_core::Error).
#[derive_where(Clone, Debug)]
pub struct Events<T> {
inner: CoreEvents<T>,
}
impl<T: Config> Events<T> {
/// Create a new [`Events`] instance from the given bytes.
pub fn decode_from(event_bytes: Vec<u8>, metadata: Metadata) -> Self {
Self {
inner: CoreEvents::decode_from(event_bytes, metadata),
}
}
/// The number of events.
pub fn len(&self) -> u32 {
self.inner.len()
}
/// Are there no events in this block?
// Note: mainly here to satisfy clippy..
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
/// Return the bytes representing all of the events.
pub fn bytes(&self) -> &[u8] {
self.inner.bytes()
}
/// Iterate over all of the events, using metadata to dynamically
/// decode them as we go, and returning the raw bytes and other associated
/// details. If an error occurs, all subsequent iterations return `None`.
// Dev note: The returned iterator is 'static + Send so that we can box it up and make
// use of it with our `FilterEvents` stuff.
pub fn iter(
&self,
) -> impl Iterator<Item = Result<EventDetails<T>, EventsError>> + Send + Sync + 'static {
self.inner
.iter()
.map(|item| item.map(|e| EventDetails { inner: e }).map_err(Into::into))
}
/// Iterate through the events using metadata to dynamically decode and skip
/// them, and return only those which should decode to the provided `Ev` type.
/// If an error occurs, all subsequent iterations return `None`.
pub fn find<Ev: StaticEvent>(&self) -> impl Iterator<Item = Result<Ev, EventsError>> {
self.inner.find::<Ev>().map(|item| item.map_err(Into::into))
}
/// Iterate through the events using metadata to dynamically decode and skip
/// them, and return the first event found which decodes to the provided `Ev` type.
pub fn find_first<Ev: StaticEvent>(&self) -> Result<Option<Ev>, EventsError> {
self.inner.find_first::<Ev>().map_err(Into::into)
}
/// Iterate through the events using metadata to dynamically decode and skip
/// them, and return the last event found which decodes to the provided `Ev` type.
pub fn find_last<Ev: StaticEvent>(&self) -> Result<Option<Ev>, EventsError> {
self.inner.find_last::<Ev>().map_err(Into::into)
}
/// Find an event that decodes to the type provided. Returns true if it was found.
pub fn has<Ev: StaticEvent>(&self) -> Result<bool, EventsError> {
self.inner.has::<Ev>().map_err(Into::into)
}
}
/// The event details.
#[derive(Debug, Clone)]
pub struct EventDetails<T: Config> {
inner: CoreEventDetails<T>,
}
impl<T: Config> EventDetails<T> {
/// When was the event produced?
pub fn phase(&self) -> Phase {
self.inner.phase()
}
/// What index is this event in the stored events for this block.
pub fn index(&self) -> u32 {
self.inner.index()
}
/// The index of the pallet that the event originated from.
pub fn pallet_index(&self) -> u8 {
self.inner.pallet_index()
}
/// The index of the event variant that the event originated from.
pub fn variant_index(&self) -> u8 {
self.inner.variant_index()
}
/// The name of the pallet from whence the Event originated.
pub fn pallet_name(&self) -> &str {
self.inner.pallet_name()
}
/// The name of the event (ie the name of the variant that it corresponds to).
pub fn variant_name(&self) -> &str {
self.inner.variant_name()
}
/// Fetch details from the metadata for this event.
pub fn event_metadata(&self) -> EventMetadataDetails<'_> {
self.inner.event_metadata()
}
/// Return _all_ of the bytes representing this event, which include, in order:
/// - The phase.
/// - Pallet and event index.
/// - Event fields.
/// - Event Topics.
pub fn bytes(&self) -> &[u8] {
self.inner.bytes()
}
/// Return the bytes representing the fields stored in this event.
pub fn field_bytes(&self) -> &[u8] {
self.inner.field_bytes()
}
/// Decode and provide the event fields back in the form of a [`scale_value::Composite`]
/// type which represents the named or unnamed fields that were present in the event.
pub fn decode_as_fields<E: DecodeAsFields>(&self) -> Result<E, EventsError> {
self.inner.decode_as_fields().map_err(Into::into)
}
/// Attempt to decode these [`EventDetails`] into a type representing the event fields.
/// Such types are exposed in the codegen as `pallet_name::events::EventName` types.
pub fn as_event<E: StaticEvent>(&self) -> Result<Option<E>, EventsError> {
self.inner.as_event::<E>().map_err(Into::into)
}
/// Attempt to decode these [`EventDetails`] into a root event type (which includes
/// the pallet and event enum variants as well as the event fields). A compatible
/// type for this is exposed via static codegen as a root level `Event` type.
pub fn as_root_event<E: DecodeAsType>(&self) -> Result<E, EventsError> {
self.inner.as_root_event::<E>().map_err(Into::into)
}
/// Return the topics associated with this event.
pub fn topics(&self) -> &[HashFor<T>] {
self.inner.topics()
}
}
+34
View File
@@ -0,0 +1,34 @@
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
//! This module exposes the types and such necessary for working with events.
//! The two main entry points into events are [`crate::OnlineClient::events()`]
//! and calls like [crate::tx::TxProgress::wait_for_finalized_success()].
mod events_client;
mod events_type;
use crate::client::OnlineClientT;
use crate::error::EventsError;
use pezkuwi_subxt_core::{
Metadata,
config::{Config, HashFor},
};
pub use events_client::EventsClient;
pub use events_type::{EventDetails, EventMetadataDetails, Events, Phase, StaticEvent};
/// Creates a new [`Events`] instance by fetching the corresponding bytes at `block_hash` from the client.
pub async fn new_events_from_client<T, C>(
metadata: Metadata,
block_hash: HashFor<T>,
client: C,
) -> Result<Events<T>, EventsError>
where
T: Config,
C: OnlineClientT<T>,
{
let event_bytes = events_client::get_event_bytes(client.backend(), block_hash).await?;
Ok(Events::<T>::decode_from(event_bytes, metadata))
}
+370
View File
@@ -0,0 +1,370 @@
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
//! Subxt is a library for interacting with Substrate based nodes. Using it looks something like this:
//!
//! ```rust,ignore
#![doc = include_str!("../examples/tx_basic.rs")]
//! ```
//!
//! Take a look at [the Subxt guide](book) to learn more about how to use Subxt.
#![cfg_attr(docsrs, feature(doc_cfg))]
#[cfg(any(
all(feature = "web", feature = "native"),
not(any(feature = "web", feature = "native"))
))]
compile_error!("subxt: exactly one of the 'web' and 'native' features should be used.");
// Internal helper macros
#[macro_use]
mod macros;
// The guide is here.
pub mod book;
// Suppress an unused dependency warning because tokio is
// only used in example code snippets at the time of writing.
#[cfg(test)]
mod only_used_in_docs_or_tests {
use pezkuwi_subxt_signer as _;
use tokio as _;
}
// Suppress an unused dependency warning because tracing_subscriber is
// only used in example code snippets at the time of writing.
#[cfg(test)]
use tracing_subscriber as _;
pub mod backend;
pub mod blocks;
pub mod client;
pub mod constants;
pub mod custom_values;
pub mod error;
pub mod events;
pub mod runtime_api;
pub mod storage;
pub mod tx;
pub mod utils;
pub mod view_functions;
/// This module provides a [`Config`] type, which is used to define various
/// types that are important in order to speak to a particular chain.
/// [`SubstrateConfig`] provides a default set of these types suitable for the
/// default Substrate node implementation, and [`PolkadotConfig`] for a
/// Polkadot node.
pub mod config {
pub use pezkuwi_subxt_core::config::{
Config, DefaultExtrinsicParams, DefaultExtrinsicParamsBuilder, ExtrinsicParams,
ExtrinsicParamsEncoder, Hash, HashFor, Hasher, Header, PolkadotConfig,
PolkadotExtrinsicParams, SubstrateConfig, SubstrateExtrinsicParams, TransactionExtension,
polkadot, substrate, transaction_extensions,
};
pub use pezkuwi_subxt_core::error::ExtrinsicParamsError;
}
/// Types representing the metadata obtained from a node.
pub mod metadata {
pub use pezkuwi_subxt_metadata::*;
}
/// Submit dynamic transactions.
pub mod dynamic {
pub use pezkuwi_subxt_core::dynamic::*;
}
// Expose light client bits
cfg_unstable_light_client! {
pub use pezkuwi_subxt_lightclient as lightclient;
}
// Expose a few of the most common types at root,
// but leave most types behind their respective modules.
pub use crate::{
client::{OfflineClient, OnlineClient},
config::{Config, PolkadotConfig, SubstrateConfig},
error::Error,
metadata::Metadata,
};
/// Re-export external crates that are made use of in the subxt API.
pub mod ext {
pub use codec;
pub use frame_metadata;
pub use futures;
pub use scale_bits;
pub use scale_decode;
pub use scale_encode;
pub use scale_value;
pub use pezkuwi_subxt_core;
pub use pezkuwi_subxt_rpcs;
cfg_jsonrpsee! {
pub use jsonrpsee;
}
}
/// Generate a strongly typed API for interacting with a Substrate runtime from its metadata of WASM.
///
/// # Metadata
///
/// First, you'll need to get hold of some metadata for the node you'd like to interact with. One
/// way to do this is by using the `subxt` CLI tool:
///
/// ```bash
/// # Install the CLI tool:
/// cargo install subxt-cli
/// # Use it to download metadata (in this case, from a node running locally)
/// subxt metadata > polkadot_metadata.scale
/// ```
///
/// Run `subxt metadata --help` for more options.
///
/// # Basic usage
///
/// We can generate an interface to a chain given either:
/// - A locally saved SCALE encoded metadata file (see above) for that chain,
/// - The Runtime WASM for that chain, or
/// - A URL pointing at the JSON-RPC interface for a node on that chain.
///
/// In each case, the `subxt` macro will use this data to populate the annotated module with all of the methods
/// and types required for interacting with the chain that the Runtime/metadata was loaded from.
///
/// Let's look at each of these:
///
/// ## Using a locally saved metadata file
///
/// Annotate a Rust module with the `subxt` attribute referencing a metadata file like so:
///
/// ```rust,no_run,standalone_crate
/// #[subxt::subxt(
/// runtime_metadata_path = "../artifacts/polkadot_metadata_full.scale",
/// )]
/// mod polkadot {}
/// ```
///
/// You can use the `$OUT_DIR` placeholder in the path to reference metadata generated at build time:
///
/// ```rust,ignore
/// #[subxt::subxt(
/// runtime_metadata_path = "$OUT_DIR/metadata.scale",
/// )]
/// mod polkadot {}
/// ```
///
/// ## Using a WASM runtime via `runtime_path = "..."`
///
/// This requires the `runtime-wasm-path` feature flag.
///
/// Annotate a Rust module with the `subxt` attribute referencing some runtime WASM like so:
///
/// ```rust,ignore
/// #[subxt::subxt(
/// runtime_path = "../artifacts/westend_runtime.wasm",
/// )]
/// mod polkadot {}
/// ```
///
/// You can also use the `$OUT_DIR` placeholder in the path to reference WASM files generated at build time:
///
/// ```rust,ignore
/// #[subxt::subxt(
/// runtime_path = "$OUT_DIR/runtime.wasm",
/// )]
/// mod polkadot {}
/// ```
///
/// ## Connecting to a node to download metadata via `runtime_metadata_insecure_url = "..."`
///
/// This will, at compile time, connect to the JSON-RPC interface for some node at the URL given,
/// download the metadata from it, and use that. This can be useful in CI, but is **not recommended**
/// in production code, because:
///
/// - The compilation time is increased since we have to download metadata from a URL each time. If
/// the node we connect to is unresponsive, this will be slow or could fail.
/// - The metadata may change from what is expected without notice, causing compilation to fail if
/// it leads to changes in the generated interfaces that are being used.
/// - The node that you connect to could be malicious and provide incorrect metadata for the chain.
///
/// ```rust,ignore
/// #[subxt::subxt(
/// runtime_metadata_insecure_url = "wss://rpc.polkadot.io:443"
/// )]
/// mod polkadot {}
/// ```
///
/// # Configuration
///
/// This macro supports a number of attributes to configure what is generated:
///
/// ## `crate = "..."`
///
/// Use this attribute to specify a custom path to the `pezkuwi_subxt_core` crate:
///
/// ```rust,standalone_crate
/// # pub extern crate pezkuwi_subxt_core;
/// # pub mod path { pub mod to { pub use pezkuwi_subxt_core; } }
/// # fn main() {}
/// #[subxt::subxt(
/// runtime_metadata_path = "../artifacts/polkadot_metadata_full.scale",
/// crate = "crate::path::to::pezkuwi_subxt_core"
/// )]
/// mod polkadot {}
/// ```
///
/// This is useful if you write a library which uses this macro, but don't want to force users to depend on `subxt`
/// at the top level too. By default the path `::subxt` is used.
///
/// ## `substitute_type(path = "...", with = "...")`
///
/// This attribute replaces any reference to the generated type at the path given by `path` with a
/// reference to the path given by `with`.
///
/// ```rust,standalone_crate
/// #[subxt::subxt(
/// runtime_metadata_path = "../artifacts/polkadot_metadata_full.scale",
/// substitute_type(path = "sp_arithmetic::per_things::Perbill", with = "crate::Foo")
/// )]
/// mod polkadot {}
///
/// # #[derive(
/// # scale_encode::EncodeAsType,
/// # scale_decode::DecodeAsType,
/// # codec::Encode,
/// # codec::Decode,
/// # Clone,
/// # Debug,
/// # )]
/// // In reality this needs some traits implementing on
/// // it to allow it to be used in place of Perbill:
/// pub struct Foo(u32);
/// # impl codec::CompactAs for Foo {
/// # type As = u32;
/// # fn encode_as(&self) -> &Self::As {
/// # &self.0
/// # }
/// # fn decode_from(x: Self::As) -> Result<Self, codec::Error> {
/// # Ok(Foo(x))
/// # }
/// # }
/// # impl From<codec::Compact<Foo>> for Foo {
/// # fn from(v: codec::Compact<Foo>) -> Foo {
/// # v.0
/// # }
/// # }
/// # fn main() {}
/// ```
///
/// If the type you're substituting contains generic parameters, you can "pattern match" on those, and
/// make use of them in the substituted type, like so:
///
/// ```rust,no_run,standalone_crate
/// #[subxt::subxt(
/// runtime_metadata_path = "../artifacts/polkadot_metadata_full.scale",
/// substitute_type(
/// path = "sp_runtime::multiaddress::MultiAddress<A, B>",
/// with = "::pezkuwi_subxt::utils::Static<sp_runtime::MultiAddress<A, B>>"
/// )
/// )]
/// mod polkadot {}
/// ```
///
/// The above is also an example of using the [`crate::utils::Static`] type to wrap some type which doesn't
/// on it's own implement [`scale_encode::EncodeAsType`] or [`scale_decode::DecodeAsType`], which are required traits
/// for any substitute type to implement by default.
///
/// ## `derive_for_all_types = "..."`
///
/// By default, all generated types derive a small set of traits. This attribute allows you to derive additional
/// traits on all generated types:
///
/// ```rust,no_run,standalone_crate
/// #[subxt::subxt(
/// runtime_metadata_path = "../artifacts/polkadot_metadata_full.scale",
/// derive_for_all_types = "Eq, PartialEq"
/// )]
/// mod polkadot {}
/// ```
///
/// Any substituted types (including the default substitutes) must also implement these traits in order to avoid errors
/// here.
///
/// ## `derive_for_type(path = "...", derive = "...")`
///
/// Unlike the above, which derives some trait on every generated type, this attribute allows you to derive traits only
/// for specific types. Note that any types which are used inside the specified type may also need to derive the same traits.
///
/// ```rust,no_run,standalone_crate
/// #[subxt::subxt(
/// runtime_metadata_path = "../artifacts/polkadot_metadata_full.scale",
/// derive_for_all_types = "Eq, PartialEq",
/// derive_for_type(path = "frame_support::PalletId", derive = "Ord, PartialOrd"),
/// derive_for_type(path = "sp_runtime::ModuleError", derive = "Hash"),
/// )]
/// mod polkadot {}
/// ```
///
/// ## `generate_docs`
///
/// By default, documentation is not generated via the macro, since IDEs do not typically make use of it. This attribute
/// forces documentation to be generated, too.
///
/// ```rust,no_run,standalone_crate
/// #[subxt::subxt(
/// runtime_metadata_path = "../artifacts/polkadot_metadata_full.scale",
/// generate_docs
/// )]
/// mod polkadot {}
/// ```
///
/// ## `runtime_types_only`
///
/// By default, the macro will generate various interfaces to make using Subxt simpler in addition with any types that need
/// generating to make this possible. This attribute makes the codegen only generate the types and not the Subxt interface.
///
/// ```rust,no_run,standalone_crate
/// #[subxt::subxt(
/// runtime_metadata_path = "../artifacts/polkadot_metadata_full.scale",
/// runtime_types_only
/// )]
/// mod polkadot {}
/// ```
///
/// ## `no_default_derives`
///
/// By default, the macro will add all derives necessary for the generated code to play nicely with Subxt. Adding this attribute
/// removes all default derives.
///
/// ```rust,no_run,standalone_crate
/// #[subxt::subxt(
/// runtime_metadata_path = "../artifacts/polkadot_metadata_full.scale",
/// runtime_types_only,
/// no_default_derives,
/// derive_for_all_types="codec::Encode, codec::Decode"
/// )]
/// mod polkadot {}
/// ```
///
/// **Note**: At the moment, you must derive at least one of `codec::Encode` or `codec::Decode` or `scale_encode::EncodeAsType` or
/// `scale_decode::DecodeAsType` (because we add `#[codec(..)]` attributes on some fields/types during codegen), and you must use this
/// feature in conjunction with `runtime_types_only` (or manually specify a bunch of defaults to make codegen work properly when
/// generating the subxt interfaces).
///
/// ## `unstable_metadata`
///
/// This attribute works only in combination with `runtime_metadata_insecure_url`. By default, the macro will fetch the latest stable
/// version of the metadata from the target node. This attribute makes the codegen attempt to fetch the unstable version of
/// the metadata first. This is **not recommended** in production code, since the unstable metadata a node is providing is likely
/// to be incompatible with Subxt.
///
/// ```rust,ignore
/// #[subxt::subxt(
/// runtime_metadata_insecure_url = "wss://rpc.polkadot.io:443",
/// unstable_metadata
/// )]
/// mod polkadot {}
/// ```
pub use pezkuwi_subxt_macro::subxt;
+59
View File
@@ -0,0 +1,59 @@
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
macro_rules! cfg_feature {
($feature:literal, $($item:item)*) => {
$(
#[cfg(feature = $feature)]
#[cfg_attr(docsrs, doc(cfg(feature = $feature)))]
$item
)*
}
}
macro_rules! cfg_unstable_light_client {
($($item:item)*) => {
crate::macros::cfg_feature!("unstable-light-client", $($item)*);
};
}
macro_rules! cfg_reconnecting_rpc_client {
($($item:item)*) => {
crate::macros::cfg_feature!("reconnecting-rpc-client", $($item)*);
};
}
macro_rules! cfg_jsonrpsee {
($($item:item)*) => {
crate::macros::cfg_feature!("jsonrpsee", $($item)*);
};
}
#[allow(unused)]
macro_rules! cfg_jsonrpsee_native {
($($item:item)*) => {
$(
#[cfg(all(feature = "jsonrpsee", feature = "native"))]
#[cfg_attr(docsrs, doc(cfg(all(feature = "jsonrpsee", feature = "native"))))]
$item
)*
}
}
#[allow(unused)]
macro_rules! cfg_jsonrpsee_web {
($($item:item)*) => {
$(
#[cfg(all(feature = "jsonrpsee", feature = "web"))]
#[cfg_attr(docsrs, doc(cfg(all(feature = "jsonrpsee", feature = "web"))))]
$item
)*
}
}
pub(crate) use {cfg_feature, cfg_jsonrpsee, cfg_unstable_light_client};
// Only used by light-client.
#[allow(unused)]
pub(crate) use {cfg_jsonrpsee_native, cfg_jsonrpsee_web, cfg_reconnecting_rpc_client};
+12
View File
@@ -0,0 +1,12 @@
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
//! Types associated with executing runtime API calls.
mod runtime_client;
mod runtime_types;
pub use runtime_client::RuntimeApiClient;
pub use runtime_types::RuntimeApi;
pub use pezkuwi_subxt_core::runtime_api::payload::{DynamicPayload, Payload, StaticPayload, dynamic};
@@ -0,0 +1,61 @@
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
use super::runtime_types::RuntimeApi;
use crate::{
backend::BlockRef,
client::OnlineClientT,
config::{Config, HashFor},
error::RuntimeApiError,
};
use derive_where::derive_where;
use std::{future::Future, marker::PhantomData};
/// Execute runtime API calls.
#[derive_where(Clone; Client)]
pub struct RuntimeApiClient<T, Client> {
client: Client,
_marker: PhantomData<T>,
}
impl<T, Client> RuntimeApiClient<T, Client> {
/// Create a new [`RuntimeApiClient`]
pub fn new(client: Client) -> Self {
Self {
client,
_marker: PhantomData,
}
}
}
impl<T, Client> RuntimeApiClient<T, Client>
where
T: Config,
Client: OnlineClientT<T>,
{
/// Obtain a runtime API interface at some block hash.
pub fn at(&self, block_ref: impl Into<BlockRef<HashFor<T>>>) -> RuntimeApi<T, Client> {
RuntimeApi::new(self.client.clone(), block_ref.into())
}
/// Obtain a runtime API interface at the latest finalized block.
pub fn at_latest(
&self,
) -> impl Future<Output = Result<RuntimeApi<T, Client>, RuntimeApiError>> + Send + 'static {
// Clone and pass the client in like this so that we can explicitly
// return a Future that's Send + 'static, rather than tied to &self.
let client = self.client.clone();
async move {
// get the ref for the latest finalized block and use that.
let block_ref = client
.backend()
.latest_finalized_block_ref()
.await
.map_err(RuntimeApiError::CannotGetLatestFinalizedBlock)?;
Ok(RuntimeApi::new(client, block_ref))
}
}
}
@@ -0,0 +1,100 @@
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
use super::Payload;
use crate::{
backend::BlockRef,
client::OnlineClientT,
config::{Config, HashFor},
error::RuntimeApiError,
};
use derive_where::derive_where;
use std::{future::Future, marker::PhantomData};
/// Execute runtime API calls.
#[derive_where(Clone; Client)]
pub struct RuntimeApi<T: Config, Client> {
client: Client,
block_ref: BlockRef<HashFor<T>>,
_marker: PhantomData<T>,
}
impl<T: Config, Client> RuntimeApi<T, Client> {
/// Create a new [`RuntimeApi`]
pub(crate) fn new(client: Client, block_ref: BlockRef<HashFor<T>>) -> Self {
Self {
client,
block_ref,
_marker: PhantomData,
}
}
}
impl<T, Client> RuntimeApi<T, Client>
where
T: Config,
Client: OnlineClientT<T>,
{
/// Run the validation logic against some runtime API payload you'd like to use. Returns `Ok(())`
/// if the payload is valid (or if it's not possible to check since the payload has no validation hash).
/// Return an error if the payload was not valid or something went wrong trying to validate it (ie
/// the runtime API in question do not exist at all)
pub fn validate<Call: Payload>(&self, payload: Call) -> Result<(), RuntimeApiError> {
pezkuwi_subxt_core::runtime_api::validate(payload, &self.client.metadata()).map_err(Into::into)
}
/// Execute a raw runtime API call. This returns the raw bytes representing the result
/// of this call. The caller is responsible for decoding the result.
pub fn call_raw<'a>(
&self,
function: &'a str,
call_parameters: Option<&'a [u8]>,
) -> impl Future<Output = Result<Vec<u8>, RuntimeApiError>> + use<'a, Client, T> {
let client = self.client.clone();
let block_hash = self.block_ref.hash();
// Ensure that the returned future doesn't have a lifetime tied to api.runtime_api(),
// which is a temporary thing we'll be throwing away quickly:
async move {
let data = client
.backend()
.call(function, call_parameters, block_hash)
.await
.map_err(RuntimeApiError::CannotCallApi)?;
Ok(data)
}
}
/// Execute a runtime API call.
pub fn call<Call: Payload>(
&self,
payload: Call,
) -> impl Future<Output = Result<Call::ReturnType, RuntimeApiError>> + use<Call, Client, T>
{
let client = self.client.clone();
let block_hash = self.block_ref.hash();
// Ensure that the returned future doesn't have a lifetime tied to api.runtime_api(),
// which is a temporary thing we'll be throwing away quickly:
async move {
let metadata = client.metadata();
// Validate the runtime API payload hash against the compile hash from codegen.
pezkuwi_subxt_core::runtime_api::validate(&payload, &metadata)?;
// Encode the arguments of the runtime call.
let call_name = pezkuwi_subxt_core::runtime_api::call_name(&payload);
let call_args = pezkuwi_subxt_core::runtime_api::call_args(&payload, &metadata)?;
// Make the call.
let bytes = client
.backend()
.call(&call_name, Some(call_args.as_slice()), block_hash)
.await
.map_err(RuntimeApiError::CannotCallApi)?;
// Decode the response.
let value = pezkuwi_subxt_core::runtime_api::decode_value(&mut &*bytes, &payload, &metadata)?;
Ok(value)
}
}
}
+12
View File
@@ -0,0 +1,12 @@
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
//! Types associated with accessing and working with storage items.
mod storage_client;
mod storage_client_at;
pub use storage_client::StorageClient;
pub use storage_client_at::{StorageClientAt, StorageEntryClient, StorageKeyValue, StorageValue};
pub use pezkuwi_subxt_core::storage::address::{Address, DynamicAddress, StaticAddress, dynamic};
@@ -0,0 +1,76 @@
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
use super::storage_client_at::StorageClientAt;
use crate::{
backend::BlockRef,
client::{OfflineClientT, OnlineClientT},
config::{Config, HashFor},
error::StorageError,
};
use derive_where::derive_where;
use std::{future::Future, marker::PhantomData};
use pezkuwi_subxt_core::storage::address::Address;
/// Query the runtime storage.
#[derive_where(Clone; Client)]
pub struct StorageClient<T, Client> {
client: Client,
_marker: PhantomData<T>,
}
impl<T, Client> StorageClient<T, Client> {
/// Create a new [`StorageClient`]
pub fn new(client: Client) -> Self {
Self {
client,
_marker: PhantomData,
}
}
}
impl<T, Client> StorageClient<T, Client>
where
T: Config,
Client: OfflineClientT<T>,
{
/// Run the validation logic against some storage address you'd like to access. Returns `Ok(())`
/// if the address is valid (or if it's not possible to check since the address has no validation hash).
/// Return an error if the address was not valid or something went wrong trying to validate it (ie
/// the pallet or storage entry in question do not exist at all).
pub fn validate<Addr: Address>(&self, address: &Addr) -> Result<(), StorageError> {
pezkuwi_subxt_core::storage::validate(address, &self.client.metadata()).map_err(Into::into)
}
}
impl<T, Client> StorageClient<T, Client>
where
T: Config,
Client: OnlineClientT<T>,
{
/// Obtain storage at some block hash.
pub fn at(&self, block_ref: impl Into<BlockRef<HashFor<T>>>) -> StorageClientAt<T, Client> {
StorageClientAt::new(self.client.clone(), block_ref.into())
}
/// Obtain storage at the latest finalized block.
pub fn at_latest(
&self,
) -> impl Future<Output = Result<StorageClientAt<T, Client>, StorageError>> + Send + 'static
{
// Clone and pass the client in like this so that we can explicitly
// return a Future that's Send + 'static, rather than tied to &self.
let client = self.client.clone();
async move {
// get the ref for the latest finalized block and use that.
let block_ref = client
.backend()
.latest_finalized_block_ref()
.await
.map_err(StorageError::CannotGetLatestFinalizedBlock)?;
Ok(StorageClientAt::new(client, block_ref))
}
}
}
@@ -0,0 +1,383 @@
// Copyright 2019-2025 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::{BackendExt, BlockRef},
client::{OfflineClientT, OnlineClientT},
config::{Config, HashFor},
error::StorageError,
};
use derive_where::derive_where;
use futures::StreamExt;
use std::marker::PhantomData;
use pezkuwi_subxt_core::Metadata;
use pezkuwi_subxt_core::storage::{PrefixOf, address::Address};
use pezkuwi_subxt_core::utils::{Maybe, Yes};
pub use pezkuwi_subxt_core::storage::{StorageKeyValue, StorageValue};
/// Query the runtime storage.
#[derive_where(Clone; Client)]
pub struct StorageClientAt<T: Config, Client> {
client: Client,
metadata: Metadata,
block_ref: BlockRef<HashFor<T>>,
_marker: PhantomData<T>,
}
impl<T, Client> StorageClientAt<T, Client>
where
T: Config,
Client: OfflineClientT<T>,
{
/// Create a new [`StorageClientAt`].
pub(crate) fn new(client: Client, block_ref: BlockRef<HashFor<T>>) -> Self {
// Retrieve and store metadata here so that we can borrow it in
// subsequent structs, and thus also borrow storage info and
// things that borrow from metadata.
let metadata = client.metadata();
Self {
client,
metadata,
block_ref,
_marker: PhantomData,
}
}
}
impl<T, Client> StorageClientAt<T, Client>
where
T: Config,
Client: OfflineClientT<T>,
{
/// This returns a [`StorageEntryClient`], which allows working with the storage entry at the provided address.
pub fn entry<Addr: Address>(
&self,
address: Addr,
) -> Result<StorageEntryClient<'_, T, Client, Addr, Addr::IsPlain>, StorageError> {
let inner = pezkuwi_subxt_core::storage::entry(address, &self.metadata)?;
Ok(StorageEntryClient {
inner,
client: self.client.clone(),
block_ref: self.block_ref.clone(),
_marker: core::marker::PhantomData,
})
}
}
impl<T, Client> StorageClientAt<T, Client>
where
T: Config,
Client: OnlineClientT<T>,
{
/// This is essentially a shorthand for `client.entry(addr)?.fetch(key_parts)`. See [`StorageEntryClient::fetch()`].
pub async fn fetch<Addr: Address>(
&self,
addr: Addr,
key_parts: Addr::KeyParts,
) -> Result<StorageValue<'_, Addr::Value>, StorageError> {
let entry = pezkuwi_subxt_core::storage::entry(addr, &self.metadata)?;
fetch(&entry, &self.client, self.block_ref.hash(), key_parts).await
}
/// This is essentially a shorthand for `client.entry(addr)?.try_fetch(key_parts)`. See [`StorageEntryClient::try_fetch()`].
pub async fn try_fetch<Addr: Address>(
&self,
addr: Addr,
key_parts: Addr::KeyParts,
) -> Result<Option<StorageValue<'_, Addr::Value>>, StorageError> {
let entry = pezkuwi_subxt_core::storage::entry(addr, &self.metadata)?;
try_fetch(&entry, &self.client, self.block_ref.hash(), key_parts).await
}
/// This is essentially a shorthand for `client.entry(addr)?.iter(key_parts)`. See [`StorageEntryClient::iter()`].
pub async fn iter<Addr: Address, KeyParts: PrefixOf<Addr::KeyParts>>(
&'_ self,
addr: Addr,
key_parts: KeyParts,
) -> Result<
impl futures::Stream<Item = Result<StorageKeyValue<'_, Addr>, StorageError>>
+ use<'_, Addr, Client, T, KeyParts>,
StorageError,
> {
let entry = pezkuwi_subxt_core::storage::entry(addr, &self.metadata)?;
iter(entry, &self.client, self.block_ref.hash(), key_parts).await
}
/// In rare cases, you may wish to fetch a storage value that does not live at a typical address. This method
/// is a fallback for those cases, and allows you to provide the raw storage key bytes corresponding to the
/// entry you wish to obtain. The response will either be the bytes for the value found at that location, or
/// otherwise an error. [`StorageError::NoValueFound`] will be returned in the event that the request was valid
/// but no value lives at the given location).
pub async fn fetch_raw(&self, key_bytes: Vec<u8>) -> Result<Vec<u8>, StorageError> {
let block_hash = self.block_ref.hash();
let value = self
.client
.backend()
.storage_fetch_value(key_bytes, block_hash)
.await
.map_err(StorageError::CannotFetchValue)?
.ok_or(StorageError::NoValueFound)?;
Ok(value)
}
/// The storage version of a pallet.
/// The storage version refers to the `frame_support::traits::Metadata::StorageVersion` type.
pub async fn storage_version(&self, pallet_name: impl AsRef<str>) -> Result<u16, StorageError> {
// construct the storage key. This is done similarly in
// `frame_support::traits::metadata::StorageVersion::storage_key()`:
let mut key_bytes: Vec<u8> = vec![];
key_bytes.extend(&pezsp_crypto_hashing::twox_128(
pallet_name.as_ref().as_bytes(),
));
key_bytes.extend(&pezsp_crypto_hashing::twox_128(b":__STORAGE_VERSION__:"));
// fetch the raw bytes and decode them into the StorageVersion struct:
let storage_version_bytes = self.fetch_raw(key_bytes).await?;
<u16 as codec::Decode>::decode(&mut &storage_version_bytes[..])
.map_err(StorageError::CannotDecodeStorageVersion)
}
/// Fetch the runtime WASM code.
pub async fn runtime_wasm_code(&self) -> Result<Vec<u8>, StorageError> {
// note: this should match the `CODE` constant in `sp_core::storage::well_known_keys`
self.fetch_raw(b":code".to_vec()).await
}
}
/// This represents a single storage entry (be it a plain value or map)
/// and the operations that can be performed on it.
pub struct StorageEntryClient<'atblock, T: Config, Client, Addr, IsPlain> {
inner: pezkuwi_subxt_core::storage::StorageEntry<'atblock, Addr>,
client: Client,
block_ref: BlockRef<HashFor<T>>,
_marker: PhantomData<(T, IsPlain)>,
}
impl<'atblock, T, Client, Addr, IsPlain> StorageEntryClient<'atblock, T, Client, Addr, IsPlain>
where
T: Config,
Addr: Address,
{
/// Name of the pallet containing this storage entry.
pub fn pallet_name(&self) -> &str {
self.inner.pallet_name()
}
/// Name of the storage entry.
pub fn entry_name(&self) -> &str {
self.inner.entry_name()
}
/// Is the storage entry a plain value?
pub fn is_plain(&self) -> bool {
self.inner.is_plain()
}
/// Is the storage entry a map?
pub fn is_map(&self) -> bool {
self.inner.is_map()
}
/// Return the default value for this storage entry, if there is one. Returns `None` if there
/// is no default value.
pub fn default_value(&self) -> Option<StorageValue<'atblock, Addr::Value>> {
self.inner.default_value()
}
}
// Plain values get a fetch method with no extra arguments.
impl<'atblock, T, Client, Addr> StorageEntryClient<'atblock, T, Client, Addr, Yes>
where
T: Config,
Addr: Address,
Client: OnlineClientT<T>,
{
/// Fetch the storage value at this location. If no value is found, the default value will be returned
/// for this entry if one exists. If no value is found and no default value exists, an error will be returned.
pub async fn fetch(&self) -> Result<StorageValue<'atblock, Addr::Value>, StorageError> {
let value = self.try_fetch().await?.map_or_else(
|| self.inner.default_value().ok_or(StorageError::NoValueFound),
Ok,
)?;
Ok(value)
}
/// Fetch the storage value at this location. If no value is found, `None` will be returned.
pub async fn try_fetch(
&self,
) -> Result<Option<StorageValue<'atblock, Addr::Value>>, StorageError> {
let value = self
.client
.backend()
.storage_fetch_value(self.key_prefix().to_vec(), self.block_ref.hash())
.await
.map_err(StorageError::CannotFetchValue)?
.map(|bytes| self.inner.value(bytes));
Ok(value)
}
/// This is identical to [`StorageEntryClient::key_prefix()`] and is the full
/// key for this storage entry.
pub fn key(&self) -> [u8; 32] {
self.inner.key_prefix()
}
/// The keys for plain storage values are always 32 byte hashes.
pub fn key_prefix(&self) -> [u8; 32] {
self.inner.key_prefix()
}
}
// When HasDefaultValue = Yes, we expect there to exist a valid default value and will use that
// if we fetch an entry and get nothing back.
impl<'atblock, T, Client, Addr> StorageEntryClient<'atblock, T, Client, Addr, Maybe>
where
T: Config,
Addr: Address,
Client: OnlineClientT<T>,
{
/// Fetch a storage value within this storage entry.
///
/// This entry may be a map, and so you must provide the relevant values for each part of the storage
/// key that is required in order to point to a single value.
///
/// If no value is found, the default value will be returned for this entry if one exists. If no value is
/// found and no default value exists, an error will be returned.
pub async fn fetch(
&self,
key_parts: Addr::KeyParts,
) -> Result<StorageValue<'atblock, Addr::Value>, StorageError> {
fetch(&self.inner, &self.client, self.block_ref.hash(), key_parts).await
}
/// Fetch a storage value within this storage entry.
///
/// This entry may be a map, and so you must provide the relevant values for each part of the storage
/// key that is required in order to point to a single value.
///
/// If no value is found, `None` will be returned.
pub async fn try_fetch(
&self,
key_parts: Addr::KeyParts,
) -> Result<Option<StorageValue<'atblock, Addr::Value>>, StorageError> {
try_fetch(&self.inner, &self.client, self.block_ref.hash(), key_parts).await
}
/// Iterate over storage values within this storage entry.
///
/// You may provide any prefix of the values needed to point to a single value. Normally you will
/// provide `()` to iterate over _everything_, or `(first_key,)` to iterate over everything underneath
/// `first_key` in the map, or `(first_key, second_key)` to iterate over everything underneath `first_key`
/// and `second_key` in the map, and so on, up to the actual depth of the map - 1.
pub async fn iter<KeyParts: PrefixOf<Addr::KeyParts>>(
&self,
key_parts: KeyParts,
) -> Result<
impl futures::Stream<Item = Result<StorageKeyValue<'atblock, Addr>, StorageError>>
+ use<'atblock, Addr, Client, T, KeyParts>,
StorageError,
> {
iter(
self.inner.clone(),
&self.client,
self.block_ref.hash(),
key_parts,
)
.await
}
/// This returns a full key to a single value in this storage entry.
pub fn key(&self, key_parts: Addr::KeyParts) -> Result<Vec<u8>, StorageError> {
let key = self.inner.fetch_key(key_parts)?;
Ok(key)
}
/// This returns valid keys to iterate over the storage entry at the available levels.
pub fn iter_key<KeyParts: PrefixOf<Addr::KeyParts>>(
&self,
key_parts: KeyParts,
) -> Result<Vec<u8>, StorageError> {
let key = self.inner.iter_key(key_parts)?;
Ok(key)
}
/// The first 32 bytes of the storage entry key, which points to the entry but not necessarily
/// a single storage value (unless the entry is a plain value).
pub fn key_prefix(&self) -> [u8; 32] {
self.inner.key_prefix()
}
}
async fn fetch<'atblock, T: Config, Client: OnlineClientT<T>, Addr: Address>(
entry: &pezkuwi_subxt_core::storage::StorageEntry<'atblock, Addr>,
client: &Client,
block_hash: HashFor<T>,
key_parts: Addr::KeyParts,
) -> Result<StorageValue<'atblock, Addr::Value>, StorageError> {
let value = try_fetch(entry, client, block_hash, key_parts)
.await?
.or_else(|| entry.default_value())
.unwrap();
Ok(value)
}
async fn try_fetch<'atblock, T: Config, Client: OnlineClientT<T>, Addr: Address>(
entry: &pezkuwi_subxt_core::storage::StorageEntry<'atblock, Addr>,
client: &Client,
block_hash: HashFor<T>,
key_parts: Addr::KeyParts,
) -> Result<Option<StorageValue<'atblock, Addr::Value>>, StorageError> {
let key = entry.fetch_key(key_parts)?;
let value = client
.backend()
.storage_fetch_value(key, block_hash)
.await
.map_err(StorageError::CannotFetchValue)?
.map(|bytes| entry.value(bytes))
.or_else(|| entry.default_value());
Ok(value)
}
async fn iter<
'atblock,
T: Config,
Client: OnlineClientT<T>,
Addr: Address,
KeyParts: PrefixOf<Addr::KeyParts>,
>(
entry: pezkuwi_subxt_core::storage::StorageEntry<'atblock, Addr>,
client: &Client,
block_hash: HashFor<T>,
key_parts: KeyParts,
) -> Result<
impl futures::Stream<Item = Result<StorageKeyValue<'atblock, Addr>, StorageError>>
+ use<'atblock, Addr, Client, T, KeyParts>,
StorageError,
> {
let key_bytes = entry.iter_key(key_parts)?;
let stream = client
.backend()
.storage_fetch_descendant_values(key_bytes, block_hash)
.await
.map_err(StorageError::CannotIterateValues)?
.map(move |kv| {
let kv = match kv {
Ok(kv) => kv,
Err(e) => return Err(StorageError::StreamFailure(e)),
};
Ok(entry.key_value(kv.key, kv.value))
});
Ok(Box::pin(stream))
}
+21
View File
@@ -0,0 +1,21 @@
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
//! Create and submit extrinsics.
//!
//! An extrinsic is submitted with an "signed extra" and "additional" parameters, which can be
//! different for each chain. The trait [`crate::config::ExtrinsicParams`] determines exactly which
//! additional and signed extra parameters are used when constructing an extrinsic, and is a part
//! of the chain configuration (see [`crate::config::Config`]).
mod tx_client;
mod tx_progress;
pub use pezkuwi_subxt_core::tx::payload::{DefaultPayload, DynamicPayload, Payload, dynamic};
pub use pezkuwi_subxt_core::tx::signer::{self, Signer};
pub use tx_client::{
DefaultParams, PartialTransaction, SubmittableTransaction, TransactionInvalid,
TransactionUnknown, TxClient, ValidationResult,
};
pub use tx_progress::{TxInBlock, TxProgress, TxStatus};
+997
View File
@@ -0,0 +1,997 @@
// Copyright 2019-2025 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::{BackendExt, BlockRef, TransactionStatus},
client::{OfflineClientT, OnlineClientT},
config::{Config, ExtrinsicParams, HashFor, Header},
error::{ExtrinsicError, TransactionStatusError},
tx::{Payload, Signer as SignerT, TxProgress},
utils::PhantomDataSendSync,
};
use codec::{Compact, Decode, Encode};
use derive_where::derive_where;
use futures::future::{TryFutureExt, try_join};
use pezkuwi_subxt_core::tx::TransactionVersion;
/// A client for working with transactions.
#[derive_where(Clone; Client)]
pub struct TxClient<T: Config, Client> {
client: Client,
_marker: PhantomDataSendSync<T>,
}
impl<T: Config, Client> TxClient<T, Client> {
/// Create a new [`TxClient`]
pub fn new(client: Client) -> Self {
Self {
client,
_marker: PhantomDataSendSync::new(),
}
}
}
impl<T: Config, C: OfflineClientT<T>> TxClient<T, C> {
/// Run the validation logic against some transaction you'd like to submit. Returns `Ok(())`
/// if the call is valid (or if it's not possible to check since the call has no validation hash).
/// Return an error if the call was not valid or something went wrong trying to validate it (ie
/// the pallet or call in question do not exist at all).
pub fn validate<Call>(&self, call: &Call) -> Result<(), ExtrinsicError>
where
Call: Payload,
{
pezkuwi_subxt_core::tx::validate(call, &self.client.metadata()).map_err(Into::into)
}
/// Return the SCALE encoded bytes representing the call data of the transaction.
pub fn call_data<Call>(&self, call: &Call) -> Result<Vec<u8>, ExtrinsicError>
where
Call: Payload,
{
pezkuwi_subxt_core::tx::call_data(call, &self.client.metadata()).map_err(Into::into)
}
/// Creates an unsigned transaction without submitting it. Depending on the metadata, we might end
/// up constructing either a v4 or v5 transaction. See [`Self::create_v4_unsigned`] or
/// [`Self::create_v5_bare`] if you'd like to explicitly create an unsigned transaction of a certain version.
pub fn create_unsigned<Call>(
&self,
call: &Call,
) -> Result<SubmittableTransaction<T, C>, ExtrinsicError>
where
Call: Payload,
{
let metadata = self.client.metadata();
let tx = match pezkuwi_subxt_core::tx::suggested_version(&metadata)? {
TransactionVersion::V4 => pezkuwi_subxt_core::tx::create_v4_unsigned(call, &metadata),
TransactionVersion::V5 => pezkuwi_subxt_core::tx::create_v5_bare(call, &metadata),
}?;
Ok(SubmittableTransaction {
client: self.client.clone(),
inner: tx,
})
}
/// Creates a v4 unsigned (no signature or transaction extensions) transaction without submitting it.
///
/// Prefer [`Self::create_unsigned()`] if you don't know which version to create; this will pick the
/// most suitable one for the given chain.
pub fn create_v4_unsigned<Call>(
&self,
call: &Call,
) -> Result<SubmittableTransaction<T, C>, ExtrinsicError>
where
Call: Payload,
{
let metadata = self.client.metadata();
let tx = pezkuwi_subxt_core::tx::create_v4_unsigned(call, &metadata)?;
Ok(SubmittableTransaction {
client: self.client.clone(),
inner: tx,
})
}
/// Creates a v5 "bare" (no signature or transaction extensions) transaction without submitting it.
///
/// Prefer [`Self::create_unsigned()`] if you don't know which version to create; this will pick the
/// most suitable one for the given chain.
pub fn create_v5_bare<Call>(
&self,
call: &Call,
) -> Result<SubmittableTransaction<T, C>, ExtrinsicError>
where
Call: Payload,
{
let metadata = self.client.metadata();
let tx = pezkuwi_subxt_core::tx::create_v5_bare(call, &metadata)?;
Ok(SubmittableTransaction {
client: self.client.clone(),
inner: tx,
})
}
/// Create a partial transaction. Depending on the metadata, we might end up constructing either a v4 or
/// v5 transaction. See [`pezkuwi_subxt_core::tx`] if you'd like to manually pick the version to construct
///
/// Note: if not provided, the default account nonce will be set to 0 and the default mortality will be _immortal_.
/// This is because this method runs offline, and so is unable to fetch the data needed for more appropriate values.
pub fn create_partial_offline<Call>(
&self,
call: &Call,
params: <T::ExtrinsicParams as ExtrinsicParams<T>>::Params,
) -> Result<PartialTransaction<T, C>, ExtrinsicError>
where
Call: Payload,
{
let metadata = self.client.metadata();
let tx = match pezkuwi_subxt_core::tx::suggested_version(&metadata)? {
TransactionVersion::V4 => PartialTransactionInner::V4(
pezkuwi_subxt_core::tx::create_v4_signed(call, &self.client.client_state(), params)?,
),
TransactionVersion::V5 => PartialTransactionInner::V5(
pezkuwi_subxt_core::tx::create_v5_general(call, &self.client.client_state(), params)?,
),
};
Ok(PartialTransaction {
client: self.client.clone(),
inner: tx,
})
}
/// Create a v4 partial transaction, ready to sign.
///
/// Note: if not provided, the default account nonce will be set to 0 and the default mortality will be _immortal_.
/// This is because this method runs offline, and so is unable to fetch the data needed for more appropriate values.
///
/// Prefer [`Self::create_partial_offline()`] if you don't know which version to create; this will pick the
/// most suitable one for the given chain.
pub fn create_v4_partial_offline<Call>(
&self,
call: &Call,
params: <T::ExtrinsicParams as ExtrinsicParams<T>>::Params,
) -> Result<PartialTransaction<T, C>, ExtrinsicError>
where
Call: Payload,
{
let tx = PartialTransactionInner::V4(pezkuwi_subxt_core::tx::create_v4_signed(
call,
&self.client.client_state(),
params,
)?);
Ok(PartialTransaction {
client: self.client.clone(),
inner: tx,
})
}
/// Create a v5 partial transaction, ready to sign.
///
/// Note: if not provided, the default account nonce will be set to 0 and the default mortality will be _immortal_.
/// This is because this method runs offline, and so is unable to fetch the data needed for more appropriate values.
///
/// Prefer [`Self::create_partial_offline()`] if you don't know which version to create; this will pick the
/// most suitable one for the given chain.
pub fn create_v5_partial_offline<Call>(
&self,
call: &Call,
params: <T::ExtrinsicParams as ExtrinsicParams<T>>::Params,
) -> Result<PartialTransaction<T, C>, ExtrinsicError>
where
Call: Payload,
{
let tx = PartialTransactionInner::V5(pezkuwi_subxt_core::tx::create_v5_general(
call,
&self.client.client_state(),
params,
)?);
Ok(PartialTransaction {
client: self.client.clone(),
inner: tx,
})
}
}
impl<T, C> TxClient<T, C>
where
T: Config,
C: OnlineClientT<T>,
{
/// Get the account nonce for a given account ID.
pub async fn account_nonce(&self, account_id: &T::AccountId) -> Result<u64, ExtrinsicError> {
let block_ref = self
.client
.backend()
.latest_finalized_block_ref()
.await
.map_err(ExtrinsicError::CannotGetLatestFinalizedBlock)?;
crate::blocks::get_account_nonce(&self.client, account_id, block_ref.hash())
.await
.map_err(|e| ExtrinsicError::AccountNonceError {
block_hash: block_ref.hash().into(),
account_id: account_id.encode().into(),
reason: e,
})
}
/// Creates a partial transaction, without submitting it. This can then be signed and submitted.
pub async fn create_partial<Call>(
&self,
call: &Call,
account_id: &T::AccountId,
mut params: <T::ExtrinsicParams as ExtrinsicParams<T>>::Params,
) -> Result<PartialTransaction<T, C>, ExtrinsicError>
where
Call: Payload,
{
inject_account_nonce_and_block(&self.client, account_id, &mut params).await?;
self.create_partial_offline(call, params)
}
/// Creates a partial V4 transaction, without submitting it. This can then be signed and submitted.
///
/// Prefer [`Self::create_partial()`] if you don't know which version to create; this will pick the
/// most suitable one for the given chain.
pub async fn create_v4_partial<Call>(
&self,
call: &Call,
account_id: &T::AccountId,
mut params: <T::ExtrinsicParams as ExtrinsicParams<T>>::Params,
) -> Result<PartialTransaction<T, C>, ExtrinsicError>
where
Call: Payload,
{
inject_account_nonce_and_block(&self.client, account_id, &mut params).await?;
self.create_v4_partial_offline(call, params)
}
/// Creates a partial V5 transaction, without submitting it. This can then be signed and submitted.
///
/// Prefer [`Self::create_partial()`] if you don't know which version to create; this will pick the
/// most suitable one for the given chain.
pub async fn create_v5_partial<Call>(
&self,
call: &Call,
account_id: &T::AccountId,
mut params: <T::ExtrinsicParams as ExtrinsicParams<T>>::Params,
) -> Result<PartialTransaction<T, C>, ExtrinsicError>
where
Call: Payload,
{
inject_account_nonce_and_block(&self.client, account_id, &mut params).await?;
self.create_v5_partial_offline(call, params)
}
/// Creates a signed transaction, without submitting it.
pub async fn create_signed<Call, Signer>(
&mut self,
call: &Call,
signer: &Signer,
params: <T::ExtrinsicParams as ExtrinsicParams<T>>::Params,
) -> Result<SubmittableTransaction<T, C>, ExtrinsicError>
where
Call: Payload,
Signer: SignerT<T>,
{
let mut partial = self
.create_partial(call, &signer.account_id(), params)
.await?;
Ok(partial.sign(signer))
}
/// Creates and signs an transaction and submits it to the chain. Passes default parameters
/// to construct the "signed extra" and "additional" payloads needed by the transaction.
///
/// Returns a [`TxProgress`], which can be used to track the status of the transaction
/// and obtain details about it, once it has made it into a block.
pub async fn sign_and_submit_then_watch_default<Call, Signer>(
&mut self,
call: &Call,
signer: &Signer,
) -> Result<TxProgress<T, C>, ExtrinsicError>
where
Call: Payload,
Signer: SignerT<T>,
<T::ExtrinsicParams as ExtrinsicParams<T>>::Params: DefaultParams,
{
self.sign_and_submit_then_watch(call, signer, DefaultParams::default_params())
.await
}
/// Creates and signs an transaction and submits it to the chain.
///
/// Returns a [`TxProgress`], which can be used to track the status of the transaction
/// and obtain details about it, once it has made it into a block.
pub async fn sign_and_submit_then_watch<Call, Signer>(
&mut self,
call: &Call,
signer: &Signer,
params: <T::ExtrinsicParams as ExtrinsicParams<T>>::Params,
) -> Result<TxProgress<T, C>, ExtrinsicError>
where
Call: Payload,
Signer: SignerT<T>,
{
self.create_signed(call, signer, params)
.await?
.submit_and_watch()
.await
}
/// Creates and signs an transaction and submits to the chain for block inclusion. Passes
/// default parameters to construct the "signed extra" and "additional" payloads needed
/// by the transaction.
///
/// Returns `Ok` with the transaction hash if it is valid transaction.
///
/// # Note
///
/// Success does not mean the transaction has been included in the block, just that it is valid
/// and has been included in the transaction pool.
pub async fn sign_and_submit_default<Call, Signer>(
&mut self,
call: &Call,
signer: &Signer,
) -> Result<HashFor<T>, ExtrinsicError>
where
Call: Payload,
Signer: SignerT<T>,
<T::ExtrinsicParams as ExtrinsicParams<T>>::Params: DefaultParams,
{
self.sign_and_submit(call, signer, DefaultParams::default_params())
.await
}
/// Creates and signs an transaction and submits to the chain for block inclusion.
///
/// Returns `Ok` with the transaction hash if it is valid transaction.
///
/// # Note
///
/// Success does not mean the transaction has been included in the block, just that it is valid
/// and has been included in the transaction pool.
pub async fn sign_and_submit<Call, Signer>(
&mut self,
call: &Call,
signer: &Signer,
params: <T::ExtrinsicParams as ExtrinsicParams<T>>::Params,
) -> Result<HashFor<T>, ExtrinsicError>
where
Call: Payload,
Signer: SignerT<T>,
{
self.create_signed(call, signer, params)
.await?
.submit()
.await
}
}
/// This payload contains the information needed to produce an transaction.
pub struct PartialTransaction<T: Config, C> {
client: C,
inner: PartialTransactionInner<T>,
}
enum PartialTransactionInner<T: Config> {
V4(pezkuwi_subxt_core::tx::PartialTransactionV4<T>),
V5(pezkuwi_subxt_core::tx::PartialTransactionV5<T>),
}
impl<T, C> PartialTransaction<T, C>
where
T: Config,
C: OfflineClientT<T>,
{
/// Return the signer payload for this transaction. These are the bytes that must
/// be signed in order to produce a valid signature for the transaction.
pub fn signer_payload(&self) -> Vec<u8> {
match &self.inner {
PartialTransactionInner::V4(tx) => tx.signer_payload(),
PartialTransactionInner::V5(tx) => tx.signer_payload().to_vec(),
}
}
/// Return the bytes representing the call data for this partially constructed
/// transaction.
pub fn call_data(&self) -> &[u8] {
match &self.inner {
PartialTransactionInner::V4(tx) => tx.call_data(),
PartialTransactionInner::V5(tx) => tx.call_data(),
}
}
/// Convert this [`PartialTransaction`] into a [`SubmittableTransaction`], ready to submit.
/// The provided `signer` is responsible for providing the "from" address for the transaction,
/// as well as providing a signature to attach to it.
pub fn sign<Signer>(&mut self, signer: &Signer) -> SubmittableTransaction<T, C>
where
Signer: SignerT<T>,
{
let tx = match &mut self.inner {
PartialTransactionInner::V4(tx) => tx.sign(signer),
PartialTransactionInner::V5(tx) => tx.sign(signer),
};
SubmittableTransaction {
client: self.client.clone(),
inner: tx,
}
}
/// Convert this [`PartialTransaction`] into a [`SubmittableTransaction`], ready to submit.
/// An address, and something representing a signature that can be SCALE encoded, are both
/// needed in order to construct it. If you have a `Signer` to hand, you can use
/// [`PartialTransaction::sign()`] instead.
pub fn sign_with_account_and_signature(
&mut self,
account_id: &T::AccountId,
signature: &T::Signature,
) -> SubmittableTransaction<T, C> {
let tx = match &mut self.inner {
PartialTransactionInner::V4(tx) => {
tx.sign_with_account_and_signature(account_id.clone(), signature)
}
PartialTransactionInner::V5(tx) => {
tx.sign_with_account_and_signature(account_id, signature)
}
};
SubmittableTransaction {
client: self.client.clone(),
inner: tx,
}
}
}
/// This represents an transaction that has been signed and is ready to submit.
pub struct SubmittableTransaction<T, C> {
client: C,
inner: pezkuwi_subxt_core::tx::Transaction<T>,
}
impl<T, C> SubmittableTransaction<T, C>
where
T: Config,
C: OfflineClientT<T>,
{
/// Create a [`SubmittableTransaction`] from some already-signed and prepared
/// transaction bytes, and some client (anything implementing [`OfflineClientT`]
/// or [`OnlineClientT`]).
///
/// Prefer to use [`TxClient`] to create and sign transactions. This is simply
/// exposed in case you want to skip this process and submit something you've
/// already created.
pub fn from_bytes(client: C, tx_bytes: Vec<u8>) -> Self {
Self {
client,
inner: pezkuwi_subxt_core::tx::Transaction::from_bytes(tx_bytes),
}
}
/// Calculate and return the hash of the transaction, based on the configured hasher.
pub fn hash(&self) -> HashFor<T> {
self.inner.hash_with(self.client.hasher())
}
/// Returns the SCALE encoded transaction bytes.
pub fn encoded(&self) -> &[u8] {
self.inner.encoded()
}
/// Consumes [`SubmittableTransaction`] and returns the SCALE encoded
/// transaction bytes.
pub fn into_encoded(self) -> Vec<u8> {
self.inner.into_encoded()
}
}
impl<T, C> SubmittableTransaction<T, C>
where
T: Config,
C: OnlineClientT<T>,
{
/// Submits the transaction to the chain.
///
/// Returns a [`TxProgress`], which can be used to track the status of the transaction
/// and obtain details about it, once it has made it into a block.
pub async fn submit_and_watch(&self) -> Result<TxProgress<T, C>, ExtrinsicError> {
// Get a hash of the transaction (we'll need this later).
let ext_hash = self.hash();
// Submit and watch for transaction progress.
let sub = self
.client
.backend()
.submit_transaction(self.encoded())
.await
.map_err(ExtrinsicError::ErrorSubmittingTransaction)?;
Ok(TxProgress::new(sub, self.client.clone(), ext_hash))
}
/// Submits the transaction to the chain for block inclusion.
///
/// It's usually better to call `submit_and_watch` to get an idea of the progress of the
/// submission and whether it's eventually successful or not. This call does not guarantee
/// success, and is just sending the transaction to the chain.
pub async fn submit(&self) -> Result<HashFor<T>, ExtrinsicError> {
let ext_hash = self.hash();
let mut sub = self
.client
.backend()
.submit_transaction(self.encoded())
.await
.map_err(ExtrinsicError::ErrorSubmittingTransaction)?;
// If we get a bad status or error back straight away then error, else return the hash.
match sub.next().await {
Some(Ok(status)) => match status {
TransactionStatus::Validated
| TransactionStatus::Broadcasted
| TransactionStatus::InBestBlock { .. }
| TransactionStatus::NoLongerInBestBlock
| TransactionStatus::InFinalizedBlock { .. } => Ok(ext_hash),
TransactionStatus::Error { message } => Err(
ExtrinsicError::TransactionStatusError(TransactionStatusError::Error(message)),
),
TransactionStatus::Invalid { message } => {
Err(ExtrinsicError::TransactionStatusError(
TransactionStatusError::Invalid(message),
))
}
TransactionStatus::Dropped { message } => {
Err(ExtrinsicError::TransactionStatusError(
TransactionStatusError::Dropped(message),
))
}
},
Some(Err(e)) => Err(ExtrinsicError::TransactionStatusStreamError(e)),
None => Err(ExtrinsicError::UnexpectedEndOfTransactionStatusStream),
}
}
/// Validate a transaction by submitting it to the relevant Runtime API. A transaction that is
/// valid can be added to a block, but may still end up in an error state.
///
/// Returns `Ok` with a [`ValidationResult`], which is the result of attempting to dry run the transaction.
pub async fn validate(&self) -> Result<ValidationResult, ExtrinsicError> {
let latest_block_ref = self
.client
.backend()
.latest_finalized_block_ref()
.await
.map_err(ExtrinsicError::CannotGetLatestFinalizedBlock)?;
self.validate_at(latest_block_ref).await
}
/// Validate a transaction by submitting it to the relevant Runtime API. A transaction that is
/// valid can be added to a block, but may still end up in an error state.
///
/// Returns `Ok` with a [`ValidationResult`], which is the result of attempting to dry run the transaction.
pub async fn validate_at(
&self,
at: impl Into<BlockRef<HashFor<T>>>,
) -> Result<ValidationResult, ExtrinsicError> {
let block_hash = at.into().hash();
// Approach taken from https://github.com/paritytech/json-rpc-interface-spec/issues/55.
let mut params = Vec::with_capacity(8 + self.encoded().len() + 8);
2u8.encode_to(&mut params);
params.extend(self.encoded().iter());
block_hash.encode_to(&mut params);
let res: Vec<u8> = self
.client
.backend()
.call(
"TaggedTransactionQueue_validate_transaction",
Some(&params),
block_hash,
)
.await
.map_err(ExtrinsicError::CannotGetValidationInfo)?;
ValidationResult::try_from_bytes(res)
}
/// This returns an estimate for what the transaction is expected to cost to execute, less any tips.
/// The actual amount paid can vary from block to block based on node traffic and other factors.
pub async fn partial_fee_estimate(&self) -> Result<u128, ExtrinsicError> {
let mut params = self.encoded().to_vec();
(self.encoded().len() as u32).encode_to(&mut params);
let latest_block_ref = self
.client
.backend()
.latest_finalized_block_ref()
.await
.map_err(ExtrinsicError::CannotGetLatestFinalizedBlock)?;
// destructuring RuntimeDispatchInfo, see type information <https://paritytech.github.io/substrate/master/pallet_transaction_payment_rpc_runtime_api/struct.RuntimeDispatchInfo.html>
// data layout: {weight_ref_time: Compact<u64>, weight_proof_size: Compact<u64>, class: u8, partial_fee: u128}
let (_, _, _, partial_fee) = self
.client
.backend()
.call_decoding::<(Compact<u64>, Compact<u64>, u8, u128)>(
"TransactionPaymentApi_query_info",
Some(&params),
latest_block_ref.hash(),
)
.await
.map_err(ExtrinsicError::CannotGetFeeInfo)?;
Ok(partial_fee)
}
}
/// Fetch the latest block header and account nonce from the backend and use them to refine [`ExtrinsicParams::Params`].
async fn inject_account_nonce_and_block<T: Config, Client: OnlineClientT<T>>(
client: &Client,
account_id: &T::AccountId,
params: &mut <T::ExtrinsicParams as ExtrinsicParams<T>>::Params,
) -> Result<(), ExtrinsicError> {
use pezkuwi_subxt_core::config::transaction_extensions::Params;
let block_ref = client
.backend()
.latest_finalized_block_ref()
.await
.map_err(ExtrinsicError::CannotGetLatestFinalizedBlock)?;
let (block_header, account_nonce) = try_join(
client
.backend()
.block_header(block_ref.hash())
.map_err(ExtrinsicError::CannotGetLatestFinalizedBlock),
crate::blocks::get_account_nonce(client, account_id, block_ref.hash()).map_err(|e| {
ExtrinsicError::AccountNonceError {
block_hash: block_ref.hash().into(),
account_id: account_id.encode().into(),
reason: e,
}
}),
)
.await?;
let block_header = block_header.ok_or_else(|| ExtrinsicError::CannotFindBlockHeader {
block_hash: block_ref.hash().into(),
})?;
params.inject_account_nonce(account_nonce);
params.inject_block(block_header.number().into(), block_ref.hash());
Ok(())
}
impl ValidationResult {
#[allow(clippy::get_first)]
fn try_from_bytes(bytes: Vec<u8>) -> Result<ValidationResult, ExtrinsicError> {
// TaggedTransactionQueue_validate_transaction returns this:
// https://github.com/paritytech/substrate/blob/0cdf7029017b70b7c83c21a4dc0aa1020e7914f6/primitives/runtime/src/transaction_validity.rs#L210
// We copy some of the inner types and put the three states (valid, invalid, unknown) into one enum,
// because from our perspective, the call was successful regardless.
if bytes.get(0) == Some(&0) {
// ok: valid. Decode but, for now we discard most of the information
let res = TransactionValid::decode(&mut &bytes[1..])
.map_err(ExtrinsicError::CannotDecodeValidationResult)?;
Ok(ValidationResult::Valid(res))
} else if bytes.get(0) == Some(&1) && bytes.get(1) == Some(&0) {
// error: invalid
let res = TransactionInvalid::decode(&mut &bytes[2..])
.map_err(ExtrinsicError::CannotDecodeValidationResult)?;
Ok(ValidationResult::Invalid(res))
} else if bytes.get(0) == Some(&1) && bytes.get(1) == Some(&1) {
// error: unknown
let res = TransactionUnknown::decode(&mut &bytes[2..])
.map_err(ExtrinsicError::CannotDecodeValidationResult)?;
Ok(ValidationResult::Unknown(res))
} else {
// unable to decode the bytes; they aren't what we expect.
Err(ExtrinsicError::UnexpectedValidationResultBytes(bytes))
}
}
}
/// The result of performing [`SubmittableTransaction::validate()`].
#[derive(Clone, Debug, PartialEq)]
pub enum ValidationResult {
/// The transaction is valid
Valid(TransactionValid),
/// The transaction is invalid
Invalid(TransactionInvalid),
/// Unable to validate the transaction
Unknown(TransactionUnknown),
}
impl ValidationResult {
/// Is the transaction valid.
pub fn is_valid(&self) -> bool {
matches!(self, ValidationResult::Valid(_))
}
}
/// Transaction is valid; here is some more information about it.
#[derive(Decode, Clone, Debug, PartialEq)]
pub struct TransactionValid {
/// Priority of the transaction.
///
/// Priority determines the ordering of two transactions that have all
/// their dependencies (required tags) satisfied.
pub priority: u64,
/// Transaction dependencies
///
/// A non-empty list signifies that some other transactions which provide
/// given tags are required to be included before that one.
pub requires: Vec<Vec<u8>>,
/// Provided tags
///
/// A list of tags this transaction provides. Successfully importing the transaction
/// will enable other transactions that depend on (require) those tags to be included as well.
/// Provided and required tags allow Substrate to build a dependency graph of transactions
/// and import them in the right (linear) order.
pub provides: Vec<Vec<u8>>,
/// Transaction longevity
///
/// Longevity describes minimum number of blocks the validity is correct.
/// After this period transaction should be removed from the pool or revalidated.
pub longevity: u64,
/// A flag indicating if the transaction should be propagated to other peers.
///
/// By setting `false` here the transaction will still be considered for
/// including in blocks that are authored on the current node, but will
/// never be sent to other peers.
pub propagate: bool,
}
/// The runtime was unable to validate the transaction.
#[derive(Decode, Clone, Debug, PartialEq)]
pub enum TransactionUnknown {
/// Could not lookup some information that is required to validate the transaction.
CannotLookup,
/// No validator found for the given unsigned transaction.
NoUnsignedValidator,
/// Any other custom unknown validity that is not covered by this enum.
Custom(u8),
}
/// The transaction is invalid.
#[derive(Decode, Clone, Debug, PartialEq)]
pub enum TransactionInvalid {
/// The call of the transaction is not expected.
Call,
/// General error to do with the inability to pay some fees (e.g. account balance too low).
Payment,
/// General error to do with the transaction not yet being valid (e.g. nonce too high).
Future,
/// General error to do with the transaction being outdated (e.g. nonce too low).
Stale,
/// General error to do with the transaction's proofs (e.g. signature).
///
/// # Possible causes
///
/// When using a signed extension that provides additional data for signing, it is required
/// that the signing and the verifying side use the same additional data. Additional
/// data will only be used to generate the signature, but will not be part of the transaction
/// itself. As the verifying side does not know which additional data was used while signing
/// it will only be able to assume a bad signature and cannot express a more meaningful error.
BadProof,
/// The transaction birth block is ancient.
///
/// # Possible causes
///
/// 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.)
AncientBirthBlock,
/// The transaction would exhaust the resources of current block.
///
/// The transaction might be valid, but there are not enough resources
/// left in the current block.
ExhaustsResources,
/// Any other custom invalid validity that is not covered by this enum.
Custom(u8),
/// An transaction with a Mandatory dispatch resulted in Error. This is indicative of either a
/// malicious validator or a buggy `provide_inherent`. In any case, it can result in
/// dangerously overweight blocks and therefore if found, invalidates the block.
BadMandatory,
/// An transaction with a mandatory dispatch tried to be validated.
/// This is invalid; only inherent transactions are allowed to have mandatory dispatches.
MandatoryValidation,
/// The sending address is disabled or known to be invalid.
BadSigner,
}
/// This trait is used to create default values for extrinsic params. We use this instead of
/// [`Default`] because we want to be able to support params which are tuples of more than 12
/// entries (which is the maximum tuple size Rust currently implements [`Default`] for on tuples),
/// given that we aren't far off having more than 12 transaction extensions already.
///
/// If you have params which are _not_ a tuple and which you'd like to be instantiated automatically
/// when calling [`TxClient::sign_and_submit_default()`] or [`TxClient::sign_and_submit_then_watch_default()`],
/// then you'll need to implement this trait for them.
pub trait DefaultParams: Sized {
/// Instantiate a default instance of the parameters.
fn default_params() -> Self;
}
impl<const N: usize, P: Default> DefaultParams for [P; N] {
fn default_params() -> Self {
core::array::from_fn(|_| P::default())
}
}
macro_rules! impl_default_params_for_tuple {
($($ident:ident),+) => {
impl <$($ident : Default),+> DefaultParams for ($($ident,)+){
fn default_params() -> Self {
(
$($ident::default(),)+
)
}
}
}
}
#[rustfmt::skip]
const _: () = {
impl_default_params_for_tuple!(A);
impl_default_params_for_tuple!(A, B);
impl_default_params_for_tuple!(A, B, C);
impl_default_params_for_tuple!(A, B, C, D);
impl_default_params_for_tuple!(A, B, C, D, E);
impl_default_params_for_tuple!(A, B, C, D, E, F);
impl_default_params_for_tuple!(A, B, C, D, E, F, G);
impl_default_params_for_tuple!(A, B, C, D, E, F, G, H);
impl_default_params_for_tuple!(A, B, C, D, E, F, G, H, I);
impl_default_params_for_tuple!(A, B, C, D, E, F, G, H, I, J);
impl_default_params_for_tuple!(A, B, C, D, E, F, G, H, I, J, K);
impl_default_params_for_tuple!(A, B, C, D, E, F, G, H, I, J, K, L);
impl_default_params_for_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M);
impl_default_params_for_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N);
impl_default_params_for_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O);
impl_default_params_for_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P);
impl_default_params_for_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q);
impl_default_params_for_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R);
impl_default_params_for_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S);
impl_default_params_for_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T);
impl_default_params_for_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U);
impl_default_params_for_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V);
impl_default_params_for_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W);
impl_default_params_for_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X);
impl_default_params_for_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y);
impl_default_params_for_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z);
};
#[cfg(test)]
mod test {
use super::*;
#[test]
fn transaction_validity_decoding_empty_bytes() {
// No panic should occur decoding empty bytes.
let decoded = ValidationResult::try_from_bytes(vec![]);
assert!(decoded.is_err())
}
#[test]
fn transaction_validity_decoding_is_ok() {
use sp_runtime::transaction_validity as sp;
use sp_runtime::transaction_validity::TransactionValidity as T;
let pairs = vec![
(
T::Ok(sp::ValidTransaction {
..Default::default()
}),
ValidationResult::Valid(TransactionValid {
// By default, tx is immortal
longevity: u64::MAX,
// Default is true
propagate: true,
priority: 0,
provides: vec![],
requires: vec![],
}),
),
(
T::Err(sp::TransactionValidityError::Invalid(
sp::InvalidTransaction::BadProof,
)),
ValidationResult::Invalid(TransactionInvalid::BadProof),
),
(
T::Err(sp::TransactionValidityError::Invalid(
sp::InvalidTransaction::Call,
)),
ValidationResult::Invalid(TransactionInvalid::Call),
),
(
T::Err(sp::TransactionValidityError::Invalid(
sp::InvalidTransaction::Payment,
)),
ValidationResult::Invalid(TransactionInvalid::Payment),
),
(
T::Err(sp::TransactionValidityError::Invalid(
sp::InvalidTransaction::Future,
)),
ValidationResult::Invalid(TransactionInvalid::Future),
),
(
T::Err(sp::TransactionValidityError::Invalid(
sp::InvalidTransaction::Stale,
)),
ValidationResult::Invalid(TransactionInvalid::Stale),
),
(
T::Err(sp::TransactionValidityError::Invalid(
sp::InvalidTransaction::AncientBirthBlock,
)),
ValidationResult::Invalid(TransactionInvalid::AncientBirthBlock),
),
(
T::Err(sp::TransactionValidityError::Invalid(
sp::InvalidTransaction::ExhaustsResources,
)),
ValidationResult::Invalid(TransactionInvalid::ExhaustsResources),
),
(
T::Err(sp::TransactionValidityError::Invalid(
sp::InvalidTransaction::BadMandatory,
)),
ValidationResult::Invalid(TransactionInvalid::BadMandatory),
),
(
T::Err(sp::TransactionValidityError::Invalid(
sp::InvalidTransaction::MandatoryValidation,
)),
ValidationResult::Invalid(TransactionInvalid::MandatoryValidation),
),
(
T::Err(sp::TransactionValidityError::Invalid(
sp::InvalidTransaction::BadSigner,
)),
ValidationResult::Invalid(TransactionInvalid::BadSigner),
),
(
T::Err(sp::TransactionValidityError::Invalid(
sp::InvalidTransaction::Custom(123),
)),
ValidationResult::Invalid(TransactionInvalid::Custom(123)),
),
(
T::Err(sp::TransactionValidityError::Unknown(
sp::UnknownTransaction::CannotLookup,
)),
ValidationResult::Unknown(TransactionUnknown::CannotLookup),
),
(
T::Err(sp::TransactionValidityError::Unknown(
sp::UnknownTransaction::NoUnsignedValidator,
)),
ValidationResult::Unknown(TransactionUnknown::NoUnsignedValidator),
),
(
T::Err(sp::TransactionValidityError::Unknown(
sp::UnknownTransaction::Custom(123),
)),
ValidationResult::Unknown(TransactionUnknown::Custom(123)),
),
];
for (sp, validation_result) in pairs {
let encoded = sp.encode();
let decoded = ValidationResult::try_from_bytes(encoded).expect("should decode OK");
assert_eq!(decoded, validation_result);
}
}
}
+465
View File
@@ -0,0 +1,465 @@
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
//! Types representing extrinsics/transactions that have been submitted to a node.
use std::task::Poll;
use crate::{
backend::{BlockRef, StreamOfResults, TransactionStatus as BackendTxStatus},
client::OnlineClientT,
config::{Config, HashFor},
error::{
DispatchError, TransactionEventsError, TransactionFinalizedSuccessError,
TransactionProgressError, TransactionStatusError,
},
events::EventsClient,
utils::strip_compact_prefix,
};
use derive_where::derive_where;
use futures::{Stream, StreamExt};
/// This struct represents a subscription to the progress of some transaction.
pub struct TxProgress<T: Config, C> {
sub: Option<StreamOfResults<BackendTxStatus<HashFor<T>>>>,
ext_hash: HashFor<T>,
client: C,
}
impl<T: Config, C> std::fmt::Debug for TxProgress<T, C> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TxProgress")
.field("sub", &"<subscription>")
.field("ext_hash", &self.ext_hash)
.field("client", &"<client>")
.finish()
}
}
// The above type is not `Unpin` by default unless the generic param `T` is,
// so we manually make it clear that Unpin is actually fine regardless of `T`
// (we don't care if this moves around in memory while it's "pinned").
impl<T: Config, C> Unpin for TxProgress<T, C> {}
impl<T: Config, C> TxProgress<T, C> {
/// Instantiate a new [`TxProgress`] from a custom subscription.
pub fn new(
sub: StreamOfResults<BackendTxStatus<HashFor<T>>>,
client: C,
ext_hash: HashFor<T>,
) -> Self {
Self {
sub: Some(sub),
client,
ext_hash,
}
}
/// Return the hash of the extrinsic.
pub fn extrinsic_hash(&self) -> HashFor<T> {
self.ext_hash
}
}
impl<T, C> TxProgress<T, C>
where
T: Config,
C: OnlineClientT<T>,
{
/// Return the next transaction status when it's emitted. This just delegates to the
/// [`futures::Stream`] implementation for [`TxProgress`], but allows you to
/// avoid importing that trait if you don't otherwise need it.
pub async fn next(&mut self) -> Option<Result<TxStatus<T, C>, TransactionProgressError>> {
StreamExt::next(self).await
}
/// Wait for the transaction to be finalized, and return a [`TxInBlock`]
/// instance when it is, or an error if there was a problem waiting for finalization.
///
/// **Note:** consumes `self`. If you'd like to perform multiple actions as the state of the
/// transaction progresses, use [`TxProgress::next()`] instead.
///
/// **Note:** transaction statuses like `Invalid`/`Usurped`/`Dropped` indicate with some
/// probability that the transaction will not make it into a block but there is no guarantee
/// that this is true. In those cases the stream is closed however, so you currently have no way to find
/// out if they finally made it into a block or not.
pub async fn wait_for_finalized(mut self) -> Result<TxInBlock<T, C>, TransactionProgressError> {
while let Some(status) = self.next().await {
match status? {
// Finalized! Return.
TxStatus::InFinalizedBlock(s) => return Ok(s),
// Error scenarios; return the error.
TxStatus::Error { message } => {
return Err(TransactionStatusError::Error(message).into());
}
TxStatus::Invalid { message } => {
return Err(TransactionStatusError::Invalid(message).into());
}
TxStatus::Dropped { message } => {
return Err(TransactionStatusError::Dropped(message).into());
}
// Ignore and wait for next status event:
_ => continue,
}
}
Err(TransactionProgressError::UnexpectedEndOfTransactionStatusStream)
}
/// Wait for the transaction to be finalized, and for the transaction events to indicate
/// that the transaction was successful. Returns the events associated with the transaction,
/// as well as a couple of other details (block hash and extrinsic hash).
///
/// **Note:** consumes self. If you'd like to perform multiple actions as progress is made,
/// use [`TxProgress::next()`] instead.
///
/// **Note:** transaction statuses like `Invalid`/`Usurped`/`Dropped` indicate with some
/// probability that the transaction will not make it into a block but there is no guarantee
/// that this is true. In those cases the stream is closed however, so you currently have no way to find
/// out if they finally made it into a block or not.
pub async fn wait_for_finalized_success(
self,
) -> Result<crate::blocks::ExtrinsicEvents<T>, TransactionFinalizedSuccessError> {
let evs = self.wait_for_finalized().await?.wait_for_success().await?;
Ok(evs)
}
}
impl<T: Config, C: Clone> Stream for TxProgress<T, C> {
type Item = Result<TxStatus<T, C>, TransactionProgressError>;
fn poll_next(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Self::Item>> {
let sub = match self.sub.as_mut() {
Some(sub) => sub,
None => return Poll::Ready(None),
};
sub.poll_next_unpin(cx)
.map_err(TransactionProgressError::CannotGetNextProgressUpdate)
.map_ok(|status| {
match status {
BackendTxStatus::Validated => TxStatus::Validated,
BackendTxStatus::Broadcasted => TxStatus::Broadcasted,
BackendTxStatus::NoLongerInBestBlock => TxStatus::NoLongerInBestBlock,
BackendTxStatus::InBestBlock { hash } => TxStatus::InBestBlock(TxInBlock::new(
hash,
self.ext_hash,
self.client.clone(),
)),
// These stream events mean that nothing further will be sent:
BackendTxStatus::InFinalizedBlock { hash } => {
self.sub = None;
TxStatus::InFinalizedBlock(TxInBlock::new(
hash,
self.ext_hash,
self.client.clone(),
))
}
BackendTxStatus::Error { message } => {
self.sub = None;
TxStatus::Error { message }
}
BackendTxStatus::Invalid { message } => {
self.sub = None;
TxStatus::Invalid { message }
}
BackendTxStatus::Dropped { message } => {
self.sub = None;
TxStatus::Dropped { message }
}
}
})
}
}
/// Possible transaction statuses returned from our [`TxProgress::next()`] call.
#[derive_where(Debug; C)]
pub enum TxStatus<T: Config, C> {
/// Transaction is part of the future queue.
Validated,
/// The transaction has been broadcast to other nodes.
Broadcasted,
/// Transaction is no longer in a best block.
NoLongerInBestBlock,
/// Transaction has been included in block with given hash.
InBestBlock(TxInBlock<T, C>),
/// Transaction has been finalized by a finality-gadget, e.g GRANDPA
InFinalizedBlock(TxInBlock<T, C>),
/// Something went wrong in the node.
Error {
/// Human readable message; what went wrong.
message: String,
},
/// Transaction is invalid (bad nonce, signature etc).
Invalid {
/// Human readable message; why was it invalid.
message: String,
},
/// The transaction was dropped.
Dropped {
/// Human readable message; why was it dropped.
message: String,
},
}
impl<T: Config, C> TxStatus<T, C> {
/// A convenience method to return the finalized details. Returns
/// [`None`] if the enum variant is not [`TxStatus::InFinalizedBlock`].
pub fn as_finalized(&self) -> Option<&TxInBlock<T, C>> {
match self {
Self::InFinalizedBlock(val) => Some(val),
_ => None,
}
}
/// A convenience method to return the best block details. Returns
/// [`None`] if the enum variant is not [`TxStatus::InBestBlock`].
pub fn as_in_block(&self) -> Option<&TxInBlock<T, C>> {
match self {
Self::InBestBlock(val) => Some(val),
_ => None,
}
}
}
/// This struct represents a transaction that has made it into a block.
#[derive_where(Debug; C)]
pub struct TxInBlock<T: Config, C> {
block_ref: BlockRef<HashFor<T>>,
ext_hash: HashFor<T>,
client: C,
}
impl<T: Config, C> TxInBlock<T, C> {
pub(crate) fn new(block_ref: BlockRef<HashFor<T>>, ext_hash: HashFor<T>, client: C) -> Self {
Self {
block_ref,
ext_hash,
client,
}
}
/// Return the hash of the block that the transaction has made it into.
pub fn block_hash(&self) -> HashFor<T> {
self.block_ref.hash()
}
/// Return the hash of the extrinsic that was submitted.
pub fn extrinsic_hash(&self) -> HashFor<T> {
self.ext_hash
}
}
impl<T: Config, C: OnlineClientT<T>> TxInBlock<T, C> {
/// Fetch the events associated with this transaction. If the transaction
/// was successful (ie no `ExtrinsicFailed`) events were found, then we return
/// the events associated with it. If the transaction was not successful, or
/// something else went wrong, we return an error.
///
/// **Note:** If multiple `ExtrinsicFailed` errors are returned (for instance
/// because a pallet chooses to emit one as an event, which is considered
/// abnormal behaviour), it is not specified which of the errors is returned here.
/// You can use [`TxInBlock::fetch_events`] instead if you'd like to
/// work with multiple "error" events.
///
/// **Note:** This has to download block details from the node and decode events
/// from them.
pub async fn wait_for_success(
&self,
) -> Result<crate::blocks::ExtrinsicEvents<T>, TransactionEventsError> {
let events = self.fetch_events().await?;
// Try to find any errors; return the first one we encounter.
for (ev_idx, ev) in events.iter().enumerate() {
let ev = ev.map_err(|e| TransactionEventsError::CannotDecodeEventInBlock {
event_index: ev_idx,
block_hash: self.block_hash().into(),
error: e,
})?;
if ev.pallet_name() == "System" && ev.variant_name() == "ExtrinsicFailed" {
let dispatch_error =
DispatchError::decode_from(ev.field_bytes(), self.client.metadata()).map_err(
|e| TransactionEventsError::CannotDecodeDispatchError {
error: e,
bytes: ev.field_bytes().to_vec(),
},
)?;
return Err(dispatch_error.into());
}
}
Ok(events)
}
/// Fetch all of the events associated with this transaction. This succeeds whether
/// the transaction was a success or not; it's up to you to handle the error and
/// success events however you prefer.
///
/// **Note:** This has to download block details from the node and decode events
/// from them.
pub async fn fetch_events(
&self,
) -> Result<crate::blocks::ExtrinsicEvents<T>, TransactionEventsError> {
let hasher = self.client.hasher();
let block_body = self
.client
.backend()
.block_body(self.block_ref.hash())
.await
.map_err(|e| TransactionEventsError::CannotFetchBlockBody {
block_hash: self.block_hash().into(),
error: e,
})?
.ok_or_else(|| TransactionEventsError::BlockNotFound {
block_hash: self.block_hash().into(),
})?;
let extrinsic_idx = block_body
.iter()
.position(|ext| {
use crate::config::Hasher;
let Ok((_, stripped)) = strip_compact_prefix(ext) else {
return false;
};
let hash = hasher.hash_of(&stripped);
hash == self.ext_hash
})
// If we successfully obtain the block hash we think contains our
// extrinsic, the extrinsic should be in there somewhere..
.ok_or_else(|| TransactionEventsError::CannotFindTransactionInBlock {
block_hash: self.block_hash().into(),
transaction_hash: self.ext_hash.into(),
})?;
let events = EventsClient::new(self.client.clone())
.at(self.block_ref.clone())
.await
.map_err(
|e| TransactionEventsError::CannotFetchEventsForTransaction {
block_hash: self.block_hash().into(),
transaction_hash: self.ext_hash.into(),
error: e,
},
)?;
Ok(crate::blocks::ExtrinsicEvents::new(
self.ext_hash,
extrinsic_idx as u32,
events,
))
}
}
#[cfg(test)]
mod test {
use super::*;
use pezkuwi_subxt_core::client::RuntimeVersion;
use crate::{
SubstrateConfig,
backend::{StreamOfResults, TransactionStatus},
client::{OfflineClientT, OnlineClientT},
config::{Config, HashFor},
tx::TxProgress,
};
type MockTxProgress = TxProgress<SubstrateConfig, MockClient>;
type MockHash = HashFor<SubstrateConfig>;
type MockSubstrateTxStatus = TransactionStatus<MockHash>;
/// a mock client to satisfy trait bounds in tests
#[derive(Clone, Debug)]
struct MockClient;
impl OfflineClientT<SubstrateConfig> for MockClient {
fn metadata(&self) -> crate::Metadata {
unimplemented!("just a mock impl to satisfy trait bounds")
}
fn genesis_hash(&self) -> MockHash {
unimplemented!("just a mock impl to satisfy trait bounds")
}
fn runtime_version(&self) -> RuntimeVersion {
unimplemented!("just a mock impl to satisfy trait bounds")
}
fn hasher(&self) -> <SubstrateConfig as Config>::Hasher {
unimplemented!("just a mock impl to satisfy trait bounds")
}
fn client_state(&self) -> pezkuwi_subxt_core::client::ClientState<SubstrateConfig> {
unimplemented!("just a mock impl to satisfy trait bounds")
}
}
impl OnlineClientT<SubstrateConfig> for MockClient {
fn backend(&self) -> &dyn crate::backend::Backend<SubstrateConfig> {
unimplemented!("just a mock impl to satisfy trait bounds")
}
}
#[tokio::test]
async fn wait_for_finalized_returns_err_when_error() {
let tx_progress = mock_tx_progress(vec![
MockSubstrateTxStatus::Broadcasted,
MockSubstrateTxStatus::Error {
message: "err".into(),
},
]);
let finalized_result = tx_progress.wait_for_finalized().await;
assert!(matches!(
finalized_result,
Err(TransactionProgressError::TransactionStatusError(TransactionStatusError::Error(e))) if e == "err"
));
}
#[tokio::test]
async fn wait_for_finalized_returns_err_when_invalid() {
let tx_progress = mock_tx_progress(vec![
MockSubstrateTxStatus::Broadcasted,
MockSubstrateTxStatus::Invalid {
message: "err".into(),
},
]);
let finalized_result = tx_progress.wait_for_finalized().await;
assert!(matches!(
finalized_result,
Err(TransactionProgressError::TransactionStatusError(TransactionStatusError::Invalid(e))) if e == "err"
));
}
#[tokio::test]
async fn wait_for_finalized_returns_err_when_dropped() {
let tx_progress = mock_tx_progress(vec![
MockSubstrateTxStatus::Broadcasted,
MockSubstrateTxStatus::Dropped {
message: "err".into(),
},
]);
let finalized_result = tx_progress.wait_for_finalized().await;
assert!(matches!(
finalized_result,
Err(TransactionProgressError::TransactionStatusError(TransactionStatusError::Dropped(e))) if e == "err"
));
}
fn mock_tx_progress(statuses: Vec<MockSubstrateTxStatus>) -> MockTxProgress {
let sub = create_substrate_tx_status_subscription(statuses);
TxProgress::new(sub, MockClient, Default::default())
}
fn create_substrate_tx_status_subscription(
elements: Vec<MockSubstrateTxStatus>,
) -> StreamOfResults<MockSubstrateTxStatus> {
let results = elements.into_iter().map(Ok);
let stream = Box::pin(futures::stream::iter(results));
let sub: StreamOfResults<MockSubstrateTxStatus> = StreamOfResults::new(stream);
sub
}
}
+113
View File
@@ -0,0 +1,113 @@
// 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::macros::{cfg_jsonrpsee_native, cfg_jsonrpsee_web};
use serde_json::value::RawValue;
/// Possible errors encountered trying to fetch a chain spec from an RPC node.
#[derive(thiserror::Error, Debug)]
#[allow(missing_docs)]
pub enum FetchChainspecError {
#[error("Cannot fetch chain spec: RPC error: {0}.")]
RpcError(String),
#[error("Cannot fetch chain spec: Invalid URL.")]
InvalidUrl,
#[error("Cannot fetch chain spec: Invalid URL scheme.")]
InvalidScheme,
#[error("Cannot fetch chain spec: Handshake error establishing WS connection.")]
HandshakeError,
}
/// Fetch a chain spec from an RPC node at the given URL.
pub async fn fetch_chainspec_from_rpc_node(
url: impl AsRef<str>,
) -> Result<Box<RawValue>, FetchChainspecError> {
use jsonrpsee::core::client::{ClientT, SubscriptionClientT};
use jsonrpsee::rpc_params;
let client = jsonrpsee_helpers::client(url.as_ref()).await?;
let result = client
.request("sync_state_genSyncSpec", jsonrpsee::rpc_params![true])
.await
.map_err(|err| FetchChainspecError::RpcError(err.to_string()))?;
// Subscribe to the finalized heads of the chain.
let mut subscription = SubscriptionClientT::subscribe::<Box<RawValue>, _>(
&client,
"chain_subscribeFinalizedHeads",
rpc_params![],
"chain_unsubscribeFinalizedHeads",
)
.await
.map_err(|err| FetchChainspecError::RpcError(err.to_string()))?;
// We must ensure that the finalized block of the chain is not the block included
// in the chainSpec.
// This is a temporary workaround for: https://github.com/smol-dot/smoldot/issues/1562.
// The first finalized block that is received might by the finalized block could be the one
// included in the chainSpec. Decoding the chainSpec for this purpose is too complex.
let _ = subscription.next().await;
let _ = subscription.next().await;
Ok(result)
}
cfg_jsonrpsee_native! {
mod jsonrpsee_helpers {
use super::FetchChainspecError;
use tokio_util::compat::Compat;
pub use jsonrpsee::{
client_transport::ws::{self, EitherStream, Url, WsTransportClientBuilder},
core::client::Client,
};
pub type Sender = ws::Sender<Compat<EitherStream>>;
pub type Receiver = ws::Receiver<Compat<EitherStream>>;
/// Build WS RPC client from URL
pub async fn client(url: &str) -> Result<Client, FetchChainspecError> {
let url = Url::parse(url).map_err(|_| FetchChainspecError::InvalidUrl)?;
if url.scheme() != "ws" && url.scheme() != "wss" {
return Err(FetchChainspecError::InvalidScheme);
}
let (sender, receiver) = ws_transport(url).await?;
Ok(Client::builder()
.max_buffer_capacity_per_subscription(4096)
.build_with_tokio(sender, receiver))
}
async fn ws_transport(url: Url) -> Result<(Sender, Receiver), FetchChainspecError> {
WsTransportClientBuilder::default()
.build(url)
.await
.map_err(|_| FetchChainspecError::HandshakeError)
}
}
}
cfg_jsonrpsee_web! {
mod jsonrpsee_helpers {
use super::FetchChainspecError;
pub use jsonrpsee::{
client_transport::web,
core::client::{Client, ClientBuilder},
};
/// Build web RPC client from URL
pub async fn client(url: &str) -> Result<Client, FetchChainspecError> {
let (sender, receiver) = web::connect(url)
.await
.map_err(|_| FetchChainspecError::HandshakeError)?;
Ok(ClientBuilder::default()
.max_buffer_capacity_per_subscription(4096)
.build_with_wasm(sender, receiver))
}
}
}
+20
View File
@@ -0,0 +1,20 @@
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
//! Miscellaneous utility helpers.
use crate::macros::cfg_jsonrpsee;
pub use pezkuwi_subxt_core::utils::{
AccountId32, Encoded, Era, H160, H256, H512, KeyedVec, MultiAddress, MultiSignature,
PhantomDataSendSync, Static, UncheckedExtrinsic, WrapperKeepOpaque, Yes, bits,
strip_compact_prefix, to_hex,
};
pub use pezkuwi_subxt_rpcs::utils::url_is_secure;
cfg_jsonrpsee! {
mod fetch_chain_spec;
pub use fetch_chain_spec::{fetch_chainspec_from_rpc_node, FetchChainspecError};
}
+12
View File
@@ -0,0 +1,12 @@
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
//! Types associated with executing View Function calls.
mod view_function_types;
mod view_functions_client;
pub use pezkuwi_subxt_core::view_functions::payload::{DynamicPayload, Payload, StaticPayload, dynamic};
pub use view_function_types::ViewFunctionsApi;
pub use view_functions_client::ViewFunctionsClient;
@@ -0,0 +1,81 @@
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
use super::Payload;
use crate::{
backend::BlockRef,
client::OnlineClientT,
config::{Config, HashFor},
error::ViewFunctionError,
};
use derive_where::derive_where;
use std::{future::Future, marker::PhantomData};
/// Execute View Function calls.
#[derive_where(Clone; Client)]
pub struct ViewFunctionsApi<T: Config, Client> {
client: Client,
block_ref: BlockRef<HashFor<T>>,
_marker: PhantomData<T>,
}
impl<T: Config, Client> ViewFunctionsApi<T, Client> {
/// Create a new [`ViewFunctionsApi`]
pub(crate) fn new(client: Client, block_ref: BlockRef<HashFor<T>>) -> Self {
Self {
client,
block_ref,
_marker: PhantomData,
}
}
}
impl<T, Client> ViewFunctionsApi<T, Client>
where
T: Config,
Client: OnlineClientT<T>,
{
/// Run the validation logic against some View Function payload you'd like to use. Returns `Ok(())`
/// if the payload is valid (or if it's not possible to check since the payload has no validation hash).
/// Return an error if the payload was not valid or something went wrong trying to validate it (ie
/// the View Function in question do not exist at all)
pub fn validate<Call: Payload>(&self, payload: Call) -> Result<(), ViewFunctionError> {
pezkuwi_subxt_core::view_functions::validate(payload, &self.client.metadata()).map_err(Into::into)
}
/// Execute a View Function call.
pub fn call<Call: Payload>(
&self,
payload: Call,
) -> impl Future<Output = Result<Call::ReturnType, ViewFunctionError>> + use<Call, Client, T>
{
let client = self.client.clone();
let block_hash = self.block_ref.hash();
// Ensure that the returned future doesn't have a lifetime tied to api.view_functions(),
// which is a temporary thing we'll be throwing away quickly:
async move {
let metadata = client.metadata();
// Validate the View Function payload hash against the compile hash from codegen.
pezkuwi_subxt_core::view_functions::validate(&payload, &metadata)?;
// Assemble the data to call the "execute_view_function" runtime API, which
// then calls the relevant view function.
let call_name = pezkuwi_subxt_core::view_functions::CALL_NAME;
let call_args = pezkuwi_subxt_core::view_functions::call_args(&payload, &metadata)?;
// Make the call.
let bytes = client
.backend()
.call(call_name, Some(call_args.as_slice()), block_hash)
.await
.map_err(ViewFunctionError::CannotCallApi)?;
// Decode the response.
let value =
pezkuwi_subxt_core::view_functions::decode_value(&mut &*bytes, &payload, &metadata)?;
Ok(value)
}
}
}
@@ -0,0 +1,62 @@
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.
use super::view_function_types::ViewFunctionsApi;
use crate::{
backend::BlockRef,
client::OnlineClientT,
config::{Config, HashFor},
error::ViewFunctionError,
};
use derive_where::derive_where;
use std::{future::Future, marker::PhantomData};
/// Make View Function calls at some block.
#[derive_where(Clone; Client)]
pub struct ViewFunctionsClient<T, Client> {
client: Client,
_marker: PhantomData<T>,
}
impl<T, Client> ViewFunctionsClient<T, Client> {
/// Create a new [`ViewFunctionsClient`]
pub fn new(client: Client) -> Self {
Self {
client,
_marker: PhantomData,
}
}
}
impl<T, Client> ViewFunctionsClient<T, Client>
where
T: Config,
Client: OnlineClientT<T>,
{
/// Obtain an interface to call View Functions at some block hash.
pub fn at(&self, block_ref: impl Into<BlockRef<HashFor<T>>>) -> ViewFunctionsApi<T, Client> {
ViewFunctionsApi::new(self.client.clone(), block_ref.into())
}
/// Obtain an interface to call View Functions at the latest finalized block.
pub fn at_latest(
&self,
) -> impl Future<Output = Result<ViewFunctionsApi<T, Client>, ViewFunctionError>> + Send + 'static
{
// Clone and pass the client in like this so that we can explicitly
// return a Future that's Send + 'static, rather than tied to &self.
let client = self.client.clone();
async move {
// get the ref for the latest finalized block and use that.
let block_ref = client
.backend()
.latest_finalized_block_ref()
.await
.map_err(ViewFunctionError::CannotGetLatestFinalizedBlock)?;
Ok(ViewFunctionsApi::new(client, block_ref))
}
}
}