Skip to main content

canic_core/dto/
rpc.rs

1use crate::dto::{
2    auth::{
3        DelegationProvisionResponse, DelegationRequest, RoleAttestationRequest,
4        SignedRoleAttestation,
5    },
6    prelude::*,
7};
8
9//
10// Request
11//
12// Root orchestration request.
13//
14
15#[derive(CandidType, Clone, Debug, Deserialize)]
16pub enum Request {
17    CreateCanister(CreateCanisterRequest),
18    UpgradeCanister(UpgradeCanisterRequest),
19    RecycleCanister(RecycleCanisterRequest),
20    Cycles(CyclesRequest),
21    IssueDelegation(DelegationRequest),
22    IssueRoleAttestation(RoleAttestationRequest),
23}
24
25impl Request {
26    // create_canister
27    //
28    // Build a root request for canister provisioning.
29    #[must_use]
30    pub const fn create_canister(request: CreateCanisterRequest) -> Self {
31        Self::CreateCanister(request)
32    }
33
34    // upgrade_canister
35    //
36    // Build a root request for upgrading an existing canister.
37    #[must_use]
38    pub const fn upgrade_canister(request: UpgradeCanisterRequest) -> Self {
39        Self::UpgradeCanister(request)
40    }
41
42    // recycle_canister
43    //
44    // Build a root request for recycling one child canister back into the pool.
45    #[must_use]
46    pub const fn recycle_canister(request: RecycleCanisterRequest) -> Self {
47        Self::RecycleCanister(request)
48    }
49
50    // cycles
51    //
52    // Build a root request for requesting/transferring cycles.
53    #[must_use]
54    pub const fn cycles(request: CyclesRequest) -> Self {
55        Self::Cycles(request)
56    }
57
58    // issue_delegation
59    //
60    // Build a root request for delegated token issuance.
61    #[must_use]
62    pub const fn issue_delegation(request: DelegationRequest) -> Self {
63        Self::IssueDelegation(request)
64    }
65
66    // issue_role_attestation
67    //
68    // Build a root request for role attestation issuance.
69    #[must_use]
70    pub const fn issue_role_attestation(request: RoleAttestationRequest) -> Self {
71        Self::IssueRoleAttestation(request)
72    }
73
74    // family
75    //
76    // Resolve the request capability family without exposing variant matches at call sites.
77    #[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::IssueDelegation(_) => RequestFamily::IssueDelegation,
85            Self::IssueRoleAttestation(_) => RequestFamily::IssueRoleAttestation,
86        }
87    }
88
89    // metadata
90    //
91    // Return replay metadata carried by the request variant.
92    #[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::IssueDelegation(req) => req.metadata,
100            Self::IssueRoleAttestation(req) => req.metadata,
101        }
102    }
103
104    // with_metadata
105    //
106    // Attach root replay metadata to the request payload.
107    #[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::IssueDelegation(req) => req.metadata = Some(metadata),
115            Self::IssueRoleAttestation(req) => req.metadata = Some(metadata),
116        }
117        self
118    }
119
120    // without_metadata
121    //
122    // Remove root replay metadata for canonical hashing and signature binding.
123    #[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::IssueDelegation(req) => req.metadata = None,
131            Self::IssueRoleAttestation(req) => req.metadata = None,
132        }
133        self
134    }
135
136    // upgrade_request
137    //
138    // Return the upgrade payload when this request belongs to the upgrade family.
139    #[must_use]
140    pub const fn upgrade_request(&self) -> Option<&UpgradeCanisterRequest> {
141        match self {
142            Self::UpgradeCanister(request) => Some(request),
143            _ => None,
144        }
145    }
146}
147
148//
149// RequestFamily
150//
151// Request family label.
152//
153
154#[derive(Clone, Copy, Debug, Eq, PartialEq)]
155pub enum RequestFamily {
156    Provision,
157    Upgrade,
158    RecycleCanister,
159    RequestCycles,
160    IssueDelegation,
161    IssueRoleAttestation,
162}
163
164impl RequestFamily {
165    // label
166    //
167    // Return the canonical family label used across capability checks and logs.
168    #[must_use]
169    pub const fn label(self) -> &'static str {
170        match self {
171            Self::Provision => "Provision",
172            Self::Upgrade => "Upgrade",
173            Self::RecycleCanister => "RecycleCanister",
174            Self::RequestCycles => "RequestCycles",
175            Self::IssueDelegation => "IssueDelegation",
176            Self::IssueRoleAttestation => "IssueRoleAttestation",
177        }
178    }
179}
180
181//
182// RootCapabilityCommand
183//
184// Internal root command.
185//
186
187#[derive(CandidType, Clone, Debug, Deserialize)]
188pub enum RootCapabilityCommand {
189    ProvisionCanister(CreateCanisterRequest),
190    UpgradeCanister(UpgradeCanisterRequest),
191    RecycleCanister(RecycleCanisterRequest),
192    RequestCycles(CyclesRequest),
193    IssueDelegation(DelegationRequest),
194    IssueRoleAttestation(RoleAttestationRequest),
195}
196
197impl From<Request> for RootCapabilityCommand {
198    fn from(value: Request) -> Self {
199        match value {
200            Request::CreateCanister(req) => Self::ProvisionCanister(req),
201            Request::UpgradeCanister(req) => Self::UpgradeCanister(req),
202            Request::RecycleCanister(req) => Self::RecycleCanister(req),
203            Request::Cycles(req) => Self::RequestCycles(req),
204            Request::IssueDelegation(req) => Self::IssueDelegation(req),
205            Request::IssueRoleAttestation(req) => Self::IssueRoleAttestation(req),
206        }
207    }
208}
209
210//
211// RootRequestMetadata
212//
213// Replay metadata.
214//
215
216#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
217pub struct RootRequestMetadata {
218    pub request_id: [u8; 32],
219    pub ttl_seconds: u64,
220}
221
222//
223// CreateCanisterRequest
224//
225// Create-canister payload.
226//
227
228#[derive(CandidType, Clone, Debug, Deserialize)]
229pub struct CreateCanisterRequest {
230    pub canister_role: CanisterRole,
231    pub parent: CreateCanisterParent,
232    pub extra_arg: Option<Vec<u8>>,
233    #[serde(default)]
234    pub metadata: Option<RootRequestMetadata>,
235}
236
237//
238// CreateCanisterParent
239//
240// Parent selection.
241//
242
243#[derive(CandidType, Clone, Debug, Deserialize)]
244pub enum CreateCanisterParent {
245    Root,
246    // Use the requesting canister.
247    ThisCanister,
248    // Use the caller's parent.
249    Parent,
250    Canister(Principal),
251    Index(CanisterRole),
252}
253
254//
255// UpgradeCanisterRequest
256//
257// Upgrade-canister payload.
258//
259
260#[derive(CandidType, Clone, Debug, Deserialize)]
261pub struct UpgradeCanisterRequest {
262    pub canister_pid: Principal,
263    #[serde(default)]
264    pub metadata: Option<RootRequestMetadata>,
265}
266
267//
268// RecycleCanisterRequest
269//
270// Recycle-one-child payload.
271//
272
273#[derive(CandidType, Clone, Debug, Deserialize)]
274pub struct RecycleCanisterRequest {
275    pub canister_pid: Principal,
276    #[serde(default)]
277    pub metadata: Option<RootRequestMetadata>,
278}
279
280//
281// CyclesRequest
282//
283// Cycles payload.
284//
285
286#[derive(CandidType, Clone, Debug, Deserialize)]
287pub struct CyclesRequest {
288    pub cycles: u128,
289    #[serde(default)]
290    pub metadata: Option<RootRequestMetadata>,
291}
292
293//
294// Response
295//
296// Root response payload.
297//
298
299#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
300pub enum Response {
301    CreateCanister(CreateCanisterResponse),
302    UpgradeCanister(UpgradeCanisterResponse),
303    RecycleCanister(RecycleCanisterResponse),
304    Cycles(CyclesResponse),
305    DelegationIssued(DelegationProvisionResponse),
306    RoleAttestationIssued(SignedRoleAttestation),
307}
308
309//
310// CreateCanisterResponse
311// Result of creating and installing a new canister.
312//
313
314#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
315pub struct CreateCanisterResponse {
316    pub new_canister_pid: Principal,
317}
318
319//
320// UpgradeCanisterResponse
321// Result of an upgrade request (currently empty, reserved for metadata)
322//
323
324#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
325pub struct UpgradeCanisterResponse {}
326
327//
328// RecycleCanisterResponse
329// Result of recycling one canister back into the pool.
330//
331
332#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
333pub struct RecycleCanisterResponse {}
334
335//
336// CyclesResponse
337// Result of transferring cycles to a child canister
338//
339
340#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
341pub struct CyclesResponse {
342    pub cycles_transferred: u128,
343}
344
345//
346// TESTS
347//
348
349#[cfg(test)]
350mod tests {
351    use super::*;
352
353    fn p(id: u8) -> Principal {
354        Principal::from_slice(&[id; 29])
355    }
356
357    fn metadata(id: u8) -> RootRequestMetadata {
358        RootRequestMetadata {
359            request_id: [id; 32],
360            ttl_seconds: 60,
361        }
362    }
363
364    fn requests_with_no_metadata() -> Vec<Request> {
365        vec![
366            Request::create_canister(CreateCanisterRequest {
367                canister_role: CanisterRole::new("app"),
368                parent: CreateCanisterParent::Root,
369                extra_arg: None,
370                metadata: None,
371            }),
372            Request::upgrade_canister(UpgradeCanisterRequest {
373                canister_pid: p(2),
374                metadata: None,
375            }),
376            Request::recycle_canister(RecycleCanisterRequest {
377                canister_pid: p(7),
378                metadata: None,
379            }),
380            Request::cycles(CyclesRequest {
381                cycles: 100,
382                metadata: None,
383            }),
384            Request::issue_delegation(DelegationRequest {
385                shard_pid: p(3),
386                scopes: vec!["rpc:verify".to_string()],
387                aud: vec![p(4)],
388                ttl_secs: 60,
389                verifier_targets: vec![],
390                include_root_verifier: false,
391                shard_public_key_sec1: None,
392                metadata: None,
393            }),
394            Request::issue_role_attestation(RoleAttestationRequest {
395                subject: p(5),
396                role: CanisterRole::new("test"),
397                subnet_id: None,
398                audience: Some(p(6)),
399                ttl_secs: 60,
400                epoch: 0,
401                metadata: None,
402            }),
403        ]
404    }
405
406    #[test]
407    fn request_family_matches_all_variants() {
408        let families: Vec<RequestFamily> = requests_with_no_metadata()
409            .iter()
410            .map(Request::family)
411            .collect();
412        assert_eq!(
413            families,
414            vec![
415                RequestFamily::Provision,
416                RequestFamily::Upgrade,
417                RequestFamily::RecycleCanister,
418                RequestFamily::RequestCycles,
419                RequestFamily::IssueDelegation,
420                RequestFamily::IssueRoleAttestation,
421            ]
422        );
423    }
424
425    #[test]
426    fn with_metadata_and_without_metadata_cover_all_variants() {
427        let replay_meta = metadata(7);
428
429        for request in requests_with_no_metadata() {
430            let with_meta = request.clone().with_metadata(replay_meta);
431            assert_eq!(
432                with_meta.metadata(),
433                Some(replay_meta),
434                "with_metadata must set metadata for every request variant"
435            );
436
437            let without_meta = with_meta.without_metadata();
438            assert_eq!(
439                without_meta.metadata(),
440                None,
441                "without_metadata must strip metadata for every request variant"
442            );
443        }
444    }
445
446    #[test]
447    fn upgrade_request_is_only_available_for_upgrade_variant() {
448        let upgrade = Request::upgrade_canister(UpgradeCanisterRequest {
449            canister_pid: p(9),
450            metadata: Some(metadata(9)),
451        });
452        assert!(upgrade.upgrade_request().is_some());
453
454        for request in requests_with_no_metadata() {
455            if !matches!(request, Request::UpgradeCanister(_)) {
456                assert!(request.upgrade_request().is_none());
457            }
458        }
459    }
460}