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)]
15pub enum Request {
16    CreateCanister(CreateCanisterRequest),
17    UpgradeCanister(UpgradeCanisterRequest),
18    Cycles(CyclesRequest),
19    IssueDelegation(DelegationRequest),
20    IssueRoleAttestation(RoleAttestationRequest),
21}
22
23impl Request {
24    /// create_canister
25    ///
26    /// Build a root request for canister provisioning.
27    #[must_use]
28    pub const fn create_canister(request: CreateCanisterRequest) -> Self {
29        Self::CreateCanister(request)
30    }
31
32    /// upgrade_canister
33    ///
34    /// Build a root request for upgrading an existing canister.
35    #[must_use]
36    pub const fn upgrade_canister(request: UpgradeCanisterRequest) -> Self {
37        Self::UpgradeCanister(request)
38    }
39
40    /// cycles
41    ///
42    /// Build a root request for requesting/transferring cycles.
43    #[must_use]
44    pub const fn cycles(request: CyclesRequest) -> Self {
45        Self::Cycles(request)
46    }
47
48    /// issue_delegation
49    ///
50    /// Build a root request for delegated token issuance.
51    #[must_use]
52    pub const fn issue_delegation(request: DelegationRequest) -> Self {
53        Self::IssueDelegation(request)
54    }
55
56    /// issue_role_attestation
57    ///
58    /// Build a root request for role attestation issuance.
59    #[must_use]
60    pub const fn issue_role_attestation(request: RoleAttestationRequest) -> Self {
61        Self::IssueRoleAttestation(request)
62    }
63
64    /// family
65    ///
66    /// Resolve the request capability family without exposing variant matches at call sites.
67    #[must_use]
68    pub const fn family(&self) -> RequestFamily {
69        match self {
70            Self::CreateCanister(_) => RequestFamily::Provision,
71            Self::UpgradeCanister(_) => RequestFamily::Upgrade,
72            Self::Cycles(_) => RequestFamily::RequestCycles,
73            Self::IssueDelegation(_) => RequestFamily::IssueDelegation,
74            Self::IssueRoleAttestation(_) => RequestFamily::IssueRoleAttestation,
75        }
76    }
77
78    /// metadata
79    ///
80    /// Return replay metadata carried by the request variant.
81    #[must_use]
82    pub const fn metadata(&self) -> Option<RootRequestMetadata> {
83        match self {
84            Self::CreateCanister(req) => req.metadata,
85            Self::UpgradeCanister(req) => req.metadata,
86            Self::Cycles(req) => req.metadata,
87            Self::IssueDelegation(req) => req.metadata,
88            Self::IssueRoleAttestation(req) => req.metadata,
89        }
90    }
91
92    /// with_metadata
93    ///
94    /// Attach root replay metadata to the request payload.
95    #[must_use]
96    pub const fn with_metadata(mut self, metadata: RootRequestMetadata) -> Self {
97        match &mut self {
98            Self::CreateCanister(req) => req.metadata = Some(metadata),
99            Self::UpgradeCanister(req) => req.metadata = Some(metadata),
100            Self::Cycles(req) => req.metadata = Some(metadata),
101            Self::IssueDelegation(req) => req.metadata = Some(metadata),
102            Self::IssueRoleAttestation(req) => req.metadata = Some(metadata),
103        }
104        self
105    }
106
107    /// without_metadata
108    ///
109    /// Remove root replay metadata for canonical hashing and signature binding.
110    #[must_use]
111    pub const fn without_metadata(mut self) -> Self {
112        match &mut self {
113            Self::CreateCanister(req) => req.metadata = None,
114            Self::UpgradeCanister(req) => req.metadata = None,
115            Self::Cycles(req) => req.metadata = None,
116            Self::IssueDelegation(req) => req.metadata = None,
117            Self::IssueRoleAttestation(req) => req.metadata = None,
118        }
119        self
120    }
121
122    /// upgrade_request
123    ///
124    /// Return the upgrade payload when this request belongs to the upgrade family.
125    #[must_use]
126    pub const fn upgrade_request(&self) -> Option<&UpgradeCanisterRequest> {
127        match self {
128            Self::UpgradeCanister(request) => Some(request),
129            _ => None,
130        }
131    }
132}
133
134///
135/// RequestFamily
136/// Stable capability family identifier for request dispatch logic.
137///
138
139#[derive(Clone, Copy, Debug, Eq, PartialEq)]
140pub enum RequestFamily {
141    Provision,
142    Upgrade,
143    RequestCycles,
144    IssueDelegation,
145    IssueRoleAttestation,
146}
147
148impl RequestFamily {
149    /// label
150    ///
151    /// Return the canonical family label used across capability checks and logs.
152    #[must_use]
153    pub const fn label(self) -> &'static str {
154        match self {
155            Self::Provision => "Provision",
156            Self::Upgrade => "Upgrade",
157            Self::RequestCycles => "RequestCycles",
158            Self::IssueDelegation => "IssueDelegation",
159            Self::IssueRoleAttestation => "IssueRoleAttestation",
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)]
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)]
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)]
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)]
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)]
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)]
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)]
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)]
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)]
285pub struct UpgradeCanisterResponse {}
286
287///
288/// CyclesResponse
289/// Result of transferring cycles to a child canister
290///
291
292#[derive(CandidType, Clone, Debug, Deserialize)]
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}