1use crate::management_canister::{
2 CanisterId, CanisterIdRecord, CanisterInstallMode, CreateCanisterArgument, InstallCodeArgument,
3};
4use candid::utils::{ArgumentDecoder, ArgumentEncoder};
5use candid::{decode_args, encode_args, Principal};
6use ciborium::de::from_reader;
7use serde::de::DeserializeOwned;
8use serde::{Deserialize, Serialize};
9use serde_bytes::ByteBuf;
10use std::cell::RefCell;
11use std::fmt;
12use std::io::{Read, Write};
13use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio};
14use std::time::{Duration, SystemTime};
15
16mod management_canister;
17
18pub use management_canister::CanisterSettings;
19
20#[derive(Serialize, Deserialize)]
21pub enum Request {
22 RootKey,
23 Time,
24 SetTime(SystemTime),
25 AdvanceTime(Duration),
26 CanisterUpdateCall(CanisterCall),
27 CanisterQueryCall(CanisterCall),
28 CanisterExists(RawCanisterId),
29 CyclesBalance(RawCanisterId),
30 AddCycles(AddCyclesArg),
31 SetStableMemory(SetStableMemoryArg),
32 ReadStableMemory(RawCanisterId),
33 Tick,
34 RunUntilCompletion(RunUntilCompletionArg),
35 VerifyCanisterSig(VerifyCanisterSigArg),
36}
37
38#[derive(Serialize, Deserialize)]
39pub struct VerifyCanisterSigArg {
40 pub msg: Vec<u8>,
41 pub sig: Vec<u8>,
42 pub pubkey: Vec<u8>,
43 pub root_pubkey: Vec<u8>,
44}
45
46#[derive(Serialize, Deserialize)]
47pub struct RunUntilCompletionArg {
48 pub max_ticks: u64,
50}
51
52#[derive(Serialize, Deserialize)]
53pub struct AddCyclesArg {
54 pub canister_id: Vec<u8>,
56 pub amount: u128,
57}
58
59#[derive(Serialize, Deserialize)]
60pub struct SetStableMemoryArg {
61 pub canister_id: Vec<u8>,
63 pub data: ByteBuf,
64}
65
66#[derive(Serialize, Deserialize)]
67pub struct RawCanisterId {
68 pub canister_id: Vec<u8>,
70}
71
72impl From<Principal> for RawCanisterId {
73 fn from(principal: Principal) -> Self {
74 Self {
75 canister_id: principal.as_slice().to_vec(),
76 }
77 }
78}
79
80#[derive(Serialize, Deserialize)]
81pub struct CanisterCall {
82 pub sender: Vec<u8>,
83 pub canister_id: Vec<u8>,
84 pub method: String,
85 pub arg: Vec<u8>,
86}
87
88pub struct StateMachine {
89 proc: Child,
90 child_in: RefCell<ChildStdin>,
91 child_out: RefCell<ChildStdout>,
92}
93
94impl StateMachine {
95 pub fn new(binary_path: &str, debug: bool) -> Self {
96 let mut command = Command::new(binary_path);
97 command
98 .env("LOG_TO_STDERR", "1")
99 .stdin(Stdio::piped())
100 .stdout(Stdio::piped());
101
102 if debug {
103 command.arg("--debug");
104 }
105
106 let mut child = command.spawn().unwrap_or_else(|err| {
107 panic!(
108 "failed to start test state machine at path {}: {:?}",
109 binary_path, err
110 )
111 });
112
113 let child_in = child.stdin.take().unwrap();
114 let child_out = child.stdout.take().unwrap();
115 Self {
116 proc: child,
117 child_in: RefCell::new(child_in),
118 child_out: RefCell::new(child_out),
119 }
120 }
121
122 pub fn update_call(
123 &self,
124 canister_id: Principal,
125 sender: Principal,
126 method: &str,
127 arg: Vec<u8>,
128 ) -> Result<WasmResult, UserError> {
129 self.call_state_machine(Request::CanisterUpdateCall(CanisterCall {
130 sender: sender.as_slice().to_vec(),
131 canister_id: canister_id.as_slice().to_vec(),
132 method: method.to_string(),
133 arg,
134 }))
135 }
136
137 pub fn query_call(
138 &self,
139 canister_id: Principal,
140 sender: Principal,
141 method: &str,
142 arg: Vec<u8>,
143 ) -> Result<WasmResult, UserError> {
144 self.call_state_machine(Request::CanisterQueryCall(CanisterCall {
145 sender: sender.as_slice().to_vec(),
146 canister_id: canister_id.as_slice().to_vec(),
147 method: method.to_string(),
148 arg,
149 }))
150 }
151
152 pub fn root_key(&self) -> Vec<u8> {
153 self.call_state_machine(Request::RootKey)
154 }
155
156 pub fn create_canister(&self, sender: Option<Principal>) -> CanisterId {
157 let CanisterIdRecord { canister_id } = call_candid_as(
158 self,
159 Principal::management_canister(),
160 sender.unwrap_or(Principal::anonymous()),
161 "create_canister",
162 (CreateCanisterArgument { settings: None },),
163 )
164 .map(|(x,)| x)
165 .unwrap();
166 canister_id
167 }
168
169 pub fn create_canister_with_settings(
170 &self,
171 settings: Option<CanisterSettings>,
172 sender: Option<Principal>,
173 ) -> CanisterId {
174 let CanisterIdRecord { canister_id } = call_candid_as(
175 self,
176 Principal::management_canister(),
177 sender.unwrap_or(Principal::anonymous()),
178 "create_canister",
179 (CreateCanisterArgument { settings },),
180 )
181 .map(|(x,)| x)
182 .unwrap();
183 canister_id
184 }
185
186 pub fn install_canister(
187 &self,
188 canister_id: CanisterId,
189 wasm_module: Vec<u8>,
190 arg: Vec<u8>,
191 sender: Option<Principal>,
192 ) {
193 call_candid_as::<(InstallCodeArgument,), ()>(
194 self,
195 Principal::management_canister(),
196 sender.unwrap_or(Principal::anonymous()),
197 "install_code",
198 (InstallCodeArgument {
199 mode: CanisterInstallMode::Install,
200 canister_id,
201 wasm_module,
202 arg,
203 },),
204 )
205 .unwrap();
206 }
207
208 pub fn upgrade_canister(
209 &self,
210 canister_id: CanisterId,
211 wasm_module: Vec<u8>,
212 arg: Vec<u8>,
213 sender: Option<Principal>,
214 ) -> Result<(), CallError> {
215 call_candid_as::<(InstallCodeArgument,), ()>(
216 self,
217 Principal::management_canister(),
218 sender.unwrap_or(Principal::anonymous()),
219 "install_code",
220 (InstallCodeArgument {
221 mode: CanisterInstallMode::Upgrade,
222 canister_id,
223 wasm_module,
224 arg,
225 },),
226 )
227 }
228
229 pub fn reinstall_canister(
230 &self,
231 canister_id: CanisterId,
232 wasm_module: Vec<u8>,
233 arg: Vec<u8>,
234 sender: Option<Principal>,
235 ) -> Result<(), CallError> {
236 call_candid_as::<(InstallCodeArgument,), ()>(
237 self,
238 Principal::management_canister(),
239 sender.unwrap_or(Principal::anonymous()),
240 "install_code",
241 (InstallCodeArgument {
242 mode: CanisterInstallMode::Reinstall,
243 canister_id,
244 wasm_module,
245 arg,
246 },),
247 )
248 }
249
250 pub fn start_canister(
251 &self,
252 canister_id: CanisterId,
253 sender: Option<Principal>,
254 ) -> Result<(), CallError> {
255 call_candid_as::<(CanisterIdRecord,), ()>(
256 self,
257 Principal::management_canister(),
258 sender.unwrap_or(Principal::anonymous()),
259 "start_canister",
260 (CanisterIdRecord { canister_id },),
261 )
262 }
263
264 pub fn stop_canister(
265 &self,
266 canister_id: CanisterId,
267 sender: Option<Principal>,
268 ) -> Result<(), CallError> {
269 call_candid_as::<(CanisterIdRecord,), ()>(
270 self,
271 Principal::management_canister(),
272 sender.unwrap_or(Principal::anonymous()),
273 "stop_canister",
274 (CanisterIdRecord { canister_id },),
275 )
276 }
277
278 pub fn delete_canister(
279 &self,
280 canister_id: CanisterId,
281 sender: Option<Principal>,
282 ) -> Result<(), CallError> {
283 call_candid_as::<(CanisterIdRecord,), ()>(
284 self,
285 Principal::management_canister(),
286 sender.unwrap_or(Principal::anonymous()),
287 "delete_canister",
288 (CanisterIdRecord { canister_id },),
289 )
290 }
291
292 pub fn canister_exists(&self, canister_id: Principal) -> bool {
293 self.call_state_machine(Request::CanisterExists(RawCanisterId::from(canister_id)))
294 }
295
296 pub fn time(&self) -> SystemTime {
297 self.call_state_machine(Request::Time)
298 }
299
300 pub fn set_time(&self, time: SystemTime) {
301 self.call_state_machine(Request::SetTime(time))
302 }
303
304 pub fn advance_time(&self, duration: Duration) {
305 self.call_state_machine(Request::AdvanceTime(duration))
306 }
307
308 pub fn tick(&self) {
309 self.call_state_machine(Request::Tick)
310 }
311
312 pub fn run_until_completion(&self, max_ticks: u64) {
313 self.call_state_machine(Request::RunUntilCompletion(RunUntilCompletionArg {
314 max_ticks,
315 }))
316 }
317
318 pub fn stable_memory(&self, canister_id: Principal) -> Vec<u8> {
319 self.call_state_machine(Request::ReadStableMemory(RawCanisterId::from(canister_id)))
320 }
321
322 pub fn set_stable_memory(&self, canister_id: Principal, data: ByteBuf) {
323 self.call_state_machine(Request::SetStableMemory(SetStableMemoryArg {
324 canister_id: canister_id.as_slice().to_vec(),
325 data,
326 }))
327 }
328
329 pub fn cycle_balance(&self, canister_id: Principal) -> u128 {
330 self.call_state_machine(Request::CyclesBalance(RawCanisterId::from(canister_id)))
331 }
332
333 pub fn add_cycles(&self, canister_id: Principal, amount: u128) -> u128 {
334 self.call_state_machine(Request::AddCycles(AddCyclesArg {
335 canister_id: canister_id.as_slice().to_vec(),
336 amount,
337 }))
338 }
339
340 pub fn verify_canister_signature(
343 &self,
344 msg: Vec<u8>,
345 sig: Vec<u8>,
346 pubkey: Vec<u8>,
347 root_pubkey: Vec<u8>,
348 ) -> Result<(), String> {
349 self.call_state_machine(Request::VerifyCanisterSig(VerifyCanisterSigArg {
350 msg,
351 sig,
352 pubkey,
353 root_pubkey,
354 }))
355 }
356
357 fn call_state_machine<T: DeserializeOwned>(&self, request: Request) -> T {
358 self.send_request(request);
359 self.read_response()
360 }
361
362 fn send_request(&self, request: Request) {
363 let mut cbor = vec![];
364 ciborium::ser::into_writer(&request, &mut cbor).expect("failed to serialize request");
365 let mut child_in = self.child_in.borrow_mut();
366 child_in
367 .write_all(&(cbor.len() as u64).to_le_bytes())
368 .expect("failed to send request length");
369 child_in
370 .write_all(cbor.as_slice())
371 .expect("failed to send request data");
372 child_in.flush().expect("failed to flush child stdin");
373 }
374
375 fn read_response<T: DeserializeOwned>(&self) -> T {
376 let vec = self.read_bytes(8);
377 let size = usize::from_le_bytes(TryFrom::try_from(vec).expect("failed to read data size"));
378 from_reader(&self.read_bytes(size)[..]).expect("failed to deserialize response")
379 }
380
381 fn read_bytes(&self, num_bytes: usize) -> Vec<u8> {
382 let mut buf = vec![0u8; num_bytes];
383 self.child_out
384 .borrow_mut()
385 .read_exact(&mut buf)
386 .expect("failed to read from child_stdout");
387 buf
388 }
389}
390
391impl Drop for StateMachine {
392 fn drop(&mut self) {
393 self.proc
394 .kill()
395 .expect("failed to kill state machine process")
396 }
397}
398
399pub fn query_candid<Input, Output>(
401 env: &StateMachine,
402 canister_id: Principal,
403 method: &str,
404 input: Input,
405) -> Result<Output, CallError>
406where
407 Input: ArgumentEncoder,
408 Output: for<'a> ArgumentDecoder<'a>,
409{
410 query_candid_as(env, canister_id, Principal::anonymous(), method, input)
411}
412
413pub fn query_candid_as<Input, Output>(
415 env: &StateMachine,
416 canister_id: Principal,
417 sender: Principal,
418 method: &str,
419 input: Input,
420) -> Result<Output, CallError>
421where
422 Input: ArgumentEncoder,
423 Output: for<'a> ArgumentDecoder<'a>,
424{
425 with_candid(input, |bytes| {
426 env.query_call(canister_id, sender, method, bytes)
427 })
428}
429
430pub fn call_candid_as<Input, Output>(
433 env: &StateMachine,
434 canister_id: Principal,
435 sender: Principal,
436 method: &str,
437 input: Input,
438) -> Result<Output, CallError>
439where
440 Input: ArgumentEncoder,
441 Output: for<'a> ArgumentDecoder<'a>,
442{
443 with_candid(input, |bytes| {
444 env.update_call(canister_id, sender, method, bytes)
445 })
446}
447
448pub fn call_candid<Input, Output>(
451 env: &StateMachine,
452 canister_id: Principal,
453 method: &str,
454 input: Input,
455) -> Result<Output, CallError>
456where
457 Input: ArgumentEncoder,
458 Output: for<'a> ArgumentDecoder<'a>,
459{
460 call_candid_as(env, canister_id, Principal::anonymous(), method, input)
461}
462
463#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
470pub enum ErrorCode {
471 SubnetOversubscribed = 101,
472 MaxNumberOfCanistersReached = 102,
473 CanisterOutputQueueFull = 201,
474 IngressMessageTimeout = 202,
475 CanisterQueueNotEmpty = 203,
476 CanisterNotFound = 301,
477 CanisterMethodNotFound = 302,
478 CanisterAlreadyInstalled = 303,
479 CanisterWasmModuleNotFound = 304,
480 InsufficientMemoryAllocation = 402,
481 InsufficientCyclesForCreateCanister = 403,
482 SubnetNotFound = 404,
483 CanisterNotHostedBySubnet = 405,
484 CanisterOutOfCycles = 501,
485 CanisterTrapped = 502,
486 CanisterCalledTrap = 503,
487 CanisterContractViolation = 504,
488 CanisterInvalidWasm = 505,
489 CanisterDidNotReply = 506,
490 CanisterOutOfMemory = 507,
491 CanisterStopped = 508,
492 CanisterStopping = 509,
493 CanisterNotStopped = 510,
494 CanisterStoppingCancelled = 511,
495 CanisterInvalidController = 512,
496 CanisterFunctionNotFound = 513,
497 CanisterNonEmpty = 514,
498 CertifiedStateUnavailable = 515,
499 CanisterRejectedMessage = 516,
500 QueryCallGraphLoopDetected = 517,
501 UnknownManagementMessage = 518,
502 InvalidManagementPayload = 519,
503 InsufficientCyclesInCall = 520,
504 CanisterWasmEngineError = 521,
505 CanisterInstructionLimitExceeded = 522,
506 CanisterInstallCodeRateLimited = 523,
507 CanisterMemoryAccessLimitExceeded = 524,
508 QueryCallGraphTooDeep = 525,
509 QueryCallGraphTotalInstructionLimitExceeded = 526,
510 CompositeQueryCalledInReplicatedMode = 527,
511}
512
513impl fmt::Display for ErrorCode {
514 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
515 write!(f, "IC{:04}", *self as i32)
517 }
518}
519
520#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
524pub struct UserError {
525 pub code: ErrorCode,
526 pub description: String,
527}
528
529impl fmt::Display for UserError {
530 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
531 write!(f, "{}: {}", self.code, self.description)
533 }
534}
535
536#[derive(Debug)]
537pub enum CallError {
538 Reject(String),
539 UserError(UserError),
540}
541
542#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
545pub enum WasmResult {
546 Reply(#[serde(with = "serde_bytes")] Vec<u8>),
548 Reject(String),
551}
552
553pub fn with_candid<Input, Output>(
556 input: Input,
557 f: impl FnOnce(Vec<u8>) -> Result<WasmResult, UserError>,
558) -> Result<Output, CallError>
559where
560 Input: ArgumentEncoder,
561 Output: for<'a> ArgumentDecoder<'a>,
562{
563 let in_bytes = encode_args(input).expect("failed to encode args");
564 match f(in_bytes) {
565 Ok(WasmResult::Reply(out_bytes)) => Ok(decode_args(&out_bytes).unwrap_or_else(|e| {
566 panic!(
567 "Failed to decode response as candid type {}:\nerror: {}\nbytes: {:?}\nutf8: {}",
568 std::any::type_name::<Output>(),
569 e,
570 out_bytes,
571 String::from_utf8_lossy(&out_bytes),
572 )
573 })),
574 Ok(WasmResult::Reject(message)) => Err(CallError::Reject(message)),
575 Err(user_error) => Err(CallError::UserError(user_error)),
576 }
577}