Skip to main content

ic_agent/agent/
mod.rs

1//! The main Agent module. Contains the [Agent] type and all associated structures.
2pub(crate) mod agent_config;
3pub mod agent_error;
4pub(crate) mod builder;
5// delete this module after 0.40
6#[doc(hidden)]
7#[deprecated(since = "0.38.0", note = "use the AgentBuilder methods")]
8pub mod http_transport;
9pub(crate) mod nonce;
10pub(crate) mod response_authentication;
11pub mod route_provider;
12pub mod status;
13pub mod subnet;
14
15pub use agent_config::AgentConfig;
16pub use agent_error::AgentError;
17use agent_error::{HttpErrorPayload, Operation};
18use async_lock::Semaphore;
19use async_trait::async_trait;
20pub use builder::AgentBuilder;
21use bytes::Bytes;
22use cached::{Cached, TimedCache};
23use http::{header::CONTENT_TYPE, HeaderMap, Method, StatusCode, Uri};
24use ic_ed25519::{PublicKey, SignatureError};
25#[doc(inline)]
26pub use ic_transport_types::{
27    signed, CallResponse, Envelope, EnvelopeContent, RejectCode, RejectResponse, ReplyResponse,
28    RequestStatusResponse,
29};
30pub use nonce::{NonceFactory, NonceGenerator};
31use rangemap::RangeInclusiveMap;
32use reqwest::{Client, Request, Response};
33use route_provider::{
34    dynamic_routing::{dynamic_route_provider::DynamicRouteProviderBuilder, node::Node},
35    RouteProvider, UrlUntilReady,
36};
37pub use subnet::{Subnet, SubnetType};
38use time::OffsetDateTime;
39use tower_service::Service;
40
41#[cfg(test)]
42mod agent_test;
43
44use crate::{
45    agent::response_authentication::{
46        extract_der, lookup_canister_info, lookup_canister_metadata, lookup_canister_ranges,
47        lookup_incomplete_subnet, lookup_request_status, lookup_subnet_and_ranges,
48        lookup_subnet_canister_ranges, lookup_subnet_metrics, lookup_time, lookup_tree,
49        lookup_value,
50    },
51    agent_error::TransportError,
52    export::Principal,
53    identity::Identity,
54    to_request_id, RequestId,
55};
56use backon::{BackoffBuilder, ExponentialBackoff, ExponentialBuilder};
57use ic_certification::{Certificate, Delegation, Label};
58use ic_transport_types::{
59    signed::{SignedQuery, SignedRequestStatus, SignedUpdate},
60    QueryResponse, ReadStateResponse, SubnetMetrics, TransportCallResponse,
61};
62use serde::Serialize;
63use status::Status;
64use std::{
65    borrow::Cow,
66    convert::TryFrom,
67    fmt::{self, Debug},
68    future::{Future, IntoFuture},
69    pin::Pin,
70    str::FromStr,
71    sync::{Arc, Mutex, RwLock},
72    task::{Context, Poll},
73    time::Duration,
74};
75
76use crate::agent::response_authentication::lookup_api_boundary_nodes;
77
78/// The effective ID that routes a request to a particular endpoint shape.
79///
80/// Most calls target a canister and are routed via an
81/// [effective canister id](https://internetcomputer.org/docs/current/references/ic-interface-spec#http-effective-canister-id).
82/// A small set of management-canister calls (currently `create_canister`,
83/// `provisional_create_canister_with_cycles`, and `list_canisters`) are instead
84/// routed via an [effective subnet id](https://internetcomputer.org/docs/current/references/ic-interface-spec#http-effective-subnet-id),
85/// targeting the subnet-scoped HTTP endpoints (`/api/v4/subnet/<id>/call`,
86/// `/api/v3/subnet/<id>/read_state`, `/api/v3/subnet/<id>/query`).
87///
88/// `Principal` converts into `EffectiveId::Canister(_)`, so passing a bare
89/// `Principal` to APIs that accept `impl Into<EffectiveId>` preserves the
90/// canister-scoped behavior of earlier `ic-agent` releases.
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
92pub enum EffectiveId {
93    /// An effective canister id. The request is routed to the canister-scoped HTTP endpoints.
94    Canister(Principal),
95    /// An effective subnet id. The request is routed to the subnet-scoped HTTP endpoints.
96    Subnet(Principal),
97}
98
99impl EffectiveId {
100    /// Returns the underlying principal regardless of variant.
101    pub fn as_principal(&self) -> Principal {
102        match self {
103            EffectiveId::Canister(p) | EffectiveId::Subnet(p) => *p,
104        }
105    }
106}
107
108impl From<Principal> for EffectiveId {
109    fn from(p: Principal) -> Self {
110        EffectiveId::Canister(p)
111    }
112}
113
114pub(crate) const IC_STATE_ROOT_DOMAIN_SEPARATOR: &[u8; 14] = b"\x0Dic-state-root";
115
116pub(crate) const IC_ROOT_KEY: &[u8; 133] = b"\x30\x81\x82\x30\x1d\x06\x0d\x2b\x06\x01\x04\x01\x82\xdc\x7c\x05\x03\x01\x02\x01\x06\x0c\x2b\x06\x01\x04\x01\x82\xdc\x7c\x05\x03\x02\x01\x03\x61\x00\x81\x4c\x0e\x6e\xc7\x1f\xab\x58\x3b\x08\xbd\x81\x37\x3c\x25\x5c\x3c\x37\x1b\x2e\x84\x86\x3c\x98\xa4\xf1\xe0\x8b\x74\x23\x5d\x14\xfb\x5d\x9c\x0c\xd5\x46\xd9\x68\x5f\x91\x3a\x0c\x0b\x2c\xc5\x34\x15\x83\xbf\x4b\x43\x92\xe4\x67\xdb\x96\xd6\x5b\x9b\xb4\xcb\x71\x71\x12\xf8\x47\x2e\x0d\x5a\x4d\x14\x50\x5f\xfd\x74\x84\xb0\x12\x91\x09\x1c\x5f\x87\xb9\x88\x83\x46\x3f\x98\x09\x1a\x0b\xaa\xae";
117
118#[cfg(not(target_family = "wasm"))]
119type AgentFuture<'a, V> = Pin<Box<dyn Future<Output = Result<V, AgentError>> + Send + 'a>>;
120
121#[cfg(target_family = "wasm")]
122type AgentFuture<'a, V> = Pin<Box<dyn Future<Output = Result<V, AgentError>> + 'a>>;
123
124/// A low level Agent to make calls to a Replica endpoint.
125///
126#[cfg_attr(unix, doc = " ```rust")] // pocket-ic
127#[cfg_attr(not(unix), doc = " ```ignore")]
128/// use ic_agent::{Agent, export::Principal};
129/// use candid::{Encode, Decode, CandidType, Nat};
130/// use serde::Deserialize;
131///
132/// #[derive(CandidType)]
133/// struct Argument {
134///   amount: Option<Nat>,
135/// }
136///
137/// #[derive(CandidType, Deserialize)]
138/// struct CreateCanisterResult {
139///   canister_id: Principal,
140/// }
141///
142/// # fn create_identity() -> impl ic_agent::Identity {
143/// #     // In real code, the raw key should be either read from a pem file or generated with randomness.
144/// #     ic_agent::identity::BasicIdentity::from_raw_key(&[0u8;32])
145/// # }
146/// #
147/// async fn create_a_canister() -> Result<Principal, Box<dyn std::error::Error>> {
148/// # Ok(ref_tests::utils::with_pic(async move |pic| {
149/// # let url = ref_tests::utils::get_pic_url(&pic);
150///   let agent = Agent::builder()
151///     .with_url(url)
152///     .with_identity(create_identity())
153///     .build()?;
154///
155///   // Only do the following call when not contacting the IC main net (e.g. a local emulator).
156///   // This is important as the main net public key is static and a rogue network could return
157///   // a different key.
158///   // If you know the root key ahead of time, you can use `agent.set_root_key(root_key);`.
159///   agent.fetch_root_key().await?;
160///   let management_canister_id = Principal::from_text("aaaaa-aa")?;
161///
162///   // Create a call to the management canister to create a new canister ID, and wait for a result.
163///   // This API only works in local instances; mainnet instances must use the cycles ledger.
164///   // See `dfx info default-effective-canister-id`.
165/// # let effective_canister_id = ref_tests::utils::get_effective_canister_id(&pic).await;
166///   let response = agent.update(&management_canister_id, "provisional_create_canister_with_cycles")
167///     .with_effective_canister_id(effective_canister_id)
168///     .with_arg(Encode!(&Argument { amount: None })?)
169///     .await?;
170///
171///   let result = Decode!(response.as_slice(), CreateCanisterResult)?;
172///   let canister_id: Principal = Principal::from_text(&result.canister_id.to_text())?;
173///   Ok(canister_id)
174/// # }).await)
175/// }
176///
177/// # let mut runtime = tokio::runtime::Runtime::new().unwrap();
178/// # runtime.block_on(async {
179/// let canister_id = create_a_canister().await.unwrap();
180/// eprintln!("{}", canister_id);
181/// # });
182/// ```
183///
184/// This agent does not understand Candid, and only acts on byte buffers.
185///
186/// Some methods return certificates. While there is a `verify_certificate` method, any certificate
187/// you receive from a method has already been verified and you do not need to manually verify it.
188#[derive(Clone)]
189pub struct Agent {
190    nonce_factory: Arc<dyn NonceGenerator>,
191    identity: Arc<dyn Identity>,
192    ingress_expiry: Duration,
193    root_key: Arc<RwLock<Vec<u8>>>,
194    client: Arc<dyn HttpService>,
195    route_provider: Arc<dyn RouteProvider>,
196    subnet_key_cache: Arc<Mutex<SubnetCache>>,
197    concurrent_requests_semaphore: Arc<Semaphore>,
198    verify_query_signatures: bool,
199    max_response_body_size: Option<usize>,
200    max_polling_time: Duration,
201    #[allow(dead_code)]
202    max_tcp_error_retries: usize,
203}
204
205impl fmt::Debug for Agent {
206    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
207        f.debug_struct("Agent")
208            .field("ingress_expiry", &self.ingress_expiry)
209            .finish_non_exhaustive()
210    }
211}
212
213impl Agent {
214    /// Create an instance of an [`AgentBuilder`] for building an [`Agent`]. This is simpler than
215    /// using the [`AgentConfig`] and [`Agent::new()`].
216    pub fn builder() -> builder::AgentBuilder {
217        Default::default()
218    }
219
220    /// Create an instance of an [`Agent`].
221    pub fn new(config: agent_config::AgentConfig) -> Result<Agent, AgentError> {
222        let client = config.http_service.unwrap_or_else(|| {
223            Arc::new(Retry429Logic {
224                client: config.client.unwrap_or_else(|| {
225                    #[cfg(not(target_family = "wasm"))]
226                    {
227                        Client::builder()
228                            .use_rustls_tls()
229                            .timeout(Duration::from_secs(360))
230                            .build()
231                            .expect("Could not create HTTP client.")
232                    }
233                    #[cfg(all(target_family = "wasm", feature = "wasm-bindgen"))]
234                    {
235                        Client::new()
236                    }
237                }),
238            })
239        });
240        Ok(Agent {
241            nonce_factory: config.nonce_factory,
242            identity: config.identity,
243            ingress_expiry: config.ingress_expiry,
244            root_key: Arc::new(RwLock::new(IC_ROOT_KEY.to_vec())),
245            client: client.clone(),
246            route_provider: if let Some(route_provider) = config.route_provider {
247                route_provider
248            } else if let Some(url) = config.url {
249                if config.background_dynamic_routing {
250                    assert!(
251                        url.scheme() == "https" && url.path() == "/" && url.port().is_none() && url.domain().is_some(),
252                        "in dynamic routing mode, URL must be in the exact form https://domain with no path, port, IP, or non-HTTPS scheme"
253                    );
254                    let seeds = vec![Node::new(url.domain().unwrap()).unwrap()];
255                    UrlUntilReady::new(url, async move {
256                        let provider =
257                            DynamicRouteProviderBuilder::new(seeds, client, None).build();
258                        provider.start().await;
259                        provider
260                    }) as Arc<dyn RouteProvider>
261                } else {
262                    Arc::new(url)
263                }
264            } else {
265                panic!("either route_provider or url must be specified");
266            },
267            subnet_key_cache: Arc::new(Mutex::new(SubnetCache::new())),
268            verify_query_signatures: config.verify_query_signatures,
269            concurrent_requests_semaphore: Arc::new(Semaphore::new(config.max_concurrent_requests)),
270            max_response_body_size: config.max_response_body_size,
271            max_tcp_error_retries: config.max_tcp_error_retries,
272            max_polling_time: config.max_polling_time,
273        })
274    }
275
276    /// Set the identity provider for signing messages.
277    ///
278    /// NOTE: if you change the identity while having update calls in
279    /// flight, you will not be able to [`Agent::request_status_raw`] the status of these
280    /// messages.
281    pub fn set_identity<I>(&mut self, identity: I)
282    where
283        I: 'static + Identity,
284    {
285        self.identity = Arc::new(identity);
286    }
287    /// Set the arc identity provider for signing messages.
288    ///
289    /// NOTE: if you change the identity while having update calls in
290    /// flight, you will not be able to [`Agent::request_status_raw`] the status of these
291    /// messages.
292    pub fn set_arc_identity(&mut self, identity: Arc<dyn Identity>) {
293        self.identity = identity;
294    }
295
296    /// By default, the agent is configured to talk to the main Internet Computer, and verifies
297    /// responses using a hard-coded public key.
298    ///
299    /// This function will instruct the agent to ask the endpoint for its public key, and use
300    /// that instead. This is required when talking to a local test instance, for example.
301    ///
302    /// *Only use this when you are  _not_ talking to the main Internet Computer, otherwise
303    /// you are prone to man-in-the-middle attacks! Do not call this function by default.*
304    pub async fn fetch_root_key(&self) -> Result<(), AgentError> {
305        if self.read_root_key()[..] != IC_ROOT_KEY[..] {
306            // already fetched the root key
307            return Ok(());
308        }
309        let status = self.status().await?;
310        let Some(root_key) = status.root_key else {
311            return Err(AgentError::NoRootKeyInStatus(status));
312        };
313        self.set_root_key(root_key);
314        Ok(())
315    }
316
317    /// By default, the agent is configured to talk to the main Internet Computer, and verifies
318    /// responses using a hard-coded public key.
319    ///
320    /// Using this function you can set the root key to a known one if you know if beforehand.
321    pub fn set_root_key(&self, root_key: Vec<u8>) {
322        *self.root_key.write().unwrap() = root_key;
323    }
324
325    /// Return the root key currently in use.
326    pub fn read_root_key(&self) -> Vec<u8> {
327        self.root_key.read().unwrap().clone()
328    }
329
330    fn get_expiry_date(&self) -> u64 {
331        let expiry_raw = OffsetDateTime::now_utc() + self.ingress_expiry;
332        let mut rounded = expiry_raw.replace_nanosecond(0).unwrap();
333        if self.ingress_expiry.as_secs() > 90 {
334            rounded = rounded.replace_second(0).unwrap();
335        }
336        rounded.unix_timestamp_nanos().try_into().unwrap()
337    }
338
339    /// Return the principal of the identity.
340    pub fn get_principal(&self) -> Result<Principal, String> {
341        self.identity.sender()
342    }
343
344    async fn query_endpoint<A>(
345        &self,
346        effective_canister_id: Principal,
347        serialized_bytes: Vec<u8>,
348    ) -> Result<A, AgentError>
349    where
350        A: serde::de::DeserializeOwned,
351    {
352        let _permit = self.concurrent_requests_semaphore.acquire().await;
353        let bytes = self
354            .execute(
355                Method::POST,
356                &format!("api/v3/canister/{}/query", effective_canister_id.to_text()),
357                Some(serialized_bytes),
358            )
359            .await?
360            .1;
361        serde_cbor::from_slice(&bytes).map_err(AgentError::InvalidCborData)
362    }
363
364    async fn read_state_endpoint<A>(
365        &self,
366        effective_canister_id: Principal,
367        serialized_bytes: Vec<u8>,
368    ) -> Result<A, AgentError>
369    where
370        A: serde::de::DeserializeOwned,
371    {
372        let _permit = self.concurrent_requests_semaphore.acquire().await;
373        let endpoint = format!(
374            "api/v3/canister/{}/read_state",
375            effective_canister_id.to_text()
376        );
377        let bytes = self
378            .execute(Method::POST, &endpoint, Some(serialized_bytes))
379            .await?
380            .1;
381        serde_cbor::from_slice(&bytes).map_err(AgentError::InvalidCborData)
382    }
383
384    async fn read_subnet_state_endpoint<A>(
385        &self,
386        subnet_id: Principal,
387        serialized_bytes: Vec<u8>,
388    ) -> Result<A, AgentError>
389    where
390        A: serde::de::DeserializeOwned,
391    {
392        let _permit = self.concurrent_requests_semaphore.acquire().await;
393        let endpoint = format!("api/v3/subnet/{}/read_state", subnet_id.to_text());
394        let bytes = self
395            .execute(Method::POST, &endpoint, Some(serialized_bytes))
396            .await?
397            .1;
398        serde_cbor::from_slice(&bytes).map_err(AgentError::InvalidCborData)
399    }
400
401    async fn query_subnet_endpoint<A>(
402        &self,
403        subnet_id: Principal,
404        serialized_bytes: Vec<u8>,
405    ) -> Result<A, AgentError>
406    where
407        A: serde::de::DeserializeOwned,
408    {
409        let _permit = self.concurrent_requests_semaphore.acquire().await;
410        let endpoint = format!("api/v3/subnet/{}/query", subnet_id.to_text());
411        let bytes = self
412            .execute(Method::POST, &endpoint, Some(serialized_bytes))
413            .await?
414            .1;
415        serde_cbor::from_slice(&bytes).map_err(AgentError::InvalidCborData)
416    }
417
418    async fn query_endpoint_for<A>(
419        &self,
420        effective_id: EffectiveId,
421        serialized_bytes: Vec<u8>,
422    ) -> Result<A, AgentError>
423    where
424        A: serde::de::DeserializeOwned,
425    {
426        match effective_id {
427            EffectiveId::Canister(p) => self.query_endpoint(p, serialized_bytes).await,
428            EffectiveId::Subnet(p) => self.query_subnet_endpoint(p, serialized_bytes).await,
429        }
430    }
431
432    async fn read_state_endpoint_for<A>(
433        &self,
434        effective_id: EffectiveId,
435        serialized_bytes: Vec<u8>,
436    ) -> Result<A, AgentError>
437    where
438        A: serde::de::DeserializeOwned,
439    {
440        match effective_id {
441            EffectiveId::Canister(p) => self.read_state_endpoint(p, serialized_bytes).await,
442            EffectiveId::Subnet(p) => self.read_subnet_state_endpoint(p, serialized_bytes).await,
443        }
444    }
445
446    async fn call_endpoint_for(
447        &self,
448        effective_id: EffectiveId,
449        serialized_bytes: Vec<u8>,
450    ) -> Result<TransportCallResponse, AgentError> {
451        match effective_id {
452            EffectiveId::Canister(p) => self.call_endpoint(p, serialized_bytes).await,
453            EffectiveId::Subnet(p) => self.call_subnet_endpoint(p, serialized_bytes).await,
454        }
455    }
456
457    async fn get_subnet_for(&self, effective_id: EffectiveId) -> Result<Arc<Subnet>, AgentError> {
458        match effective_id {
459            EffectiveId::Canister(p) => self.get_subnet_by_canister(&p).await,
460            EffectiveId::Subnet(p) => self.get_subnet_by_id(&p).await,
461        }
462    }
463
464    async fn fetch_subnet_for(&self, effective_id: EffectiveId) -> Result<Arc<Subnet>, AgentError> {
465        match effective_id {
466            EffectiveId::Canister(p) => self.fetch_subnet_by_canister(&p).await,
467            EffectiveId::Subnet(p) => self.fetch_subnet_by_id(&p).await,
468        }
469    }
470
471    async fn call_endpoint(
472        &self,
473        effective_canister_id: Principal,
474        serialized_bytes: Vec<u8>,
475    ) -> Result<TransportCallResponse, AgentError> {
476        let _permit = self.concurrent_requests_semaphore.acquire().await;
477        let endpoint = format!("api/v4/canister/{}/call", effective_canister_id.to_text());
478        let (status_code, response_body) = self
479            .execute(Method::POST, &endpoint, Some(serialized_bytes))
480            .await?;
481
482        if status_code == StatusCode::ACCEPTED {
483            return Ok(TransportCallResponse::Accepted);
484        }
485
486        serde_cbor::from_slice(&response_body).map_err(AgentError::InvalidCborData)
487    }
488
489    async fn call_subnet_endpoint(
490        &self,
491        subnet_id: Principal,
492        serialized_bytes: Vec<u8>,
493    ) -> Result<TransportCallResponse, AgentError> {
494        let _permit = self.concurrent_requests_semaphore.acquire().await;
495        let endpoint = format!("api/v4/subnet/{}/call", subnet_id.to_text());
496        let (status_code, response_body) = self
497            .execute(Method::POST, &endpoint, Some(serialized_bytes))
498            .await?;
499
500        if status_code == StatusCode::ACCEPTED {
501            return Ok(TransportCallResponse::Accepted);
502        }
503
504        serde_cbor::from_slice(&response_body).map_err(AgentError::InvalidCborData)
505    }
506
507    /// The simplest way to do a query call; sends a byte array and will return a byte vector.
508    /// The encoding is left as an exercise to the user.
509    #[allow(clippy::too_many_arguments)]
510    async fn query_raw(
511        &self,
512        canister_id: Principal,
513        effective_canister_id: Principal,
514        method_name: String,
515        arg: Vec<u8>,
516        ingress_expiry_datetime: Option<u64>,
517        use_nonce: bool,
518        explicit_verify_query_signatures: Option<bool>,
519    ) -> Result<Vec<u8>, AgentError> {
520        let operation = Operation::Call {
521            canister: canister_id,
522            method: method_name.clone(),
523        };
524        let content = self.query_content(
525            canister_id,
526            method_name,
527            arg,
528            ingress_expiry_datetime,
529            use_nonce,
530        )?;
531        let serialized_bytes = sign_envelope(&content, self.identity.clone())?;
532        self.query_inner(
533            EffectiveId::Canister(effective_canister_id),
534            serialized_bytes,
535            content.to_request_id(),
536            explicit_verify_query_signatures,
537            operation,
538        )
539        .await
540    }
541
542    /// Send the signed query to the network. Will return a byte vector.
543    /// The bytes will be checked if it is a valid query.
544    /// If you want to inspect the fields of the query call, use [`signed_query_inspect`] before calling this method.
545    ///
546    /// `effective_id` may be either an effective canister id (the common case) or
547    /// an effective subnet id; in the latter case the request is routed to the
548    /// subnet-scoped `/api/v3/subnet/<id>/query` endpoint. Per the IC interface
549    /// spec, subnet-scoped queries are currently only valid for the
550    /// `list_canisters` method of the Management Canister.
551    pub async fn query_signed(
552        &self,
553        effective_id: impl Into<EffectiveId>,
554        signed_query: Vec<u8>,
555    ) -> Result<Vec<u8>, AgentError> {
556        let envelope: Envelope =
557            serde_cbor::from_slice(&signed_query).map_err(AgentError::InvalidCborData)?;
558        let EnvelopeContent::Query {
559            canister_id,
560            method_name,
561            ..
562        } = &*envelope.content
563        else {
564            return Err(AgentError::CallDataMismatch {
565                field: "request_type".to_string(),
566                value_arg: "query".to_string(),
567                value_cbor: if matches!(*envelope.content, EnvelopeContent::Call { .. }) {
568                    "update"
569                } else {
570                    "read_state"
571                }
572                .to_string(),
573            });
574        };
575        let operation = Operation::Call {
576            canister: *canister_id,
577            method: method_name.clone(),
578        };
579        self.query_inner(
580            effective_id.into(),
581            signed_query,
582            envelope.content.to_request_id(),
583            None,
584            operation,
585        )
586        .await
587    }
588
589    /// Helper function for performing both the query call and possibly a `read_state` to check the subnet node keys.
590    ///
591    /// This should be used instead of `query_endpoint`. No validation is performed on `signed_query`.
592    async fn query_inner(
593        &self,
594        effective_id: EffectiveId,
595        signed_query: Vec<u8>,
596        request_id: RequestId,
597        explicit_verify_query_signatures: Option<bool>,
598        operation: Operation,
599    ) -> Result<Vec<u8>, AgentError> {
600        let response = if explicit_verify_query_signatures.unwrap_or(self.verify_query_signatures) {
601            let (response, mut subnet) = futures_util::try_join!(
602                self.query_endpoint_for::<QueryResponse>(effective_id, signed_query),
603                self.get_subnet_for(effective_id)
604            )?;
605            if response.signatures().is_empty() {
606                return Err(AgentError::MissingSignature);
607            } else if response.signatures().len() > subnet.node_keys.len() {
608                return Err(AgentError::TooManySignatures {
609                    had: response.signatures().len(),
610                    needed: subnet.node_keys.len(),
611                });
612            }
613            for signature in response.signatures() {
614                if OffsetDateTime::now_utc()
615                    - OffsetDateTime::from_unix_timestamp_nanos(signature.timestamp.into()).unwrap()
616                    > self.ingress_expiry
617                {
618                    return Err(AgentError::CertificateOutdated(self.ingress_expiry));
619                }
620                let signable = response.signable(request_id, signature.timestamp);
621                let node_key = if let Some(node_key) = subnet.node_keys.get(&signature.identity) {
622                    node_key
623                } else {
624                    subnet = self.fetch_subnet_for(effective_id).await?;
625                    subnet
626                        .node_keys
627                        .get(&signature.identity)
628                        .ok_or(AgentError::CertificateNotAuthorized())?
629                };
630                if node_key.len() != 44 {
631                    return Err(AgentError::DerKeyLengthMismatch {
632                        expected: 44,
633                        actual: node_key.len(),
634                    });
635                }
636                const DER_PREFIX: [u8; 12] = [48, 42, 48, 5, 6, 3, 43, 101, 112, 3, 33, 0];
637                if node_key[..12] != DER_PREFIX {
638                    return Err(AgentError::DerPrefixMismatch {
639                        expected: DER_PREFIX.to_vec(),
640                        actual: node_key[..12].to_vec(),
641                    });
642                }
643                let pubkey = PublicKey::deserialize_raw(&node_key[12..])
644                    .map_err(|_| AgentError::MalformedPublicKey)?;
645
646                match pubkey.verify_signature(&signable, &signature.signature[..]) {
647                    Ok(()) => (),
648                    Err(SignatureError::InvalidSignature) => {
649                        return Err(AgentError::QuerySignatureVerificationFailed)
650                    }
651                    Err(SignatureError::InvalidLength) => {
652                        return Err(AgentError::MalformedSignature)
653                    }
654                    _ => unreachable!(),
655                }
656            }
657            response
658        } else {
659            self.query_endpoint_for::<QueryResponse>(effective_id, signed_query)
660                .await?
661        };
662
663        match response {
664            QueryResponse::Replied { reply, .. } => Ok(reply.arg),
665            QueryResponse::Rejected { reject, .. } => Err(AgentError::UncertifiedReject {
666                reject,
667                operation: Some(operation),
668            }),
669        }
670    }
671
672    fn query_content(
673        &self,
674        canister_id: Principal,
675        method_name: String,
676        arg: Vec<u8>,
677        ingress_expiry_datetime: Option<u64>,
678        use_nonce: bool,
679    ) -> Result<EnvelopeContent, AgentError> {
680        Ok(EnvelopeContent::Query {
681            sender: self.identity.sender().map_err(AgentError::SigningError)?,
682            canister_id,
683            method_name,
684            arg,
685            ingress_expiry: ingress_expiry_datetime.unwrap_or_else(|| self.get_expiry_date()),
686            nonce: use_nonce.then(|| self.nonce_factory.generate()).flatten(),
687            sender_info: self.identity.sender_info(),
688        })
689    }
690
691    /// The simplest way to do an update call; sends a byte array and will return a response, [`CallResponse`], from the replica.
692    async fn update_raw(
693        &self,
694        canister_id: Principal,
695        effective_canister_id: Principal,
696        method_name: String,
697        arg: Vec<u8>,
698        ingress_expiry_datetime: Option<u64>,
699    ) -> Result<CallResponse<(Vec<u8>, Certificate)>, AgentError> {
700        let nonce = self.nonce_factory.generate();
701        let content = self.update_content(
702            canister_id,
703            method_name.clone(),
704            arg,
705            ingress_expiry_datetime,
706            nonce,
707        )?;
708        let operation = Some(Operation::Call {
709            canister: canister_id,
710            method: method_name,
711        });
712        let request_id = to_request_id(&content)?;
713        let serialized_bytes = sign_envelope(&content, self.identity.clone())?;
714
715        let response_body = self
716            .call_endpoint(effective_canister_id, serialized_bytes)
717            .await?;
718
719        match response_body {
720            TransportCallResponse::Replied { certificate } => {
721                let certificate =
722                    serde_cbor::from_slice(&certificate).map_err(AgentError::InvalidCborData)?;
723
724                self.verify(&certificate, effective_canister_id)?;
725                let status = lookup_request_status(&certificate, &request_id)?;
726
727                match status {
728                    RequestStatusResponse::Replied(reply) => {
729                        Ok(CallResponse::Response((reply.arg, certificate)))
730                    }
731                    RequestStatusResponse::Rejected(reject_response) => {
732                        Err(AgentError::CertifiedReject {
733                            reject: reject_response,
734                            operation,
735                        })?
736                    }
737                    _ => Ok(CallResponse::Poll(request_id)),
738                }
739            }
740            TransportCallResponse::Accepted => Ok(CallResponse::Poll(request_id)),
741            TransportCallResponse::NonReplicatedRejection(reject_response) => {
742                Err(AgentError::UncertifiedReject {
743                    reject: reject_response,
744                    operation,
745                })
746            }
747        }
748    }
749
750    /// Send the signed update to the network. Will return a [`CallResponse<Vec<u8>>`].
751    /// The bytes will be checked to verify that it is a valid update.
752    /// If you want to inspect the fields of the update, use [`signed_update_inspect`] before calling this method.
753    ///
754    /// `effective_id` may be either an effective canister id (the common case) or
755    /// an effective subnet id; in the latter case the request is routed to the
756    /// subnet-scoped `/api/v4/subnet/<id>/call` endpoint. Per the IC interface
757    /// spec, subnet-scoped update calls are currently only valid for canister
758    /// creation calls to the Management Canister.
759    pub async fn update_signed(
760        &self,
761        effective_id: impl Into<EffectiveId>,
762        signed_update: Vec<u8>,
763    ) -> Result<CallResponse<Vec<u8>>, AgentError> {
764        let effective_id = effective_id.into();
765        let envelope: Envelope =
766            serde_cbor::from_slice(&signed_update).map_err(AgentError::InvalidCborData)?;
767        let EnvelopeContent::Call {
768            canister_id,
769            method_name,
770            ..
771        } = &*envelope.content
772        else {
773            return Err(AgentError::CallDataMismatch {
774                field: "request_type".to_string(),
775                value_arg: "update".to_string(),
776                value_cbor: if matches!(*envelope.content, EnvelopeContent::Query { .. }) {
777                    "query"
778                } else {
779                    "read_state"
780                }
781                .to_string(),
782            });
783        };
784        let operation = Some(Operation::Call {
785            canister: *canister_id,
786            method: method_name.clone(),
787        });
788        let request_id = to_request_id(&envelope.content)?;
789
790        let response_body = self.call_endpoint_for(effective_id, signed_update).await?;
791
792        match response_body {
793            TransportCallResponse::Replied { certificate } => {
794                let certificate =
795                    serde_cbor::from_slice(&certificate).map_err(AgentError::InvalidCborData)?;
796
797                self.verify(&certificate, effective_id)?;
798                let status = lookup_request_status(&certificate, &request_id)?;
799
800                match status {
801                    RequestStatusResponse::Replied(reply) => Ok(CallResponse::Response(reply.arg)),
802                    RequestStatusResponse::Rejected(reject_response) => {
803                        Err(AgentError::CertifiedReject {
804                            reject: reject_response,
805                            operation,
806                        })?
807                    }
808                    _ => Ok(CallResponse::Poll(request_id)),
809                }
810            }
811            TransportCallResponse::Accepted => Ok(CallResponse::Poll(request_id)),
812            TransportCallResponse::NonReplicatedRejection(reject_response) => {
813                Err(AgentError::UncertifiedReject {
814                    reject: reject_response,
815                    operation,
816                })
817            }
818        }
819    }
820
821    fn update_content(
822        &self,
823        canister_id: Principal,
824        method_name: String,
825        arg: Vec<u8>,
826        ingress_expiry_datetime: Option<u64>,
827        nonce: Option<Vec<u8>>,
828    ) -> Result<EnvelopeContent, AgentError> {
829        Ok(EnvelopeContent::Call {
830            canister_id,
831            method_name,
832            arg,
833            nonce,
834            sender: self.identity.sender().map_err(AgentError::SigningError)?,
835            ingress_expiry: ingress_expiry_datetime.unwrap_or_else(|| self.get_expiry_date()),
836            sender_info: self.identity.sender_info(),
837        })
838    }
839
840    /// Backoff schedule for polling `request_status`: 500ms initial delay, growing by 1.4x up to
841    /// 1s, jittered. `with_total_delay` bounds the cumulative sleep by `max_polling_time`, so the
842    /// iterator yields `None` (i.e. "give up") once the budget is spent.
843    fn poll_backoff(&self) -> ExponentialBackoff {
844        ExponentialBuilder::default()
845            .with_min_delay(Duration::from_millis(500))
846            .with_max_delay(Duration::from_secs(1))
847            .with_factor(1.4)
848            .with_jitter()
849            .without_max_times()
850            .with_total_delay(Some(self.max_polling_time))
851            .build()
852    }
853
854    /// Wait for `request_status` to return a Replied response and return the arg.
855    ///
856    /// `effective_id` may be either an effective canister id or an effective
857    /// subnet id; in the latter case the request is routed to the subnet-scoped
858    /// `/api/v3/subnet/<id>/read_state` endpoint.
859    pub async fn wait_signed(
860        &self,
861        request_id: &RequestId,
862        effective_id: impl Into<EffectiveId>,
863        signed_request_status: Vec<u8>,
864    ) -> Result<(Vec<u8>, Certificate), AgentError> {
865        let effective_id = effective_id.into();
866        let mut backoff = self.poll_backoff();
867        let mut slept = Duration::ZERO;
868
869        let mut request_accepted = false;
870        loop {
871            let (resp, cert) = self
872                .request_status_signed(request_id, effective_id, signed_request_status.clone())
873                .await?;
874            match resp {
875                RequestStatusResponse::Unknown => {
876                    // If the status is still `Unknown` after 5 minutes of cumulative polling
877                    // backoff, the ingress message is presumed lost. `slept` counts backoff
878                    // sleep, not wall-clock, so this excludes per-poll request latency.
879                    if slept > Duration::from_secs(5 * 60) {
880                        return Err(AgentError::TimeoutWaitingForResponse());
881                    }
882                }
883
884                RequestStatusResponse::Received | RequestStatusResponse::Processing => {
885                    if !request_accepted {
886                        backoff = self.poll_backoff();
887                        slept = Duration::ZERO;
888                        request_accepted = true;
889                    }
890                }
891
892                RequestStatusResponse::Replied(ReplyResponse { arg, .. }) => {
893                    return Ok((arg, cert))
894                }
895
896                RequestStatusResponse::Rejected(response) => {
897                    return Err(AgentError::CertifiedReject {
898                        reject: response,
899                        operation: None,
900                    })
901                }
902
903                RequestStatusResponse::Done => {
904                    return Err(AgentError::RequestStatusDoneNoReply(String::from(
905                        *request_id,
906                    )))
907                }
908            };
909
910            match backoff.next() {
911                Some(duration) => {
912                    slept += duration;
913                    crate::util::sleep(duration).await;
914                }
915                None => return Err(AgentError::TimeoutWaitingForResponse()),
916            }
917        }
918    }
919
920    /// Call `request_status` on the `RequestId` in a loop and return the response as a byte vector.
921    ///
922    /// `effective_id` may be either an effective canister id or an effective
923    /// subnet id; in the latter case state is polled from the subnet-scoped
924    /// `/api/v3/subnet/<id>/read_state` endpoint.
925    pub async fn wait(
926        &self,
927        request_id: &RequestId,
928        effective_id: impl Into<EffectiveId>,
929    ) -> Result<(Vec<u8>, Certificate), AgentError> {
930        self.wait_inner(request_id, effective_id.into(), None).await
931    }
932
933    async fn wait_inner(
934        &self,
935        request_id: &RequestId,
936        effective_id: EffectiveId,
937        operation: Option<Operation>,
938    ) -> Result<(Vec<u8>, Certificate), AgentError> {
939        let mut backoff = self.poll_backoff();
940        let mut slept = Duration::ZERO;
941
942        let mut request_accepted = false;
943        loop {
944            let (resp, cert) = self.request_status_raw(request_id, effective_id).await?;
945            match resp {
946                RequestStatusResponse::Unknown => {
947                    // If the status is still `Unknown` after 5 minutes of cumulative polling
948                    // backoff, the ingress message is presumed lost. `slept` counts backoff
949                    // sleep, not wall-clock, so this excludes per-poll request latency.
950                    if slept > Duration::from_secs(5 * 60) {
951                        return Err(AgentError::TimeoutWaitingForResponse());
952                    }
953                }
954
955                RequestStatusResponse::Received | RequestStatusResponse::Processing => {
956                    if !request_accepted {
957                        // The system will return RequestStatusResponse::Unknown
958                        // until the request is accepted
959                        // and we generally cannot know how long that will take.
960                        // State transitions between Received and Processing may be
961                        // instantaneous. Therefore, once we know the request is accepted,
962                        // we should restart the backoff so the request does not time out.
963
964                        backoff = self.poll_backoff();
965                        slept = Duration::ZERO;
966                        request_accepted = true;
967                    }
968                }
969
970                RequestStatusResponse::Replied(ReplyResponse { arg, .. }) => {
971                    return Ok((arg, cert))
972                }
973
974                RequestStatusResponse::Rejected(response) => {
975                    return Err(AgentError::CertifiedReject {
976                        reject: response,
977                        operation,
978                    })
979                }
980
981                RequestStatusResponse::Done => {
982                    return Err(AgentError::RequestStatusDoneNoReply(String::from(
983                        *request_id,
984                    )))
985                }
986            };
987
988            match backoff.next() {
989                Some(duration) => {
990                    slept += duration;
991                    crate::util::sleep(duration).await;
992                }
993                None => return Err(AgentError::TimeoutWaitingForResponse()),
994            }
995        }
996    }
997
998    /// Request the raw state tree directly.
999    /// See [the protocol docs](https://internetcomputer.org/docs/current/references/ic-interface-spec#http-read-state) for more information.
1000    ///
1001    /// `effective_id` may be either an effective canister id or an effective
1002    /// subnet id; in the latter case the request is read from the subnet-scoped
1003    /// `/api/v3/subnet/<id>/read_state` endpoint.
1004    pub async fn read_state_raw(
1005        &self,
1006        paths: Vec<Vec<Label>>,
1007        effective_id: impl Into<EffectiveId>,
1008    ) -> Result<Certificate, AgentError> {
1009        let effective_id = effective_id.into();
1010        let content = self.read_state_content(paths)?;
1011        let serialized_bytes = sign_envelope(&content, self.identity.clone())?;
1012
1013        let read_state_response: ReadStateResponse = self
1014            .read_state_endpoint_for(effective_id, serialized_bytes)
1015            .await?;
1016        let cert: Certificate = serde_cbor::from_slice(&read_state_response.certificate)
1017            .map_err(AgentError::InvalidCborData)?;
1018        self.verify(&cert, effective_id)?;
1019        Ok(cert)
1020    }
1021
1022    /// Request the raw state tree directly, under a subnet ID.
1023    ///
1024    /// Equivalent to calling [`read_state_raw`](Self::read_state_raw) with
1025    /// [`EffectiveId::Subnet`]; retained for backward compatibility.
1026    pub async fn read_subnet_state_raw(
1027        &self,
1028        paths: Vec<Vec<Label>>,
1029        subnet_id: Principal,
1030    ) -> Result<Certificate, AgentError> {
1031        self.read_state_raw(paths, EffectiveId::Subnet(subnet_id))
1032            .await
1033    }
1034
1035    fn read_state_content(&self, paths: Vec<Vec<Label>>) -> Result<EnvelopeContent, AgentError> {
1036        Ok(EnvelopeContent::ReadState {
1037            sender: self.identity.sender().map_err(AgentError::SigningError)?,
1038            paths,
1039            ingress_expiry: self.get_expiry_date(),
1040        })
1041    }
1042
1043    /// Verify a certificate, checking delegation if present.
1044    ///
1045    /// For [`EffectiveId::Canister`] the certificate must have authority over
1046    /// the canister (its canister-id ranges must include the principal). For
1047    /// [`EffectiveId::Subnet`] the certificate must instead be authoritative
1048    /// for the specified subnet.
1049    pub fn verify(
1050        &self,
1051        cert: &Certificate,
1052        effective_id: impl Into<EffectiveId>,
1053    ) -> Result<(), AgentError> {
1054        match effective_id.into() {
1055            EffectiveId::Canister(p) => self.verify_cert(cert, p)?,
1056            EffectiveId::Subnet(p) => self.verify_cert_for_subnet(cert, p)?,
1057        }
1058        self.verify_cert_timestamp(cert)?;
1059        Ok(())
1060    }
1061
1062    fn verify_cert(
1063        &self,
1064        cert: &Certificate,
1065        effective_canister_id: Principal,
1066    ) -> Result<(), AgentError> {
1067        let sig = &cert.signature;
1068
1069        let root_hash = cert.tree.digest();
1070        let mut msg = vec![];
1071        msg.extend_from_slice(IC_STATE_ROOT_DOMAIN_SEPARATOR);
1072        msg.extend_from_slice(&root_hash);
1073
1074        let der_key = self.check_delegation(&cert.delegation, effective_canister_id)?;
1075        let key = extract_der(der_key)?;
1076
1077        ic_verify_bls_signature::verify_bls_signature(sig, &msg, &key)
1078            .map_err(|_| AgentError::CertificateVerificationFailed())?;
1079        Ok(())
1080    }
1081
1082    /// Verify a certificate, checking delegation if present.
1083    /// Only passes if the certificate is for the specified subnet.
1084    ///
1085    /// Equivalent to calling [`verify`](Self::verify) with
1086    /// [`EffectiveId::Subnet`]; retained for backward compatibility.
1087    pub fn verify_for_subnet(
1088        &self,
1089        cert: &Certificate,
1090        subnet_id: Principal,
1091    ) -> Result<(), AgentError> {
1092        self.verify(cert, EffectiveId::Subnet(subnet_id))
1093    }
1094
1095    fn verify_cert_for_subnet(
1096        &self,
1097        cert: &Certificate,
1098        subnet_id: Principal,
1099    ) -> Result<(), AgentError> {
1100        let sig = &cert.signature;
1101
1102        let root_hash = cert.tree.digest();
1103        let mut msg = vec![];
1104        msg.extend_from_slice(IC_STATE_ROOT_DOMAIN_SEPARATOR);
1105        msg.extend_from_slice(&root_hash);
1106
1107        let der_key = self.check_delegation_for_subnet(&cert.delegation, subnet_id)?;
1108        let key = extract_der(der_key)?;
1109
1110        ic_verify_bls_signature::verify_bls_signature(sig, &msg, &key)
1111            .map_err(|_| AgentError::CertificateVerificationFailed())?;
1112        Ok(())
1113    }
1114
1115    fn verify_cert_timestamp(&self, cert: &Certificate) -> Result<(), AgentError> {
1116        // Verify that the certificate is not older than ingress expiry
1117        // Certificates with timestamps in the future are allowed
1118        let time = lookup_time(cert)?;
1119        if (OffsetDateTime::now_utc()
1120            - OffsetDateTime::from_unix_timestamp_nanos(time.into()).unwrap())
1121            > self.ingress_expiry
1122        {
1123            Err(AgentError::CertificateOutdated(self.ingress_expiry))
1124        } else {
1125            Ok(())
1126        }
1127    }
1128
1129    fn check_delegation(
1130        &self,
1131        delegation: &Option<Delegation>,
1132        effective_canister_id: Principal,
1133    ) -> Result<Vec<u8>, AgentError> {
1134        match delegation {
1135            None => Ok(self.read_root_key()),
1136            Some(delegation) => {
1137                let cert: Certificate = serde_cbor::from_slice(&delegation.certificate)
1138                    .map_err(AgentError::InvalidCborData)?;
1139                if cert.delegation.is_some() {
1140                    return Err(AgentError::CertificateHasTooManyDelegations);
1141                }
1142                self.verify_cert(&cert, effective_canister_id)?;
1143                let canister_range_shards_lookup =
1144                    ["canister_ranges".as_bytes(), delegation.subnet_id.as_ref()];
1145                let ranges: Vec<(Principal, Principal)> =
1146                    match lookup_tree(&cert.tree, canister_range_shards_lookup) {
1147                        Ok(canister_range_shards) => {
1148                            let mut shard_paths = canister_range_shards
1149                                .list_paths() // /canister_ranges/<subnet_id>/<shard>
1150                                .into_iter()
1151                                .map(|mut x| {
1152                                    x.pop() // flatten [label] to label
1153                                        .ok_or_else(AgentError::CertificateVerificationFailed)
1154                                })
1155                                .collect::<Result<Vec<_>, _>>()?;
1156                            if shard_paths.is_empty() {
1157                                return Err(AgentError::CertificateNotAuthorized());
1158                            }
1159                            shard_paths.sort_unstable();
1160                            let shard_division = shard_paths.partition_point(|shard| {
1161                                shard.as_bytes() <= effective_canister_id.as_slice()
1162                            });
1163                            if shard_division == 0 {
1164                                // the certificate is not authorized to answer calls for this canister
1165                                return Err(AgentError::CertificateNotAuthorized());
1166                            }
1167                            let max_potential_shard = &shard_paths[shard_division - 1];
1168                            let canister_range = lookup_value(
1169                                &canister_range_shards,
1170                                [max_potential_shard.as_bytes()],
1171                            )?;
1172                            serde_cbor::from_slice(canister_range)
1173                                .map_err(AgentError::InvalidCborData)?
1174                        }
1175                        // Fall back to /subnet/<subnet_id>/canister_ranges (non-sharded)
1176                        Err(AgentError::LookupPathAbsent(_) | AgentError::LookupPathUnknown(_)) => {
1177                            let subnet_ranges_path = [
1178                                "subnet".as_bytes(),
1179                                delegation.subnet_id.as_ref(),
1180                                "canister_ranges".as_bytes(),
1181                            ];
1182                            let canister_range = lookup_value(&cert.tree, subnet_ranges_path)?;
1183                            serde_cbor::from_slice(canister_range)
1184                                .map_err(AgentError::InvalidCborData)?
1185                        }
1186                        Err(e) => return Err(e),
1187                    };
1188                if !principal_is_within_ranges(&effective_canister_id, &ranges[..]) {
1189                    // the certificate is not authorized to answer calls for this canister
1190                    return Err(AgentError::CertificateNotAuthorized());
1191                }
1192
1193                let public_key_path = [
1194                    "subnet".as_bytes(),
1195                    delegation.subnet_id.as_ref(),
1196                    "public_key".as_bytes(),
1197                ];
1198                lookup_value(&cert.tree, public_key_path).map(<[u8]>::to_vec)
1199            }
1200        }
1201    }
1202
1203    fn check_delegation_for_subnet(
1204        &self,
1205        delegation: &Option<Delegation>,
1206        subnet_id: Principal,
1207    ) -> Result<Vec<u8>, AgentError> {
1208        match delegation {
1209            None => Ok(self.read_root_key()),
1210            Some(delegation) => {
1211                let cert: Certificate = serde_cbor::from_slice(&delegation.certificate)
1212                    .map_err(AgentError::InvalidCborData)?;
1213                if cert.delegation.is_some() {
1214                    return Err(AgentError::CertificateHasTooManyDelegations);
1215                }
1216                self.verify_cert_for_subnet(&cert, subnet_id)?;
1217                let public_key_path = [
1218                    "subnet".as_bytes(),
1219                    subnet_id.as_ref(),
1220                    "public_key".as_bytes(),
1221                ];
1222                let pk = lookup_value(&cert.tree, public_key_path)
1223                    .map_err(|_| AgentError::CertificateNotAuthorized())?
1224                    .to_vec();
1225                Ok(pk)
1226            }
1227        }
1228    }
1229
1230    /// Request information about a particular canister for a single state subkey.
1231    /// See [the protocol docs](https://internetcomputer.org/docs/current/references/ic-interface-spec#state-tree-canister-information) for more information.
1232    pub async fn read_state_canister_info(
1233        &self,
1234        canister_id: Principal,
1235        path: &str,
1236    ) -> Result<Vec<u8>, AgentError> {
1237        let paths: Vec<Vec<Label>> = vec![vec![
1238            "canister".into(),
1239            Label::from_bytes(canister_id.as_slice()),
1240            path.into(),
1241        ]];
1242
1243        let cert = self.read_state_raw(paths, canister_id).await?;
1244
1245        lookup_canister_info(cert, canister_id, path)
1246    }
1247
1248    /// Request the controller list of a given canister.
1249    pub async fn read_state_canister_controllers(
1250        &self,
1251        canister_id: Principal,
1252    ) -> Result<Vec<Principal>, AgentError> {
1253        let blob = self
1254            .read_state_canister_info(canister_id, "controllers")
1255            .await?;
1256        let controllers: Vec<Principal> =
1257            serde_cbor::from_slice(&blob).map_err(AgentError::InvalidCborData)?;
1258        Ok(controllers)
1259    }
1260
1261    /// Request the module hash of a given canister.
1262    pub async fn read_state_canister_module_hash(
1263        &self,
1264        canister_id: Principal,
1265    ) -> Result<Vec<u8>, AgentError> {
1266        self.read_state_canister_info(canister_id, "module_hash")
1267            .await
1268    }
1269
1270    /// Request the bytes of the canister's custom section `icp:public <path>` or `icp:private <path>`.
1271    pub async fn read_state_canister_metadata(
1272        &self,
1273        canister_id: Principal,
1274        path: &str,
1275    ) -> Result<Vec<u8>, AgentError> {
1276        let paths: Vec<Vec<Label>> = vec![vec![
1277            "canister".into(),
1278            Label::from_bytes(canister_id.as_slice()),
1279            "metadata".into(),
1280            path.into(),
1281        ]];
1282
1283        let cert = self.read_state_raw(paths, canister_id).await?;
1284
1285        lookup_canister_metadata(cert, canister_id, path)
1286    }
1287
1288    /// Request a list of metrics about the subnet.
1289    pub async fn read_state_subnet_metrics(
1290        &self,
1291        subnet_id: Principal,
1292    ) -> Result<SubnetMetrics, AgentError> {
1293        let paths = vec![vec![
1294            "subnet".into(),
1295            Label::from_bytes(subnet_id.as_slice()),
1296            "metrics".into(),
1297        ]];
1298        let cert = self.read_subnet_state_raw(paths, subnet_id).await?;
1299        lookup_subnet_metrics(cert, subnet_id)
1300    }
1301
1302    /// Request a list of metrics about the subnet.
1303    pub async fn read_state_subnet_canister_ranges(
1304        &self,
1305        subnet_id: Principal,
1306    ) -> Result<Vec<(Principal, Principal)>, AgentError> {
1307        let paths = vec![vec![
1308            "subnet".into(),
1309            Label::from_bytes(subnet_id.as_slice()),
1310            "canister_ranges".into(),
1311        ]];
1312        let cert = self.read_subnet_state_raw(paths, subnet_id).await?;
1313        lookup_subnet_canister_ranges(&cert, subnet_id)
1314    }
1315
1316    /// Fetches the status of a particular request by its ID.
1317    ///
1318    /// `effective_id` may be either an effective canister id or an effective
1319    /// subnet id; in the latter case the request is read from the subnet-scoped
1320    /// `/api/v3/subnet/<id>/read_state` endpoint.
1321    pub async fn request_status_raw(
1322        &self,
1323        request_id: &RequestId,
1324        effective_id: impl Into<EffectiveId>,
1325    ) -> Result<(RequestStatusResponse, Certificate), AgentError> {
1326        let paths: Vec<Vec<Label>> =
1327            vec![vec!["request_status".into(), request_id.to_vec().into()]];
1328
1329        let cert = self.read_state_raw(paths, effective_id).await?;
1330
1331        Ok((lookup_request_status(&cert, request_id)?, cert))
1332    }
1333
1334    /// Send the signed `request_status` to the network. Will return [`RequestStatusResponse`].
1335    /// The bytes will be checked to verify that it is a valid `request_status`.
1336    /// If you want to inspect the fields of the `request_status`, use [`signed_request_status_inspect`] before calling this method.
1337    ///
1338    /// `effective_id` may be either an effective canister id or an effective
1339    /// subnet id; in the latter case the request is read from the subnet-scoped
1340    /// `/api/v3/subnet/<id>/read_state` endpoint.
1341    pub async fn request_status_signed(
1342        &self,
1343        request_id: &RequestId,
1344        effective_id: impl Into<EffectiveId>,
1345        signed_request_status: Vec<u8>,
1346    ) -> Result<(RequestStatusResponse, Certificate), AgentError> {
1347        let effective_id = effective_id.into();
1348        let _envelope: Envelope =
1349            serde_cbor::from_slice(&signed_request_status).map_err(AgentError::InvalidCborData)?;
1350        let read_state_response: ReadStateResponse = self
1351            .read_state_endpoint_for(effective_id, signed_request_status)
1352            .await?;
1353
1354        let cert: Certificate = serde_cbor::from_slice(&read_state_response.certificate)
1355            .map_err(AgentError::InvalidCborData)?;
1356        self.verify(&cert, effective_id)?;
1357        Ok((lookup_request_status(&cert, request_id)?, cert))
1358    }
1359
1360    /// Returns an `UpdateBuilder` enabling the construction of an update call without
1361    /// passing all arguments.
1362    pub fn update<S: Into<String>>(
1363        &self,
1364        canister_id: &Principal,
1365        method_name: S,
1366    ) -> UpdateBuilder<'_> {
1367        UpdateBuilder::new(self, *canister_id, method_name.into())
1368    }
1369
1370    /// Calls and returns the information returned by the status endpoint of a replica.
1371    pub async fn status(&self) -> Result<Status, AgentError> {
1372        let endpoint = "api/v2/status";
1373        let bytes = self.execute(Method::GET, endpoint, None).await?.1;
1374
1375        let cbor: serde_cbor::Value =
1376            serde_cbor::from_slice(&bytes).map_err(AgentError::InvalidCborData)?;
1377
1378        Status::try_from(&cbor).map_err(|_| AgentError::InvalidReplicaStatus)
1379    }
1380
1381    /// Returns a `QueryBuilder` enabling the construction of a query call without
1382    /// passing all arguments.
1383    pub fn query<S: Into<String>>(
1384        &self,
1385        canister_id: &Principal,
1386        method_name: S,
1387    ) -> QueryBuilder<'_> {
1388        QueryBuilder::new(self, *canister_id, method_name.into())
1389    }
1390
1391    /// Sign a `request_status` call. This will return a [`signed::SignedRequestStatus`]
1392    /// which contains all fields of the `request_status` and the signed `request_status` in CBOR encoding.
1393    ///
1394    /// `effective_id` may be either an effective canister ID or an effective subnet ID,
1395    /// depending on which endpoint the caller will submit the signed message to.
1396    pub fn sign_request_status(
1397        &self,
1398        effective_id: impl Into<EffectiveId>,
1399        request_id: RequestId,
1400    ) -> Result<SignedRequestStatus, AgentError> {
1401        let effective_id = effective_id.into();
1402        let paths: Vec<Vec<Label>> =
1403            vec![vec!["request_status".into(), request_id.to_vec().into()]];
1404        let read_state_content = self.read_state_content(paths)?;
1405        let signed_request_status = sign_envelope(&read_state_content, self.identity.clone())?;
1406        let ingress_expiry = read_state_content.ingress_expiry();
1407        let sender = *read_state_content.sender();
1408        Ok(SignedRequestStatus {
1409            ingress_expiry,
1410            sender,
1411            effective_canister_id: effective_id.as_principal(),
1412            request_id,
1413            signed_request_status,
1414        })
1415    }
1416
1417    /// Retrieve subnet information for a canister. This uses an internal five-minute cache, fresh data can
1418    /// be fetched with [`fetch_subnet_by_canister`](Self::fetch_subnet_by_canister).
1419    pub async fn get_subnet_by_canister(
1420        &self,
1421        canister: &Principal,
1422    ) -> Result<Arc<Subnet>, AgentError> {
1423        let subnet = self
1424            .subnet_key_cache
1425            .lock()
1426            .unwrap()
1427            .get_subnet_by_canister(canister);
1428        if let Some(subnet) = subnet {
1429            Ok(subnet)
1430        } else {
1431            self.fetch_subnet_by_canister(canister).await
1432        }
1433    }
1434
1435    /// Retrieve subnet information for a subnet ID. This uses an internal five-minute cache, fresh data can
1436    /// be fetched with [`fetch_subnet_by_id`](Self::fetch_subnet_by_id).
1437    pub async fn get_subnet_by_id(&self, subnet_id: &Principal) -> Result<Arc<Subnet>, AgentError> {
1438        let subnet = self
1439            .subnet_key_cache
1440            .lock()
1441            .unwrap()
1442            .get_subnet_by_id(subnet_id);
1443        if let Some(subnet) = subnet {
1444            Ok(subnet)
1445        } else {
1446            self.fetch_subnet_by_id(subnet_id).await
1447        }
1448    }
1449
1450    /// Retrieve all existing API boundary nodes from the state tree via endpoint `/api/v3/canister/<effective_canister_id>/read_state`
1451    pub async fn fetch_api_boundary_nodes_by_canister_id(
1452        &self,
1453        canister_id: Principal,
1454    ) -> Result<Vec<ApiBoundaryNode>, AgentError> {
1455        let paths = vec![vec!["api_boundary_nodes".into()]];
1456        let certificate = self.read_state_raw(paths, canister_id).await?;
1457        let api_boundary_nodes = lookup_api_boundary_nodes(certificate)?;
1458        Ok(api_boundary_nodes)
1459    }
1460
1461    /// Retrieve all existing API boundary nodes from the state tree via endpoint `/api/v3/subnet/<subnet_id>/read_state`
1462    pub async fn fetch_api_boundary_nodes_by_subnet_id(
1463        &self,
1464        subnet_id: Principal,
1465    ) -> Result<Vec<ApiBoundaryNode>, AgentError> {
1466        let paths = vec![vec!["api_boundary_nodes".into()]];
1467        let certificate = self.read_subnet_state_raw(paths, subnet_id).await?;
1468        let api_boundary_nodes = lookup_api_boundary_nodes(certificate)?;
1469        Ok(api_boundary_nodes)
1470    }
1471
1472    /// Fetches and caches the subnet information for a canister.
1473    ///
1474    /// This function does not read from the cache; most users want
1475    /// [`get_subnet_by_canister`](Self::get_subnet_by_canister) instead.
1476    pub async fn fetch_subnet_by_canister(
1477        &self,
1478        canister: &Principal,
1479    ) -> Result<Arc<Subnet>, AgentError> {
1480        let canister_cert = self
1481            .read_state_raw(vec![vec!["subnet".into()]], *canister)
1482            .await?;
1483        let subnet_id = if let Some(delegation) = canister_cert.delegation.as_ref() {
1484            Principal::from_slice(&delegation.subnet_id)
1485        } else {
1486            // if no delegation, it comes from the root subnet
1487            Principal::self_authenticating(&self.root_key.read().unwrap()[..])
1488        };
1489        let mut subnet = lookup_incomplete_subnet(&subnet_id, &canister_cert)?;
1490        let canister_ranges = if let Some(delegation) = canister_cert.delegation.as_ref() {
1491            // non-root subnets will not serve /subnet/<>/canister_ranges when looked up by canister, but their delegation will contain /canister_ranges
1492            let delegation_cert: Certificate = serde_cbor::from_slice(&delegation.certificate)?;
1493            lookup_canister_ranges(&subnet_id, &delegation_cert)?
1494        } else {
1495            lookup_canister_ranges(&subnet_id, &canister_cert)?
1496        };
1497        subnet.canister_ranges = canister_ranges;
1498        if !subnet.canister_ranges.contains(canister) {
1499            return Err(AgentError::CertificateNotAuthorized());
1500        }
1501        let subnet = Arc::new(subnet);
1502        self.subnet_key_cache
1503            .lock()
1504            .unwrap()
1505            .insert_subnet(subnet_id, subnet.clone());
1506        Ok(subnet)
1507    }
1508
1509    /// Fetches and caches the subnet information for a subnet ID.
1510    ///
1511    /// This function does not read from the cache; most users want
1512    /// [`get_subnet_by_id`](Self::get_subnet_by_id) instead.
1513    pub async fn fetch_subnet_by_id(
1514        &self,
1515        subnet_id: &Principal,
1516    ) -> Result<Arc<Subnet>, AgentError> {
1517        let subnet_cert = self
1518            .read_subnet_state_raw(
1519                vec![
1520                    vec!["canister_ranges".into(), subnet_id.as_slice().into()],
1521                    vec!["subnet".into(), subnet_id.as_slice().into()],
1522                ],
1523                *subnet_id,
1524            )
1525            .await?;
1526        let subnet = lookup_subnet_and_ranges(subnet_id, &subnet_cert)?;
1527        let subnet = Arc::new(subnet);
1528        self.subnet_key_cache
1529            .lock()
1530            .unwrap()
1531            .insert_subnet(*subnet_id, subnet.clone());
1532        Ok(subnet)
1533    }
1534
1535    async fn request(
1536        &self,
1537        method: Method,
1538        endpoint: &str,
1539        body: Option<Vec<u8>>,
1540    ) -> Result<(StatusCode, HeaderMap, Vec<u8>), AgentError> {
1541        let body = body.map(Bytes::from);
1542
1543        let create_request_with_generated_url = || -> Result<http::Request<Bytes>, AgentError> {
1544            let url = self.route_provider.route()?.join(endpoint)?;
1545            let uri = Uri::from_str(url.as_str())
1546                .map_err(|e| AgentError::InvalidReplicaUrl(e.to_string()))?;
1547            let body = body.clone().unwrap_or_default();
1548            let request = http::Request::builder()
1549                .method(method.clone())
1550                .uri(uri)
1551                .header(CONTENT_TYPE, "application/cbor")
1552                .body(body)
1553                .map_err(|e| {
1554                    AgentError::TransportError(TransportError::Generic(format!(
1555                        "unable to create request: {e:#}"
1556                    )))
1557                })?;
1558
1559            Ok(request)
1560        };
1561
1562        let response = self
1563            .client
1564            .call(
1565                &create_request_with_generated_url,
1566                self.max_tcp_error_retries,
1567                self.max_response_body_size,
1568            )
1569            .await?;
1570
1571        let (parts, body) = response.into_parts();
1572
1573        Ok((parts.status, parts.headers, body.to_vec()))
1574    }
1575
1576    async fn execute(
1577        &self,
1578        method: Method,
1579        endpoint: &str,
1580        body: Option<Vec<u8>>,
1581    ) -> Result<(StatusCode, Vec<u8>), AgentError> {
1582        let request_result = self.request(method.clone(), endpoint, body.clone()).await?;
1583
1584        let status = request_result.0;
1585        let headers = request_result.1;
1586        let body = request_result.2;
1587
1588        if status.is_client_error() || status.is_server_error() {
1589            Err(AgentError::HttpError(HttpErrorPayload {
1590                status: status.into(),
1591                content_type: headers
1592                    .get(CONTENT_TYPE)
1593                    .and_then(|value| value.to_str().ok())
1594                    .map(str::to_string),
1595                content: body,
1596            }))
1597        } else if !(status == StatusCode::OK || status == StatusCode::ACCEPTED) {
1598            Err(AgentError::InvalidHttpResponse(format!(
1599                "Expected `200`, `202`, 4xx`, or `5xx` HTTP status code. Got: {status}",
1600            )))
1601        } else {
1602            Ok((status, body))
1603        }
1604    }
1605}
1606
1607// Checks if a principal is contained within a list of principal ranges
1608// A range is a tuple: (low: Principal, high: Principal), as described here: https://internetcomputer.org/docs/current/references/ic-interface-spec#state-tree-subnet
1609fn principal_is_within_ranges(principal: &Principal, ranges: &[(Principal, Principal)]) -> bool {
1610    ranges
1611        .iter()
1612        .any(|r| principal >= &r.0 && principal <= &r.1)
1613}
1614
1615fn sign_envelope(
1616    content: &EnvelopeContent,
1617    identity: Arc<dyn Identity>,
1618) -> Result<Vec<u8>, AgentError> {
1619    let signature = identity.sign(content).map_err(AgentError::SigningError)?;
1620
1621    let envelope = Envelope {
1622        content: Cow::Borrowed(content),
1623        sender_pubkey: signature.public_key,
1624        sender_sig: signature.signature,
1625        sender_delegation: signature.delegations,
1626    };
1627
1628    let mut serialized_bytes = Vec::new();
1629    let mut serializer = serde_cbor::Serializer::new(&mut serialized_bytes);
1630    serializer.self_describe()?;
1631    envelope.serialize(&mut serializer)?;
1632
1633    Ok(serialized_bytes)
1634}
1635
1636/// Inspect the bytes to be sent as a query
1637/// Return Ok only when the bytes can be deserialized as a query and all fields match with the arguments
1638pub fn signed_query_inspect(
1639    sender: Principal,
1640    canister_id: Principal,
1641    method_name: &str,
1642    arg: &[u8],
1643    ingress_expiry: u64,
1644    signed_query: Vec<u8>,
1645) -> Result<(), AgentError> {
1646    let envelope: Envelope =
1647        serde_cbor::from_slice(&signed_query).map_err(AgentError::InvalidCborData)?;
1648    match envelope.content.as_ref() {
1649        EnvelopeContent::Query {
1650            ingress_expiry: ingress_expiry_cbor,
1651            sender: sender_cbor,
1652            canister_id: canister_id_cbor,
1653            method_name: method_name_cbor,
1654            arg: arg_cbor,
1655            nonce: _nonce,
1656            sender_info: _,
1657        } => {
1658            if ingress_expiry != *ingress_expiry_cbor {
1659                return Err(AgentError::CallDataMismatch {
1660                    field: "ingress_expiry".to_string(),
1661                    value_arg: ingress_expiry.to_string(),
1662                    value_cbor: ingress_expiry_cbor.to_string(),
1663                });
1664            }
1665            if sender != *sender_cbor {
1666                return Err(AgentError::CallDataMismatch {
1667                    field: "sender".to_string(),
1668                    value_arg: sender.to_string(),
1669                    value_cbor: sender_cbor.to_string(),
1670                });
1671            }
1672            if canister_id != *canister_id_cbor {
1673                return Err(AgentError::CallDataMismatch {
1674                    field: "canister_id".to_string(),
1675                    value_arg: canister_id.to_string(),
1676                    value_cbor: canister_id_cbor.to_string(),
1677                });
1678            }
1679            if method_name != *method_name_cbor {
1680                return Err(AgentError::CallDataMismatch {
1681                    field: "method_name".to_string(),
1682                    value_arg: method_name.to_string(),
1683                    value_cbor: method_name_cbor.clone(),
1684                });
1685            }
1686            if arg != *arg_cbor {
1687                return Err(AgentError::CallDataMismatch {
1688                    field: "arg".to_string(),
1689                    value_arg: format!("{arg:?}"),
1690                    value_cbor: format!("{arg_cbor:?}"),
1691                });
1692            }
1693        }
1694        EnvelopeContent::Call { .. } => {
1695            return Err(AgentError::CallDataMismatch {
1696                field: "request_type".to_string(),
1697                value_arg: "query".to_string(),
1698                value_cbor: "call".to_string(),
1699            })
1700        }
1701        EnvelopeContent::ReadState { .. } => {
1702            return Err(AgentError::CallDataMismatch {
1703                field: "request_type".to_string(),
1704                value_arg: "query".to_string(),
1705                value_cbor: "read_state".to_string(),
1706            })
1707        }
1708    }
1709    Ok(())
1710}
1711
1712/// Inspect the bytes to be sent as an update
1713/// Return Ok only when the bytes can be deserialized as an update and all fields match with the arguments
1714pub fn signed_update_inspect(
1715    sender: Principal,
1716    canister_id: Principal,
1717    method_name: &str,
1718    arg: &[u8],
1719    ingress_expiry: u64,
1720    signed_update: Vec<u8>,
1721) -> Result<(), AgentError> {
1722    let envelope: Envelope =
1723        serde_cbor::from_slice(&signed_update).map_err(AgentError::InvalidCborData)?;
1724    match envelope.content.as_ref() {
1725        EnvelopeContent::Call {
1726            nonce: _nonce,
1727            ingress_expiry: ingress_expiry_cbor,
1728            sender: sender_cbor,
1729            canister_id: canister_id_cbor,
1730            method_name: method_name_cbor,
1731            arg: arg_cbor,
1732            sender_info: _,
1733        } => {
1734            if ingress_expiry != *ingress_expiry_cbor {
1735                return Err(AgentError::CallDataMismatch {
1736                    field: "ingress_expiry".to_string(),
1737                    value_arg: ingress_expiry.to_string(),
1738                    value_cbor: ingress_expiry_cbor.to_string(),
1739                });
1740            }
1741            if sender != *sender_cbor {
1742                return Err(AgentError::CallDataMismatch {
1743                    field: "sender".to_string(),
1744                    value_arg: sender.to_string(),
1745                    value_cbor: sender_cbor.to_string(),
1746                });
1747            }
1748            if canister_id != *canister_id_cbor {
1749                return Err(AgentError::CallDataMismatch {
1750                    field: "canister_id".to_string(),
1751                    value_arg: canister_id.to_string(),
1752                    value_cbor: canister_id_cbor.to_string(),
1753                });
1754            }
1755            if method_name != *method_name_cbor {
1756                return Err(AgentError::CallDataMismatch {
1757                    field: "method_name".to_string(),
1758                    value_arg: method_name.to_string(),
1759                    value_cbor: method_name_cbor.clone(),
1760                });
1761            }
1762            if arg != *arg_cbor {
1763                return Err(AgentError::CallDataMismatch {
1764                    field: "arg".to_string(),
1765                    value_arg: format!("{arg:?}"),
1766                    value_cbor: format!("{arg_cbor:?}"),
1767                });
1768            }
1769        }
1770        EnvelopeContent::ReadState { .. } => {
1771            return Err(AgentError::CallDataMismatch {
1772                field: "request_type".to_string(),
1773                value_arg: "call".to_string(),
1774                value_cbor: "read_state".to_string(),
1775            })
1776        }
1777        EnvelopeContent::Query { .. } => {
1778            return Err(AgentError::CallDataMismatch {
1779                field: "request_type".to_string(),
1780                value_arg: "call".to_string(),
1781                value_cbor: "query".to_string(),
1782            })
1783        }
1784    }
1785    Ok(())
1786}
1787
1788/// Inspect the bytes to be sent as a `request_status`
1789/// Return Ok only when the bytes can be deserialized as a `request_status` and all fields match with the arguments
1790pub fn signed_request_status_inspect(
1791    sender: Principal,
1792    request_id: &RequestId,
1793    ingress_expiry: u64,
1794    signed_request_status: Vec<u8>,
1795) -> Result<(), AgentError> {
1796    let paths: Vec<Vec<Label>> = vec![vec!["request_status".into(), request_id.to_vec().into()]];
1797    let envelope: Envelope =
1798        serde_cbor::from_slice(&signed_request_status).map_err(AgentError::InvalidCborData)?;
1799    match envelope.content.as_ref() {
1800        EnvelopeContent::ReadState {
1801            ingress_expiry: ingress_expiry_cbor,
1802            sender: sender_cbor,
1803            paths: paths_cbor,
1804        } => {
1805            if ingress_expiry != *ingress_expiry_cbor {
1806                return Err(AgentError::CallDataMismatch {
1807                    field: "ingress_expiry".to_string(),
1808                    value_arg: ingress_expiry.to_string(),
1809                    value_cbor: ingress_expiry_cbor.to_string(),
1810                });
1811            }
1812            if sender != *sender_cbor {
1813                return Err(AgentError::CallDataMismatch {
1814                    field: "sender".to_string(),
1815                    value_arg: sender.to_string(),
1816                    value_cbor: sender_cbor.to_string(),
1817                });
1818            }
1819
1820            if paths != *paths_cbor {
1821                return Err(AgentError::CallDataMismatch {
1822                    field: "paths".to_string(),
1823                    value_arg: format!("{paths:?}"),
1824                    value_cbor: format!("{paths_cbor:?}"),
1825                });
1826            }
1827        }
1828        EnvelopeContent::Query { .. } => {
1829            return Err(AgentError::CallDataMismatch {
1830                field: "request_type".to_string(),
1831                value_arg: "read_state".to_string(),
1832                value_cbor: "query".to_string(),
1833            })
1834        }
1835        EnvelopeContent::Call { .. } => {
1836            return Err(AgentError::CallDataMismatch {
1837                field: "request_type".to_string(),
1838                value_arg: "read_state".to_string(),
1839                value_cbor: "call".to_string(),
1840            })
1841        }
1842    }
1843    Ok(())
1844}
1845
1846#[derive(Clone)]
1847struct SubnetCache {
1848    subnets: TimedCache<Principal, Arc<Subnet>>,
1849    canister_index: RangeInclusiveMap<Principal, Principal>,
1850}
1851
1852impl SubnetCache {
1853    fn new() -> Self {
1854        Self {
1855            subnets: TimedCache::with_lifespan(Duration::from_secs(300)),
1856            canister_index: RangeInclusiveMap::new(),
1857        }
1858    }
1859
1860    fn get_subnet_by_canister(&mut self, canister: &Principal) -> Option<Arc<Subnet>> {
1861        self.canister_index
1862            .get(canister)
1863            .and_then(|subnet_id| self.subnets.cache_get(subnet_id).cloned())
1864            .filter(|subnet| subnet.canister_ranges.contains(canister))
1865    }
1866
1867    fn get_subnet_by_id(&mut self, subnet_id: &Principal) -> Option<Arc<Subnet>> {
1868        self.subnets.cache_get(subnet_id).cloned()
1869    }
1870
1871    fn insert_subnet(&mut self, subnet_id: Principal, subnet: Arc<Subnet>) {
1872        self.subnets.cache_set(subnet_id, subnet.clone());
1873        for range in subnet.canister_ranges.iter() {
1874            self.canister_index.insert(range.clone(), subnet_id);
1875        }
1876    }
1877}
1878
1879/// API boundary node, which routes /api calls to IC replica nodes.
1880#[derive(Debug, Clone)]
1881pub struct ApiBoundaryNode {
1882    /// Domain name
1883    pub domain: String,
1884    /// IPv6 address in the hexadecimal notation with colons.
1885    pub ipv6_address: String,
1886    /// IPv4 address in the dotted-decimal notation.
1887    pub ipv4_address: Option<String>,
1888}
1889
1890/// A query request builder.
1891///
1892/// This makes it easier to do query calls without actually passing all arguments.
1893#[derive(Debug, Clone)]
1894#[non_exhaustive]
1895pub struct QueryBuilder<'agent> {
1896    agent: &'agent Agent,
1897    /// The [effective canister ID](https://internetcomputer.org/docs/current/references/ic-interface-spec#http-effective-canister-id) of the destination.
1898    pub effective_canister_id: Principal,
1899    /// The principal ID of the canister being called.
1900    pub canister_id: Principal,
1901    /// The name of the canister method being called.
1902    pub method_name: String,
1903    /// The argument blob to be passed to the method.
1904    pub arg: Vec<u8>,
1905    /// The Unix timestamp that the request will expire at.
1906    pub ingress_expiry_datetime: Option<u64>,
1907    /// Whether to include a nonce with the message.
1908    pub use_nonce: bool,
1909}
1910
1911impl<'agent> QueryBuilder<'agent> {
1912    /// Creates a new query builder with an agent for a particular canister method.
1913    pub fn new(agent: &'agent Agent, canister_id: Principal, method_name: String) -> Self {
1914        Self {
1915            agent,
1916            effective_canister_id: canister_id,
1917            canister_id,
1918            method_name,
1919            arg: vec![],
1920            ingress_expiry_datetime: None,
1921            use_nonce: false,
1922        }
1923    }
1924
1925    /// Sets the [effective canister ID](https://internetcomputer.org/docs/current/references/ic-interface-spec#http-effective-canister-id) of the destination.
1926    pub fn with_effective_canister_id(mut self, canister_id: Principal) -> Self {
1927        self.effective_canister_id = canister_id;
1928        self
1929    }
1930
1931    /// Sets the argument blob to pass to the canister. For most canisters this should be a Candid-serialized tuple.
1932    pub fn with_arg<A: Into<Vec<u8>>>(mut self, arg: A) -> Self {
1933        self.arg = arg.into();
1934        self
1935    }
1936
1937    /// Sets `ingress_expiry_datetime` to the provided timestamp, at nanosecond precision.
1938    pub fn expire_at(mut self, time: impl Into<OffsetDateTime>) -> Self {
1939        self.ingress_expiry_datetime = Some(time.into().unix_timestamp_nanos() as u64);
1940        self
1941    }
1942
1943    /// Sets `ingress_expiry_datetime` to `max(now, 4min)`.
1944    pub fn expire_after(mut self, duration: Duration) -> Self {
1945        self.ingress_expiry_datetime = Some(
1946            OffsetDateTime::now_utc()
1947                .saturating_add(duration.try_into().expect("negative duration"))
1948                .unix_timestamp_nanos() as u64,
1949        );
1950        self
1951    }
1952
1953    /// Uses a nonce generated with the agent's configured nonce factory. By default queries do not use nonces,
1954    /// and thus may get a (briefly) cached response.
1955    pub fn with_nonce_generation(mut self) -> Self {
1956        self.use_nonce = true;
1957        self
1958    }
1959
1960    /// Make a query call. This will return a byte vector.
1961    pub async fn call(self) -> Result<Vec<u8>, AgentError> {
1962        self.agent
1963            .query_raw(
1964                self.canister_id,
1965                self.effective_canister_id,
1966                self.method_name,
1967                self.arg,
1968                self.ingress_expiry_datetime,
1969                self.use_nonce,
1970                None,
1971            )
1972            .await
1973    }
1974
1975    /// Make a query call with signature verification. This will return a byte vector.
1976    ///
1977    /// Compared with [call][Self::call], this method will **always** verify the signature of the query response
1978    /// regardless the Agent level configuration from [`AgentBuilder::with_verify_query_signatures`].
1979    pub async fn call_with_verification(self) -> Result<Vec<u8>, AgentError> {
1980        self.agent
1981            .query_raw(
1982                self.canister_id,
1983                self.effective_canister_id,
1984                self.method_name,
1985                self.arg,
1986                self.ingress_expiry_datetime,
1987                self.use_nonce,
1988                Some(true),
1989            )
1990            .await
1991    }
1992
1993    /// Make a query call without signature verification. This will return a byte vector.
1994    ///
1995    /// Compared with [call][Self::call], this method will **never** verify the signature of the query response
1996    /// regardless the Agent level configuration from [`AgentBuilder::with_verify_query_signatures`].
1997    pub async fn call_without_verification(self) -> Result<Vec<u8>, AgentError> {
1998        self.agent
1999            .query_raw(
2000                self.canister_id,
2001                self.effective_canister_id,
2002                self.method_name,
2003                self.arg,
2004                self.ingress_expiry_datetime,
2005                self.use_nonce,
2006                Some(false),
2007            )
2008            .await
2009    }
2010
2011    /// Sign a query call. This will return a [`signed::SignedQuery`]
2012    /// which contains all fields of the query and the signed query in CBOR encoding
2013    pub fn sign(self) -> Result<SignedQuery, AgentError> {
2014        let effective_canister_id = self.effective_canister_id;
2015        let identity = self.agent.identity.clone();
2016        let content = self.into_envelope()?;
2017        let signed_query = sign_envelope(&content, identity)?;
2018        let EnvelopeContent::Query {
2019            ingress_expiry,
2020            sender,
2021            canister_id,
2022            method_name,
2023            arg,
2024            nonce,
2025            sender_info,
2026        } = content
2027        else {
2028            unreachable!()
2029        };
2030        Ok(SignedQuery {
2031            ingress_expiry,
2032            sender,
2033            canister_id,
2034            method_name,
2035            arg,
2036            effective_canister_id,
2037            signed_query,
2038            nonce,
2039            sender_info,
2040        })
2041    }
2042
2043    /// Converts the query builder into [`EnvelopeContent`] for external signing or storage.
2044    pub fn into_envelope(self) -> Result<EnvelopeContent, AgentError> {
2045        self.agent.query_content(
2046            self.canister_id,
2047            self.method_name,
2048            self.arg,
2049            self.ingress_expiry_datetime,
2050            self.use_nonce,
2051        )
2052    }
2053}
2054
2055impl<'agent> IntoFuture for QueryBuilder<'agent> {
2056    type IntoFuture = AgentFuture<'agent, Vec<u8>>;
2057    type Output = Result<Vec<u8>, AgentError>;
2058    fn into_future(self) -> Self::IntoFuture {
2059        Box::pin(self.call())
2060    }
2061}
2062
2063/// An in-flight canister update call. Useful primarily as a `Future`.
2064pub struct UpdateCall<'agent> {
2065    agent: &'agent Agent,
2066    response_future: AgentFuture<'agent, CallResponse<(Vec<u8>, Certificate)>>,
2067    effective_canister_id: Principal,
2068    canister_id: Principal,
2069    method_name: String,
2070}
2071
2072impl fmt::Debug for UpdateCall<'_> {
2073    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2074        f.debug_struct("UpdateCall")
2075            .field("agent", &self.agent)
2076            .field("effective_canister_id", &self.effective_canister_id)
2077            .finish_non_exhaustive()
2078    }
2079}
2080
2081impl Future for UpdateCall<'_> {
2082    type Output = Result<CallResponse<(Vec<u8>, Certificate)>, AgentError>;
2083    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
2084        self.response_future.as_mut().poll(cx)
2085    }
2086}
2087
2088impl<'a> UpdateCall<'a> {
2089    /// Waits for the update call to be completed, polling if necessary.
2090    pub async fn and_wait(self) -> Result<(Vec<u8>, Certificate), AgentError> {
2091        let response = self.response_future.await?;
2092
2093        match response {
2094            CallResponse::Response(response) => Ok(response),
2095            CallResponse::Poll(request_id) => {
2096                self.agent
2097                    .wait_inner(
2098                        &request_id,
2099                        EffectiveId::Canister(self.effective_canister_id),
2100                        Some(Operation::Call {
2101                            canister: self.canister_id,
2102                            method: self.method_name,
2103                        }),
2104                    )
2105                    .await
2106            }
2107        }
2108    }
2109}
2110/// An update request Builder.
2111///
2112/// This makes it easier to do update calls without actually passing all arguments or specifying
2113/// if you want to wait or not.
2114#[derive(Debug)]
2115pub struct UpdateBuilder<'agent> {
2116    agent: &'agent Agent,
2117    /// The [effective canister ID](https://internetcomputer.org/docs/current/references/ic-interface-spec#http-effective-canister-id) of the destination.
2118    pub effective_canister_id: Principal,
2119    /// The principal ID of the canister being called.
2120    pub canister_id: Principal,
2121    /// The name of the canister method being called.
2122    pub method_name: String,
2123    /// The argument blob to be passed to the method.
2124    pub arg: Vec<u8>,
2125    /// The Unix timestamp that the request will expire at.
2126    pub ingress_expiry_datetime: Option<u64>,
2127}
2128
2129impl<'agent> UpdateBuilder<'agent> {
2130    /// Creates a new update builder with an agent for a particular canister method.
2131    pub fn new(agent: &'agent Agent, canister_id: Principal, method_name: String) -> Self {
2132        Self {
2133            agent,
2134            effective_canister_id: canister_id,
2135            canister_id,
2136            method_name,
2137            arg: vec![],
2138            ingress_expiry_datetime: None,
2139        }
2140    }
2141
2142    /// Sets the [effective canister ID](https://internetcomputer.org/docs/current/references/ic-interface-spec#http-effective-canister-id) of the destination.
2143    pub fn with_effective_canister_id(mut self, canister_id: Principal) -> Self {
2144        self.effective_canister_id = canister_id;
2145        self
2146    }
2147
2148    /// Sets the argument blob to pass to the canister. For most canisters this should be a Candid-serialized tuple.
2149    pub fn with_arg<A: Into<Vec<u8>>>(mut self, arg: A) -> Self {
2150        self.arg = arg.into();
2151        self
2152    }
2153
2154    /// Sets `ingress_expiry_datetime` to the provided timestamp, at nanosecond precision.
2155    pub fn expire_at(mut self, time: impl Into<OffsetDateTime>) -> Self {
2156        self.ingress_expiry_datetime = Some(time.into().unix_timestamp_nanos() as u64);
2157        self
2158    }
2159
2160    /// Sets `ingress_expiry_datetime` to `min(now, 4min)`.
2161    pub fn expire_after(mut self, duration: Duration) -> Self {
2162        self.ingress_expiry_datetime = Some(
2163            OffsetDateTime::now_utc()
2164                .saturating_add(duration.try_into().expect("negative duration"))
2165                .unix_timestamp_nanos() as u64,
2166        );
2167        self
2168    }
2169
2170    /// Make an update call. This will call `request_status` on the `RequestId` in a loop and return
2171    /// the response as a byte vector.
2172    pub async fn call_and_wait(self) -> Result<Vec<u8>, AgentError> {
2173        self.call().and_wait().await.map(|x| x.0)
2174    }
2175
2176    /// Make an update call. This will return a `RequestId`.
2177    /// The `RequestId` should then be used for `request_status` (most likely in a loop).
2178    pub fn call(self) -> UpdateCall<'agent> {
2179        let method_name = self.method_name.clone();
2180        let response_future = async move {
2181            self.agent
2182                .update_raw(
2183                    self.canister_id,
2184                    self.effective_canister_id,
2185                    self.method_name,
2186                    self.arg,
2187                    self.ingress_expiry_datetime,
2188                )
2189                .await
2190        };
2191        UpdateCall {
2192            agent: self.agent,
2193            response_future: Box::pin(response_future),
2194            effective_canister_id: self.effective_canister_id,
2195            canister_id: self.canister_id,
2196            method_name,
2197        }
2198    }
2199
2200    /// Sign a update call. This will return a [`signed::SignedUpdate`]
2201    /// which contains all fields of the update and the signed update in CBOR encoding
2202    pub fn sign(self) -> Result<SignedUpdate, AgentError> {
2203        let identity = self.agent.identity.clone();
2204        let effective_canister_id = self.effective_canister_id;
2205        let content = self.into_envelope()?;
2206        let signed_update = sign_envelope(&content, identity)?;
2207        let request_id = to_request_id(&content)?;
2208        let EnvelopeContent::Call {
2209            nonce,
2210            ingress_expiry,
2211            sender,
2212            canister_id,
2213            method_name,
2214            arg,
2215            sender_info,
2216        } = content
2217        else {
2218            unreachable!()
2219        };
2220        Ok(SignedUpdate {
2221            nonce,
2222            ingress_expiry,
2223            sender,
2224            canister_id,
2225            method_name,
2226            arg,
2227            effective_canister_id,
2228            signed_update,
2229            request_id,
2230            sender_info,
2231        })
2232    }
2233
2234    /// Converts the update builder into an [`EnvelopeContent`] for external signing or storage.
2235    pub fn into_envelope(self) -> Result<EnvelopeContent, AgentError> {
2236        let nonce = self.agent.nonce_factory.generate();
2237        self.agent.update_content(
2238            self.canister_id,
2239            self.method_name,
2240            self.arg,
2241            self.ingress_expiry_datetime,
2242            nonce,
2243        )
2244    }
2245}
2246
2247impl<'agent> IntoFuture for UpdateBuilder<'agent> {
2248    type IntoFuture = AgentFuture<'agent, Vec<u8>>;
2249    type Output = Result<Vec<u8>, AgentError>;
2250    fn into_future(self) -> Self::IntoFuture {
2251        Box::pin(self.call_and_wait())
2252    }
2253}
2254
2255/// HTTP client middleware. Implemented automatically for `reqwest`-compatible by-ref `tower::Service`, such as `reqwest_middleware`.
2256#[cfg_attr(target_family = "wasm", async_trait(?Send))]
2257#[cfg_attr(not(target_family = "wasm"), async_trait)]
2258pub trait HttpService: Send + Sync + Debug {
2259    /// Perform a HTTP request. Any retry logic should call `req` again to get a new request.
2260    async fn call<'a>(
2261        &'a self,
2262        req: &'a (dyn Fn() -> Result<http::Request<Bytes>, AgentError> + Send + Sync),
2263        max_retries: usize,
2264        size_limit: Option<usize>,
2265    ) -> Result<http::Response<Bytes>, AgentError>;
2266}
2267
2268/// Convert from http Request to reqwest's one
2269fn from_http_request(req: http::Request<Bytes>) -> Result<Request, AgentError> {
2270    let (parts, body) = req.into_parts();
2271    let body = reqwest::Body::from(body);
2272    // I think it can never fail since it converts from `Url` to `Uri` and `Url` is a subset of `Uri`,
2273    // but just to be safe let's handle it.
2274    let request = http::Request::from_parts(parts, body)
2275        .try_into()
2276        .map_err(|e: reqwest::Error| AgentError::InvalidReplicaUrl(e.to_string()))?;
2277
2278    Ok(request)
2279}
2280
2281/// Convert from reqwests's Response to http one
2282#[cfg(not(target_family = "wasm"))]
2283async fn to_http_response(
2284    resp: Response,
2285    size_limit: Option<usize>,
2286) -> Result<http::Response<Bytes>, AgentError> {
2287    use http_body_util::{BodyExt, Limited};
2288
2289    let resp: http::Response<reqwest::Body> = resp.into();
2290    let (parts, body) = resp.into_parts();
2291    let body = Limited::new(body, size_limit.unwrap_or(usize::MAX));
2292    let body = body
2293        .collect()
2294        .await
2295        .map_err(|e| {
2296            AgentError::TransportError(TransportError::Generic(format!(
2297                "unable to read response body: {e:#}"
2298            )))
2299        })?
2300        .to_bytes();
2301    let resp = http::Response::from_parts(parts, body);
2302
2303    Ok(resp)
2304}
2305
2306/// Convert from reqwests's Response to http one.
2307/// WASM Response in reqwest doesn't have direct conversion for http::Response,
2308/// so we have to hack around using streams.
2309#[cfg(target_family = "wasm")]
2310async fn to_http_response(
2311    resp: Response,
2312    size_limit: Option<usize>,
2313) -> Result<http::Response<Bytes>, AgentError> {
2314    use futures_util::StreamExt;
2315    use http_body::Frame;
2316    use http_body_util::{Limited, StreamBody};
2317
2318    // Save headers
2319    let status = resp.status();
2320    let headers = resp.headers().clone();
2321
2322    // Convert body
2323    let stream = resp.bytes_stream().map(|x| x.map(Frame::data));
2324    let body = StreamBody::new(stream);
2325    let body = Limited::new(body, size_limit.unwrap_or(usize::MAX));
2326    let body = http_body_util::BodyExt::collect(body)
2327        .await
2328        .map_err(|e| {
2329            AgentError::TransportError(TransportError::Generic(format!(
2330                "unable to read response body: {e:#}"
2331            )))
2332        })?
2333        .to_bytes();
2334
2335    let mut resp = http::Response::new(body);
2336    *resp.status_mut() = status;
2337    *resp.headers_mut() = headers;
2338
2339    Ok(resp)
2340}
2341
2342#[cfg(not(target_family = "wasm"))]
2343#[async_trait]
2344impl<T> HttpService for T
2345where
2346    for<'a> &'a T: Service<Request, Response = Response, Error = reqwest::Error>,
2347    for<'a> <&'a Self as Service<Request>>::Future: Send,
2348    T: Send + Sync + Debug + ?Sized,
2349{
2350    #[allow(clippy::needless_arbitrary_self_type)]
2351    async fn call<'a>(
2352        mut self: &'a Self,
2353        req: &'a (dyn Fn() -> Result<http::Request<Bytes>, AgentError> + Send + Sync),
2354        max_retries: usize,
2355        size_limit: Option<usize>,
2356    ) -> Result<http::Response<Bytes>, AgentError> {
2357        let mut retry_count = 0;
2358        loop {
2359            let request = from_http_request(req()?)?;
2360
2361            match Service::call(&mut self, request).await {
2362                Err(err) => {
2363                    // Network-related errors can be retried.
2364                    if err.is_connect() {
2365                        if retry_count >= max_retries {
2366                            return Err(AgentError::TransportError(TransportError::Reqwest(err)));
2367                        }
2368                        retry_count += 1;
2369                    }
2370                    // All other errors return immediately.
2371                    else {
2372                        return Err(AgentError::TransportError(TransportError::Reqwest(err)));
2373                    }
2374                }
2375
2376                Ok(resp) => {
2377                    let resp = to_http_response(resp, size_limit).await?;
2378                    return Ok(resp);
2379                }
2380            }
2381        }
2382    }
2383}
2384
2385#[cfg(target_family = "wasm")]
2386#[async_trait(?Send)]
2387impl<T> HttpService for T
2388where
2389    for<'a> &'a T: Service<Request, Response = Response, Error = reqwest::Error>,
2390    T: Send + Sync + Debug + ?Sized,
2391{
2392    #[allow(clippy::needless_arbitrary_self_type)]
2393    async fn call<'a>(
2394        mut self: &'a Self,
2395        req: &'a (dyn Fn() -> Result<http::Request<Bytes>, AgentError> + Send + Sync),
2396        _retries: usize,
2397        _size_limit: Option<usize>,
2398    ) -> Result<http::Response<Bytes>, AgentError> {
2399        let request = from_http_request(req()?)?;
2400        let response = Service::call(&mut self, request)
2401            .await
2402            .map_err(|e| AgentError::TransportError(TransportError::Reqwest(e)))?;
2403
2404        to_http_response(response, _size_limit).await
2405    }
2406}
2407
2408#[derive(Debug)]
2409struct Retry429Logic {
2410    client: Client,
2411}
2412
2413#[cfg_attr(target_family = "wasm", async_trait(?Send))]
2414#[cfg_attr(not(target_family = "wasm"), async_trait)]
2415impl HttpService for Retry429Logic {
2416    async fn call<'a>(
2417        &'a self,
2418        req: &'a (dyn Fn() -> Result<http::Request<Bytes>, AgentError> + Send + Sync),
2419        _max_tcp_retries: usize,
2420        _size_limit: Option<usize>,
2421    ) -> Result<http::Response<Bytes>, AgentError> {
2422        let mut retries = 0;
2423        loop {
2424            #[cfg(not(target_family = "wasm"))]
2425            let resp = self.client.call(req, _max_tcp_retries, _size_limit).await?;
2426            // Client inconveniently does not implement Service on wasm
2427            #[cfg(target_family = "wasm")]
2428            let resp = {
2429                let request = from_http_request(req()?)?;
2430                let resp = self
2431                    .client
2432                    .execute(request)
2433                    .await
2434                    .map_err(|e| AgentError::TransportError(TransportError::Reqwest(e)))?;
2435
2436                to_http_response(resp, _size_limit).await?
2437            };
2438
2439            if resp.status() == StatusCode::TOO_MANY_REQUESTS {
2440                if retries == 6 {
2441                    break Ok(resp);
2442                } else {
2443                    retries += 1;
2444                    crate::util::sleep(Duration::from_millis(250)).await;
2445                    continue;
2446                }
2447            } else {
2448                break Ok(resp);
2449            }
2450        }
2451    }
2452}
2453
2454#[cfg(all(test, not(target_family = "wasm")))]
2455mod offline_tests {
2456    use super::*;
2457    use tokio::net::TcpListener;
2458    // Any tests that involve the network should go in agent_test, not here.
2459
2460    #[test]
2461    fn rounded_expiry() {
2462        let agent = Agent::builder()
2463            .with_url("http://not-a-real-url")
2464            .build()
2465            .unwrap();
2466        let mut prev_expiry = None;
2467        let mut num_timestamps = 0;
2468        for _ in 0..6 {
2469            let update = agent
2470                .update(&Principal::management_canister(), "not_a_method")
2471                .sign()
2472                .unwrap();
2473            if prev_expiry < Some(update.ingress_expiry) {
2474                prev_expiry = Some(update.ingress_expiry);
2475                num_timestamps += 1;
2476            }
2477        }
2478        // in six requests, there should be no more than two timestamps
2479        assert!(num_timestamps <= 2, "num_timestamps:{num_timestamps} > 2");
2480    }
2481
2482    #[tokio::test]
2483    async fn client_ratelimit() {
2484        let mock_server = TcpListener::bind("127.0.0.1:0").await.unwrap();
2485        let count = Arc::new(Mutex::new(0));
2486        let port = mock_server.local_addr().unwrap().port();
2487        tokio::spawn({
2488            let count = count.clone();
2489            async move {
2490                loop {
2491                    let (mut conn, _) = mock_server.accept().await.unwrap();
2492                    *count.lock().unwrap() += 1;
2493                    tokio::spawn(
2494                        // read all data, never reply
2495                        async move { tokio::io::copy(&mut conn, &mut tokio::io::sink()).await },
2496                    );
2497                }
2498            }
2499        });
2500        let agent = Agent::builder()
2501            .with_http_client(Client::builder().http1_only().build().unwrap())
2502            .with_url(format!("http://127.0.0.1:{port}"))
2503            .with_max_concurrent_requests(2)
2504            .build()
2505            .unwrap();
2506        for _ in 0..3 {
2507            let agent = agent.clone();
2508            tokio::spawn(async move {
2509                agent
2510                    .query(&"ryjl3-tyaaa-aaaaa-aaaba-cai".parse().unwrap(), "greet")
2511                    .call()
2512                    .await
2513            });
2514        }
2515        crate::util::sleep(Duration::from_millis(250)).await;
2516        assert_eq!(*count.lock().unwrap(), 2);
2517    }
2518}