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