Skip to main content

canic_core/dto/
rpc.rs

1use crate::dto::{
2    auth::{RoleAttestationRequest, SignedRoleAttestation},
3    prelude::*,
4};
5
6//
7// Request
8//
9// Root orchestration request.
10//
11
12#[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    // create_canister
23    //
24    // Build a root request for canister provisioning.
25    #[must_use]
26    pub const fn create_canister(request: CreateCanisterRequest) -> Self {
27        Self::CreateCanister(request)
28    }
29
30    // upgrade_canister
31    //
32    // Build a root request for upgrading an existing canister.
33    #[must_use]
34    pub const fn upgrade_canister(request: UpgradeCanisterRequest) -> Self {
35        Self::UpgradeCanister(request)
36    }
37
38    // recycle_canister
39    //
40    // Build a root request for recycling one child canister back into the pool.
41    #[must_use]
42    pub const fn recycle_canister(request: RecycleCanisterRequest) -> Self {
43        Self::RecycleCanister(request)
44    }
45
46    // cycles
47    //
48    // Build a root request for requesting/transferring cycles.
49    #[must_use]
50    pub const fn cycles(request: CyclesRequest) -> Self {
51        Self::Cycles(request)
52    }
53
54    // issue_role_attestation
55    //
56    // Build a root request for role attestation issuance.
57    #[must_use]
58    pub const fn issue_role_attestation(request: RoleAttestationRequest) -> Self {
59        Self::IssueRoleAttestation(request)
60    }
61
62    // family
63    //
64    // Resolve the request capability family without exposing variant matches at call sites.
65    #[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    // metadata
77    //
78    // Return replay metadata carried by the request variant.
79    #[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    // with_metadata
91    //
92    // Attach root replay metadata to the request payload.
93    #[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    // without_metadata
106    //
107    // Remove root replay metadata for canonical hashing and signature binding.
108    #[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    // upgrade_request
121    //
122    // Return the upgrade payload when this request belongs to the upgrade family.
123    #[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//
133// RequestFamily
134//
135// Request family label.
136//
137
138#[derive(Clone, Copy, Debug, Eq, PartialEq)]
139pub enum RequestFamily {
140    Provision,
141    Upgrade,
142    RecycleCanister,
143    RequestCycles,
144    IssueRoleAttestation,
145}
146
147impl RequestFamily {
148    // label
149    //
150    // Return the canonical family label used across capability checks and logs.
151    #[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//
164// RootCapabilityCommand
165//
166// Internal root command.
167//
168
169#[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//
191// RootRequestMetadata
192//
193// Replay metadata.
194//
195
196#[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//
203// CreateCanisterRequest
204//
205// Create-canister payload.
206//
207
208#[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//
218// CreateCanisterParent
219//
220// Parent selection.
221//
222
223#[derive(CandidType, Clone, Debug, Deserialize)]
224pub enum CreateCanisterParent {
225    Root,
226    // Use the requesting canister.
227    ThisCanister,
228    // Use the caller's parent.
229    Parent,
230    Canister(Principal),
231    Index(CanisterRole),
232}
233
234//
235// UpgradeCanisterRequest
236//
237// Upgrade-canister payload.
238//
239
240#[derive(CandidType, Clone, Debug, Deserialize)]
241pub struct UpgradeCanisterRequest {
242    pub canister_pid: Principal,
243    #[serde(default)]
244    pub metadata: Option<RootRequestMetadata>,
245}
246
247//
248// RecycleCanisterRequest
249//
250// Recycle-one-child payload.
251//
252
253#[derive(CandidType, Clone, Debug, Deserialize)]
254pub struct RecycleCanisterRequest {
255    pub canister_pid: Principal,
256    #[serde(default)]
257    pub metadata: Option<RootRequestMetadata>,
258}
259
260//
261// CyclesRequest
262//
263// Cycles payload.
264//
265
266#[derive(CandidType, Clone, Debug, Deserialize)]
267pub struct CyclesRequest {
268    pub cycles: u128,
269    #[serde(default)]
270    pub metadata: Option<RootRequestMetadata>,
271}
272
273//
274// Response
275//
276// Root response payload.
277//
278
279#[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//
289// CreateCanisterResponse
290// Result of creating and installing a new canister.
291//
292
293#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
294pub struct CreateCanisterResponse {
295    pub new_canister_pid: Principal,
296}
297
298//
299// UpgradeCanisterResponse
300// Result of an upgrade request (currently empty, reserved for metadata)
301//
302
303#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
304pub struct UpgradeCanisterResponse {}
305
306//
307// RecycleCanisterResponse
308// Result of recycling one canister back into the pool.
309//
310
311#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
312pub struct RecycleCanisterResponse {}
313
314//
315// CyclesResponse
316// Result of transferring cycles to a child canister
317//
318
319#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
320pub struct CyclesResponse {
321    pub cycles_transferred: u128,
322}
323
324//
325// TESTS
326//
327
328#[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: Some(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}