-
Notifications
You must be signed in to change notification settings - Fork 258
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Introduce Backend trait to allow different RPC (or other) backends to be implemented #1126
Conversation
…m instead of RpcClient
… types prefixed with RawRpc so it's less ocnfusing which is which
…ate(), to signal that the calls are different
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I looked over the entire thing, looks good. From my understanding we will add a newer backend soon as a replacement of the LegacyBackend
? If that is the case, what will be the mechanism by which it is determined what backend is used when an OnlineClient
is constructed?
/// events will be emitted. | ||
#[derive(Debug, Clone, PartialEq, Eq)] | ||
pub enum TransactionStatus<Hash> { | ||
/// Transaction is part of the future queue. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the new TransactionStatus
!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's basically much more similar to the one that the new APIs will return, so making this change now means that hopefully we won't need to change anything else as we implement the new backend :)
/// Prevent the backend trait being implemented externally. | ||
#[doc(hidden)] | ||
pub(crate) mod sealed { | ||
pub trait Sealed {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the reason to put Sealed
into a separate mod
? Would pub(crate) trait Sealed {}
not suffice?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good question! This is a bit of a hack, because if you have pub (crate) trait Sealed
on it's own then you run into:
crate-private trait `Sealed` in public interface
can't leak crate-private trait
But having the trait be fully pub
(but in a module that people can't access, so the thing is pub but nobody can name it) circumvents this. It's weird, but it's a generally established pattern that crates use for this purpose :)
(I have only skimmed it but maybe eg https://predr.ag/blog/definitive-guide-to-sealed-traits-in-rust/ is interesting; it also talks about another approach via methods that can't be implemented :))
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh that is very interesting!
|
||
/// Fetch system health | ||
pub async fn system_health(&self) -> Result<SystemHealth, Error> { | ||
self.client.request("system_health", rpc_params![]).await |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did not know these RPC methods were available, maybe we should add an example, that showcases all the little things we can fetch from a node?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm only really leaving them here because I felt a little bad stripping away all of the RPC methods that we don't use immediately, but in the long run we'll ultimately deprecate and remove these things once the new RPCs are stable and in widespread usage. For this reason, I'm happy to not really "publicise" these legacy RPC methods, because the more people rely on them, the more they will be disappointed when they go away (which, to be clear, may still be over a year away :))
At the moment, the "simple" constructors on I've also added The |
@@ -24,8 +23,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> { | |||
println!(" Extrinsics:"); | |||
|
|||
// Log each of the extrinsic with it's associated events: | |||
let body = block.body().await?; | |||
for ext in body.extrinsics().iter() { | |||
let extrinsics = block.extrinsics().await?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice simplification for the end user!
If (ever) justification turns to be a thing in the future we could expose it on the same level as extrinsics
(ie block.extrinsics())
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup definitely; if they are a thing then they won't be in the block body I guess anyway (since the body of a block afaik is just the extrinsics). I'm not actually sure where they live offhand; maybe theres a storage or runtime API call to fetch them (I haven't looked!)
subxt/src/tx/tx_client.rs
Outdated
// 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[0] == 0 { | ||
// ok: valid (more detail is available here, but ). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: ok: valid (more detail is available here, but we only care about the Result enum variant).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hehe I feel like I do this quite often; get distracted and forget to finish comments!
subxt/src/tx/tx_client.rs
Outdated
// 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[0] == 0 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: In case we somehow don't receive an Result<ValidTransaction, TransactionValidityError>
and end-up with some empty bytes, would it be beneficial to do a bytes.get(0).expect("Invalid wire format for ValidationResult")
? Or return the Error::Unkown
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point; I should handle these cases better :)
let api = ctx.client(); | ||
|
||
// Check subscription with runtime updates set on false. | ||
let mut blocks = api.rpc().chainhead_unstable_follow(false).await.unwrap(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is fine since we plan to add them back under a dedicated backend for the Spec V2.
In the meanwhile, I remember this is used by polakadot-interceptor here. However I think, the same could be achieved with the new Subxt API (for reference @AndreiEres ) which will allow seamless transition between chainHead
and the legacy RPC.
IIRC this was used to create a metric about the duration time between two reported blocks (either best / finalized), we could work together here to find an appropriate solution with the new API (we might merge the 2 streams for best and finalized)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for mentioning me! I should've expected it by unstable
mark in chainHead
call :-)
So I see that I should switch our subscription in introspector to new API, right? Which one, btw?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After we release the next subxt version, merging the Backend::stream_best_block_headers and Backend::stream_finalized_block_headers should produce the same information as the chainHead
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would this call also work (if all you care about is the time between any new block)?
api.blocks().subscribe_all().await
or
api.blocks().subscribe_best().await
If you care about all new "best" blocks (likewise there is a version for finalized blocks only. Hopefully that way you can lean on the higher level stable APIs more :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Amazing work here! 👍
Previously, Subxt looked something like:
This PR introduces a
Backend
trait inbetween those; a trait which provides the minimal set of things that the high level APIs need (while trying to cater for what the new RPCs can provide). It then implements that trait for the "legacy" API methods, and migrates the high level code to using the trait rather than directly calling methods (in fact; making this impossible). So now we have:(Where the RPC calls are just an implementation detail of the backend implementation)
I'd suggest to look at the
subxt/backend
folder first to see what this trait looks like, then thesubxt/backend/legacy
to see theLegacyBackend
implementation, which is based on the old RPC methods.The idea is that we can add a
v2
backend which is based on the new methods.The examples are handy to look at next to see what's changed. Much of the rest of the code is fallout from adding this new backend thing!
There are a number of breaking changes here (mostly I'll cover below in the notes), but the high level APIs haven't changed significantly. Manually calling RPC methods is now more difficult, but still possible via creating an
RpcClient
manually, and using that to instantiate either/both ofLegacyRpcMethods
to make RPC calls andOnlineClient
to do high level Subxt things.Random Notes:
chainHead
RPC stuff at the moment. The plan is to put this stuff back as part of the new backend.block.body().await.extrinsics()
is now justblock.extrinsics().await
; the body of a block only contains extrinsics anyway (the new API inparticular only returns extrinsics, and not justifications, which we didn't expose anyway).RpcClient
instead of anArc<impl RpcClientT>
.RpcClient::from_url
replacesdefault_rpc_client()
, or you can instantiateRpcClient
from an underlying trait. I've done renames so that the raw trait stuff is better separated now (egRaw*
names) to make the difference clearer. One reason for this all is that calling RPC methods requires an RPC client, so it's easier if people can create a singleRpcClient
quite easily and then use that in the places it's needed).BlockRef
type that, in the new backend, can try to "hold onto" a block, ie keep it from being unpinned, if it's alive. Things that may need to get details at a particular block should now keep the correspondingBlockRef
around until they've finished.fetch_keys
tofetch_keys_raw
(it returns raw bytes like fetch_raw), and makingiter()
hand back a properStream
which should make it more ergonomic (and changes results from beingResult<Option<T>>
toOption<Result<T>>
). Currently it fetches keys in batches but values individually; we can look to improve that again though if needbe.client.rpc()
, because this is now an implementation detail of a specificBackend
. Instead, you canOnlineClient::from_backend
to pass a backend which has been instantiated with some RPC client that you can also use elsewhere.tx.dry_run()
is nowtx.validate()
and uses a runtime call for validating (in part because the new RPCs won't expose a dry run call). This returns a different result though, leading to breakage here, so I've also kept the RPC call around so that one can manually call that instead if they need the old behaviour.tx.submit()
used to directly call a submit RPC which handed the hash back. Now we delegate tosubmit_and_watch
and return the hash once the first non-error status comes back, since the new APIs don't (currently) have a way to just submit a TX anyway.TransactionProgress
events are altered to better fit the new API results (omitting a couple of details that the old API can't replicate). The high level API around submitting and watching is otherwise unchanged.next()
method on them instead of egnext_item()
now, which is mostly just a convenience to avoid needing to importStreamExt
for this common op.