1use crate::dto::{
2 auth::{
3 InternalInvocationProofRequest, RoleAttestationRequest, SignedInternalInvocationProofV1,
4 SignedRoleAttestation,
5 },
6 prelude::*,
7};
8
9#[derive(CandidType, Clone, Debug, Deserialize)]
16pub enum Request {
17 CreateCanister(CreateCanisterRequest),
18 UpgradeCanister(UpgradeCanisterRequest),
19 RecycleCanister(RecycleCanisterRequest),
20 Cycles(CyclesRequest),
21 IssueRoleAttestation(RoleAttestationRequest),
22 IssueInternalInvocationProof(InternalInvocationProofRequest),
23}
24
25impl Request {
26 #[must_use]
30 pub const fn create_canister(request: CreateCanisterRequest) -> Self {
31 Self::CreateCanister(request)
32 }
33
34 #[must_use]
38 pub const fn upgrade_canister(request: UpgradeCanisterRequest) -> Self {
39 Self::UpgradeCanister(request)
40 }
41
42 #[must_use]
46 pub const fn recycle_canister(request: RecycleCanisterRequest) -> Self {
47 Self::RecycleCanister(request)
48 }
49
50 #[must_use]
54 pub const fn cycles(request: CyclesRequest) -> Self {
55 Self::Cycles(request)
56 }
57
58 #[must_use]
62 pub const fn issue_role_attestation(request: RoleAttestationRequest) -> Self {
63 Self::IssueRoleAttestation(request)
64 }
65
66 #[must_use]
70 pub const fn issue_internal_invocation_proof(request: InternalInvocationProofRequest) -> Self {
71 Self::IssueInternalInvocationProof(request)
72 }
73
74 #[must_use]
78 pub const fn family(&self) -> RequestFamily {
79 match self {
80 Self::CreateCanister(_) => RequestFamily::Provision,
81 Self::UpgradeCanister(_) => RequestFamily::Upgrade,
82 Self::RecycleCanister(_) => RequestFamily::RecycleCanister,
83 Self::Cycles(_) => RequestFamily::RequestCycles,
84 Self::IssueRoleAttestation(_) => RequestFamily::IssueRoleAttestation,
85 Self::IssueInternalInvocationProof(_) => RequestFamily::IssueInternalInvocationProof,
86 }
87 }
88
89 #[must_use]
93 pub const fn metadata(&self) -> Option<RootRequestMetadata> {
94 match self {
95 Self::CreateCanister(req) => req.metadata,
96 Self::UpgradeCanister(req) => req.metadata,
97 Self::RecycleCanister(req) => req.metadata,
98 Self::Cycles(req) => req.metadata,
99 Self::IssueRoleAttestation(req) => req.metadata,
100 Self::IssueInternalInvocationProof(req) => req.metadata,
101 }
102 }
103
104 #[must_use]
108 pub const fn with_metadata(mut self, metadata: RootRequestMetadata) -> Self {
109 match &mut self {
110 Self::CreateCanister(req) => req.metadata = Some(metadata),
111 Self::UpgradeCanister(req) => req.metadata = Some(metadata),
112 Self::RecycleCanister(req) => req.metadata = Some(metadata),
113 Self::Cycles(req) => req.metadata = Some(metadata),
114 Self::IssueRoleAttestation(req) => req.metadata = Some(metadata),
115 Self::IssueInternalInvocationProof(req) => req.metadata = Some(metadata),
116 }
117 self
118 }
119
120 #[must_use]
124 pub const fn without_metadata(mut self) -> Self {
125 match &mut self {
126 Self::CreateCanister(req) => req.metadata = None,
127 Self::UpgradeCanister(req) => req.metadata = None,
128 Self::RecycleCanister(req) => req.metadata = None,
129 Self::Cycles(req) => req.metadata = None,
130 Self::IssueRoleAttestation(req) => req.metadata = None,
131 Self::IssueInternalInvocationProof(req) => req.metadata = None,
132 }
133 self
134 }
135
136 #[must_use]
141 pub const fn canonical_capability_payload(mut self) -> Self {
142 self = self.without_metadata();
143 if let Self::IssueRoleAttestation(req) = &mut self {
144 req.epoch = 0;
145 }
146 self
147 }
148
149 #[must_use]
153 pub const fn upgrade_request(&self) -> Option<&UpgradeCanisterRequest> {
154 match self {
155 Self::UpgradeCanister(request) => Some(request),
156 _ => None,
157 }
158 }
159}
160
161#[derive(Clone, Copy, Debug, Eq, PartialEq)]
168pub enum RequestFamily {
169 Provision,
170 Upgrade,
171 RecycleCanister,
172 RequestCycles,
173 IssueRoleAttestation,
174 IssueInternalInvocationProof,
175}
176
177impl RequestFamily {
178 #[must_use]
182 pub const fn label(self) -> &'static str {
183 match self {
184 Self::Provision => "Provision",
185 Self::Upgrade => "Upgrade",
186 Self::RecycleCanister => "RecycleCanister",
187 Self::RequestCycles => "RequestCycles",
188 Self::IssueRoleAttestation => "IssueRoleAttestation",
189 Self::IssueInternalInvocationProof => "IssueInternalInvocationProof",
190 }
191 }
192}
193
194#[derive(CandidType, Clone, Debug, Deserialize)]
201pub enum RootCapabilityCommand {
202 ProvisionCanister(CreateCanisterRequest),
203 UpgradeCanister(UpgradeCanisterRequest),
204 RecycleCanister(RecycleCanisterRequest),
205 RequestCycles(CyclesRequest),
206 IssueRoleAttestation(RoleAttestationRequest),
207 IssueInternalInvocationProof(InternalInvocationProofRequest),
208}
209
210impl From<Request> for RootCapabilityCommand {
211 fn from(value: Request) -> Self {
212 match value {
213 Request::CreateCanister(req) => Self::ProvisionCanister(req),
214 Request::UpgradeCanister(req) => Self::UpgradeCanister(req),
215 Request::RecycleCanister(req) => Self::RecycleCanister(req),
216 Request::Cycles(req) => Self::RequestCycles(req),
217 Request::IssueRoleAttestation(req) => Self::IssueRoleAttestation(req),
218 Request::IssueInternalInvocationProof(req) => Self::IssueInternalInvocationProof(req),
219 }
220 }
221}
222
223#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
230pub struct RootRequestMetadata {
231 pub request_id: [u8; 32],
232 pub ttl_seconds: u64,
233}
234
235#[derive(CandidType, Clone, Debug, Deserialize)]
242pub struct CreateCanisterRequest {
243 pub canister_role: CanisterRole,
244 pub parent: CreateCanisterParent,
245 pub extra_arg: Option<Vec<u8>>,
246 #[serde(default)]
247 pub metadata: Option<RootRequestMetadata>,
248}
249
250#[derive(CandidType, Clone, Debug, Deserialize)]
257pub enum CreateCanisterParent {
258 Root,
259 ThisCanister,
261 Parent,
263 Canister(Principal),
264 Index(CanisterRole),
265}
266
267#[derive(CandidType, Clone, Debug, Deserialize)]
274pub struct UpgradeCanisterRequest {
275 pub canister_pid: Principal,
276 #[serde(default)]
277 pub metadata: Option<RootRequestMetadata>,
278}
279
280#[derive(CandidType, Clone, Debug, Deserialize)]
287pub struct RecycleCanisterRequest {
288 pub canister_pid: Principal,
289 #[serde(default)]
290 pub metadata: Option<RootRequestMetadata>,
291}
292
293#[derive(CandidType, Clone, Debug, Deserialize)]
300pub struct CyclesRequest {
301 pub cycles: u128,
302 #[serde(default)]
303 pub metadata: Option<RootRequestMetadata>,
304}
305
306#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
313pub enum Response {
314 CreateCanister(CreateCanisterResponse),
315 UpgradeCanister(UpgradeCanisterResponse),
316 RecycleCanister(RecycleCanisterResponse),
317 Cycles(CyclesResponse),
318 RoleAttestationIssued(SignedRoleAttestation),
319 InternalInvocationProofIssued(SignedInternalInvocationProofV1),
320}
321
322#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
328pub struct CreateCanisterResponse {
329 pub new_canister_pid: Principal,
330}
331
332#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
338pub struct UpgradeCanisterResponse {}
339
340#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
346pub struct RecycleCanisterResponse {}
347
348#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
354pub struct CyclesResponse {
355 pub cycles_transferred: u128,
356}
357
358#[cfg(test)]
363mod tests {
364 use super::*;
365
366 fn p(id: u8) -> Principal {
367 Principal::from_slice(&[id; 29])
368 }
369
370 fn metadata(id: u8) -> RootRequestMetadata {
371 RootRequestMetadata {
372 request_id: [id; 32],
373 ttl_seconds: 60,
374 }
375 }
376
377 fn requests_with_no_metadata() -> Vec<Request> {
378 vec![
379 Request::create_canister(CreateCanisterRequest {
380 canister_role: CanisterRole::new("app"),
381 parent: CreateCanisterParent::Root,
382 extra_arg: None,
383 metadata: None,
384 }),
385 Request::upgrade_canister(UpgradeCanisterRequest {
386 canister_pid: p(2),
387 metadata: None,
388 }),
389 Request::recycle_canister(RecycleCanisterRequest {
390 canister_pid: p(7),
391 metadata: None,
392 }),
393 Request::cycles(CyclesRequest {
394 cycles: 100,
395 metadata: None,
396 }),
397 Request::issue_role_attestation(RoleAttestationRequest {
398 subject: p(5),
399 role: CanisterRole::new("test"),
400 subnet_id: None,
401 audience: p(6),
402 ttl_secs: 60,
403 epoch: 0,
404 metadata: None,
405 }),
406 Request::issue_internal_invocation_proof(InternalInvocationProofRequest {
407 subject: p(5),
408 role: CanisterRole::new("test"),
409 subnet_id: None,
410 audience: p(6),
411 audience_method: "canic_internal".to_string(),
412 ttl_secs: 60,
413 metadata: None,
414 }),
415 ]
416 }
417
418 #[test]
419 fn request_family_matches_all_variants() {
420 let families: Vec<RequestFamily> = requests_with_no_metadata()
421 .iter()
422 .map(Request::family)
423 .collect();
424 assert_eq!(
425 families,
426 vec![
427 RequestFamily::Provision,
428 RequestFamily::Upgrade,
429 RequestFamily::RecycleCanister,
430 RequestFamily::RequestCycles,
431 RequestFamily::IssueRoleAttestation,
432 RequestFamily::IssueInternalInvocationProof,
433 ]
434 );
435 }
436
437 #[test]
438 fn with_metadata_and_without_metadata_cover_all_variants() {
439 let replay_meta = metadata(7);
440
441 for request in requests_with_no_metadata() {
442 let with_meta = request.clone().with_metadata(replay_meta);
443 assert_eq!(
444 with_meta.metadata(),
445 Some(replay_meta),
446 "with_metadata must set metadata for every request variant"
447 );
448
449 let without_meta = with_meta.without_metadata();
450 assert_eq!(
451 without_meta.metadata(),
452 None,
453 "without_metadata must strip metadata for every request variant"
454 );
455 }
456 }
457
458 #[test]
459 fn canonical_capability_payload_strips_metadata_and_ignored_epoch() {
460 let canonical = Request::issue_role_attestation(RoleAttestationRequest {
461 subject: p(5),
462 role: CanisterRole::new("test"),
463 subnet_id: None,
464 audience: p(6),
465 ttl_secs: 60,
466 epoch: 99,
467 metadata: Some(metadata(9)),
468 })
469 .canonical_capability_payload();
470
471 match canonical {
472 Request::IssueRoleAttestation(req) => {
473 assert_eq!(req.metadata, None);
474 assert_eq!(req.epoch, 0);
475 }
476 other => panic!("expected role-attestation request, got {other:?}"),
477 }
478 }
479
480 #[test]
481 fn upgrade_request_is_only_available_for_upgrade_variant() {
482 let upgrade = Request::upgrade_canister(UpgradeCanisterRequest {
483 canister_pid: p(9),
484 metadata: Some(metadata(9)),
485 });
486 assert!(upgrade.upgrade_request().is_some());
487
488 for request in requests_with_no_metadata() {
489 if !matches!(request, Request::UpgradeCanister(_)) {
490 assert!(request.upgrade_request().is_none());
491 }
492 }
493 }
494}