1pub(crate) mod agent_config;
3pub mod agent_error;
4pub(crate) mod builder;
5pub mod http_transport;
6pub(crate) mod nonce;
7pub(crate) mod replica_api;
8pub(crate) mod response;
9pub(crate) mod response_authentication;
10pub mod signed;
11pub mod status;
12
13pub use agent_config::AgentConfig;
14pub use agent_error::AgentError;
15pub use builder::AgentBuilder;
16pub use nonce::{NonceFactory, NonceGenerator};
17pub use replica_api::{RejectCode, RejectResponse};
18pub use response::{Replied, RequestStatusResponse};
19
20#[cfg(test)]
21mod agent_test;
22
23use crate::{
24 agent::{
25 replica_api::{
26 CallRequestContent, Envelope, QueryContent, ReadStateContent, ReadStateResponse,
27 },
28 response_authentication::{
29 extract_der, lookup_canister_info, lookup_canister_metadata, lookup_request_status,
30 lookup_value,
31 },
32 },
33 export::Principal,
34 identity::Identity,
35 to_request_id, RequestId,
36};
37use backoff::{backoff::Backoff, ExponentialBackoffBuilder};
38use ic_certification::{Certificate, Delegation, Label};
39use serde::Serialize;
40use status::Status;
41use std::{
42 convert::TryFrom,
43 fmt,
44 future::Future,
45 pin::Pin,
46 sync::{Arc, RwLock},
47 task::{Context, Poll},
48 time::Duration,
49};
50
51const IC_REQUEST_DOMAIN_SEPARATOR: &[u8; 11] = b"\x0Aic-request";
52const IC_STATE_ROOT_DOMAIN_SEPARATOR: &[u8; 14] = b"\x0Dic-state-root";
53
54const 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";
55
56#[cfg(not(target_family = "wasm"))]
57type AgentFuture<'a, V> = Pin<Box<dyn Future<Output = Result<V, AgentError>> + Send + 'a>>;
58
59#[cfg(target_family = "wasm")]
60type AgentFuture<'a, V> = Pin<Box<dyn Future<Output = Result<V, AgentError>> + 'a>>;
61
62pub trait Transport: Send + Sync {
72 fn call(
77 &self,
78 effective_canister_id: Principal,
79 envelope: Vec<u8>,
80 request_id: RequestId,
81 ) -> AgentFuture<()>;
82
83 fn read_state(
88 &self,
89 effective_canister_id: Principal,
90 envelope: Vec<u8>,
91 ) -> AgentFuture<Vec<u8>>;
92
93 fn query(&self, effective_canister_id: Principal, envelope: Vec<u8>) -> AgentFuture<Vec<u8>>;
98
99 fn status(&self) -> AgentFuture<Vec<u8>>;
103}
104
105impl<I: Transport + ?Sized> Transport for Box<I> {
106 fn call(
107 &self,
108 effective_canister_id: Principal,
109 envelope: Vec<u8>,
110 request_id: RequestId,
111 ) -> AgentFuture<()> {
112 (**self).call(effective_canister_id, envelope, request_id)
113 }
114 fn read_state(
115 &self,
116 effective_canister_id: Principal,
117 envelope: Vec<u8>,
118 ) -> AgentFuture<Vec<u8>> {
119 (**self).read_state(effective_canister_id, envelope)
120 }
121 fn query(&self, effective_canister_id: Principal, envelope: Vec<u8>) -> AgentFuture<Vec<u8>> {
122 (**self).query(effective_canister_id, envelope)
123 }
124 fn status(&self) -> AgentFuture<Vec<u8>> {
125 (**self).status()
126 }
127}
128impl<I: Transport + ?Sized> Transport for Arc<I> {
129 fn call(
130 &self,
131 effective_canister_id: Principal,
132 envelope: Vec<u8>,
133 request_id: RequestId,
134 ) -> AgentFuture<()> {
135 (**self).call(effective_canister_id, envelope, request_id)
136 }
137 fn read_state(
138 &self,
139 effective_canister_id: Principal,
140 envelope: Vec<u8>,
141 ) -> AgentFuture<Vec<u8>> {
142 (**self).read_state(effective_canister_id, envelope)
143 }
144 fn query(&self, effective_canister_id: Principal, envelope: Vec<u8>) -> AgentFuture<Vec<u8>> {
145 (**self).query(effective_canister_id, envelope)
146 }
147 fn status(&self) -> AgentFuture<Vec<u8>> {
148 (**self).status()
149 }
150}
151
152#[derive(Debug)]
154pub enum PollResult {
155 Submitted,
158
159 Accepted,
161
162 Completed(Vec<u8>),
164}
165
166#[derive(Clone)]
235pub struct Agent {
236 nonce_factory: Arc<dyn NonceGenerator>,
237 identity: Arc<dyn Identity>,
238 ingress_expiry: Duration,
239 root_key: Arc<RwLock<Vec<u8>>>,
240 transport: Arc<dyn Transport>,
241}
242
243impl fmt::Debug for Agent {
244 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
245 f.debug_struct("Agent")
246 .field("ingress_expiry", &self.ingress_expiry)
247 .finish_non_exhaustive()
248 }
249}
250
251impl Agent {
252 pub fn builder() -> builder::AgentBuilder {
255 Default::default()
256 }
257
258 pub fn new(config: agent_config::AgentConfig) -> Result<Agent, AgentError> {
260 Ok(Agent {
261 nonce_factory: config.nonce_factory,
262 identity: config.identity,
263 ingress_expiry: config
264 .ingress_expiry
265 .unwrap_or_else(|| Duration::from_secs(300)),
266 root_key: Arc::new(RwLock::new(IC_ROOT_KEY.to_vec())),
267 transport: config
268 .transport
269 .ok_or_else(AgentError::MissingReplicaTransport)?,
270 })
271 }
272
273 pub fn set_transport<F: 'static + Transport>(&mut self, transport: F) {
275 self.transport = Arc::new(transport);
276 }
277
278 pub fn set_identity<I>(&mut self, identity: I)
284 where
285 I: 'static + Identity,
286 {
287 self.identity = Arc::new(identity);
288 }
289
290 pub async fn fetch_root_key(&self) -> Result<(), AgentError> {
299 if self.read_root_key() != IC_ROOT_KEY.to_vec() {
300 return Ok(());
302 }
303 let status = self.status().await?;
304 let root_key = status
305 .root_key
306 .clone()
307 .ok_or(AgentError::NoRootKeyInStatus(status))?;
308 self.set_root_key(root_key);
309 Ok(())
310 }
311
312 pub fn set_root_key(&self, root_key: Vec<u8>) {
317 *self.root_key.write().unwrap() = root_key;
318 }
319
320 pub fn read_root_key(&self) -> Vec<u8> {
322 self.root_key.read().unwrap().clone()
323 }
324
325 fn get_expiry_date(&self) -> u64 {
326 let permitted_drift = Duration::from_secs(60);
328 (self
329 .ingress_expiry
330 .saturating_add({
331 #[cfg(not(target_family = "wasm"))]
332 {
333 std::time::SystemTime::now()
334 .duration_since(std::time::UNIX_EPOCH)
335 .expect("Time wrapped around.")
336 }
337 #[cfg(all(target_family = "wasm", feature = "wasm-bindgen"))]
338 {
339 Duration::from_nanos((js_sys::Date::now() * 1_000_000.) as _)
340 }
341 })
342 .saturating_sub(permitted_drift))
343 .as_nanos() as u64
344 }
345
346 pub fn get_principal(&self) -> Result<Principal, String> {
348 self.identity.sender()
349 }
350
351 async fn query_endpoint<A>(
352 &self,
353 effective_canister_id: Principal,
354 serialized_bytes: Vec<u8>,
355 ) -> Result<A, AgentError>
356 where
357 A: serde::de::DeserializeOwned,
358 {
359 let bytes = self
360 .transport
361 .query(effective_canister_id, serialized_bytes)
362 .await?;
363 serde_cbor::from_slice(&bytes).map_err(AgentError::InvalidCborData)
364 }
365
366 async fn read_state_endpoint<A>(
367 &self,
368 effective_canister_id: Principal,
369 serialized_bytes: Vec<u8>,
370 ) -> Result<A, AgentError>
371 where
372 A: serde::de::DeserializeOwned,
373 {
374 let bytes = self
375 .transport
376 .read_state(effective_canister_id, serialized_bytes)
377 .await?;
378 serde_cbor::from_slice(&bytes).map_err(AgentError::InvalidCborData)
379 }
380
381 async fn call_endpoint(
382 &self,
383 effective_canister_id: Principal,
384 request_id: RequestId,
385 serialized_bytes: Vec<u8>,
386 ) -> Result<RequestId, AgentError> {
387 self.transport
388 .call(effective_canister_id, serialized_bytes, request_id)
389 .await?;
390 Ok(request_id)
391 }
392
393 async fn query_raw(
396 &self,
397 canister_id: &Principal,
398 effective_canister_id: Principal,
399 method_name: &str,
400 arg: &[u8],
401 ingress_expiry_datetime: Option<u64>,
402 ) -> Result<Vec<u8>, AgentError> {
403 let request = self.query_content(canister_id, method_name, arg, ingress_expiry_datetime)?;
404 let serialized_bytes = sign_request(&request, self.identity.clone())?;
405 self.query_endpoint::<replica_api::QueryResponse>(effective_canister_id, serialized_bytes)
406 .await
407 .and_then(|response| match response {
408 replica_api::QueryResponse::Replied { reply } => Ok(reply.arg),
409 replica_api::QueryResponse::Rejected(response) => {
410 Err(AgentError::ReplicaError(response))
411 }
412 })
413 }
414
415 pub async fn query_signed(
419 &self,
420 effective_canister_id: Principal,
421 signed_query: Vec<u8>,
422 ) -> Result<Vec<u8>, AgentError> {
423 let _envelope: Envelope<QueryContent> =
424 serde_cbor::from_slice(&signed_query).map_err(AgentError::InvalidCborData)?;
425 self.query_endpoint::<replica_api::QueryResponse>(effective_canister_id, signed_query)
426 .await
427 .and_then(|response| match response {
428 replica_api::QueryResponse::Replied { reply } => Ok(reply.arg),
429 replica_api::QueryResponse::Rejected(response) => {
430 Err(AgentError::ReplicaError(response))
431 }
432 })
433 }
434
435 fn query_content(
436 &self,
437 canister_id: &Principal,
438 method_name: &str,
439 arg: &[u8],
440 ingress_expiry_datetime: Option<u64>,
441 ) -> Result<QueryContent, AgentError> {
442 Ok(QueryContent::QueryRequest {
443 sender: self.identity.sender().map_err(AgentError::SigningError)?,
444 canister_id: *canister_id,
445 method_name: method_name.to_string(),
446 arg: arg.to_vec(),
447 ingress_expiry: ingress_expiry_datetime.unwrap_or_else(|| self.get_expiry_date()),
448 })
449 }
450
451 async fn update_raw(
454 &self,
455 canister_id: &Principal,
456 effective_canister_id: Principal,
457 method_name: &str,
458 arg: &[u8],
459 ingress_expiry_datetime: Option<u64>,
460 ) -> Result<RequestId, AgentError> {
461 let request =
462 self.update_content(canister_id, method_name, arg, ingress_expiry_datetime)?;
463 let request_id = to_request_id(&request)?;
464 let serialized_bytes = sign_request(&request, self.identity.clone())?;
465
466 self.call_endpoint(effective_canister_id, request_id, serialized_bytes)
467 .await
468 }
469
470 pub async fn update_signed(
474 &self,
475 effective_canister_id: Principal,
476 signed_update: Vec<u8>,
477 ) -> Result<RequestId, AgentError> {
478 let envelope: Envelope<CallRequestContent> =
479 serde_cbor::from_slice(&signed_update).map_err(AgentError::InvalidCborData)?;
480 let request_id = to_request_id(&envelope.content)?;
481 self.call_endpoint(effective_canister_id, request_id, signed_update)
482 .await
483 }
484
485 fn update_content(
486 &self,
487 canister_id: &Principal,
488 method_name: &str,
489 arg: &[u8],
490 ingress_expiry_datetime: Option<u64>,
491 ) -> Result<CallRequestContent, AgentError> {
492 Ok(CallRequestContent::CallRequest {
493 canister_id: *canister_id,
494 method_name: method_name.into(),
495 arg: arg.to_vec(),
496 nonce: self.nonce_factory.generate().map(|b| b.as_slice().into()),
497 sender: self.identity.sender().map_err(AgentError::SigningError)?,
498 ingress_expiry: ingress_expiry_datetime.unwrap_or_else(|| self.get_expiry_date()),
499 })
500 }
501
502 pub async fn poll(
504 &self,
505 request_id: &RequestId,
506 effective_canister_id: Principal,
507 ) -> Result<PollResult, AgentError> {
508 match self
509 .request_status_raw(request_id, effective_canister_id)
510 .await?
511 {
512 RequestStatusResponse::Unknown => Ok(PollResult::Submitted),
513
514 RequestStatusResponse::Received | RequestStatusResponse::Processing => {
515 Ok(PollResult::Accepted)
516 }
517
518 RequestStatusResponse::Replied {
519 reply: Replied::CallReplied(arg),
520 } => Ok(PollResult::Completed(arg)),
521
522 RequestStatusResponse::Rejected(response) => Err(AgentError::ReplicaError(response)),
523
524 RequestStatusResponse::Done => Err(AgentError::RequestStatusDoneNoReply(String::from(
525 *request_id,
526 ))),
527 }
528 }
529
530 pub async fn wait(
532 &self,
533 request_id: RequestId,
534 effective_canister_id: Principal,
535 ) -> Result<Vec<u8>, AgentError> {
536 let mut retry_policy = ExponentialBackoffBuilder::new()
537 .with_initial_interval(Duration::from_millis(500))
538 .with_max_interval(Duration::from_secs(1))
539 .with_multiplier(1.4)
540 .with_max_elapsed_time(Some(Duration::from_secs(60 * 5)))
541 .build();
542 let mut request_accepted = false;
543 loop {
544 match self.poll(&request_id, effective_canister_id).await? {
545 PollResult::Submitted => {}
546 PollResult::Accepted => {
547 if !request_accepted {
548 retry_policy.reset();
556 request_accepted = true;
557 }
558 }
559 PollResult::Completed(result) => return Ok(result),
560 };
561
562 match retry_policy.next_backoff() {
563 #[cfg(not(target_family = "wasm"))]
564 Some(duration) => tokio::time::sleep(duration).await,
565 #[cfg(all(target_family = "wasm", feature = "wasm-bindgen"))]
566 Some(duration) => {
567 wasm_bindgen_futures::JsFuture::from(js_sys::Promise::new(&mut |rs, rj| {
568 if let Err(e) = web_sys::window()
569 .expect("global window unavailable")
570 .set_timeout_with_callback_and_timeout_and_arguments_0(
571 &rs,
572 duration.as_millis() as _,
573 )
574 {
575 use wasm_bindgen::UnwrapThrowExt;
576 rj.call1(&rj, &e).unwrap_throw();
577 }
578 }))
579 .await
580 .expect("unable to setTimeout");
581 }
582 None => return Err(AgentError::TimeoutWaitingForResponse()),
583 }
584 }
585 }
586
587 pub async fn read_state_raw(
589 &self,
590 paths: Vec<Vec<Label>>,
591 effective_canister_id: Principal,
592 ) -> Result<Certificate, AgentError> {
593 let request = self.read_state_content(paths)?;
594 let serialized_bytes = sign_request(&request, self.identity.clone())?;
595
596 let read_state_response: ReadStateResponse = self
597 .read_state_endpoint(effective_canister_id, serialized_bytes)
598 .await?;
599 let cert: Certificate = serde_cbor::from_slice(&read_state_response.certificate)
600 .map_err(AgentError::InvalidCborData)?;
601 self.verify(&cert, effective_canister_id)?;
602 Ok(cert)
603 }
604
605 fn read_state_content(&self, paths: Vec<Vec<Label>>) -> Result<ReadStateContent, AgentError> {
606 Ok(ReadStateContent::ReadStateRequest {
607 sender: self.identity.sender().map_err(AgentError::SigningError)?,
608 paths,
609 ingress_expiry: self.get_expiry_date(),
610 })
611 }
612
613 pub fn verify(
616 &self,
617 cert: &Certificate,
618 effective_canister_id: Principal,
619 ) -> Result<(), AgentError> {
620 let sig = &cert.signature;
621
622 let root_hash = cert.tree.digest();
623 let mut msg = vec![];
624 msg.extend_from_slice(IC_STATE_ROOT_DOMAIN_SEPARATOR);
625 msg.extend_from_slice(&root_hash);
626
627 let der_key = self.check_delegation(&cert.delegation, effective_canister_id)?;
628 let key = extract_der(der_key)?;
629
630 ic_verify_bls_signature::verify_bls_signature(sig, &msg, &key)
631 .map_err(|_| AgentError::CertificateVerificationFailed())
632 }
633
634 fn check_delegation(
635 &self,
636 delegation: &Option<Delegation>,
637 effective_canister_id: Principal,
638 ) -> Result<Vec<u8>, AgentError> {
639 match delegation {
640 None => Ok(self.read_root_key()),
641 Some(delegation) => {
642 let cert: Certificate = serde_cbor::from_slice(&delegation.certificate)
643 .map_err(AgentError::InvalidCborData)?;
644 self.verify(&cert, effective_canister_id)?;
645 let canister_range_lookup = [
646 "subnet".as_bytes(),
647 delegation.subnet_id.as_ref(),
648 "canister_ranges".as_bytes(),
649 ];
650 let canister_range = lookup_value(&cert, canister_range_lookup)?;
651 let ranges: Vec<(Principal, Principal)> =
652 serde_cbor::from_slice(canister_range).map_err(AgentError::InvalidCborData)?;
653 if !principal_is_within_ranges(&effective_canister_id, &ranges[..]) {
654 return Err(AgentError::CertificateNotAuthorized());
656 }
657
658 let public_key_path = [
659 "subnet".as_bytes(),
660 delegation.subnet_id.as_ref(),
661 "public_key".as_bytes(),
662 ];
663 lookup_value(&cert, public_key_path).map(|pk| pk.to_vec())
664 }
665 }
666 }
667
668 pub async fn read_state_canister_info(
670 &self,
671 canister_id: Principal,
672 path: &str,
673 ) -> Result<Vec<u8>, AgentError> {
674 let paths: Vec<Vec<Label>> = vec![vec![
675 "canister".into(),
676 Label::from_bytes(canister_id.as_slice()),
677 path.into(),
678 ]];
679
680 let cert = self.read_state_raw(paths, canister_id).await?;
681
682 lookup_canister_info(cert, canister_id, path)
683 }
684
685 pub async fn read_state_canister_metadata(
687 &self,
688 canister_id: Principal,
689 path: &str,
690 ) -> Result<Vec<u8>, AgentError> {
691 let paths: Vec<Vec<Label>> = vec![vec![
692 "canister".into(),
693 Label::from_bytes(canister_id.as_slice()),
694 "metadata".into(),
695 path.into(),
696 ]];
697
698 let cert = self.read_state_raw(paths, canister_id).await?;
699
700 lookup_canister_metadata(cert, canister_id, path)
701 }
702
703 pub async fn request_status_raw(
705 &self,
706 request_id: &RequestId,
707 effective_canister_id: Principal,
708 ) -> Result<RequestStatusResponse, AgentError> {
709 let paths: Vec<Vec<Label>> =
710 vec![vec!["request_status".into(), request_id.to_vec().into()]];
711
712 let cert = self.read_state_raw(paths, effective_canister_id).await?;
713
714 lookup_request_status(cert, request_id)
715 }
716
717 pub async fn request_status_signed(
721 &self,
722 request_id: &RequestId,
723 effective_canister_id: Principal,
724 signed_request_status: Vec<u8>,
725 ) -> Result<RequestStatusResponse, AgentError> {
726 let _envelope: Envelope<ReadStateContent> =
727 serde_cbor::from_slice(&signed_request_status).map_err(AgentError::InvalidCborData)?;
728 let read_state_response: ReadStateResponse = self
729 .read_state_endpoint(effective_canister_id, signed_request_status)
730 .await?;
731
732 let cert: Certificate = serde_cbor::from_slice(&read_state_response.certificate)
733 .map_err(AgentError::InvalidCborData)?;
734 self.verify(&cert, effective_canister_id)?;
735 lookup_request_status(cert, request_id)
736 }
737
738 pub fn update<S: Into<String>>(
741 &self,
742 canister_id: &Principal,
743 method_name: S,
744 ) -> UpdateBuilder {
745 UpdateBuilder::new(self, *canister_id, method_name.into())
746 }
747
748 pub async fn status(&self) -> Result<Status, AgentError> {
750 let bytes = self.transport.status().await?;
751
752 let cbor: serde_cbor::Value =
753 serde_cbor::from_slice(&bytes).map_err(AgentError::InvalidCborData)?;
754
755 Status::try_from(&cbor).map_err(|_| AgentError::InvalidReplicaStatus)
756 }
757
758 pub fn query<S: Into<String>>(&self, canister_id: &Principal, method_name: S) -> QueryBuilder {
761 QueryBuilder::new(self, *canister_id, method_name.into())
762 }
763
764 pub fn sign_request_status(
767 &self,
768 effective_canister_id: Principal,
769 request_id: RequestId,
770 ) -> Result<signed::SignedRequestStatus, AgentError> {
771 let paths: Vec<Vec<Label>> =
772 vec![vec!["request_status".into(), request_id.to_vec().into()]];
773 let read_state_content = self.read_state_content(paths)?;
774 let signed_request_status = sign_request(&read_state_content, self.identity.clone())?;
775 match read_state_content {
776 ReadStateContent::ReadStateRequest {
777 ingress_expiry,
778 sender,
779 paths: _path,
780 } => Ok(signed::SignedRequestStatus {
781 ingress_expiry,
782 sender,
783 effective_canister_id,
784 request_id,
785 signed_request_status,
786 }),
787 }
788 }
789}
790
791fn principal_is_within_ranges(principal: &Principal, ranges: &[(Principal, Principal)]) -> bool {
794 ranges
795 .iter()
796 .any(|r| principal >= &r.0 && principal <= &r.1)
797}
798
799fn construct_message(request_id: &RequestId) -> Vec<u8> {
800 let mut buf = vec![];
801 buf.extend_from_slice(IC_REQUEST_DOMAIN_SEPARATOR);
802 buf.extend_from_slice(request_id.as_slice());
803 buf
804}
805
806fn sign_request<'a, V>(request: &V, identity: Arc<dyn Identity>) -> Result<Vec<u8>, AgentError>
807where
808 V: 'a + Serialize,
809{
810 let request_id = to_request_id(&request)?;
811 let msg = construct_message(&request_id);
812 let signature = identity.sign(&msg).map_err(AgentError::SigningError)?;
813
814 let envelope = Envelope {
815 content: request,
816 sender_pubkey: signature.public_key,
817 sender_sig: signature.signature,
818 };
819
820 let mut serialized_bytes = Vec::new();
821 let mut serializer = serde_cbor::Serializer::new(&mut serialized_bytes);
822 serializer.self_describe()?;
823 envelope.serialize(&mut serializer)?;
824
825 Ok(serialized_bytes)
826}
827
828pub fn signed_query_inspect(
831 sender: Principal,
832 canister_id: Principal,
833 method_name: &str,
834 arg: &[u8],
835 ingress_expiry: u64,
836 signed_query: Vec<u8>,
837) -> Result<(), AgentError> {
838 let envelope: Envelope<QueryContent> =
839 serde_cbor::from_slice(&signed_query).map_err(AgentError::InvalidCborData)?;
840 match envelope.content {
841 QueryContent::QueryRequest {
842 ingress_expiry: ingress_expiry_cbor,
843 sender: sender_cbor,
844 canister_id: canister_id_cbor,
845 method_name: method_name_cbor,
846 arg: arg_cbor,
847 } => {
848 if ingress_expiry != ingress_expiry_cbor {
849 return Err(AgentError::CallDataMismatch {
850 field: "ingress_expiry".to_string(),
851 value_arg: ingress_expiry.to_string(),
852 value_cbor: ingress_expiry_cbor.to_string(),
853 });
854 }
855 if sender != sender_cbor {
856 return Err(AgentError::CallDataMismatch {
857 field: "sender".to_string(),
858 value_arg: sender.to_string(),
859 value_cbor: sender_cbor.to_string(),
860 });
861 }
862 if canister_id != canister_id_cbor {
863 return Err(AgentError::CallDataMismatch {
864 field: "canister_id".to_string(),
865 value_arg: canister_id.to_string(),
866 value_cbor: canister_id_cbor.to_string(),
867 });
868 }
869 if method_name != method_name_cbor {
870 return Err(AgentError::CallDataMismatch {
871 field: "method_name".to_string(),
872 value_arg: method_name.to_string(),
873 value_cbor: method_name_cbor,
874 });
875 }
876 if arg != arg_cbor {
877 return Err(AgentError::CallDataMismatch {
878 field: "arg".to_string(),
879 value_arg: format!("{:?}", arg),
880 value_cbor: format!("{:?}", arg_cbor),
881 });
882 }
883 }
884 }
885 Ok(())
886}
887
888pub fn signed_update_inspect(
891 sender: Principal,
892 canister_id: Principal,
893 method_name: &str,
894 arg: &[u8],
895 ingress_expiry: u64,
896 signed_update: Vec<u8>,
897) -> Result<(), AgentError> {
898 let envelope: Envelope<CallRequestContent> =
899 serde_cbor::from_slice(&signed_update).map_err(AgentError::InvalidCborData)?;
900 match envelope.content {
901 CallRequestContent::CallRequest {
902 nonce: _nonce,
903 ingress_expiry: ingress_expiry_cbor,
904 sender: sender_cbor,
905 canister_id: canister_id_cbor,
906 method_name: method_name_cbor,
907 arg: arg_cbor,
908 } => {
909 if ingress_expiry != ingress_expiry_cbor {
910 return Err(AgentError::CallDataMismatch {
911 field: "ingress_expiry".to_string(),
912 value_arg: ingress_expiry.to_string(),
913 value_cbor: ingress_expiry_cbor.to_string(),
914 });
915 }
916 if sender != sender_cbor {
917 return Err(AgentError::CallDataMismatch {
918 field: "sender".to_string(),
919 value_arg: sender.to_string(),
920 value_cbor: sender_cbor.to_string(),
921 });
922 }
923 if canister_id != canister_id_cbor {
924 return Err(AgentError::CallDataMismatch {
925 field: "canister_id".to_string(),
926 value_arg: canister_id.to_string(),
927 value_cbor: canister_id_cbor.to_string(),
928 });
929 }
930 if method_name != method_name_cbor {
931 return Err(AgentError::CallDataMismatch {
932 field: "method_name".to_string(),
933 value_arg: method_name.to_string(),
934 value_cbor: method_name_cbor,
935 });
936 }
937 if arg != arg_cbor {
938 return Err(AgentError::CallDataMismatch {
939 field: "arg".to_string(),
940 value_arg: format!("{:?}", arg),
941 value_cbor: format!("{:?}", arg_cbor),
942 });
943 }
944 }
945 }
946 Ok(())
947}
948
949pub fn signed_request_status_inspect(
952 sender: Principal,
953 request_id: &RequestId,
954 ingress_expiry: u64,
955 signed_request_status: Vec<u8>,
956) -> Result<(), AgentError> {
957 let paths: Vec<Vec<Label>> = vec![vec!["request_status".into(), request_id.to_vec().into()]];
958 let envelope: Envelope<ReadStateContent> =
959 serde_cbor::from_slice(&signed_request_status).map_err(AgentError::InvalidCborData)?;
960 match envelope.content {
961 ReadStateContent::ReadStateRequest {
962 ingress_expiry: ingress_expiry_cbor,
963 sender: sender_cbor,
964 paths: paths_cbor,
965 } => {
966 if ingress_expiry != ingress_expiry_cbor {
967 return Err(AgentError::CallDataMismatch {
968 field: "ingress_expiry".to_string(),
969 value_arg: ingress_expiry.to_string(),
970 value_cbor: ingress_expiry_cbor.to_string(),
971 });
972 }
973 if sender != sender_cbor {
974 return Err(AgentError::CallDataMismatch {
975 field: "sender".to_string(),
976 value_arg: sender.to_string(),
977 value_cbor: sender_cbor.to_string(),
978 });
979 }
980
981 if paths != paths_cbor {
982 return Err(AgentError::CallDataMismatch {
983 field: "paths".to_string(),
984 value_arg: format!("{:?}", paths),
985 value_cbor: format!("{:?}", paths_cbor),
986 });
987 }
988 }
989 }
990 Ok(())
991}
992
993#[derive(Debug)]
997pub struct QueryBuilder<'agent> {
998 agent: &'agent Agent,
999 pub effective_canister_id: Principal,
1001 pub canister_id: Principal,
1003 pub method_name: String,
1005 pub arg: Vec<u8>,
1007 pub ingress_expiry_datetime: Option<u64>,
1009}
1010
1011impl<'agent> QueryBuilder<'agent> {
1012 pub fn new(agent: &'agent Agent, canister_id: Principal, method_name: String) -> Self {
1014 Self {
1015 agent,
1016 effective_canister_id: canister_id,
1017 canister_id,
1018 method_name,
1019 arg: vec![],
1020 ingress_expiry_datetime: None,
1021 }
1022 }
1023
1024 pub fn with_effective_canister_id(&mut self, canister_id: Principal) -> &mut Self {
1026 self.effective_canister_id = canister_id;
1027 self
1028 }
1029
1030 pub fn with_arg<A: AsRef<[u8]>>(&mut self, arg: A) -> &mut Self {
1032 self.arg = arg.as_ref().to_vec();
1033 self
1034 }
1035
1036 pub fn expire_at(&mut self, time: std::time::SystemTime) -> &mut Self {
1040 self.ingress_expiry_datetime = Some(
1041 time.duration_since(std::time::UNIX_EPOCH)
1042 .expect("Time wrapped around")
1043 .as_nanos() as u64,
1044 );
1045 self
1046 }
1047
1048 pub fn expire_after(&mut self, duration: std::time::Duration) -> &mut Self {
1053 let permitted_drift = Duration::from_secs(60);
1054 self.ingress_expiry_datetime = Some(
1055 (duration
1056 .as_nanos()
1057 .saturating_add(
1058 std::time::SystemTime::now()
1059 .duration_since(std::time::UNIX_EPOCH)
1060 .expect("Time wrapped around")
1061 .as_nanos(),
1062 )
1063 .saturating_sub(permitted_drift.as_nanos())) as u64,
1064 );
1065 self
1066 }
1067
1068 pub async fn call(&self) -> Result<Vec<u8>, AgentError> {
1070 self.agent
1071 .query_raw(
1072 &self.canister_id,
1073 self.effective_canister_id,
1074 self.method_name.as_str(),
1075 self.arg.as_slice(),
1076 self.ingress_expiry_datetime,
1077 )
1078 .await
1079 }
1080
1081 pub fn sign(&self) -> Result<signed::SignedQuery, AgentError> {
1084 let request = self.agent.query_content(
1085 &self.canister_id,
1086 &self.method_name,
1087 &self.arg,
1088 self.ingress_expiry_datetime,
1089 )?;
1090
1091 let signed_query = sign_request(&request, self.agent.identity.clone())?;
1092 match request {
1093 QueryContent::QueryRequest {
1094 ingress_expiry,
1095 sender,
1096 canister_id,
1097 method_name,
1098 arg,
1099 } => Ok(signed::SignedQuery {
1100 ingress_expiry,
1101 sender,
1102 canister_id,
1103 method_name,
1104 arg,
1105 effective_canister_id: self.effective_canister_id,
1106 signed_query,
1107 }),
1108 }
1109 }
1110}
1111
1112pub struct UpdateCall<'agent> {
1114 agent: &'agent Agent,
1115 request_id: AgentFuture<'agent, RequestId>,
1116 effective_canister_id: Principal,
1117}
1118
1119impl fmt::Debug for UpdateCall<'_> {
1120 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1121 f.debug_struct("UpdateCall")
1122 .field("agent", &self.agent)
1123 .field("effective_canister_id", &self.effective_canister_id)
1124 .finish_non_exhaustive()
1125 }
1126}
1127
1128impl Future for UpdateCall<'_> {
1129 type Output = Result<RequestId, AgentError>;
1130 fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
1131 self.request_id.as_mut().poll(cx)
1132 }
1133}
1134impl<'a> UpdateCall<'a> {
1135 async fn and_wait(self) -> Result<Vec<u8>, AgentError> {
1136 let request_id = self.request_id.await?;
1137 self.agent
1138 .wait(request_id, self.effective_canister_id)
1139 .await
1140 }
1141}
1142#[derive(Debug)]
1147pub struct UpdateBuilder<'agent> {
1148 agent: &'agent Agent,
1149 pub effective_canister_id: Principal,
1151 pub canister_id: Principal,
1153 pub method_name: String,
1155 pub arg: Vec<u8>,
1157 pub ingress_expiry_datetime: Option<u64>,
1159}
1160
1161impl<'agent> UpdateBuilder<'agent> {
1162 pub fn new(agent: &'agent Agent, canister_id: Principal, method_name: String) -> Self {
1164 Self {
1165 agent,
1166 effective_canister_id: canister_id,
1167 canister_id,
1168 method_name,
1169 arg: vec![],
1170 ingress_expiry_datetime: None,
1171 }
1172 }
1173
1174 pub fn with_effective_canister_id(&mut self, canister_id: Principal) -> &mut Self {
1176 self.effective_canister_id = canister_id;
1177 self
1178 }
1179
1180 pub fn with_arg<A: AsRef<[u8]>>(&mut self, arg: A) -> &mut Self {
1182 self.arg = arg.as_ref().to_vec();
1183 self
1184 }
1185
1186 pub fn expire_at(&mut self, time: std::time::SystemTime) -> &mut Self {
1190 self.ingress_expiry_datetime = Some(
1191 time.duration_since(std::time::UNIX_EPOCH)
1192 .expect("Time wrapped around")
1193 .as_nanos() as u64,
1194 );
1195 self
1196 }
1197
1198 pub fn expire_after(&mut self, duration: std::time::Duration) -> &mut Self {
1203 let permitted_drift = Duration::from_secs(60);
1204 self.ingress_expiry_datetime = Some(
1205 (duration
1206 .as_nanos()
1207 .saturating_add(
1208 std::time::SystemTime::now()
1209 .duration_since(std::time::UNIX_EPOCH)
1210 .expect("Time wrapped around")
1211 .as_nanos(),
1212 )
1213 .saturating_sub(permitted_drift.as_nanos())) as u64,
1214 );
1215 self
1216 }
1217
1218 pub async fn call_and_wait(&self) -> Result<Vec<u8>, AgentError> {
1221 self.call().and_wait().await
1222 }
1223
1224 pub fn call(&self) -> UpdateCall {
1227 let request_id_future = self.agent.update_raw(
1228 &self.canister_id,
1229 self.effective_canister_id,
1230 self.method_name.as_str(),
1231 self.arg.as_slice(),
1232 self.ingress_expiry_datetime,
1233 );
1234 UpdateCall {
1235 agent: self.agent,
1236 request_id: Box::pin(request_id_future),
1237 effective_canister_id: self.effective_canister_id,
1238 }
1239 }
1240
1241 pub fn sign(&self) -> Result<signed::SignedUpdate, AgentError> {
1244 let request = self.agent.update_content(
1245 &self.canister_id,
1246 &self.method_name,
1247 &self.arg,
1248 self.ingress_expiry_datetime,
1249 )?;
1250 let signed_update = sign_request(&request, self.agent.identity.clone())?;
1251 let request_id = to_request_id(&request)?;
1252 match request {
1253 CallRequestContent::CallRequest {
1254 nonce,
1255 ingress_expiry,
1256 sender,
1257 canister_id,
1258 method_name,
1259 arg,
1260 } => Ok(signed::SignedUpdate {
1261 nonce,
1262 ingress_expiry,
1263 sender,
1264 canister_id,
1265 method_name,
1266 arg,
1267 effective_canister_id: self.effective_canister_id,
1268 signed_update,
1269 request_id,
1270 }),
1271 }
1272 }
1273}