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