In the Module System, we provide handy macros to make it easy to generate RPC server implementations. In this document, we'll walk you through all of the steps that you need to take to enable RPC if you're implementing your rollup from scratch.
There are 5 steps that need to be completed to enable RPC on the full node:
- Annotate your modules with
rpc_gen
andrpc_method
. - Annotate your
native
Runtime
with theexpose_rpc
macro. - Import and call
get_rpc_methods
in your full node implementation. - Configure and start your RPC server in your full node implementation.
To add an RPC method to a module, simply annotate the desired impl
block with the rpc_gen
macro and tag each
method you want to expose with the rpc_method
annotation. As noted in its rustdoc
s, the rpc_gen
macro
has identical syntax to jsonrpsee::rpc
except that the method
annotation has been renamed to rpc_method
to clarify its purpose.
// This code goes in your module's query.rs file
use sov_modules_api::macros::rpc_gen;
#[rpc_gen(client, server, namespace = "bank")]
impl<C: Context> Bank<C> {
#[rpc_method(name = "balanceOf")]
pub(crate) fn balance_of(
&self,
user_address: C::Address,
token_address: C::Address,
working_set: &mut WorkingSet<C>,
) -> RpcResult<BalanceResponse> {
...
}
#[rpc_method(name = "supplyOf")]
pub(crate) fn supply_of(
&self,
token_address: C::Address,
working_set: &mut WorkingSet<C>,
) -> RpcResult<TotalSupplyResponse> {
...
}
}
This example code will generate an RPC module which can process the bank_balanceOf
and bank_supplyOf
queries.
Under the hood rpc_gen
and rpc_method
create two traits - one called <module_name>RpcImpl and one called <module_name>RpcServer.
It's important to note that the _RpcImpl and _RpcServer traits do not need to be implemented - this is done automatically by the SDK.
However, they do need to be imported to the file where the expose_rpc
macro is called.
The next layer of abstraction where we need to think about RPC is the Runtime
. Just because a module defines
some RPC methods doesn't necessarily mean that we want to use them. So, when we're building a Runtime
, we have
to enable RPC servers of the modules.
// This code goes in your state transition function crate. For example demo-stf/runtime.rs
use sov_bank::{BankRpcImpl, BankRpcServer};
#[cfg_attr(
feature = "native",
expose_rpc(DefaultContext)
)]
#[derive(Genesis, DispatchCall, MessageCodec, DefaultRuntime)]
#[serialization(borsh::BorshDeserialize, borsh::BorshSerialize)]
pub struct Runtime<C: Context> {
pub bank: sov_bank::Bank<C>,
...
}
Note thatexpose_rpc
takes a tuple as argument, each element of the tuple is a concrete Context.
Now that we've implemented all of the necessary traits, a get_rpc_methods
function will be auto-generated.
To use it, simply import it from your state transition function. Given access to Storage
, this function instantiates
jsonrpsee::Methods
which your full node can
execute.
// This code goes in your full node implementation. For example demo-rollup/main.rs
use demo_stf::runtime::get_rpc_methods;
#[tokio::main]
fn main() {
// ...
let mut app = App...;
let storage = app.get_storage();
let methods = get_rpc_methods(storage);
// ...
}
The last step is simply binding our generated jsonrpsee::Methods
to a port:
async fn start_rpc_server(methods: RpcModule<()>, address: SocketAddr) {
let server = jsonrpsee::server::ServerBuilder::default()
.build([address].as_ref())
.await
.unwrap();
let _server_handle = server.start(methods).unwrap();
futures::future::pending::<()>().await;
}
#[tokio::main]
fn main() {
// ...
let mut demo_runner = App...;
let storage = demo_runner.get_storage();
let methods = get_rpc_methods(storage);
let _handle = tokio::spawn(async move {
start_rpc_server(methods, address).await;
});
}
- We use
working_set: &mut WorkingSet<C>
in order to query state.WorkingSet
has a functionworking_set.set_archival_version(v)
where v is of typeu64
and represents the block height. - Once the
set_archival_version
is called, the working_set is configured to query against the state at heightv
. - To modify an RPC query of the form
pub fn balance_of(
&self,
user_address: C::Address,
token_address: C::Address,
working_set: &mut WorkingSet<C>,
) -> RpcResult<BalanceResponse> {
Ok(BalanceResponse {
amount: self.get_balance_of(user_address, token_address, working_set),
})
}
We need to make the following changes
pub fn balance_of(
&self,
version: Option<u64>,
user_address: C::Address,
token_address: C::Address,
working_set: &mut WorkingSet<C>,
) -> RpcResult<BalanceResponse> {
if let Some(v) = version {
working_set.set_archival_version(v)
}
Ok(BalanceResponse {
amount: self.get_balance_of(user_address, token_address, working_set),
})
}
- NOTE:
set_archival_version
handles configuringWorkingSet
for both JMT state as well as accessory state