Skip to main content

Rust agent

Intermediate
Agents

Overview

The Rust agent (ic-agent) by DFINITY is a simple library that enables you to build applications and interact with ICP, serving as a low-level Rust backend for the IC SDK.

The agent is designed to be compatible with multiple versions of the replica API, exposing both low-level APIs for communicating with components like the replica and higher-level APIs for communicating with software applications deployed as canisters.

One example of a project that uses the ic-agent is dfx.

Adding the agent as a dependency

To add the ic-agent crate as a dependency in your project, use the command:

cargo add ic-agent

Initializing the agent

Before using the agent in your project, it must be initialized using the Agent::builder() function. Here is an example of how to initialize the Rust agent:

use anyhow::Result;
use ic_agent::Agent;

pub async fn create_agent(url: &str, is_mainnet: bool) -> Result<Agent> {
    let agent = Agent::builder().with_url(url).build()?;
    if !is_mainnet {
        agent.fetch_root_key().await?;
    }
    Ok(agent)
}

Authentication

The Rust agent's Identity object provides signatures that can be used for HTTP requests or identity delegations. It represents the principal ID of the sender.

Identity represents a single identity and cannot contain multiple identity values.

async fn create_a_canister() -> Result<Principal, Box<dyn std::error::Error>> {
  let agent = Agent::builder()
    .with_url(URL)
    .with_identity(create_identity())
    .build()?;

Identities can have different types, such as:

  • AnonymousIdentity: A unit type used through with_identity(AnonymousIdentity).

  • BasicIdentity, Secp256k1Identity, and Prime256v1Identity: Created from pre-existing keys through either function:     - BasicIdentity::from_pem_file("path/to/identity.pem")    -BasicIdentity::from_key_pair(key_material)`     There are minor variations in the function name.

  • ic-identity-hsm crate: Used for hardware security modules (HSM) like Yubikey or Nitrokey through HardwareIdentity::new(pkcs11_module_path, slot_index, key_id, || get_pin()).

Making calls

The Rust agent can be used to make calls to other canisters. To make a call to a canister, use the agent.update method for an update call or agent.query for a query call. Then, pass in the canister's ID and the method of the canister you'd like to call.

The following example calls the ICP ledger and returns an account balance:

let icp_ledger = Principal::from_text("ryjl3-tyaaa-aaaaa-aaaba-cai").unwrap();
let response = agent.update(&icp_ledger, "account_balance")
    .with_arg(Encode!(&AccountBalanceArgs { account: some_account_id })?)
    .call_and_wait()
    .await?;
let tokens = Decode!(&response, Tokens)?;

ic-utils

The ic-utils crate provides a high level interface that is designed to be canister-oriented and aware of the canister's Candid interface. Canister objects with ic-utils resemble the following:

let canister = Canister::builder()
    .with_agent(&agent)
    .with_canister_id(principal)
    .build()?;
canister.query("account_balance")
    .with_arg(&AccountBalanceArg { user_id })
    .build()
    .await

ic-utils provides several interfaces, including the management canister, cycles wallet canister, and Bitcoin integration canister. For example, the management canister can be used with a call such as ManagementCanister::create(&agent).canister_status(&canister_id).await.

Learn more in the ic-utils documentation.

Response verification

When using the certified queries feature, the agent must verify that the certificate returned with the query response is valid. A certificate consists of a tree, a signature on the tree's root hash that is valid under a public key, and an optional delegation linking the public key to the root public key. To validate the root hash, the agent uses the HashTree module.

Below is an example annotated with notes that explains how to verify responses using the HashTree module:

// Define a function that initializes the agent and builds a certified query call to get the XDR and ICP conversion rate from the cycles minter canister:
pub async fn xdr_permyriad_per_icp(agent: &Agent) -> DfxResult<u64> {
    let canister = Canister::builder()
        .with_agent(agent)
        .with_canister_id(MAINNET_CYCLE_MINTER_CANISTER_ID)
        .build()?;
    let (certified_rate,): (IcpXdrConversionRateCertifiedResponse,) = canister
        .query("get_icp_xdr_conversion_rate")
        .build()
        .call()
        .await?;
    // Check the certificate with a query call
    let cert = serde_cbor::from_slice(&certified_rate.certificate)?;
    agent
        .verify(&cert, MAINNET_CYCLE_MINTER_CANISTER_ID)
        .context(
            "The origin of the certificate for the XDR <> ICP exchange rate could not be verified",
        )?;
    // Verify that the certificate can be trusted:
    let witness = lookup_value(
        &cert,
        [
            b"canister",
            MAINNET_CYCLE_MINTER_CANISTER_ID.as_slice(),
            b"certified_data",
        ],
    )
    .context("The IC's certificate for the XDR <> ICP exchange rate could not be verified")?;
    // Call the HashTree for the certified_rate call:
    let tree = serde_cbor::from_slice::<HashTree<Vec<u8>>>(&certified_rate.hash_tree)?;
    ensure!(
        tree.digest() == witness,
        "The CMC's certificate for the XDR <> ICP exchange rate did not match the IC's certificate"
    );
    // Verify that the HashTree can be trusted:
    let lookup = tree.lookup_path([b"ICP_XDR_CONVERSION_RATE"]);
    let certified_data = if let LookupResult::Found(content) = lookup {
        content
    } else {
        bail!("The CMC's certificate did not contain the XDR <> ICP exchange rate");
    };
    let encoded_data = Encode!(&certified_rate.data)?;
    ensure!(
        certified_data == encoded_data,
        "The CMC's certificate for the XDR <> ICP exchange rate did not match the provided rate"
    );
    // If the above checks are successful, you can trust the exchange rate that has been returned:
    Ok(certified_rate.data.xdr_permyriad_per_icp)
}

Another application of HashTree can be found in the ic-certified-assets code.

The ic-asset canister is written in Rust and uses agent-rs to provide a client API that can be used to list assets, return asset properties, and upload assets to the canister. API endpoints for this canister include:

  • api_version: Returns the current API version.

  • commit_batch: Used to commit batch operations.

  • compute_evidence: Compute the hash evidence over the batch operations required to update the assets.

  • create_batch: Create a batch operation.

  • create_chunk: Create a chunk operation to be part of a batch.

  • get_asset_properties: Return an asset's properties.

  • list: List current assets.

  • propose_commit_batch: Propose a batch operation to be committed.

For example, the list endpoint uses the following code to list assets using the agent:

use crate::canister_api::methods::method_names::LIST;
use crate::canister_api::types::{asset::AssetDetails, list::ListAssetsRequest};
use ic_agent::AgentError;
use ic_utils::call::SyncCall;
use ic_utils::Canister;
use std::collections::HashMap;

pub(crate) async fn list_assets(
    canister: &Canister<'_>,
) -> Result<HashMap<String, AssetDetails>, AgentError> {
    let (entries,): (Vec<AssetDetails>,) = canister
        .query(LIST)
        .with_arg(ListAssetsRequest {})
        .build()
        .call()
        .await?;

    let assets: HashMap<_, _> = entries.into_iter().map(|d| (d.key.clone(), d)).collect();

    Ok(assets)
}

More information about the ic-asset canister can be found in the source code.

Example

The following is an example of how to use the agent interface to make a call to a canister (in this example, the ICP ledger) deployed on the mainnet:

use ic_agent::{Agent, export::Principal};
use candid::{Encode, Decode, CandidType, Nat};
use serde::Deserialize;

#[derive(CandidType)]
struct AccountBalanceArgs {
    account: Vec<u8>,
}
#[derive(CandidType, Deserialize)]
struct Tokens {
    e8s: u64,
}
let icp_ledger = Principal::from_text("ryjl3-tyaaa-aaaaa-aaaba-cai").unwrap();
let response = agent.update(&icp_ledger, "account_balance")
    .with_arg(Encode!(&AccountBalanceArgs { account: some_account_id })?)
    .call_and_wait()
    .await?;
let tokens = Decode!(&response, Tokens)?;

Resources