Skip to main content

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.

info

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.