1use 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#[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 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 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}