diff --git a/.github/workflows/ic-ref.yml b/.github/workflows/ic-ref.yml index 0f24d22e..fa699f93 100644 --- a/.github/workflows/ic-ref.yml +++ b/.github/workflows/ic-ref.yml @@ -28,7 +28,7 @@ jobs: - name: Install dfx uses: dfinity/setup-dfx@main with: - dfx-version: "latest" + dfx-version: "0.22.0-beta.0" - name: Cargo cache uses: actions/cache@v4 diff --git a/CHANGELOG.md b/CHANGELOG.md index f676d5b9..6da84493 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Changed `ic_utils::interfaces::management_canister::builders::InstallMode::Upgrade` variant to be `Option`: * `CanisterUpgradeOptions` is a new struct which covers the new upgrade option: `wasm_memory_persistence: Option`. * `WasmMemoryPersistence` is a new enum which controls Wasm main memory retention on upgrades which has two variants: `Keep` and `Replace`. +* Added an experimental feature, `experimental_sync_call`, to enable synchronous update calls. The feature adds a toggle to the `ReqwestTransport` and `HyperTransport` to enable synchronous update calls. ## [0.36.0] - 2024-06-04 diff --git a/ic-agent/Cargo.toml b/ic-agent/Cargo.toml index 8e684898..18923d8e 100644 --- a/ic-agent/Cargo.toml +++ b/ic-agent/Cargo.toml @@ -104,6 +104,7 @@ web-sys = { version = "0.3", features = [ [features] default = ["pem", "reqwest"] +experimental_sync_call = [] reqwest = ["dep:reqwest"] hyper = [ "dep:hyper", diff --git a/ic-agent/src/agent/agent_error.rs b/ic-agent/src/agent/agent_error.rs index 08cf77a1..26e5509f 100644 --- a/ic-agent/src/agent/agent_error.rs +++ b/ic-agent/src/agent/agent_error.rs @@ -205,6 +205,10 @@ pub enum AgentError { /// Route provider failed to generate a url for some reason. #[error("Route provider failed to generate url: {0}")] RouteProviderError(String), + + /// Invalid HTTP response. + #[error("Invalid HTTP response: {0}")] + InvalidHttpResponse(String), } impl PartialEq for AgentError { diff --git a/ic-agent/src/agent/agent_test.rs b/ic-agent/src/agent/agent_test.rs index 3284239b..204aed68 100644 --- a/ic-agent/src/agent/agent_test.rs +++ b/ic-agent/src/agent/agent_test.rs @@ -12,10 +12,11 @@ use crate::{ use candid::{Encode, Nat}; use futures_util::FutureExt; use ic_certification::{Delegation, Label}; -use ic_transport_types::{NodeSignature, QueryResponse, RejectCode, RejectResponse, ReplyResponse}; +use ic_transport_types::{ + NodeSignature, QueryResponse, RejectCode, RejectResponse, ReplyResponse, TransportCallResponse, +}; use reqwest::Client; -use std::sync::Arc; -use std::{collections::BTreeMap, time::Duration}; +use std::{collections::BTreeMap, str::FromStr, sync::Arc, time::Duration}; #[cfg(all(target_family = "wasm", feature = "wasm-bindgen"))] use wasm_bindgen_test::wasm_bindgen_test; @@ -23,9 +24,21 @@ use crate::agent::http_transport::route_provider::{RoundRobinRouteProvider, Rout #[cfg(all(target_family = "wasm", feature = "wasm-bindgen"))] wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); +fn make_transport(url: &str) -> ReqwestTransport { + let transport = ReqwestTransport::create(url).unwrap(); + #[cfg(feature = "experimental_sync_call")] + { + transport.with_use_call_v3_endpoint() + } + #[cfg(not(feature = "experimental_sync_call"))] + { + transport + } +} + fn make_agent(url: &str) -> Agent { Agent::builder() - .with_transport(ReqwestTransport::create(url).unwrap()) + .with_transport(make_transport(url)) .with_verify_query_signatures(false) .build() .unwrap() @@ -214,7 +227,20 @@ async fn query_rejected() -> Result<(), AgentError> { #[cfg_attr(not(target_family = "wasm"), tokio::test)] #[cfg_attr(target_family = "wasm", wasm_bindgen_test)] async fn call_error() -> Result<(), AgentError> { - let (call_mock, url) = mock("POST", "/api/v2/canister/aaaaa-aa/call", 500, vec![], None).await; + let version = if cfg!(feature = "experimental_sync_call") { + "3" + } else { + "2" + }; + + let (call_mock, url) = mock( + "POST", + format!("/api/v{version}/canister/aaaaa-aa/call").as_str(), + 500, + vec![], + None, + ) + .await; let agent = make_agent(&url); @@ -234,17 +260,25 @@ async fn call_error() -> Result<(), AgentError> { #[cfg_attr(not(target_family = "wasm"), tokio::test)] #[cfg_attr(target_family = "wasm", wasm_bindgen_test)] async fn call_rejected() -> Result<(), AgentError> { - let reject_body = RejectResponse { + let reject_response = RejectResponse { reject_code: RejectCode::SysTransient, reject_message: "Test reject message".to_string(), error_code: Some("Test error code".to_string()), }; + let reject_body = TransportCallResponse::NonReplicatedRejection(reject_response.clone()); + let body = serde_cbor::to_vec(&reject_body).unwrap(); + let version = if cfg!(feature = "experimental_sync_call") { + "3" + } else { + "2" + }; + let (call_mock, url) = mock( "POST", - "/api/v2/canister/aaaaa-aa/call", + format!("/api/v{version}/canister/aaaaa-aa/call").as_str(), 200, body, Some("application/cbor"), @@ -261,7 +295,7 @@ async fn call_rejected() -> Result<(), AgentError> { assert_mock(call_mock).await; - let expected_response = Err(AgentError::UncertifiedReject(reject_body)); + let expected_response = Err(AgentError::UncertifiedReject(reject_response)); assert_eq!(expected_response, result); Ok(()) @@ -270,17 +304,27 @@ async fn call_rejected() -> Result<(), AgentError> { #[cfg_attr(not(target_family = "wasm"), tokio::test)] #[cfg_attr(target_family = "wasm", wasm_bindgen_test)] async fn call_rejected_without_error_code() -> Result<(), AgentError> { - let reject_body = RejectResponse { + let non_replicated_reject = RejectResponse { reject_code: RejectCode::SysTransient, reject_message: "Test reject message".to_string(), error_code: None, }; + let reject_body = TransportCallResponse::NonReplicatedRejection(non_replicated_reject.clone()); + + let canister_id_str = "aaaaa-aa"; + let body = serde_cbor::to_vec(&reject_body).unwrap(); + let version = if cfg!(feature = "experimental_sync_call") { + "3" + } else { + "2" + }; + let (call_mock, url) = mock( "POST", - "/api/v2/canister/aaaaa-aa/call", + format!("/api/v{version}/canister/{}/call", canister_id_str).as_str(), 200, body, Some("application/cbor"), @@ -290,14 +334,14 @@ async fn call_rejected_without_error_code() -> Result<(), AgentError> { let agent = make_agent(&url); let result = agent - .update(&Principal::management_canister(), "greet") + .update(&Principal::from_str(canister_id_str).unwrap(), "greet") .with_arg([]) .call() .await; assert_mock(call_mock).await; - let expected_response = Err(AgentError::UncertifiedReject(reject_body)); + let expected_response = Err(AgentError::UncertifiedReject(non_replicated_reject)); assert_eq!(expected_response, result); Ok(()) diff --git a/ic-agent/src/agent/http_transport/hyper_transport.rs b/ic-agent/src/agent/http_transport/hyper_transport.rs index 5b372b8c..f8d71db8 100644 --- a/ic-agent/src/agent/http_transport/hyper_transport.rs +++ b/ic-agent/src/agent/http_transport/hyper_transport.rs @@ -13,6 +13,7 @@ use hyper::{header::CONTENT_TYPE, Method, Request, Response}; use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder}; use hyper_util::client::legacy::{connect::HttpConnector, Client}; use hyper_util::rt::TokioExecutor; +use ic_transport_types::{RejectResponse, TransportCallResponse}; use tower::Service; use crate::{ @@ -22,7 +23,7 @@ use crate::{ AgentFuture, Transport, }, export::Principal, - AgentError, RequestId, + AgentError, }; /// A [`Transport`] using [`hyper`] to make HTTP calls to the Internet Computer. @@ -34,6 +35,7 @@ pub struct HyperTransport, B1>> { #[allow(dead_code)] max_tcp_error_retries: usize, service: S, + use_call_v3_endpoint: bool, } /// Trait representing the contraints on [`HttpBody`] that [`HyperTransport`] requires @@ -56,7 +58,7 @@ where type BodyError = B::Error; } -/// Trait representing the contraints on [`Service`] that [`HyperTransport`] requires. +/// Trait representing the constraints on [`Service`] that [`HyperTransport`] requires. pub trait HyperService: Send + Sync @@ -124,6 +126,7 @@ where service, max_response_body_size: None, max_tcp_error_retries: 0, + use_call_v3_endpoint: false, }) } @@ -143,12 +146,27 @@ where } } + /// Use call v3 endpoint for synchronous update calls. + /// __This is an experimental feature, and should not be used in production, + /// as the endpoint is not available yet on the mainnet IC.__ + /// + /// By enabling this feature, the agent will use the `v3` endpoint for update calls, + /// which is synchronous. This means the replica will wait for a certificate for the call, + /// meaning the agent will not need to poll for the certificate. + #[cfg(feature = "experimental_sync_call")] + pub fn with_use_call_v3_endpoint(self) -> Self { + Self { + use_call_v3_endpoint: true, + ..self + } + } + async fn request( &self, method: Method, endpoint: &str, body: Option>, - ) -> Result, AgentError> { + ) -> Result<(StatusCode, Vec), AgentError> { let body = body.unwrap_or_default(); fn map_error(err: E) -> AgentError { if any::TypeId::of::() == any::TypeId::of::() { @@ -253,7 +271,7 @@ where content: body, })) } else { - Ok(body) + Ok((status, body)) } } } @@ -267,12 +285,36 @@ where &self, effective_canister_id: Principal, envelope: Vec, - _request_id: RequestId, - ) -> AgentFuture<()> { + ) -> AgentFuture { Box::pin(async move { - let endpoint = &format!("canister/{effective_canister_id}/call"); - self.request(Method::POST, endpoint, Some(envelope)).await?; - Ok(()) + let api_version = if self.use_call_v3_endpoint { + "v3" + } else { + "v2" + }; + + let endpoint = format!( + "api/{}/canister/{}/call", + &api_version, + effective_canister_id.to_text() + ); + let (status_code, response_body) = self + .request(Method::POST, &endpoint, Some(envelope)) + .await?; + + if status_code == StatusCode::ACCEPTED { + return Ok(TransportCallResponse::Accepted); + } + + // status_code == OK (200) + if self.use_call_v3_endpoint { + serde_cbor::from_slice(&response_body).map_err(AgentError::InvalidCborData) + } else { + let reject_response = serde_cbor::from_slice::(&response_body) + .map_err(AgentError::InvalidCborData)?; + + Err(AgentError::UncertifiedReject(reject_response)) + } }) } @@ -282,29 +324,37 @@ where envelope: Vec, ) -> AgentFuture> { Box::pin(async move { - let endpoint = &format!("canister/{effective_canister_id}/read_state"); - self.request(Method::POST, endpoint, Some(envelope)).await + let endpoint = format!("canister/{effective_canister_id}/read_state",); + self.request(Method::POST, &endpoint, Some(envelope)) + .await + .map(|(_, body)| body) }) } fn read_subnet_state(&self, subnet_id: Principal, envelope: Vec) -> AgentFuture> { Box::pin(async move { - let endpoint = &format!("subnet/{subnet_id}/read_state"); - self.request(Method::POST, endpoint, Some(envelope)).await + let endpoint = format!("api/v2/subnet/{subnet_id}/read_state",); + self.request(Method::POST, &endpoint, Some(envelope)) + .await + .map(|(_, body)| body) }) } fn query(&self, effective_canister_id: Principal, envelope: Vec) -> AgentFuture> { Box::pin(async move { - let endpoint = &format!("canister/{effective_canister_id}/query"); - self.request(Method::POST, endpoint, Some(envelope)).await + let endpoint = format!("api/v2/canister/{effective_canister_id}/query",); + self.request(Method::POST, &endpoint, Some(envelope)) + .await + .map(|(_, body)| body) }) } fn status(&self) -> AgentFuture> { Box::pin(async move { - let endpoint = "status".to_string(); - self.request(Method::GET, &endpoint, None).await + let endpoint = "api/v2/status"; + self.request(Method::GET, endpoint, None) + .await + .map(|(_, body)| body) }) } } @@ -341,21 +391,21 @@ mod test { ); } - test("https://ic0.app", "https://ic0.app/api/v2/"); - test("https://IC0.app", "https://ic0.app/api/v2/"); - test("https://foo.ic0.app", "https://ic0.app/api/v2/"); - test("https://foo.IC0.app", "https://ic0.app/api/v2/"); - test("https://foo.Ic0.app", "https://ic0.app/api/v2/"); - test("https://foo.iC0.app", "https://ic0.app/api/v2/"); - test("https://foo.bar.ic0.app", "https://ic0.app/api/v2/"); - test("https://ic0.app/foo/", "https://ic0.app/foo/api/v2/"); - test("https://foo.ic0.app/foo/", "https://ic0.app/foo/api/v2/"); - - test("https://ic1.app", "https://ic1.app/api/v2/"); - test("https://foo.ic1.app", "https://foo.ic1.app/api/v2/"); - test("https://ic0.app.ic1.app", "https://ic0.app.ic1.app/api/v2/"); - - test("https://fooic0.app", "https://fooic0.app/api/v2/"); - test("https://fooic0.app.ic0.app", "https://ic0.app/api/v2/"); + test("https://ic0.app", "https://ic0.app/"); + test("https://IC0.app", "https://ic0.app/"); + test("https://foo.ic0.app", "https://ic0.app/"); + test("https://foo.IC0.app", "https://ic0.app/"); + test("https://foo.Ic0.app", "https://ic0.app/"); + test("https://foo.iC0.app", "https://ic0.app/"); + test("https://foo.bar.ic0.app", "https://ic0.app/"); + test("https://ic0.app/foo/", "https://ic0.app/foo/"); + test("https://foo.ic0.app/foo/", "https://ic0.app/foo/"); + + test("https://ic1.app", "https://ic1.app/"); + test("https://foo.ic1.app", "https://foo.ic1.app/"); + test("https://ic0.app.ic1.app", "https://ic0.app.ic1.app/"); + + test("https://fooic0.app", "https://fooic0.app/"); + test("https://fooic0.app.ic0.app", "https://ic0.app/"); } } diff --git a/ic-agent/src/agent/http_transport/reqwest_transport.rs b/ic-agent/src/agent/http_transport/reqwest_transport.rs index a6f7dabd..0d758641 100644 --- a/ic-agent/src/agent/http_transport/reqwest_transport.rs +++ b/ic-agent/src/agent/http_transport/reqwest_transport.rs @@ -1,10 +1,9 @@ //! A [`Transport`] that connects using a [`reqwest`] client. #![cfg(feature = "reqwest")] -use std::{sync::Arc, time::Duration}; - -use ic_transport_types::RejectResponse; +use ic_transport_types::{RejectResponse, TransportCallResponse}; pub use reqwest; +use std::{sync::Arc, time::Duration}; use futures_util::StreamExt; use reqwest::{ @@ -19,7 +18,7 @@ use crate::{ AgentFuture, Transport, }, export::Principal, - AgentError, RequestId, + AgentError, }; /// A [`Transport`] using [`reqwest`] to make HTTP calls to the Internet Computer. @@ -30,6 +29,7 @@ pub struct ReqwestTransport { max_response_body_size: Option, #[allow(dead_code)] max_tcp_error_retries: usize, + use_call_v3_endpoint: bool, } impl ReqwestTransport { @@ -69,6 +69,7 @@ impl ReqwestTransport { client, max_response_body_size: None, max_tcp_error_retries: 0, + use_call_v3_endpoint: false, }) } @@ -88,6 +89,21 @@ impl ReqwestTransport { } } + /// Use call v3 endpoint for synchronous update calls. + /// __This is an experimental feature, and should not be used in production, + /// as the endpoint is not available yet on the mainnet IC.__ + /// + /// By enabling this feature, the agent will use the `v3` endpoint for update calls, + /// which is synchronous. This means the replica will wait for a certificate for the call, + /// meaning the agent will not need to poll for the certificate. + #[cfg(feature = "experimental_sync_call")] + pub fn with_use_call_v3_endpoint(self) -> Self { + ReqwestTransport { + use_call_v3_endpoint: true, + ..self + } + } + async fn request( &self, method: Method, @@ -180,7 +196,7 @@ impl ReqwestTransport { method: Method, endpoint: &str, body: Option>, - ) -> Result, AgentError> { + ) -> Result<(StatusCode, Vec), AgentError> { let request_result = loop { let result = self .request(method.clone(), endpoint, body.as_ref().cloned()) @@ -194,19 +210,7 @@ impl ReqwestTransport { let headers = request_result.1; let body = request_result.2; - // status == OK means we have an error message for call requests - // see https://internetcomputer.org/docs/current/references/ic-interface-spec#http-call - if status == StatusCode::OK && endpoint.ends_with("call") { - let cbor_decoded_body: Result = - serde_cbor::from_slice(&body); - - let agent_error = match cbor_decoded_body { - Ok(replica_error) => AgentError::UncertifiedReject(replica_error), - Err(cbor_error) => AgentError::InvalidCborData(cbor_error), - }; - - Err(agent_error) - } else if status.is_client_error() || status.is_server_error() { + if status.is_client_error() || status.is_server_error() { Err(AgentError::HttpError(HttpErrorPayload { status: status.into(), content_type: headers @@ -215,8 +219,13 @@ impl ReqwestTransport { .map(|x| x.to_string()), content: body, })) + } else if !(status == StatusCode::OK || status == StatusCode::ACCEPTED) { + Err(AgentError::InvalidHttpResponse(format!( + "Expected `200`, `202`, 4xx`, or `5xx` HTTP status code. Got: {}", + status + ))) } else { - Ok(body) + Ok((status, body)) } } } @@ -226,13 +235,36 @@ impl Transport for ReqwestTransport { &self, effective_canister_id: Principal, envelope: Vec, - _request_id: RequestId, - ) -> AgentFuture<()> { + ) -> AgentFuture { Box::pin(async move { - let endpoint = format!("canister/{}/call", effective_canister_id.to_text()); - self.execute(Method::POST, &endpoint, Some(envelope)) + let api_version = if self.use_call_v3_endpoint { + "v3" + } else { + "v2" + }; + + let endpoint = format!( + "api/{}/canister/{}/call", + api_version, + effective_canister_id.to_text() + ); + let (status_code, response_body) = self + .execute(Method::POST, &endpoint, Some(envelope)) .await?; - Ok(()) + + if status_code == StatusCode::ACCEPTED { + return Ok(TransportCallResponse::Accepted); + } + + // status_code == OK (200) + if self.use_call_v3_endpoint { + serde_cbor::from_slice(&response_body).map_err(AgentError::InvalidCborData) + } else { + let reject_response = serde_cbor::from_slice::(&response_body) + .map_err(AgentError::InvalidCborData)?; + + Err(AgentError::UncertifiedReject(reject_response)) + } }) } @@ -241,28 +273,41 @@ impl Transport for ReqwestTransport { effective_canister_id: Principal, envelope: Vec, ) -> AgentFuture> { + let endpoint = format!( + "api/v2/canister/{}/read_state", + effective_canister_id.to_text() + ); + Box::pin(async move { - let endpoint = format!("canister/{effective_canister_id}/read_state"); - self.execute(Method::POST, &endpoint, Some(envelope)).await + self.execute(Method::POST, &endpoint, Some(envelope)) + .await + .map(|r| r.1) }) } fn read_subnet_state(&self, subnet_id: Principal, envelope: Vec) -> AgentFuture> { Box::pin(async move { - let endpoint = format!("subnet/{subnet_id}/read_state"); - self.execute(Method::POST, &endpoint, Some(envelope)).await + let endpoint = format!("api/v2/subnet/{}/read_state", subnet_id.to_text()); + self.execute(Method::POST, &endpoint, Some(envelope)) + .await + .map(|r| r.1) }) } fn query(&self, effective_canister_id: Principal, envelope: Vec) -> AgentFuture> { Box::pin(async move { - let endpoint = format!("canister/{effective_canister_id}/query"); - self.execute(Method::POST, &endpoint, Some(envelope)).await + let endpoint = format!("api/v2/canister/{}/query", effective_canister_id.to_text()); + self.execute(Method::POST, &endpoint, Some(envelope)) + .await + .map(|r| r.1) }) } fn status(&self) -> AgentFuture> { - Box::pin(async move { self.execute(Method::GET, "status", None).await }) + Box::pin(async move { + let endpoint = "api/v2/status"; + self.execute(Method::GET, endpoint, None).await.map(|r| r.1) + }) } } @@ -288,45 +333,45 @@ mod test { ); } - test("https://ic0.app", "https://ic0.app/api/v2/"); - test("https://IC0.app", "https://ic0.app/api/v2/"); - test("https://foo.ic0.app", "https://ic0.app/api/v2/"); - test("https://foo.IC0.app", "https://ic0.app/api/v2/"); - test("https://foo.Ic0.app", "https://ic0.app/api/v2/"); - test("https://foo.iC0.app", "https://ic0.app/api/v2/"); - test("https://foo.bar.ic0.app", "https://ic0.app/api/v2/"); - test("https://ic0.app/foo/", "https://ic0.app/foo/api/v2/"); - test("https://foo.ic0.app/foo/", "https://ic0.app/foo/api/v2/"); + test("https://ic0.app", "https://ic0.app/"); + test("https://IC0.app", "https://ic0.app/"); + test("https://foo.ic0.app", "https://ic0.app/"); + test("https://foo.IC0.app", "https://ic0.app/"); + test("https://foo.Ic0.app", "https://ic0.app/"); + test("https://foo.iC0.app", "https://ic0.app/"); + test("https://foo.bar.ic0.app", "https://ic0.app/"); + test("https://ic0.app/foo/", "https://ic0.app/foo/"); + test("https://foo.ic0.app/foo/", "https://ic0.app/foo/"); test( "https://ryjl3-tyaaa-aaaaa-aaaba-cai.ic0.app", - "https://ic0.app/api/v2/", + "https://ic0.app/", ); - test("https://ic1.app", "https://ic1.app/api/v2/"); - test("https://foo.ic1.app", "https://foo.ic1.app/api/v2/"); - test("https://ic0.app.ic1.app", "https://ic0.app.ic1.app/api/v2/"); + test("https://ic1.app", "https://ic1.app/"); + test("https://foo.ic1.app", "https://foo.ic1.app/"); + test("https://ic0.app.ic1.app", "https://ic0.app.ic1.app/"); - test("https://fooic0.app", "https://fooic0.app/api/v2/"); - test("https://fooic0.app.ic0.app", "https://ic0.app/api/v2/"); + test("https://fooic0.app", "https://fooic0.app/"); + test("https://fooic0.app.ic0.app", "https://ic0.app/"); - test("https://icp0.io", "https://icp0.io/api/v2/"); + test("https://icp0.io", "https://icp0.io/"); test( "https://ryjl3-tyaaa-aaaaa-aaaba-cai.icp0.io", - "https://icp0.io/api/v2/", + "https://icp0.io/", ); - test("https://ic0.app.icp0.io", "https://icp0.io/api/v2/"); + test("https://ic0.app.icp0.io", "https://icp0.io/"); - test("https://icp-api.io", "https://icp-api.io/api/v2/"); + test("https://icp-api.io", "https://icp-api.io/"); test( "https://ryjl3-tyaaa-aaaaa-aaaba-cai.icp-api.io", - "https://icp-api.io/api/v2/", + "https://icp-api.io/", ); - test("https://icp0.io.icp-api.io", "https://icp-api.io/api/v2/"); + test("https://icp0.io.icp-api.io", "https://icp-api.io/"); - test("http://localhost:4943", "http://localhost:4943/api/v2/"); + test("http://localhost:4943", "http://localhost:4943/"); test( "http://ryjl3-tyaaa-aaaaa-aaaba-cai.localhost:4943", - "http://localhost:4943/api/v2/", + "http://localhost:4943/", ); } } diff --git a/ic-agent/src/agent/http_transport/route_provider.rs b/ic-agent/src/agent/http_transport/route_provider.rs index c554ded5..608b2888 100644 --- a/ic-agent/src/agent/http_transport/route_provider.rs +++ b/ic-agent/src/agent/http_transport/route_provider.rs @@ -27,6 +27,7 @@ pub struct RoundRobinRouteProvider { } impl RouteProvider for RoundRobinRouteProvider { + /// Generates a url for the given endpoint. fn route(&self) -> Result { if self.routes.is_empty() { return Err(AgentError::RouteProviderError( @@ -58,7 +59,7 @@ impl RoundRobinRouteProvider { url.set_host(Some(LOCALHOST_DOMAIN))?; } } - url.join("api/v2/") + Ok(url) }) }) .collect(); @@ -88,11 +89,7 @@ mod tests { fn test_routes_rotation() { let provider = RoundRobinRouteProvider::new(vec!["https://url1.com", "https://url2.com"]) .expect("failed to create a route provider"); - let url_strings = [ - "https://url1.com/api/v2/", - "https://url2.com/api/v2/", - "https://url1.com/api/v2/", - ]; + let url_strings = ["https://url1.com", "https://url2.com", "https://url1.com"]; let expected_urls: Vec = url_strings .iter() .map(|url_str| Url::parse(url_str).expect("Invalid URL")) diff --git a/ic-agent/src/agent/mod.rs b/ic-agent/src/agent/mod.rs index 2ab74185..83765186 100644 --- a/ic-agent/src/agent/mod.rs +++ b/ic-agent/src/agent/mod.rs @@ -15,7 +15,7 @@ use cached::{Cached, TimedCache}; use ed25519_consensus::{Error as Ed25519Error, Signature, VerificationKey}; #[doc(inline)] pub use ic_transport_types::{ - signed, Envelope, EnvelopeContent, RejectCode, RejectResponse, ReplyResponse, + signed, CallResponse, Envelope, EnvelopeContent, RejectCode, RejectResponse, ReplyResponse, RequestStatusResponse, }; pub use nonce::{NonceFactory, NonceGenerator}; @@ -39,7 +39,7 @@ use backoff::{exponential::ExponentialBackoff, SystemClock}; use ic_certification::{Certificate, Delegation, Label}; use ic_transport_types::{ signed::{SignedQuery, SignedRequestStatus, SignedUpdate}, - QueryResponse, ReadStateResponse, SubnetMetrics, + QueryResponse, ReadStateResponse, SubnetMetrics, TransportCallResponse, }; use serde::Serialize; use status::Status; @@ -77,16 +77,14 @@ type AgentFuture<'a, V> = Pin> + ' /// /// Any error returned by these methods will bubble up to the code that called the [Agent]. pub trait Transport: Send + Sync { - /// Sends an asynchronous request to a replica. The Request ID is non-mutable and - /// depends on the content of the envelope. + /// Sends a synchronous call request to a replica. /// - /// This normally corresponds to the `/api/v2/canister//call` endpoint. + /// This normally corresponds to the `/api/v3/canister//call` endpoint. fn call( &self, effective_canister_id: Principal, envelope: Vec, - request_id: RequestId, - ) -> AgentFuture<()>; + ) -> AgentFuture; /// Sends a synchronous request to a replica. This call includes the body of the request message /// itself (envelope). @@ -121,10 +119,10 @@ impl Transport for Box { &self, effective_canister_id: Principal, envelope: Vec, - request_id: RequestId, - ) -> AgentFuture<()> { - (**self).call(effective_canister_id, envelope, request_id) + ) -> AgentFuture { + (**self).call(effective_canister_id, envelope) } + fn read_state( &self, effective_canister_id: Principal, @@ -147,9 +145,8 @@ impl Transport for Arc { &self, effective_canister_id: Principal, envelope: Vec, - request_id: RequestId, - ) -> AgentFuture<()> { - (**self).call(effective_canister_id, envelope, request_id) + ) -> AgentFuture { + (**self).call(effective_canister_id, envelope) } fn read_state( &self, @@ -169,20 +166,6 @@ impl Transport for Arc { } } -/// Classification of the result of a request_status_raw (poll) call. -#[derive(Debug)] -pub enum PollResult { - /// The request has been submitted, but we do not know yet if it - /// has been accepted or not. - Submitted, - - /// The request has been received and may be processing. - Accepted, - - /// The request completed and returned some data. - Completed(Vec), -} - /// A low level Agent to make calls to a Replica endpoint. /// /// ```ignore @@ -417,14 +400,12 @@ impl Agent { async fn call_endpoint( &self, effective_canister_id: Principal, - request_id: RequestId, serialized_bytes: Vec, - ) -> Result { + ) -> Result { let _permit = self.concurrent_requests_semaphore.acquire().await; self.transport - .call(effective_canister_id, serialized_bytes, request_id) - .await?; - Ok(request_id) + .call(effective_canister_id, serialized_bytes) + .await } /// The simplest way to do a query call; sends a byte array and will return a byte vector. @@ -583,8 +564,7 @@ impl Agent { }) } - /// The simplest way to do an update call; sends a byte array and will return a RequestId. - /// The RequestId should then be used for request_status (most likely in a loop). + /// The simplest way to do an update call; sends a byte array and will return a response, [`CallResponse`], from the replica. async fn update_raw( &self, canister_id: Principal, @@ -592,7 +572,7 @@ impl Agent { method_name: String, arg: Vec, ingress_expiry_datetime: Option, - ) -> Result { + ) -> Result>, AgentError> { let nonce = self.nonce_factory.generate(); let content = self.update_content( canister_id, @@ -604,23 +584,70 @@ impl Agent { let request_id = to_request_id(&content)?; let serialized_bytes = sign_envelope(&content, self.identity.clone())?; - self.call_endpoint(effective_canister_id, request_id, serialized_bytes) - .await + let response_body = self + .call_endpoint(effective_canister_id, serialized_bytes) + .await?; + + match response_body { + TransportCallResponse::Replied { certificate } => { + let certificate = + serde_cbor::from_slice(&certificate).map_err(AgentError::InvalidCborData)?; + + self.verify(&certificate, effective_canister_id)?; + let status = lookup_request_status(certificate, &request_id)?; + + match status { + RequestStatusResponse::Replied(reply) => Ok(CallResponse::Response(reply.arg)), + RequestStatusResponse::Rejected(reject_response) => { + Err(AgentError::CertifiedReject(reject_response))? + } + _ => Ok(CallResponse::Poll(request_id)), + } + } + TransportCallResponse::Accepted => Ok(CallResponse::Poll(request_id)), + TransportCallResponse::NonReplicatedRejection(reject_response) => { + Err(AgentError::UncertifiedReject(reject_response)) + } + } } - /// Send the signed update to the network. Will return a [`RequestId`]. + /// Send the signed update to the network. Will return a [`CallResponse>`]. /// The bytes will be checked to verify that it is a valid update. /// If you want to inspect the fields of the update, use [`signed_update_inspect`] before calling this method. pub async fn update_signed( &self, effective_canister_id: Principal, signed_update: Vec, - ) -> Result { + ) -> Result>, AgentError> { let envelope: Envelope = serde_cbor::from_slice(&signed_update).map_err(AgentError::InvalidCborData)?; let request_id = to_request_id(&envelope.content)?; - self.call_endpoint(effective_canister_id, request_id, signed_update) - .await + + let response_body = self + .call_endpoint(effective_canister_id, signed_update) + .await?; + + match response_body { + TransportCallResponse::Replied { certificate } => { + let certificate = + serde_cbor::from_slice(&certificate).map_err(AgentError::InvalidCborData)?; + + self.verify(&certificate, effective_canister_id)?; + let status = lookup_request_status(certificate, &request_id)?; + + match status { + RequestStatusResponse::Replied(reply) => Ok(CallResponse::Response(reply.arg)), + RequestStatusResponse::Rejected(reject_response) => { + Err(AgentError::CertifiedReject(reject_response))? + } + _ => Ok(CallResponse::Poll(request_id)), + } + } + TransportCallResponse::Accepted => Ok(CallResponse::Poll(request_id)), + TransportCallResponse::NonReplicatedRejection(reject_response) => { + Err(AgentError::UncertifiedReject(reject_response)) + } + } } fn update_content( @@ -641,34 +668,6 @@ impl Agent { }) } - /// Call request_status on the RequestId once and classify the result - pub async fn poll( - &self, - request_id: &RequestId, - effective_canister_id: Principal, - ) -> Result { - match self - .request_status_raw(request_id, effective_canister_id) - .await? - { - RequestStatusResponse::Unknown => Ok(PollResult::Submitted), - - RequestStatusResponse::Received | RequestStatusResponse::Processing => { - Ok(PollResult::Accepted) - } - - RequestStatusResponse::Replied(ReplyResponse { arg, .. }) => { - Ok(PollResult::Completed(arg)) - } - - RequestStatusResponse::Rejected(response) => Err(AgentError::CertifiedReject(response)), - - RequestStatusResponse::Done => Err(AgentError::RequestStatusDoneNoReply(String::from( - *request_id, - ))), - } - } - fn get_retry_policy() -> ExponentialBackoff { ExponentialBackoffBuilder::new() .with_initial_interval(Duration::from_millis(500)) @@ -730,19 +729,23 @@ impl Agent { /// Call request_status on the RequestId in a loop and return the response as a byte vector. pub async fn wait( &self, - request_id: RequestId, + request_id: &RequestId, effective_canister_id: Principal, ) -> Result, AgentError> { let mut retry_policy = Self::get_retry_policy(); let mut request_accepted = false; loop { - match self.poll(&request_id, effective_canister_id).await? { - PollResult::Submitted => {} - PollResult::Accepted => { + match self + .request_status_raw(request_id, effective_canister_id) + .await? + { + RequestStatusResponse::Unknown => {} + + RequestStatusResponse::Received | RequestStatusResponse::Processing => { if !request_accepted { // The system will return RequestStatusResponse::Unknown - // (PollResult::Submitted) until the request is accepted + // until the request is accepted // and we generally cannot know how long that will take. // State transitions between Received and Processing may be // instantaneous. Therefore, once we know the request is accepted, @@ -752,7 +755,18 @@ impl Agent { request_accepted = true; } } - PollResult::Completed(result) => return Ok(result), + + RequestStatusResponse::Replied(ReplyResponse { arg, .. }) => return Ok(arg), + + RequestStatusResponse::Rejected(response) => { + return Err(AgentError::CertifiedReject(response)) + } + + RequestStatusResponse::Done => { + return Err(AgentError::RequestStatusDoneNoReply(String::from( + *request_id, + ))) + } }; match retry_policy.next_backoff() { @@ -1624,7 +1638,7 @@ impl<'agent> IntoFuture for QueryBuilder<'agent> { /// An in-flight canister update call. Useful primarily as a `Future`. pub struct UpdateCall<'agent> { agent: &'agent Agent, - request_id: AgentFuture<'agent, RequestId>, + response_future: AgentFuture<'agent, CallResponse>>, effective_canister_id: Principal, } @@ -1638,17 +1652,23 @@ impl fmt::Debug for UpdateCall<'_> { } impl Future for UpdateCall<'_> { - type Output = Result; + type Output = Result>, AgentError>; fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - self.request_id.as_mut().poll(cx) + self.response_future.as_mut().poll(cx) } } impl<'a> UpdateCall<'a> { async fn and_wait(self) -> Result, AgentError> { - let request_id = self.request_id.await?; - self.agent - .wait(request_id, self.effective_canister_id) - .await + let response = self.response_future.await?; + + match response { + CallResponse::Response(response) => Ok(response), + CallResponse::Poll(request_id) => { + self.agent + .wait(&request_id, self.effective_canister_id) + .await + } + } } } /// An Update Request Builder. @@ -1720,7 +1740,7 @@ impl<'agent> UpdateBuilder<'agent> { /// Make an update call. This will return a RequestId. /// The RequestId should then be used for request_status (most likely in a loop). pub fn call(self) -> UpdateCall<'agent> { - let request_id_future = async move { + let response_future = async move { self.agent .update_raw( self.canister_id, @@ -1733,7 +1753,7 @@ impl<'agent> UpdateBuilder<'agent> { }; UpdateCall { agent: self.agent, - request_id: Box::pin(request_id_future), + response_future: Box::pin(response_future), effective_canister_id: self.effective_canister_id, } } @@ -1816,7 +1836,11 @@ mod offline_tests { async fn client_ratelimit() { struct SlowTransport(Arc>); impl Transport for SlowTransport { - fn call(&self, _: Principal, _: Vec, _: RequestId) -> AgentFuture<()> { + fn call( + &self, + _effective_canister_id: Principal, + _envelope: Vec, + ) -> AgentFuture { *self.0.lock().unwrap() += 1; Box::pin(pending()) } diff --git a/ic-agent/src/lib.rs b/ic-agent/src/lib.rs index ef10028d..bb249132 100644 --- a/ic-agent/src/lib.rs +++ b/ic-agent/src/lib.rs @@ -121,7 +121,7 @@ use agent::response_authentication::LookupPath; #[doc(inline)] pub use agent::{agent_error, agent_error::AgentError, Agent, NonceFactory, NonceGenerator}; #[doc(inline)] -pub use ic_transport_types::{to_request_id, RequestId, RequestIdError}; +pub use ic_transport_types::{to_request_id, RequestId, RequestIdError, TransportCallResponse}; #[doc(inline)] pub use identity::{Identity, Signature}; diff --git a/ic-transport-types/src/lib.rs b/ic-transport-types/src/lib.rs index 65fc6524..689ecb3e 100644 --- a/ic-transport-types/src/lib.rs +++ b/ic-transport-types/src/lib.rs @@ -117,6 +117,36 @@ pub struct ReadStateResponse { pub certificate: Vec, } +/// The parsed response from a request to the v3 `call` endpoint. A request to the `call` endpoint. +#[derive(Debug, Serialize, Deserialize)] +#[serde(tag = "status", rename_all = "snake_case")] +pub enum TransportCallResponse { + /// The IC responded with a certified response. + Replied { + /// The CBOR serialized certificate for the call response. + #[serde(with = "serde_bytes")] + certificate: Vec, + }, + + /// The replica responded with a non replicated rejection. + NonReplicatedRejection(RejectResponse), + + /// The replica timed out the sync request, but forwarded the ingress message + /// to the canister. The request id should be used to poll for the response + /// The status of the request must be polled. + Accepted, +} + +/// The response from a request to the `call` endpoint. +#[derive(Debug, PartialEq, Eq)] +pub enum CallResponse { + /// The call completed, and the response is available. + Response(Out), + /// The replica timed out the update call, and the request id should be used to poll for the response + /// using the `Agent::wait` method. + Poll(RequestId), +} + /// Possible responses to a query call. #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(tag = "status", rename_all = "snake_case")] diff --git a/ic-utils/src/call.rs b/ic-utils/src/call.rs index cccf69eb..6889bee3 100644 --- a/ic-utils/src/call.rs +++ b/ic-utils/src/call.rs @@ -1,6 +1,10 @@ use async_trait::async_trait; use candid::{decode_args, decode_one, utils::ArgumentDecoder, CandidType}; -use ic_agent::{agent::UpdateBuilder, export::Principal, Agent, AgentError, RequestId}; +use ic_agent::{ + agent::{CallResponse, UpdateBuilder}, + export::Principal, + Agent, AgentError, +}; use serde::de::DeserializeOwned; use std::fmt; use std::future::{Future, IntoFuture}; @@ -48,7 +52,7 @@ pub trait AsyncCall: CallIntoFuture> { /// the result, and try to deserialize it as a [String]. This would be caught by /// Rust type system, but in this case it will be checked at runtime (as Request /// Id does not have a type associated with it). - async fn call(self) -> Result; + async fn call(self) -> Result, AgentError>; /// Execute the call, and wait for an answer using an exponential-backoff strategy. The return /// type is encoded in the trait. @@ -254,8 +258,16 @@ where } /// See [`AsyncCall::call`]. - pub async fn call(self) -> Result { - self.build_call()?.call().await + pub async fn call(self) -> Result, AgentError> { + let response_bytes = match self.build_call()?.call().await? { + CallResponse::Response(response_bytes) => response_bytes, + CallResponse::Poll(request_id) => return Ok(CallResponse::Poll(request_id)), + }; + + let decoded_response = + decode_args(&response_bytes).map_err(|e| AgentError::CandidError(Box::new(e)))?; + + Ok(CallResponse::Response(decoded_response)) } /// See [`AsyncCall::call_and_wait`]. @@ -294,7 +306,7 @@ where Out: for<'de> ArgumentDecoder<'de> + Send + 'agent, { type Value = Out; - async fn call(self) -> Result { + async fn call(self) -> Result, AgentError> { self.call().await } async fn call_and_wait(self) -> Result { @@ -370,8 +382,18 @@ where } /// See [`AsyncCall::call`]. - pub async fn call(self) -> Result { - self.inner.call().await + pub async fn call(self) -> Result, AgentError> { + let raw_response = self.inner.call().await?; + + let response = match raw_response { + CallResponse::Response(response_bytes) => { + let mapped_response = (self.and_then)(response_bytes); + CallResponse::Response(mapped_response.await?) + } + CallResponse::Poll(request_id) => CallResponse::Poll(request_id), + }; + + Ok(response) } /// See [`AsyncCall::call_and_wait`]. pub async fn call_and_wait(self) -> Result { @@ -418,7 +440,7 @@ where { type Value = Out2; - async fn call(self) -> Result { + async fn call(self) -> Result, AgentError> { self.call().await } @@ -495,8 +517,14 @@ where } /// See [`AsyncCall::call`]. - pub async fn call(self) -> Result { - self.inner.call().await + pub async fn call(self) -> Result, AgentError> { + self.inner.call().await.map(|response| match response { + CallResponse::Response(response_bytes) => { + let mapped_response = (self.map)(response_bytes); + CallResponse::Response(mapped_response) + } + CallResponse::Poll(request_id) => CallResponse::Poll(request_id), + }) } /// See [`AsyncCall::call_and_wait`]. @@ -539,7 +567,7 @@ where { type Value = Out2; - async fn call(self) -> Result { + async fn call(self) -> Result, AgentError> { self.call().await } diff --git a/ic-utils/src/canister.rs b/ic-utils/src/canister.rs index 2d12f168..17e9ac77 100644 --- a/ic-utils/src/canister.rs +++ b/ic-utils/src/canister.rs @@ -134,7 +134,7 @@ impl<'agent> Canister<'agent> { /// Call request_status on the RequestId in a loop and return the response as a byte vector. pub async fn wait<'canister>( &'canister self, - request_id: RequestId, + request_id: &RequestId, ) -> Result, AgentError> { self.agent.wait(request_id, self.canister_id).await } diff --git a/ic-utils/src/interfaces/management_canister/builders.rs b/ic-utils/src/interfaces/management_canister/builders.rs index 5cab9802..f81d19af 100644 --- a/ic-utils/src/interfaces/management_canister/builders.rs +++ b/ic-utils/src/interfaces/management_canister/builders.rs @@ -10,20 +10,21 @@ use crate::{ call::AsyncCall, canister::Argument, interfaces::management_canister::MgmtMethod, Canister, }; use async_trait::async_trait; -use candid::utils::ArgumentEncoder; -use candid::{CandidType, Deserialize, Nat}; +use candid::{utils::ArgumentEncoder, CandidType, Deserialize, Nat}; use futures_util::{ future::ready, stream::{self, FuturesUnordered}, FutureExt, Stream, StreamExt, TryStreamExt, }; -use ic_agent::{export::Principal, AgentError, RequestId}; +use ic_agent::{agent::CallResponse, export::Principal, AgentError}; use sha2::{Digest, Sha256}; -use std::collections::BTreeSet; -use std::convert::{From, TryInto}; -use std::future::IntoFuture; -use std::pin::Pin; -use std::str::FromStr; +use std::{ + collections::BTreeSet, + convert::{From, TryInto}, + future::IntoFuture, + pin::Pin, + str::FromStr, +}; /// The set of possible canister settings. Similar to [`DefiniteCanisterSettings`](super::DefiniteCanisterSettings), /// but all the fields are optional. @@ -440,7 +441,7 @@ impl<'agent, 'canister: 'agent> CreateCanisterBuilder<'agent, 'canister> { } /// Make a call. This is equivalent to the [AsyncCall::call]. - pub async fn call(self) -> Result { + pub async fn call(self) -> Result, AgentError> { self.build()?.call().await } @@ -455,7 +456,7 @@ impl<'agent, 'canister: 'agent> CreateCanisterBuilder<'agent, 'canister> { impl<'agent, 'canister: 'agent> AsyncCall for CreateCanisterBuilder<'agent, 'canister> { type Value = (Principal,); - async fn call(self) -> Result { + async fn call(self) -> Result, AgentError> { self.build()?.call().await } @@ -611,7 +612,7 @@ impl<'agent, 'canister: 'agent> InstallCodeBuilder<'agent, 'canister> { } /// Make a call. This is equivalent to the [AsyncCall::call]. - pub async fn call(self) -> Result { + pub async fn call(self) -> Result, AgentError> { self.build()?.call().await } @@ -626,7 +627,7 @@ impl<'agent, 'canister: 'agent> InstallCodeBuilder<'agent, 'canister> { impl<'agent, 'canister: 'agent> AsyncCall for InstallCodeBuilder<'agent, 'canister> { type Value = (); - async fn call(self) -> Result { + async fn call(self) -> Result, AgentError> { self.build()?.call().await } @@ -751,7 +752,7 @@ impl<'agent: 'canister, 'canister> InstallChunkedCodeBuilder<'agent, 'canister> } /// Make the call. This is equivalent to [`AsyncCall::call`]. - pub async fn call(self) -> Result { + pub async fn call(self) -> Result, AgentError> { self.build()?.call().await } @@ -766,7 +767,7 @@ impl<'agent: 'canister, 'canister> InstallChunkedCodeBuilder<'agent, 'canister> impl<'agent, 'canister: 'agent> AsyncCall for InstallChunkedCodeBuilder<'agent, 'canister> { type Value = (); - async fn call(self) -> Result { + async fn call(self) -> Result, AgentError> { self.call().await } @@ -1230,7 +1231,7 @@ impl<'agent, 'canister: 'agent> UpdateCanisterBuilder<'agent, 'canister> { } /// Make a call. This is equivalent to the [AsyncCall::call]. - pub async fn call(self) -> Result { + pub async fn call(self) -> Result, AgentError> { self.build()?.call().await } @@ -1244,7 +1245,7 @@ impl<'agent, 'canister: 'agent> UpdateCanisterBuilder<'agent, 'canister> { #[cfg_attr(not(target_family = "wasm"), async_trait)] impl<'agent, 'canister: 'agent> AsyncCall for UpdateCanisterBuilder<'agent, 'canister> { type Value = (); - async fn call(self) -> Result { + async fn call(self) -> Result, AgentError> { self.build()?.call().await } diff --git a/ic-utils/src/interfaces/wallet.rs b/ic-utils/src/interfaces/wallet.rs index 5591fa4b..a93041b8 100644 --- a/ic-utils/src/interfaces/wallet.rs +++ b/ic-utils/src/interfaces/wallet.rs @@ -18,7 +18,7 @@ use crate::{ }; use async_trait::async_trait; use candid::{decode_args, utils::ArgumentDecoder, CandidType, Deserialize, Nat}; -use ic_agent::{export::Principal, Agent, AgentError, RequestId}; +use ic_agent::{agent::CallResponse, export::Principal, Agent, AgentError}; use once_cell::sync::Lazy; use semver::{Version, VersionReq}; @@ -119,7 +119,7 @@ where } /// Calls the forwarded canister call on the wallet canister. Equivalent to `.build().call()`. - pub fn call(self) -> impl Future> + 'agent { + pub fn call(self) -> impl Future, AgentError>> + 'agent { let call = self.build(); async { call?.call().await } } @@ -139,7 +139,7 @@ where { type Value = Out; - async fn call(self) -> Result { + async fn call(self) -> Result, AgentError> { self.call().await } diff --git a/icx/src/main.rs b/icx/src/main.rs index c624c8ff..c546131c 100644 --- a/icx/src/main.rs +++ b/icx/src/main.rs @@ -7,10 +7,11 @@ use candid::{ use candid_parser::{check_prog, parse_idl_args, parse_idl_value, IDLProg}; use clap::{crate_authors, crate_version, Parser, ValueEnum}; use ic_agent::{ - agent::{self, signed::SignedUpdate}, agent::{ + self, agent_error::HttpErrorPayload, - signed::{SignedQuery, SignedRequestStatus}, + signed::{SignedQuery, SignedRequestStatus, SignedUpdate}, + CallResponse, }, export::Principal, identity::BasicIdentity, @@ -550,14 +551,23 @@ async fn main() -> Result<()> { if let Ok(signed_update) = serde_json::from_str::(&buffer) { fetch_root_key_from_non_ic(&agent, &opts.replica).await?; - let request_id = agent + let call_response = agent .update_signed( signed_update.effective_canister_id, signed_update.signed_update, ) .await .context("Got an AgentError when send the signed update call")?; - eprintln!("RequestID: 0x{}", String::from(request_id)); + + match call_response { + CallResponse::Response(blob) => { + print_idl_blob(&blob, &ArgType::Idl, &None) + .context("Failed to print update result")?; + } + CallResponse::Poll(request_id) => { + eprintln!("RequestID: 0x{}", String::from(request_id)); + } + }; } else if let Ok(signed_query) = serde_json::from_str::(&buffer) { let blob = agent .query_signed( diff --git a/ref-tests/Cargo.toml b/ref-tests/Cargo.toml index 4fb506b3..b8c784c1 100644 --- a/ref-tests/Cargo.toml +++ b/ref-tests/Cargo.toml @@ -18,3 +18,6 @@ tokio = { workspace = true, features = ["full"] } [dev-dependencies] serde_cbor = { workspace = true } ic-certification = { workspace = true } + +[features] +experimental_sync_call = ["ic-agent/experimental_sync_call"] diff --git a/ref-tests/src/utils.rs b/ref-tests/src/utils.rs index 1053c5b9..e701ec4a 100644 --- a/ref-tests/src/utils.rs +++ b/ref-tests/src/utils.rs @@ -99,8 +99,20 @@ pub async fn create_agent(identity: impl Identity + 'static) -> Result() .expect("Could not parse the IC_REF_PORT environment variable as an integer."); + let transport = ReqwestTransport::create(format!("http://127.0.0.1:{}", port)).unwrap(); + let transport = { + #[cfg(feature = "experimental_sync_call")] + { + transport.with_use_call_v3_endpoint() + } + #[cfg(not(feature = "experimental_sync_call"))] + { + transport + } + }; + Agent::builder() - .with_transport(ReqwestTransport::create(format!("http://127.0.0.1:{}", port)).unwrap()) + .with_transport(transport) .with_identity(identity) .build() .map_err(|e| format!("{:?}", e))