canic_host/icp/
identity.rs1use super::{error::IcpCommandError, model::IcpCli, run::run_output};
8
9impl IcpCli {
10 pub(crate) fn bind_selected_identity(&self) -> Result<(), IcpCommandError> {
13 if self.selected_identity.get().is_none() {
14 let identity = self.selected_identity_name()?;
15 let _ = self.selected_identity.set(identity);
16 }
17 Ok(())
18 }
19
20 pub(super) fn selected_identity_name(&self) -> Result<String, IcpCommandError> {
21 if let Some(identity) = self.selected_identity.get() {
22 return Ok(identity.clone());
23 }
24 let mut command = self.request_command();
25 command.args(["identity", "default"]);
26 let identity = run_output(&mut command, self)?;
27 if identity.is_empty() {
28 return Err(std::io::Error::new(
29 std::io::ErrorKind::InvalidData,
30 "ICP returned an empty selected identity",
31 )
32 .into());
33 }
34 Ok(identity)
35 }
36
37 pub fn identity_principal_text(&self) -> Result<String, IcpCommandError> {
39 let mut command = self.identity_principal_command();
40 run_output(&mut command, self)
41 }
42
43 pub fn identity_account_id_text(
45 &self,
46 format: IcpIdentityAccountFormat,
47 ) -> Result<String, IcpCommandError> {
48 let mut command = self.identity_account_id_command(format);
49 run_output(&mut command, self)
50 }
51
52 fn identity_principal_command(&self) -> std::process::Command {
53 let mut command = self.request_command();
54 command.args(["identity", "principal"]);
55 self.add_selected_identity_arg(&mut command);
56 command
57 }
58
59 fn identity_account_id_command(
60 &self,
61 format: IcpIdentityAccountFormat,
62 ) -> std::process::Command {
63 let mut command = self.request_command();
64 command.args(["identity", "account-id", "--format", format.label()]);
65 self.add_selected_identity_arg(&mut command);
66 command
67 }
68}
69
70#[derive(Clone, Copy, Debug, Eq, PartialEq)]
76pub enum IcpIdentityAccountFormat {
77 IcpLedger,
78 Icrc1,
79}
80
81impl IcpIdentityAccountFormat {
82 const fn label(self) -> &'static str {
83 match self {
84 Self::IcpLedger => "ledger",
85 Self::Icrc1 => "icrc1",
86 }
87 }
88}
89
90#[cfg(test)]
91mod tests {
92 use super::*;
93 use crate::icp::command::command_display;
94
95 #[cfg(unix)]
96 #[test]
97 fn bound_identity_survives_default_change_for_clones_and_later_calls() {
98 use std::{fs, os::unix::fs::PermissionsExt};
99
100 let root = crate::test_support::temp_dir("bound-icp-identity");
101 fs::create_dir_all(&root).unwrap();
102 let executable = root.join("icp");
103 fs::write(
104 &executable,
105 r#"#!/bin/sh
106if [ "$1" = --version ]; then echo 'icp 1.5.0'; exit 0; fi
107while [ "$1" = --project-root-override ] || [ "$1" = --identity-password-file ]; do shift 2; done
108if [ "$1 $2" = 'identity default' ]; then cat selected; exit 0; fi
109selected=$(cat selected)
110for arg do
111 if [ "$previous" = --identity ]; then selected=$arg; fi
112 previous=$arg
113done
114printf '%s\n' "$selected"
115"#,
116 )
117 .unwrap();
118 fs::set_permissions(&executable, fs::Permissions::from_mode(0o755)).unwrap();
119 fs::write(root.join("selected"), "reviewed").unwrap();
120 let icp = IcpCli::new(executable.to_str().unwrap(), None).with_cwd(&root);
121 let clone = icp.clone();
122 icp.bind_selected_identity().unwrap();
123 assert_eq!(icp.identity_principal_text().unwrap(), "reviewed");
124 fs::write(root.join("selected"), "unrelated-encrypted").unwrap();
125 clone.bind_selected_identity().unwrap();
126 assert_eq!(clone.identity_principal_text().unwrap(), "reviewed");
127 assert_eq!(
128 clone
129 .identity_account_id_text(IcpIdentityAccountFormat::Icrc1)
130 .unwrap(),
131 "reviewed"
132 );
133 assert_eq!(clone.selected_identity_name().unwrap(), "reviewed");
134 assert_eq!(
135 clone
136 .canister_metadata_output("canister", "candid:service")
137 .unwrap(),
138 "reviewed"
139 );
140 let unbound = IcpCli::new(executable.to_str().unwrap(), None).with_cwd(&root);
141 assert_eq!(
142 unbound.identity_principal_text().unwrap(),
143 "unrelated-encrypted"
144 );
145 assert_ne!(clone, unbound);
146 fs::remove_dir_all(root).unwrap();
147 }
148
149 #[test]
150 fn identity_resolution_uses_the_active_identity_without_network_selection() {
151 let icp = IcpCli::new("icp", Some("ic".to_string())).with_cwd("/workspace/app");
152
153 assert_eq!(
154 command_display(&icp.identity_principal_command()),
155 "icp --project-root-override /workspace/app identity principal"
156 );
157 }
158
159 #[test]
160 fn account_resolution_selects_the_exact_ledger_representation() {
161 let icp = IcpCli::new("icp", Some("ic".to_string())).with_cwd("/workspace/app");
162
163 assert_eq!(
164 command_display(&icp.identity_account_id_command(IcpIdentityAccountFormat::Icrc1)),
165 "icp --project-root-override /workspace/app identity account-id --format icrc1"
166 );
167 assert_eq!(
168 command_display(&icp.identity_account_id_command(IcpIdentityAccountFormat::IcpLedger)),
169 "icp --project-root-override /workspace/app identity account-id --format ledger"
170 );
171 }
172}