1use crate::dto::{
2 auth::{RoleAttestationRequest, SignedRoleAttestation},
3 prelude::*,
4};
5
6#[derive(CandidType, Clone, Debug, Deserialize)]
13pub enum Request {
14 CreateCanister(CreateCanisterRequest),
15 UpgradeCanister(UpgradeCanisterRequest),
16 RecycleCanister(RecycleCanisterRequest),
17 Cycles(CyclesRequest),
18 IssueRoleAttestation(RoleAttestationRequest),
19}
20
21impl Request {
22 #[must_use]
26 pub const fn create_canister(request: CreateCanisterRequest) -> Self {
27 Self::CreateCanister(request)
28 }
29
30 #[must_use]
34 pub const fn upgrade_canister(request: UpgradeCanisterRequest) -> Self {
35 Self::UpgradeCanister(request)
36 }
37
38 #[must_use]
42 pub const fn recycle_canister(request: RecycleCanisterRequest) -> Self {
43 Self::RecycleCanister(request)
44 }
45
46 #[must_use]
50 pub const fn cycles(request: CyclesRequest) -> Self {
51 Self::Cycles(request)
52 }
53
54 #[must_use]
58 pub const fn issue_role_attestation(request: RoleAttestationRequest) -> Self {
59 Self::IssueRoleAttestation(request)
60 }
61
62 #[must_use]
66 pub const fn family(&self) -> RequestFamily {
67 match self {
68 Self::CreateCanister(_) => RequestFamily::Provision,
69 Self::UpgradeCanister(_) => RequestFamily::Upgrade,
70 Self::RecycleCanister(_) => RequestFamily::RecycleCanister,
71 Self::Cycles(_) => RequestFamily::RequestCycles,
72 Self::IssueRoleAttestation(_) => RequestFamily::IssueRoleAttestation,
73 }
74 }
75
76 #[must_use]
80 pub const fn metadata(&self) -> Option<RootRequestMetadata> {
81 match self {
82 Self::CreateCanister(req) => req.metadata,
83 Self::UpgradeCanister(req) => req.metadata,
84 Self::RecycleCanister(req) => req.metadata,
85 Self::Cycles(req) => req.metadata,
86 Self::IssueRoleAttestation(req) => req.metadata,
87 }
88 }
89
90 #[must_use]
94 pub const fn with_metadata(mut self, metadata: RootRequestMetadata) -> Self {
95 match &mut self {
96 Self::CreateCanister(req) => req.metadata = Some(metadata),
97 Self::UpgradeCanister(req) => req.metadata = Some(metadata),
98 Self::RecycleCanister(req) => req.metadata = Some(metadata),
99 Self::Cycles(req) => req.metadata = Some(metadata),
100 Self::IssueRoleAttestation(req) => req.metadata = Some(metadata),
101 }
102 self
103 }
104
105 #[must_use]
109 pub const fn without_metadata(mut self) -> Self {
110 match &mut self {
111 Self::CreateCanister(req) => req.metadata = None,
112 Self::UpgradeCanister(req) => req.metadata = None,
113 Self::RecycleCanister(req) => req.metadata = None,
114 Self::Cycles(req) => req.metadata = None,
115 Self::IssueRoleAttestation(req) => req.metadata = None,
116 }
117 self
118 }
119
120 #[must_use]
124 pub const fn upgrade_request(&self) -> Option<&UpgradeCanisterRequest> {
125 match self {
126 Self::UpgradeCanister(request) => Some(request),
127 _ => None,
128 }
129 }
130}
131
132#[derive(Clone, Copy, Debug, Eq, PartialEq)]
139pub enum RequestFamily {
140 Provision,
141 Upgrade,
142 RecycleCanister,
143 RequestCycles,
144 IssueRoleAttestation,
145}
146
147impl RequestFamily {
148 #[must_use]
152 pub const fn label(self) -> &'static str {
153 match self {
154 Self::Provision => "Provision",
155 Self::Upgrade => "Upgrade",
156 Self::RecycleCanister => "RecycleCanister",
157 Self::RequestCycles => "RequestCycles",
158 Self::IssueRoleAttestation => "IssueRoleAttestation",
159 }
160 }
161}
162
163#[derive(CandidType, Clone, Debug, Deserialize)]
170pub enum RootCapabilityCommand {
171 ProvisionCanister(CreateCanisterRequest),
172 UpgradeCanister(UpgradeCanisterRequest),
173 RecycleCanister(RecycleCanisterRequest),
174 RequestCycles(CyclesRequest),
175 IssueRoleAttestation(RoleAttestationRequest),
176}
177
178impl From<Request> for RootCapabilityCommand {
179 fn from(value: Request) -> Self {
180 match value {
181 Request::CreateCanister(req) => Self::ProvisionCanister(req),
182 Request::UpgradeCanister(req) => Self::UpgradeCanister(req),
183 Request::RecycleCanister(req) => Self::RecycleCanister(req),
184 Request::Cycles(req) => Self::RequestCycles(req),
185 Request::IssueRoleAttestation(req) => Self::IssueRoleAttestation(req),
186 }
187 }
188}
189
190#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
197pub struct RootRequestMetadata {
198 pub request_id: [u8; 32],
199 pub ttl_seconds: u64,
200}
201
202#[derive(CandidType, Clone, Debug, Deserialize)]
209pub struct CreateCanisterRequest {
210 pub canister_role: CanisterRole,
211 pub parent: CreateCanisterParent,
212 pub extra_arg: Option<Vec<u8>>,
213 #[serde(default)]
214 pub metadata: Option<RootRequestMetadata>,
215}
216
217#[derive(CandidType, Clone, Debug, Deserialize)]
224pub enum CreateCanisterParent {
225 Root,
226 ThisCanister,
228 Parent,
230 Canister(Principal),
231 Index(CanisterRole),
232}
233
234#[derive(CandidType, Clone, Debug, Deserialize)]
241pub struct UpgradeCanisterRequest {
242 pub canister_pid: Principal,
243 #[serde(default)]
244 pub metadata: Option<RootRequestMetadata>,
245}
246
247#[derive(CandidType, Clone, Debug, Deserialize)]
254pub struct RecycleCanisterRequest {
255 pub canister_pid: Principal,
256 #[serde(default)]
257 pub metadata: Option<RootRequestMetadata>,
258}
259
260#[derive(CandidType, Clone, Debug, Deserialize)]
267pub struct CyclesRequest {
268 pub cycles: u128,
269 #[serde(default)]
270 pub metadata: Option<RootRequestMetadata>,
271}
272
273#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
280pub enum Response {
281 CreateCanister(CreateCanisterResponse),
282 UpgradeCanister(UpgradeCanisterResponse),
283 RecycleCanister(RecycleCanisterResponse),
284 Cycles(CyclesResponse),
285 RoleAttestationIssued(SignedRoleAttestation),
286}
287
288#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
294pub struct CreateCanisterResponse {
295 pub new_canister_pid: Principal,
296}
297
298#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
304pub struct UpgradeCanisterResponse {}
305
306#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
312pub struct RecycleCanisterResponse {}
313
314#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
320pub struct CyclesResponse {
321 pub cycles_transferred: u128,
322}
323
324#[cfg(test)]
329mod tests {
330 use super::*;
331
332 fn p(id: u8) -> Principal {
333 Principal::from_slice(&[id; 29])
334 }
335
336 fn metadata(id: u8) -> RootRequestMetadata {
337 RootRequestMetadata {
338 request_id: [id; 32],
339 ttl_seconds: 60,
340 }
341 }
342
343 fn requests_with_no_metadata() -> Vec<Request> {
344 vec![
345 Request::create_canister(CreateCanisterRequest {
346 canister_role: CanisterRole::new("app"),
347 parent: CreateCanisterParent::Root,
348 extra_arg: None,
349 metadata: None,
350 }),
351 Request::upgrade_canister(UpgradeCanisterRequest {
352 canister_pid: p(2),
353 metadata: None,
354 }),
355 Request::recycle_canister(RecycleCanisterRequest {
356 canister_pid: p(7),
357 metadata: None,
358 }),
359 Request::cycles(CyclesRequest {
360 cycles: 100,
361 metadata: None,
362 }),
363 Request::issue_role_attestation(RoleAttestationRequest {
364 subject: p(5),
365 role: CanisterRole::new("test"),
366 subnet_id: None,
367 audience: p(6),
368 ttl_secs: 60,
369 epoch: 0,
370 metadata: None,
371 }),
372 ]
373 }
374
375 #[test]
376 fn request_family_matches_all_variants() {
377 let families: Vec<RequestFamily> = requests_with_no_metadata()
378 .iter()
379 .map(Request::family)
380 .collect();
381 assert_eq!(
382 families,
383 vec![
384 RequestFamily::Provision,
385 RequestFamily::Upgrade,
386 RequestFamily::RecycleCanister,
387 RequestFamily::RequestCycles,
388 RequestFamily::IssueRoleAttestation,
389 ]
390 );
391 }
392
393 #[test]
394 fn with_metadata_and_without_metadata_cover_all_variants() {
395 let replay_meta = metadata(7);
396
397 for request in requests_with_no_metadata() {
398 let with_meta = request.clone().with_metadata(replay_meta);
399 assert_eq!(
400 with_meta.metadata(),
401 Some(replay_meta),
402 "with_metadata must set metadata for every request variant"
403 );
404
405 let without_meta = with_meta.without_metadata();
406 assert_eq!(
407 without_meta.metadata(),
408 None,
409 "without_metadata must strip metadata for every request variant"
410 );
411 }
412 }
413
414 #[test]
415 fn upgrade_request_is_only_available_for_upgrade_variant() {
416 let upgrade = Request::upgrade_canister(UpgradeCanisterRequest {
417 canister_pid: p(9),
418 metadata: Some(metadata(9)),
419 });
420 assert!(upgrade.upgrade_request().is_some());
421
422 for request in requests_with_no_metadata() {
423 if !matches!(request, Request::UpgradeCanister(_)) {
424 assert!(request.upgrade_request().is_none());
425 }
426 }
427 }
428}