Runtime APIs
Each Substrate node contains a runtime. The runtime contains the business logic of the chain. It defines what transactions are valid and invalid and determines how the chain's state changes in response to transactions. The runtime is compiled to Wasm to facilitate runtime upgrades. The "outer node", everything other than the runtime, does not compile to Wasm, only to native. The outer node is responsible for handling peer discovery, transaction pooling, block and transaction gossiping, consensus, and answering RPC calls from the outside world. While performing these tasks, the outer node sometimes needs to query the runtime for information, or provide information to the runtime. A Runtime API facilitates this kind of communication between the outer node and the runtime. In this recipe, we will write our own minimal runtime API.
Our Example
For this example, we will write a pallet called sum-storage
with two storage items, both u32
s.
#![allow(unused)] fn main() { #[pallet::storage] #[pallet::getter(fn thing1)] pub type Thing1<T: Config> = StorageValue<_, u32, ValueQuery>; #[pallet::storage] #[pallet::getter(fn thing2)] pub type Thing2<T: Config> = StorageValue<_, u32, ValueQuery>; }
Substrate already comes with a runtime API for querying storage values, which is why we can easily query our two storage values from a front-end. In this example we imagine that the outer node is interested in knowing the sum of the two values, rather than either individual value. Our runtime API will provide a way for the outer node to query the runtime for this sum. Before we define the actual runtime API, let's write a public helper function in the pallet to do the summing.
#![allow(unused)] fn main() { impl<T: Config> Pallet<T> { pub fn get_sum() -> u32 { Thing1::<T>::get() + Thing2::<T>::get() } } }
So far, nothing we've done is specific to runtime APIs. In the coming sections, we will use this helper function in our runtime API's implementation.
Defining the API
The first step in adding a runtime API to your runtime is defining its interface using a Rust trait.
This is done in the sum-storage/runtime-api/src/lib.rs
file. This file can live anywhere you like,
but because it defines an API that is closely related to a particular pallet, it makes sense to
include the API definition in the pallet's directory.
The code to define the API is quite simple, and looks almost like any old Rust trait. The one
addition is that it must be placed in the
decl_runtime_apis!
macro. This
macro allows the outer node to query the runtime API at specific blocks. Although this runtime API
only provides a single function, you may write as many as you like.
#![allow(unused)] fn main() { sp_api::decl_runtime_apis! { pub trait SumStorageApi { fn get_sum() -> u32; } } }
Implementing the API
With our pallet written and our runtime API defined, we may now implement the API for our runtime.
This happens in the main runtime aggregation file. In our case we've provided the api-runtime
in
runtimes/api-runtime/src/lib.rs
.
As with defining the API, implementing a runtime API looks similar to implementing any old Rust
trait with the exception that the implementation must go inside of the
impl_runtime_apis!
macro. Every
runtime must use iml_runtime_apis!
because the
Core
API is required. We will add an
implementation for our own API alongside the others in this macro. Our implementation is
straight-forward as it merely calls the pallet's helper function that we wrote previously.
#![allow(unused)] fn main() { impl_runtime_apis! { // --snip-- impl sum_storage_rpc_runtime_api::SumStorageApi<Block> for Runtime { fn get_sum() -> u32 { SumStorage::get_sum() } } } }
You may be wondering about the Block
type parameter which is present here, but not in our
definition. This type parameter is added by the macros along with a few other features. All runtime
APIs have this type parameter to facilitate querying the runtime at arbitrary blocks. Read more
about this in the docs for
impl_runtime_apis!
.
Calling the Runtime API
We've now successfully added a runtime API to our runtime. The outer node can now call this API to query the runtime for the sum of two storage values. Given a reference to a 'client' we can make the call like this.
#![allow(unused)] fn main() { let sum_at_block_fifty = client.runtime_api().get_sum(&50); }
This recipe was about defining and implementing a custom runtime API. To see an example of calling this API in practice, see the recipe on custom RPCs, where we connect this runtime API to an RPC that can be called by an end user.