Skip to main content

canic_host/icp/
candid.rs

1//! Module: icp::candid
2//!
3//! Responsibility: perform one typed Candid update through the maintained ICP CLI process.
4//! Does not own: domain retry policy, authority, or durable effect intent.
5//! Boundary: callers persist authority before invoking this mechanical transport adapter.
6
7use crate::icp::{IcpCli, IcpCommandError, IcpJsonResponseError};
8use candid::CandidType;
9use serde::de::DeserializeOwned;
10#[cfg(unix)]
11use std::os::unix::fs::OpenOptionsExt;
12use std::{
13    fs::{self, OpenOptions},
14    io::{self, Write},
15    path::{Path, PathBuf},
16    sync::atomic::{AtomicU64, Ordering},
17};
18use thiserror::Error as ThisError;
19
20const MAX_ARGUMENT_FILE_ATTEMPTS: usize = 32;
21static NEXT_ARGUMENT_FILE: AtomicU64 = AtomicU64::new(0);
22
23/// Typed transport or codec failure for a raw Candid update.
24
25#[derive(Debug, ThisError)]
26pub enum IcpCandidCallError {
27    #[error("failed to encode Candid call argument: {0}")]
28    Encode(#[source] candid::Error),
29
30    #[error("failed to manage Candid call argument file: {0}")]
31    File(#[source] io::Error),
32
33    #[error(transparent)]
34    Icp(#[from] IcpCommandError),
35
36    #[error("failed to decode Candid call response: {0}")]
37    Response(#[source] IcpJsonResponseError),
38}
39
40impl IcpCli {
41    /// Perform one typed Candid update and return its exact decoded response.
42    pub fn canister_call_candid<I, O>(
43        &self,
44        canister: &str,
45        method: &str,
46        input: &I,
47        candid_path: Option<&Path>,
48    ) -> Result<O, IcpCandidCallError>
49    where
50        I: CandidType,
51        O: CandidType + DeserializeOwned,
52    {
53        self.canister_candid(canister, method, input, candid_path, false)
54    }
55
56    /// Perform one typed Candid query and return its exact decoded response.
57    pub fn canister_query_candid<I, O>(
58        &self,
59        canister: &str,
60        method: &str,
61        input: &I,
62        candid_path: Option<&Path>,
63    ) -> Result<O, IcpCandidCallError>
64    where
65        I: CandidType,
66        O: CandidType + DeserializeOwned,
67    {
68        self.canister_candid(canister, method, input, candid_path, true)
69    }
70
71    fn canister_candid<I, O>(
72        &self,
73        canister: &str,
74        method: &str,
75        input: &I,
76        candid_path: Option<&Path>,
77        query: bool,
78    ) -> Result<O, IcpCandidCallError>
79    where
80        I: CandidType,
81        O: CandidType + DeserializeOwned,
82    {
83        let bytes = candid::encode_one(input).map_err(IcpCandidCallError::Encode)?;
84        let path = write_argument_file(&bytes).map_err(IcpCandidCallError::File)?;
85        let output = if query {
86            self.canister_query_binary_args_output_with_candid(
87                canister,
88                method,
89                &path,
90                Some("json"),
91                candid_path,
92            )
93        } else {
94            self.canister_call_binary_args_output_with_candid(
95                canister,
96                method,
97                &path,
98                Some("json"),
99                candid_path,
100            )
101        };
102        let cleanup = fs::remove_file(&path);
103        let output = output?;
104        cleanup.map_err(IcpCandidCallError::File)?;
105        crate::icp::response::decode_json_response(&output).map_err(IcpCandidCallError::Response)
106    }
107}
108
109fn write_argument_file(bytes: &[u8]) -> io::Result<PathBuf> {
110    for _ in 0..MAX_ARGUMENT_FILE_ATTEMPTS {
111        let sequence = NEXT_ARGUMENT_FILE.fetch_add(1, Ordering::Relaxed);
112        let path = std::env::temp_dir().join(format!(
113            "canic-raw-candid-{}-{sequence}.bin",
114            std::process::id()
115        ));
116        let mut options = OpenOptions::new();
117        options.write(true).create_new(true);
118        #[cfg(unix)]
119        options.mode(0o600);
120        match options.open(&path) {
121            Ok(mut file) => {
122                if let Err(source) = file.write_all(bytes).and_then(|()| file.sync_all()) {
123                    drop(file);
124                    let _ = fs::remove_file(&path);
125                    return Err(source);
126                }
127                return Ok(path);
128            }
129            Err(source) if source.kind() == io::ErrorKind::AlreadyExists => {}
130            Err(source) => return Err(source),
131        }
132    }
133    Err(io::Error::new(
134        io::ErrorKind::AlreadyExists,
135        "could not allocate a unique Candid argument file",
136    ))
137}