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(RetryLogic {
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    ///
1477    /// This is the only path that may establish a canister-to-subnet mapping, because it is the
1478    /// only one whose canister ranges come from an NNS-root-signed delegation and are checked to
1479    /// contain the canister. Contrast [`fetch_subnet_by_id`](Self::fetch_subnet_by_id).
1480    pub async fn fetch_subnet_by_canister(
1481        &self,
1482        canister: &Principal,
1483    ) -> Result<Arc<Subnet>, AgentError> {
1484        let canister_cert = self
1485            .read_state_raw(vec![vec!["subnet".into()]], *canister)
1486            .await?;
1487        let subnet_id = if let Some(delegation) = canister_cert.delegation.as_ref() {
1488            Principal::try_from_slice(&delegation.subnet_id)?
1489        } else {
1490            // if no delegation, it comes from the root subnet
1491            Principal::self_authenticating(&self.root_key.read().unwrap()[..])
1492        };
1493        let mut subnet = lookup_incomplete_subnet(&subnet_id, &canister_cert)?;
1494        let canister_ranges = if let Some(delegation) = canister_cert.delegation.as_ref() {
1495            // non-root subnets will not serve /subnet/<>/canister_ranges when looked up by canister, but their delegation will contain /canister_ranges
1496            let delegation_cert: Certificate = serde_cbor::from_slice(&delegation.certificate)?;
1497            lookup_canister_ranges(&subnet_id, &delegation_cert)?
1498        } else {
1499            lookup_canister_ranges(&subnet_id, &canister_cert)?
1500        };
1501        subnet.canister_ranges = canister_ranges;
1502        if !subnet.canister_ranges.contains(canister) {
1503            return Err(AgentError::CertificateNotAuthorized());
1504        }
1505        let subnet = Arc::new(subnet);
1506        self.subnet_key_cache
1507            .lock()
1508            .unwrap()
1509            .insert_subnet(subnet_id, subnet.clone());
1510        Ok(subnet)
1511    }
1512
1513    /// Fetches and caches the subnet information for a subnet ID.
1514    ///
1515    /// This function does not read from the cache; most users want
1516    /// [`get_subnet_by_id`](Self::get_subnet_by_id) instead.
1517    ///
1518    /// The returned [`Subnet::iter_canister_ranges`] are *self-reported*: a subnet-scoped
1519    /// `read_state` is answered by the subnet itself, and the NNS-root-signed delegation it
1520    /// carries attests only `/subnet/<id>/public_key`, never the ranges. They are therefore not
1521    /// used to resolve canisters to subnets; that goes through
1522    /// [`fetch_subnet_by_canister`](Self::fetch_subnet_by_canister) instead.
1523    pub async fn fetch_subnet_by_id(
1524        &self,
1525        subnet_id: &Principal,
1526    ) -> Result<Arc<Subnet>, AgentError> {
1527        let subnet_cert = self
1528            .read_subnet_state_raw(
1529                vec![
1530                    vec!["canister_ranges".into(), subnet_id.as_slice().into()],
1531                    vec!["subnet".into(), subnet_id.as_slice().into()],
1532                ],
1533                *subnet_id,
1534            )
1535            .await?;
1536        let subnet = lookup_subnet_and_ranges(subnet_id, &subnet_cert)?;
1537        let subnet = Arc::new(subnet);
1538        // Deliberately keys-only: the ranges in `subnet_cert` are attested by this subnet itself,
1539        // which is not a sufficient basis for resolving canisters to it. See the method docs.
1540        self.subnet_key_cache
1541            .lock()
1542            .unwrap()
1543            .insert_subnet_keys_only(*subnet_id, subnet.clone());
1544        Ok(subnet)
1545    }
1546
1547    async fn request(
1548        &self,
1549        method: Method,
1550        endpoint: &str,
1551        body: Option<Vec<u8>>,
1552    ) -> Result<(StatusCode, HeaderMap, Vec<u8>), AgentError> {
1553        let body = body.map(Bytes::from);
1554
1555        let create_request_with_generated_url = || -> Result<http::Request<Bytes>, AgentError> {
1556            let url = self.route_provider.route()?.join(endpoint)?;
1557            let uri = Uri::from_str(url.as_str())
1558                .map_err(|e| AgentError::InvalidReplicaUrl(e.to_string()))?;
1559            let body = body.clone().unwrap_or_default();
1560            let request = http::Request::builder()
1561                .method(method.clone())
1562                .uri(uri)
1563                .header(CONTENT_TYPE, "application/cbor")
1564                .body(body)
1565                .map_err(|e| {
1566                    AgentError::TransportError(TransportError::Generic(format!(
1567                        "unable to create request: {e:#}"
1568                    )))
1569                })?;
1570
1571            Ok(request)
1572        };
1573
1574        let response = self
1575            .client
1576            .call(
1577                &create_request_with_generated_url,
1578                self.max_tcp_error_retries,
1579                self.max_response_body_size,
1580            )
1581            .await?;
1582
1583        let (parts, body) = response.into_parts();
1584
1585        Ok((parts.status, parts.headers, body.to_vec()))
1586    }
1587
1588    async fn execute(
1589        &self,
1590        method: Method,
1591        endpoint: &str,
1592        body: Option<Vec<u8>>,
1593    ) -> Result<(StatusCode, Vec<u8>), AgentError> {
1594        let request_result = self.request(method.clone(), endpoint, body.clone()).await?;
1595
1596        let status = request_result.0;
1597        let headers = request_result.1;
1598        let body = request_result.2;
1599
1600        if status.is_client_error() || status.is_server_error() {
1601            Err(AgentError::HttpError(HttpErrorPayload {
1602                status: status.into(),
1603                content_type: headers
1604                    .get(CONTENT_TYPE)
1605                    .and_then(|value| value.to_str().ok())
1606                    .map(str::to_string),
1607                content: body,
1608            }))
1609        } else if !(status == StatusCode::OK || status == StatusCode::ACCEPTED) {
1610            Err(AgentError::InvalidHttpResponse(format!(
1611                "Expected `200`, `202`, 4xx`, or `5xx` HTTP status code. Got: {status}",
1612            )))
1613        } else {
1614            Ok((status, body))
1615        }
1616    }
1617}
1618
1619// Checks if a principal is contained within a list of principal ranges
1620// A range is a tuple: (low: Principal, high: Principal), as described here: https://internetcomputer.org/docs/current/references/ic-interface-spec#state-tree-subnet
1621fn principal_is_within_ranges(principal: &Principal, ranges: &[(Principal, Principal)]) -> bool {
1622    ranges
1623        .iter()
1624        .any(|r| principal >= &r.0 && principal <= &r.1)
1625}
1626
1627fn sign_envelope(
1628    content: &EnvelopeContent,
1629    identity: Arc<dyn Identity>,
1630) -> Result<Vec<u8>, AgentError> {
1631    let signature = identity.sign(content).map_err(AgentError::SigningError)?;
1632
1633    let envelope = Envelope {
1634        content: Cow::Borrowed(content),
1635        sender_pubkey: signature.public_key,
1636        sender_sig: signature.signature,
1637        sender_delegation: signature.delegations,
1638    };
1639
1640    let mut serialized_bytes = Vec::new();
1641    let mut serializer = serde_cbor::Serializer::new(&mut serialized_bytes);
1642    serializer.self_describe()?;
1643    envelope.serialize(&mut serializer)?;
1644
1645    Ok(serialized_bytes)
1646}
1647
1648/// Inspect the bytes to be sent as a query
1649/// Return Ok only when the bytes can be deserialized as a query and all fields match with the arguments
1650pub fn signed_query_inspect(
1651    sender: Principal,
1652    canister_id: Principal,
1653    method_name: &str,
1654    arg: &[u8],
1655    ingress_expiry: u64,
1656    signed_query: Vec<u8>,
1657) -> Result<(), AgentError> {
1658    let envelope: Envelope =
1659        serde_cbor::from_slice(&signed_query).map_err(AgentError::InvalidCborData)?;
1660    match envelope.content.as_ref() {
1661        EnvelopeContent::Query {
1662            ingress_expiry: ingress_expiry_cbor,
1663            sender: sender_cbor,
1664            canister_id: canister_id_cbor,
1665            method_name: method_name_cbor,
1666            arg: arg_cbor,
1667            nonce: _nonce,
1668            sender_info: _,
1669        } => {
1670            if ingress_expiry != *ingress_expiry_cbor {
1671                return Err(AgentError::CallDataMismatch {
1672                    field: "ingress_expiry".to_string(),
1673                    value_arg: ingress_expiry.to_string(),
1674                    value_cbor: ingress_expiry_cbor.to_string(),
1675                });
1676            }
1677            if sender != *sender_cbor {
1678                return Err(AgentError::CallDataMismatch {
1679                    field: "sender".to_string(),
1680                    value_arg: sender.to_string(),
1681                    value_cbor: sender_cbor.to_string(),
1682                });
1683            }
1684            if canister_id != *canister_id_cbor {
1685                return Err(AgentError::CallDataMismatch {
1686                    field: "canister_id".to_string(),
1687                    value_arg: canister_id.to_string(),
1688                    value_cbor: canister_id_cbor.to_string(),
1689                });
1690            }
1691            if method_name != *method_name_cbor {
1692                return Err(AgentError::CallDataMismatch {
1693                    field: "method_name".to_string(),
1694                    value_arg: method_name.to_string(),
1695                    value_cbor: method_name_cbor.clone(),
1696                });
1697            }
1698            if arg != *arg_cbor {
1699                return Err(AgentError::CallDataMismatch {
1700                    field: "arg".to_string(),
1701                    value_arg: format!("{arg:?}"),
1702                    value_cbor: format!("{arg_cbor:?}"),
1703                });
1704            }
1705        }
1706        EnvelopeContent::Call { .. } => {
1707            return Err(AgentError::CallDataMismatch {
1708                field: "request_type".to_string(),
1709                value_arg: "query".to_string(),
1710                value_cbor: "call".to_string(),
1711            })
1712        }
1713        EnvelopeContent::ReadState { .. } => {
1714            return Err(AgentError::CallDataMismatch {
1715                field: "request_type".to_string(),
1716                value_arg: "query".to_string(),
1717                value_cbor: "read_state".to_string(),
1718            })
1719        }
1720    }
1721    Ok(())
1722}
1723
1724/// Inspect the bytes to be sent as an update
1725/// Return Ok only when the bytes can be deserialized as an update and all fields match with the arguments
1726pub fn signed_update_inspect(
1727    sender: Principal,
1728    canister_id: Principal,
1729    method_name: &str,
1730    arg: &[u8],
1731    ingress_expiry: u64,
1732    signed_update: Vec<u8>,
1733) -> Result<(), AgentError> {
1734    let envelope: Envelope =
1735        serde_cbor::from_slice(&signed_update).map_err(AgentError::InvalidCborData)?;
1736    match envelope.content.as_ref() {
1737        EnvelopeContent::Call {
1738            nonce: _nonce,
1739            ingress_expiry: ingress_expiry_cbor,
1740            sender: sender_cbor,
1741            canister_id: canister_id_cbor,
1742            method_name: method_name_cbor,
1743            arg: arg_cbor,
1744            sender_info: _,
1745        } => {
1746            if ingress_expiry != *ingress_expiry_cbor {
1747                return Err(AgentError::CallDataMismatch {
1748                    field: "ingress_expiry".to_string(),
1749                    value_arg: ingress_expiry.to_string(),
1750                    value_cbor: ingress_expiry_cbor.to_string(),
1751                });
1752            }
1753            if sender != *sender_cbor {
1754                return Err(AgentError::CallDataMismatch {
1755                    field: "sender".to_string(),
1756                    value_arg: sender.to_string(),
1757                    value_cbor: sender_cbor.to_string(),
1758                });
1759            }
1760            if canister_id != *canister_id_cbor {
1761                return Err(AgentError::CallDataMismatch {
1762                    field: "canister_id".to_string(),
1763                    value_arg: canister_id.to_string(),
1764                    value_cbor: canister_id_cbor.to_string(),
1765                });
1766            }
1767            if method_name != *method_name_cbor {
1768                return Err(AgentError::CallDataMismatch {
1769                    field: "method_name".to_string(),
1770                    value_arg: method_name.to_string(),
1771                    value_cbor: method_name_cbor.clone(),
1772                });
1773            }
1774            if arg != *arg_cbor {
1775                return Err(AgentError::CallDataMismatch {
1776                    field: "arg".to_string(),
1777                    value_arg: format!("{arg:?}"),
1778                    value_cbor: format!("{arg_cbor:?}"),
1779                });
1780            }
1781        }
1782        EnvelopeContent::ReadState { .. } => {
1783            return Err(AgentError::CallDataMismatch {
1784                field: "request_type".to_string(),
1785                value_arg: "call".to_string(),
1786                value_cbor: "read_state".to_string(),
1787            })
1788        }
1789        EnvelopeContent::Query { .. } => {
1790            return Err(AgentError::CallDataMismatch {
1791                field: "request_type".to_string(),
1792                value_arg: "call".to_string(),
1793                value_cbor: "query".to_string(),
1794            })
1795        }
1796    }
1797    Ok(())
1798}
1799
1800/// Inspect the bytes to be sent as a `request_status`
1801/// Return Ok only when the bytes can be deserialized as a `request_status` and all fields match with the arguments
1802pub fn signed_request_status_inspect(
1803    sender: Principal,
1804    request_id: &RequestId,
1805    ingress_expiry: u64,
1806    signed_request_status: Vec<u8>,
1807) -> Result<(), AgentError> {
1808    let paths: Vec<Vec<Label>> = vec![vec!["request_status".into(), request_id.to_vec().into()]];
1809    let envelope: Envelope =
1810        serde_cbor::from_slice(&signed_request_status).map_err(AgentError::InvalidCborData)?;
1811    match envelope.content.as_ref() {
1812        EnvelopeContent::ReadState {
1813            ingress_expiry: ingress_expiry_cbor,
1814            sender: sender_cbor,
1815            paths: paths_cbor,
1816        } => {
1817            if ingress_expiry != *ingress_expiry_cbor {
1818                return Err(AgentError::CallDataMismatch {
1819                    field: "ingress_expiry".to_string(),
1820                    value_arg: ingress_expiry.to_string(),
1821                    value_cbor: ingress_expiry_cbor.to_string(),
1822                });
1823            }
1824            if sender != *sender_cbor {
1825                return Err(AgentError::CallDataMismatch {
1826                    field: "sender".to_string(),
1827                    value_arg: sender.to_string(),
1828                    value_cbor: sender_cbor.to_string(),
1829                });
1830            }
1831
1832            if paths != *paths_cbor {
1833                return Err(AgentError::CallDataMismatch {
1834                    field: "paths".to_string(),
1835                    value_arg: format!("{paths:?}"),
1836                    value_cbor: format!("{paths_cbor:?}"),
1837                });
1838            }
1839        }
1840        EnvelopeContent::Query { .. } => {
1841            return Err(AgentError::CallDataMismatch {
1842                field: "request_type".to_string(),
1843                value_arg: "read_state".to_string(),
1844                value_cbor: "query".to_string(),
1845            })
1846        }
1847        EnvelopeContent::Call { .. } => {
1848            return Err(AgentError::CallDataMismatch {
1849                field: "request_type".to_string(),
1850                value_arg: "read_state".to_string(),
1851                value_cbor: "call".to_string(),
1852            })
1853        }
1854    }
1855    Ok(())
1856}
1857
1858/// A cached subnet, tagged with the provenance of its canister ranges.
1859///
1860/// The ranges decide which subnet may authenticate a response for a given canister, so they
1861/// are only usable for routing when they came from a certificate the NNS root signed. See
1862/// [`SubnetCache::insert_subnet_keys_only`] for why the subnet-scoped path cannot supply that.
1863#[derive(Clone)]
1864struct CachedSubnet {
1865    subnet: Arc<Subnet>,
1866    /// True when `subnet.canister_ranges` came from an NNS-root-signed delegation, i.e. from
1867    /// [`Agent::fetch_subnet_by_canister`]. False when the subnet merely attested them itself.
1868    ranges_authoritative: bool,
1869}
1870
1871#[derive(Clone)]
1872struct SubnetCache {
1873    subnets: TimedCache<Principal, CachedSubnet>,
1874    canister_index: RangeInclusiveMap<Principal, Principal>,
1875}
1876
1877impl SubnetCache {
1878    fn new() -> Self {
1879        Self {
1880            subnets: TimedCache::with_lifespan(Duration::from_secs(300)),
1881            canister_index: RangeInclusiveMap::new(),
1882        }
1883    }
1884
1885    fn get_subnet_by_canister(&mut self, canister: &Principal) -> Option<Arc<Subnet>> {
1886        self.canister_index
1887            .get(canister)
1888            .and_then(|subnet_id| self.subnets.cache_get(subnet_id).cloned())
1889            // Only root-attested ranges may resolve a canister to a subnet — including when
1890            // some earlier lookup put this subnet in `canister_index` legitimately.
1891            .filter(|cached| cached.ranges_authoritative)
1892            .map(|cached| cached.subnet)
1893            .filter(|subnet| subnet.canister_ranges.contains(canister))
1894    }
1895
1896    fn get_subnet_by_id(&mut self, subnet_id: &Principal) -> Option<Arc<Subnet>> {
1897        self.subnets
1898            .cache_get(subnet_id)
1899            .map(|cached| cached.subnet.clone())
1900    }
1901
1902    /// Caches a subnet whose canister ranges came from an NNS-root-signed delegation, and indexes
1903    /// those ranges so canisters within them resolve to this subnet.
1904    fn insert_subnet(&mut self, subnet_id: Principal, subnet: Arc<Subnet>) {
1905        for range in subnet.canister_ranges.iter() {
1906            self.canister_index.insert(range.clone(), subnet_id);
1907        }
1908        self.subnets.cache_set(
1909            subnet_id,
1910            CachedSubnet {
1911                subnet,
1912                ranges_authoritative: true,
1913            },
1914        );
1915    }
1916
1917    /// Caches a subnet looked up by subnet ID, *without* indexing its canister ranges.
1918    ///
1919    /// A subnet-scoped `read_state` is answered by the subnet itself, and the NNS-root-signed
1920    /// delegation it carries contains only `/subnet/<id>/public_key` and `/time` — the canister
1921    /// ranges exist solely in the outer tree, which the subnet signs itself. Ranges of that
1922    /// provenance are not authoritative for deciding which subnet may answer for a canister, so
1923    /// they must not enter the index. Resolution stays with [`Agent::fetch_subnet_by_canister`],
1924    /// which validates ranges against the delegation.
1925    fn insert_subnet_keys_only(&mut self, subnet_id: Principal, subnet: Arc<Subnet>) {
1926        self.subnets.cache_set(
1927            subnet_id,
1928            CachedSubnet {
1929                subnet,
1930                ranges_authoritative: false,
1931            },
1932        );
1933    }
1934}
1935
1936/// API boundary node, which routes /api calls to IC replica nodes.
1937#[derive(Debug, Clone)]
1938pub struct ApiBoundaryNode {
1939    /// Domain name
1940    pub domain: String,
1941    /// IPv6 address in the hexadecimal notation with colons.
1942    pub ipv6_address: String,
1943    /// IPv4 address in the dotted-decimal notation.
1944    pub ipv4_address: Option<String>,
1945}
1946
1947/// A query request builder.
1948///
1949/// This makes it easier to do query calls without actually passing all arguments.
1950#[derive(Debug, Clone)]
1951#[non_exhaustive]
1952pub struct QueryBuilder<'agent> {
1953    agent: &'agent Agent,
1954    /// The [effective canister ID](https://internetcomputer.org/docs/current/references/ic-interface-spec#http-effective-canister-id) of the destination.
1955    pub effective_canister_id: Principal,
1956    /// The principal ID of the canister being called.
1957    pub canister_id: Principal,
1958    /// The name of the canister method being called.
1959    pub method_name: String,
1960    /// The argument blob to be passed to the method.
1961    pub arg: Vec<u8>,
1962    /// The Unix timestamp that the request will expire at.
1963    pub ingress_expiry_datetime: Option<u64>,
1964    /// Whether to include a nonce with the message.
1965    pub use_nonce: bool,
1966}
1967
1968impl<'agent> QueryBuilder<'agent> {
1969    /// Creates a new query builder with an agent for a particular canister method.
1970    pub fn new(agent: &'agent Agent, canister_id: Principal, method_name: String) -> Self {
1971        Self {
1972            agent,
1973            effective_canister_id: canister_id,
1974            canister_id,
1975            method_name,
1976            arg: vec![],
1977            ingress_expiry_datetime: None,
1978            use_nonce: false,
1979        }
1980    }
1981
1982    /// Sets the [effective canister ID](https://internetcomputer.org/docs/current/references/ic-interface-spec#http-effective-canister-id) of the destination.
1983    pub fn with_effective_canister_id(mut self, canister_id: Principal) -> Self {
1984        self.effective_canister_id = canister_id;
1985        self
1986    }
1987
1988    /// Sets the argument blob to pass to the canister. For most canisters this should be a Candid-serialized tuple.
1989    pub fn with_arg<A: Into<Vec<u8>>>(mut self, arg: A) -> Self {
1990        self.arg = arg.into();
1991        self
1992    }
1993
1994    /// Sets `ingress_expiry_datetime` to the provided timestamp, at nanosecond precision.
1995    pub fn expire_at(mut self, time: impl Into<OffsetDateTime>) -> Self {
1996        self.ingress_expiry_datetime = Some(time.into().unix_timestamp_nanos() as u64);
1997        self
1998    }
1999
2000    /// Sets `ingress_expiry_datetime` to `max(now, 4min)`.
2001    pub fn expire_after(mut self, duration: Duration) -> Self {
2002        self.ingress_expiry_datetime = Some(
2003            OffsetDateTime::now_utc()
2004                .saturating_add(duration.try_into().expect("negative duration"))
2005                .unix_timestamp_nanos() as u64,
2006        );
2007        self
2008    }
2009
2010    /// Uses a nonce generated with the agent's configured nonce factory. By default queries do not use nonces,
2011    /// and thus may get a (briefly) cached response.
2012    pub fn with_nonce_generation(mut self) -> Self {
2013        self.use_nonce = true;
2014        self
2015    }
2016
2017    /// Make a query call. This will return a byte vector.
2018    pub async fn call(self) -> Result<Vec<u8>, AgentError> {
2019        self.agent
2020            .query_raw(
2021                self.canister_id,
2022                self.effective_canister_id,
2023                self.method_name,
2024                self.arg,
2025                self.ingress_expiry_datetime,
2026                self.use_nonce,
2027                None,
2028            )
2029            .await
2030    }
2031
2032    /// Make a query call with signature verification. This will return a byte vector.
2033    ///
2034    /// Compared with [call][Self::call], this method will **always** verify the signature of the query response
2035    /// regardless the Agent level configuration from [`AgentBuilder::with_verify_query_signatures`].
2036    pub async fn call_with_verification(self) -> Result<Vec<u8>, AgentError> {
2037        self.agent
2038            .query_raw(
2039                self.canister_id,
2040                self.effective_canister_id,
2041                self.method_name,
2042                self.arg,
2043                self.ingress_expiry_datetime,
2044                self.use_nonce,
2045                Some(true),
2046            )
2047            .await
2048    }
2049
2050    /// Make a query call without signature verification. This will return a byte vector.
2051    ///
2052    /// Compared with [call][Self::call], this method will **never** verify the signature of the query response
2053    /// regardless the Agent level configuration from [`AgentBuilder::with_verify_query_signatures`].
2054    pub async fn call_without_verification(self) -> Result<Vec<u8>, AgentError> {
2055        self.agent
2056            .query_raw(
2057                self.canister_id,
2058                self.effective_canister_id,
2059                self.method_name,
2060                self.arg,
2061                self.ingress_expiry_datetime,
2062                self.use_nonce,
2063                Some(false),
2064            )
2065            .await
2066    }
2067
2068    /// Sign a query call. This will return a [`signed::SignedQuery`]
2069    /// which contains all fields of the query and the signed query in CBOR encoding
2070    pub fn sign(self) -> Result<SignedQuery, AgentError> {
2071        let effective_canister_id = self.effective_canister_id;
2072        let identity = self.agent.identity.clone();
2073        let content = self.into_envelope()?;
2074        let signed_query = sign_envelope(&content, identity)?;
2075        let EnvelopeContent::Query {
2076            ingress_expiry,
2077            sender,
2078            canister_id,
2079            method_name,
2080            arg,
2081            nonce,
2082            sender_info,
2083        } = content
2084        else {
2085            unreachable!()
2086        };
2087        Ok(SignedQuery {
2088            ingress_expiry,
2089            sender,
2090            canister_id,
2091            method_name,
2092            arg,
2093            effective_canister_id,
2094            signed_query,
2095            nonce,
2096            sender_info,
2097        })
2098    }
2099
2100    /// Converts the query builder into [`EnvelopeContent`] for external signing or storage.
2101    pub fn into_envelope(self) -> Result<EnvelopeContent, AgentError> {
2102        self.agent.query_content(
2103            self.canister_id,
2104            self.method_name,
2105            self.arg,
2106            self.ingress_expiry_datetime,
2107            self.use_nonce,
2108        )
2109    }
2110}
2111
2112impl<'agent> IntoFuture for QueryBuilder<'agent> {
2113    type IntoFuture = AgentFuture<'agent, Vec<u8>>;
2114    type Output = Result<Vec<u8>, AgentError>;
2115    fn into_future(self) -> Self::IntoFuture {
2116        Box::pin(self.call())
2117    }
2118}
2119
2120/// An in-flight canister update call. Useful primarily as a `Future`.
2121pub struct UpdateCall<'agent> {
2122    agent: &'agent Agent,
2123    response_future: AgentFuture<'agent, CallResponse<(Vec<u8>, Certificate)>>,
2124    effective_canister_id: Principal,
2125    canister_id: Principal,
2126    method_name: String,
2127}
2128
2129impl fmt::Debug for UpdateCall<'_> {
2130    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2131        f.debug_struct("UpdateCall")
2132            .field("agent", &self.agent)
2133            .field("effective_canister_id", &self.effective_canister_id)
2134            .finish_non_exhaustive()
2135    }
2136}
2137
2138impl Future for UpdateCall<'_> {
2139    type Output = Result<CallResponse<(Vec<u8>, Certificate)>, AgentError>;
2140    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
2141        self.response_future.as_mut().poll(cx)
2142    }
2143}
2144
2145impl<'a> UpdateCall<'a> {
2146    /// Waits for the update call to be completed, polling if necessary.
2147    pub async fn and_wait(self) -> Result<(Vec<u8>, Certificate), AgentError> {
2148        let response = self.response_future.await?;
2149
2150        match response {
2151            CallResponse::Response(response) => Ok(response),
2152            CallResponse::Poll(request_id) => {
2153                self.agent
2154                    .wait_inner(
2155                        &request_id,
2156                        EffectiveId::Canister(self.effective_canister_id),
2157                        Some(Operation::Call {
2158                            canister: self.canister_id,
2159                            method: self.method_name,
2160                        }),
2161                    )
2162                    .await
2163            }
2164        }
2165    }
2166}
2167/// An update request Builder.
2168///
2169/// This makes it easier to do update calls without actually passing all arguments or specifying
2170/// if you want to wait or not.
2171#[derive(Debug)]
2172pub struct UpdateBuilder<'agent> {
2173    agent: &'agent Agent,
2174    /// The [effective canister ID](https://internetcomputer.org/docs/current/references/ic-interface-spec#http-effective-canister-id) of the destination.
2175    pub effective_canister_id: Principal,
2176    /// The principal ID of the canister being called.
2177    pub canister_id: Principal,
2178    /// The name of the canister method being called.
2179    pub method_name: String,
2180    /// The argument blob to be passed to the method.
2181    pub arg: Vec<u8>,
2182    /// The Unix timestamp that the request will expire at.
2183    pub ingress_expiry_datetime: Option<u64>,
2184}
2185
2186impl<'agent> UpdateBuilder<'agent> {
2187    /// Creates a new update builder with an agent for a particular canister method.
2188    pub fn new(agent: &'agent Agent, canister_id: Principal, method_name: String) -> Self {
2189        Self {
2190            agent,
2191            effective_canister_id: canister_id,
2192            canister_id,
2193            method_name,
2194            arg: vec![],
2195            ingress_expiry_datetime: None,
2196        }
2197    }
2198
2199    /// Sets the [effective canister ID](https://internetcomputer.org/docs/current/references/ic-interface-spec#http-effective-canister-id) of the destination.
2200    pub fn with_effective_canister_id(mut self, canister_id: Principal) -> Self {
2201        self.effective_canister_id = canister_id;
2202        self
2203    }
2204
2205    /// Sets the argument blob to pass to the canister. For most canisters this should be a Candid-serialized tuple.
2206    pub fn with_arg<A: Into<Vec<u8>>>(mut self, arg: A) -> Self {
2207        self.arg = arg.into();
2208        self
2209    }
2210
2211    /// Sets `ingress_expiry_datetime` to the provided timestamp, at nanosecond precision.
2212    pub fn expire_at(mut self, time: impl Into<OffsetDateTime>) -> Self {
2213        self.ingress_expiry_datetime = Some(time.into().unix_timestamp_nanos() as u64);
2214        self
2215    }
2216
2217    /// Sets `ingress_expiry_datetime` to `min(now, 4min)`.
2218    pub fn expire_after(mut self, duration: Duration) -> Self {
2219        self.ingress_expiry_datetime = Some(
2220            OffsetDateTime::now_utc()
2221                .saturating_add(duration.try_into().expect("negative duration"))
2222                .unix_timestamp_nanos() as u64,
2223        );
2224        self
2225    }
2226
2227    /// Make an update call. This will call `request_status` on the `RequestId` in a loop and return
2228    /// the response as a byte vector.
2229    pub async fn call_and_wait(self) -> Result<Vec<u8>, AgentError> {
2230        self.call().and_wait().await.map(|x| x.0)
2231    }
2232
2233    /// Make an update call. This will return a `RequestId`.
2234    /// The `RequestId` should then be used for `request_status` (most likely in a loop).
2235    pub fn call(self) -> UpdateCall<'agent> {
2236        let method_name = self.method_name.clone();
2237        let response_future = async move {
2238            self.agent
2239                .update_raw(
2240                    self.canister_id,
2241                    self.effective_canister_id,
2242                    self.method_name,
2243                    self.arg,
2244                    self.ingress_expiry_datetime,
2245                )
2246                .await
2247        };
2248        UpdateCall {
2249            agent: self.agent,
2250            response_future: Box::pin(response_future),
2251            effective_canister_id: self.effective_canister_id,
2252            canister_id: self.canister_id,
2253            method_name,
2254        }
2255    }
2256
2257    /// Sign a update call. This will return a [`signed::SignedUpdate`]
2258    /// which contains all fields of the update and the signed update in CBOR encoding
2259    pub fn sign(self) -> Result<SignedUpdate, AgentError> {
2260        let identity = self.agent.identity.clone();
2261        let effective_canister_id = self.effective_canister_id;
2262        let content = self.into_envelope()?;
2263        let signed_update = sign_envelope(&content, identity)?;
2264        let request_id = to_request_id(&content)?;
2265        let EnvelopeContent::Call {
2266            nonce,
2267            ingress_expiry,
2268            sender,
2269            canister_id,
2270            method_name,
2271            arg,
2272            sender_info,
2273        } = content
2274        else {
2275            unreachable!()
2276        };
2277        Ok(SignedUpdate {
2278            nonce,
2279            ingress_expiry,
2280            sender,
2281            canister_id,
2282            method_name,
2283            arg,
2284            effective_canister_id,
2285            signed_update,
2286            request_id,
2287            sender_info,
2288        })
2289    }
2290
2291    /// Converts the update builder into an [`EnvelopeContent`] for external signing or storage.
2292    pub fn into_envelope(self) -> Result<EnvelopeContent, AgentError> {
2293        let nonce = self.agent.nonce_factory.generate();
2294        self.agent.update_content(
2295            self.canister_id,
2296            self.method_name,
2297            self.arg,
2298            self.ingress_expiry_datetime,
2299            nonce,
2300        )
2301    }
2302}
2303
2304impl<'agent> IntoFuture for UpdateBuilder<'agent> {
2305    type IntoFuture = AgentFuture<'agent, Vec<u8>>;
2306    type Output = Result<Vec<u8>, AgentError>;
2307    fn into_future(self) -> Self::IntoFuture {
2308        Box::pin(self.call_and_wait())
2309    }
2310}
2311
2312/// HTTP client middleware. Implemented automatically for `reqwest`-compatible by-ref `tower::Service`, such as `reqwest_middleware`.
2313#[cfg_attr(target_family = "wasm", async_trait(?Send))]
2314#[cfg_attr(not(target_family = "wasm"), async_trait)]
2315pub trait HttpService: Send + Sync + Debug {
2316    /// Perform a HTTP request. Any retry logic should call `req` again to get a new request.
2317    async fn call<'a>(
2318        &'a self,
2319        req: &'a (dyn Fn() -> Result<http::Request<Bytes>, AgentError> + Send + Sync),
2320        max_retries: usize,
2321        size_limit: Option<usize>,
2322    ) -> Result<http::Response<Bytes>, AgentError>;
2323}
2324
2325/// Convert from http Request to reqwest's one
2326fn from_http_request(req: http::Request<Bytes>) -> Result<Request, AgentError> {
2327    let (parts, body) = req.into_parts();
2328    let body = reqwest::Body::from(body);
2329    // I think it can never fail since it converts from `Url` to `Uri` and `Url` is a subset of `Uri`,
2330    // but just to be safe let's handle it.
2331    let request = http::Request::from_parts(parts, body)
2332        .try_into()
2333        .map_err(|e: reqwest::Error| AgentError::InvalidReplicaUrl(e.to_string()))?;
2334
2335    Ok(request)
2336}
2337
2338/// Convert from reqwests's Response to http one
2339#[cfg(not(target_family = "wasm"))]
2340async fn to_http_response(
2341    resp: Response,
2342    size_limit: Option<usize>,
2343) -> Result<http::Response<Bytes>, AgentError> {
2344    use http_body_util::{BodyExt, Limited};
2345
2346    let resp: http::Response<reqwest::Body> = resp.into();
2347    let (parts, body) = resp.into_parts();
2348    let body = Limited::new(body, size_limit.unwrap_or(usize::MAX));
2349    let body = body
2350        .collect()
2351        .await
2352        .map_err(|e| {
2353            AgentError::TransportError(TransportError::Generic(format!(
2354                "unable to read response body: {e:#}"
2355            )))
2356        })?
2357        .to_bytes();
2358    let resp = http::Response::from_parts(parts, body);
2359
2360    Ok(resp)
2361}
2362
2363/// Convert from reqwests's Response to http one.
2364/// WASM Response in reqwest doesn't have direct conversion for http::Response,
2365/// so we have to hack around using streams.
2366#[cfg(target_family = "wasm")]
2367async fn to_http_response(
2368    resp: Response,
2369    size_limit: Option<usize>,
2370) -> Result<http::Response<Bytes>, AgentError> {
2371    use futures_util::StreamExt;
2372    use http_body::Frame;
2373    use http_body_util::{Limited, StreamBody};
2374
2375    // Save headers
2376    let status = resp.status();
2377    let headers = resp.headers().clone();
2378
2379    // Convert body
2380    let stream = resp.bytes_stream().map(|x| x.map(Frame::data));
2381    let body = StreamBody::new(stream);
2382    let body = Limited::new(body, size_limit.unwrap_or(usize::MAX));
2383    let body = http_body_util::BodyExt::collect(body)
2384        .await
2385        .map_err(|e| {
2386            AgentError::TransportError(TransportError::Generic(format!(
2387                "unable to read response body: {e:#}"
2388            )))
2389        })?
2390        .to_bytes();
2391
2392    let mut resp = http::Response::new(body);
2393    *resp.status_mut() = status;
2394    *resp.headers_mut() = headers;
2395
2396    Ok(resp)
2397}
2398
2399#[cfg(not(target_family = "wasm"))]
2400#[async_trait]
2401impl<T> HttpService for T
2402where
2403    for<'a> &'a T: Service<Request, Response = Response, Error = reqwest::Error>,
2404    for<'a> <&'a Self as Service<Request>>::Future: Send,
2405    T: Send + Sync + Debug + ?Sized,
2406{
2407    #[allow(clippy::needless_arbitrary_self_type)]
2408    async fn call<'a>(
2409        mut self: &'a Self,
2410        req: &'a (dyn Fn() -> Result<http::Request<Bytes>, AgentError> + Send + Sync),
2411        max_retries: usize,
2412        size_limit: Option<usize>,
2413    ) -> Result<http::Response<Bytes>, AgentError> {
2414        let mut retry_count = 0;
2415        loop {
2416            let request = from_http_request(req()?)?;
2417
2418            match Service::call(&mut self, request).await {
2419                Err(err) => {
2420                    // Network-related errors can be retried.
2421                    if err.is_connect() {
2422                        if retry_count >= max_retries {
2423                            return Err(AgentError::TransportError(TransportError::Reqwest(err)));
2424                        }
2425                        retry_count += 1;
2426                    }
2427                    // All other errors return immediately.
2428                    else {
2429                        return Err(AgentError::TransportError(TransportError::Reqwest(err)));
2430                    }
2431                }
2432
2433                Ok(resp) => {
2434                    let resp = to_http_response(resp, size_limit).await?;
2435                    return Ok(resp);
2436                }
2437            }
2438        }
2439    }
2440}
2441
2442#[cfg(target_family = "wasm")]
2443#[async_trait(?Send)]
2444impl<T> HttpService for T
2445where
2446    for<'a> &'a T: Service<Request, Response = Response, Error = reqwest::Error>,
2447    T: Send + Sync + Debug + ?Sized,
2448{
2449    #[allow(clippy::needless_arbitrary_self_type)]
2450    async fn call<'a>(
2451        mut self: &'a Self,
2452        req: &'a (dyn Fn() -> Result<http::Request<Bytes>, AgentError> + Send + Sync),
2453        _retries: usize,
2454        _size_limit: Option<usize>,
2455    ) -> Result<http::Response<Bytes>, AgentError> {
2456        let request = from_http_request(req()?)?;
2457        let response = Service::call(&mut self, request)
2458            .await
2459            .map_err(|e| AgentError::TransportError(TransportError::Reqwest(e)))?;
2460
2461        to_http_response(response, _size_limit).await
2462    }
2463}
2464
2465#[derive(Debug)]
2466struct RetryLogic {
2467    client: Client,
2468}
2469
2470#[cfg_attr(target_family = "wasm", async_trait(?Send))]
2471#[cfg_attr(not(target_family = "wasm"), async_trait)]
2472impl HttpService for RetryLogic {
2473    async fn call<'a>(
2474        &'a self,
2475        req: &'a (dyn Fn() -> Result<http::Request<Bytes>, AgentError> + Send + Sync),
2476        _max_tcp_retries: usize,
2477        _size_limit: Option<usize>,
2478    ) -> Result<http::Response<Bytes>, AgentError> {
2479        let mut retries = 0;
2480        loop {
2481            #[cfg(not(target_family = "wasm"))]
2482            let resp = self.client.call(req, _max_tcp_retries, _size_limit).await?;
2483            // Client inconveniently does not implement Service on wasm
2484            #[cfg(target_family = "wasm")]
2485            let resp = {
2486                let request = from_http_request(req()?)?;
2487                let resp = self
2488                    .client
2489                    .execute(request)
2490                    .await
2491                    .map_err(|e| AgentError::TransportError(TransportError::Reqwest(e)))?;
2492
2493                to_http_response(resp, _size_limit).await?
2494            };
2495
2496            if resp.status() == StatusCode::TOO_MANY_REQUESTS
2497                || resp.status() == StatusCode::SERVICE_UNAVAILABLE
2498            {
2499                if retries == 6 {
2500                    break Ok(resp);
2501                } else {
2502                    retries += 1;
2503                    crate::util::sleep(Duration::from_millis(250)).await;
2504                    continue;
2505                }
2506            } else {
2507                break Ok(resp);
2508            }
2509        }
2510    }
2511}
2512
2513#[cfg(all(test, not(target_family = "wasm")))]
2514mod offline_tests {
2515    use super::*;
2516    use tokio::net::TcpListener;
2517    // Any tests that involve the network should go in agent_test, not here.
2518
2519    #[test]
2520    fn rounded_expiry() {
2521        let agent = Agent::builder()
2522            .with_url("http://not-a-real-url")
2523            .build()
2524            .unwrap();
2525        let mut prev_expiry = None;
2526        let mut num_timestamps = 0;
2527        for _ in 0..6 {
2528            let update = agent
2529                .update(&Principal::management_canister(), "not_a_method")
2530                .sign()
2531                .unwrap();
2532            if prev_expiry < Some(update.ingress_expiry) {
2533                prev_expiry = Some(update.ingress_expiry);
2534                num_timestamps += 1;
2535            }
2536        }
2537        // in six requests, there should be no more than two timestamps
2538        assert!(num_timestamps <= 2, "num_timestamps:{num_timestamps} > 2");
2539    }
2540
2541    #[tokio::test]
2542    async fn client_ratelimit() {
2543        let mock_server = TcpListener::bind("127.0.0.1:0").await.unwrap();
2544        let count = Arc::new(Mutex::new(0));
2545        let port = mock_server.local_addr().unwrap().port();
2546        tokio::spawn({
2547            let count = count.clone();
2548            async move {
2549                loop {
2550                    let (mut conn, _) = mock_server.accept().await.unwrap();
2551                    *count.lock().unwrap() += 1;
2552                    tokio::spawn(
2553                        // read all data, never reply
2554                        async move { tokio::io::copy(&mut conn, &mut tokio::io::sink()).await },
2555                    );
2556                }
2557            }
2558        });
2559        let agent = Agent::builder()
2560            .with_http_client(Client::builder().http1_only().build().unwrap())
2561            .with_url(format!("http://127.0.0.1:{port}"))
2562            .with_max_concurrent_requests(2)
2563            .build()
2564            .unwrap();
2565        for _ in 0..3 {
2566            let agent = agent.clone();
2567            tokio::spawn(async move {
2568                agent
2569                    .query(&"ryjl3-tyaaa-aaaaa-aaaba-cai".parse().unwrap(), "greet")
2570                    .call()
2571                    .await
2572            });
2573        }
2574        crate::util::sleep(Duration::from_millis(250)).await;
2575        assert_eq!(*count.lock().unwrap(), 2);
2576    }
2577}