use std::{
io::Write as _,
path::Path,
process::{Command, Stdio},
};
use ic_host_tools::response::{ResponseError, ResponseFormat, ResponseLimits, decode};
use crate::icp::process::output_stderr;
pub(crate) fn encode_wire_args<T: serde::Serialize>(
method: &str,
arguments: T,
) -> Result<Vec<u8>, String> {
let endpoint = icydb::wire::Endpoint::from_name(method)
.ok_or_else(|| format!("unknown IcyDB method '{method}'"))?;
endpoint
.encode_request(arguments)
.map_err(|error| error.to_string())
}
pub(super) fn icp_query_command(
environment: &str,
canister: &str,
method: &str,
argument_file: &Path,
) -> Command {
icp_call_command(
environment,
canister,
method,
argument_file,
IcpCallKind::Query,
)
}
pub(super) fn icp_update_command(
environment: &str,
canister: &str,
method: &str,
argument_file: &Path,
) -> Command {
icp_call_command(
environment,
canister,
method,
argument_file,
IcpCallKind::Update,
)
}
enum IcpCallKind {
Query,
Update,
}
fn icp_call_command(
environment: &str,
canister: &str,
method: &str,
argument_file: &Path,
kind: IcpCallKind,
) -> Command {
let mut command = Command::new("icp");
command
.arg("canister")
.arg("call")
.arg(canister)
.arg(method)
.arg("--args-file")
.arg(argument_file)
.arg("--args-format")
.arg("bin");
match kind {
IcpCallKind::Query => {
command.arg("--query");
}
IcpCallKind::Update => {}
}
command
.arg("--output")
.arg("hex")
.arg("--environment")
.arg(environment);
command
}
pub(super) fn hex_response_bytes(output: &str) -> Result<Vec<u8>, String> {
let candidate = output
.rsplit_once("response (hex):")
.map_or(output, |(_, value)| value)
.trim();
let hex = candidate.split_whitespace().collect::<String>();
if hex.is_empty() {
return Err("icp canister call returned an empty hex response".to_string());
}
if hex.len() % 2 != 0 {
return Err("icp canister call returned odd-length hex response".to_string());
}
decode(
hex.as_bytes(),
ResponseFormat::Hex,
ResponseLimits {
input_bytes: hex.len(),
decoded_bytes: hex.len() / 2,
},
)
.map_err(|error| match error {
ResponseError::InvalidHex { offset } => hex.as_bytes().get(offset).map_or_else(
|| "icp canister call returned invalid hex response".to_string(),
|byte| {
format!(
"icp canister call returned non-hex byte '{}'",
char::from(*byte)
)
},
),
other => format!("icp canister call response decoding failed: {other}"),
})
}
pub(super) fn call_query_hex(
environment: &str,
canister: &str,
method: &str,
wire_arg: &[u8],
error_message: impl FnOnce(&str) -> String,
) -> Result<Vec<u8>, String> {
call_hex(
wire_arg,
|path| icp_query_command(environment, canister, method, path),
error_message,
)
}
pub(super) fn call_update_hex(
environment: &str,
canister: &str,
method: &str,
wire_arg: &[u8],
error_message: impl FnOnce(&str) -> String,
) -> Result<Vec<u8>, String> {
call_hex(
wire_arg,
|path| icp_update_command(environment, canister, method, path),
error_message,
)
}
pub(super) fn call_hex(
wire_arg: &[u8],
build_command: impl FnOnce(&Path) -> Command,
error_message: impl FnOnce(&str) -> String,
) -> Result<Vec<u8>, String> {
let mut arguments = tempfile::NamedTempFile::new()
.map_err(|error| format!("create ICP argument file: {error}"))?;
arguments
.write_all(wire_arg)
.map_err(|error| format!("write ICP argument file: {error}"))?;
let output = build_command(arguments.path())
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.map_err(|err| format!("failed to run icp canister call: {err}"))?;
if !output.status.success() {
let stderr = output_stderr(output.stderr.as_slice());
return Err(error_message(stderr.as_str()));
}
let stdout = String::from_utf8(output.stdout).map_err(|err| err.to_string())?;
hex_response_bytes(stdout.as_str())
}