Rust agent
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 throughwith_identity(AnonymousIdentity)
.BasicIdentity
,Secp256k1Identity
, andPrime256v1Identity: 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 throughHardwareIdentity::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)?;