Example Domain Code Walkthrough
After having played with the example domain, we will now look at its Rust code and understand what it does. This serves as first step of building your own domain application.
Get the Code
You can clone the code of the example domain to follow along and later, start changing it to build your own domain.
Template github repository coming soon
Walkthrough
We will walk through the code in the same order as it's being executed, starting
at the main() function in src/main.rs. The application starts by initializing
structured logging, then delegates to run(). If anything fails, the error is
printed to stderr for a clean user-facing message and the process exits with
code 1.
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
if let Err(e) = run().await {
eprintln!("{e}");
std::process::exit(1);
}
}
The CLI arguments are parsed and the configuration file is loaded from the specified path (defaulting to domain.yaml). Then, a Domain is built from the configuration, which provides a runner, client, and views.
async fn run() -> Result<(), Box<dyn std::error::Error>> {
let CliArgs { config, api_port } = CliArgs::parse();
let config = Config::load_from(config)?;
let shard = config.shard;
let Domain {
runner,
client,
views,
} = Domain::from_config(config).build().await?;
Before starting, the domain ensures a valid domain agreement exists. If one doesn't exist, it will be submitted automatically. The call will wait up to the specified timeout for the agreement to be confirmed.
client
.ensure_domain_agreement(
MIN_NEW_SHARD_FEE,
Duration::from_secs(10),
DOMAIN_AGREEMENT_TIMEOUT,
)
.await?;
The domain runner is started in the background, which begins handling events.
runner.run_in_background().await?;
An HTTP server is configured with three routes. The client and views are wrapped in web::Data for shared access across request handlers.
let client = web::Data::new(client);
let views = web::Data::new(views);
let server = HttpServer::new(move || {
App::new()
.app_data(client.clone())
.app_data(views.clone())
.route("/vaults/{owner_id}", web::get().to(endpoints::get_vault))
.route(
"/vaults/{owner_id}/nonce",
web::get().to(endpoints::get_nonce),
)
.route(
"/verifiables",
web::post().to(endpoints::post_signed_verifiables),
)
})
The server binds to all interfaces on the specified port from --api-port (or an OS-assigned port if none given). Signal handling is disabled since the runner manages shutdown.
.disable_signals()
.bind((Ipv6Addr::UNSPECIFIED, api_port.unwrap_or_default()))?;
Finally, the HTTP server is run until shutdown.
server.run().await?;
Ok(())
}
The three registered routes are handled by the following endpoint functions.
GET /vaults/{owner_id} looks up the vault for the given owner and returns it as JSON. Returns 404 if the vault doesn't exist, or 500 on error.
pub(crate) async fn get_vault(
owner_id: web::Path<OwnerId>,
views: web::Data<Views>,
) -> HttpResponse {
match views.domain_view().get_vault(&owner_id.into_inner()) {
Ok(Some(vault)) => HttpResponse::Ok().json(vault),
Ok(None) => HttpResponse::NotFound().finish(),
Err(_) => HttpResponse::InternalServerError().finish(),
}
}
GET /vaults/{owner_id}/nonce returns the next expected nonce for the vault, which clients need for replay protection when submitting verifiables.
pub(crate) async fn get_nonce(
owner_id: web::Path<OwnerId>,
views: web::Data<Views>,
) -> actix_web::Result<web::Json<Nonce>> {
let nonce = views
.domain_view()
.next_nonce(&owner_id.into_inner())
.map_err(ErrorInternalServerError)?;
Ok(web::Json(nonce))
}
POST /verifiables accepts a batch of signed verifiables, executes them to
compute state diffs, and applies the results to the domain state.
pub(crate) async fn post_signed_verifiables(
request: web::Json<Vec<VerifiableType>>,
client: web::Data<DomainClient>,
) -> actix_web::Result<()> {
let verifiables_with_diffs = default_execute(request.into_inner());
client
.apply(verifiables_with_diffs)
.await
.map_err(ErrorInternalServerError)
}
The admin API that provides endpoints to submit and prove SDLs to the base layer
is directly exposed from the domain SDK, there's no visible code for it except
the admin-api feature in Cargo.toml.