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    use crate::dto::auth::DelegationAudience;
353
354    fn p(id: u8) -> Principal {
355        Principal::from_slice(&[id; 29])
356    }
357
358    fn metadata(id: u8) -> RootRequestMetadata {
359        RootRequestMetadata {
360            request_id: [id; 32],
361            ttl_seconds: 60,
362        }
363    }
364
365    fn requests_with_no_metadata() -> Vec<Request> {
366        vec![
367            Request::create_canister(CreateCanisterRequest {
368                canister_role: CanisterRole::new("app"),
369                parent: CreateCanisterParent::Root,
370                extra_arg: None,
371                metadata: None,
372            }),
373            Request::upgrade_canister(UpgradeCanisterRequest {
374                canister_pid: p(2),
375                metadata: None,
376            }),
377            Request::recycle_canister(RecycleCanisterRequest {
378                canister_pid: p(7),
379                metadata: None,
380            }),
381            Request::cycles(CyclesRequest {
382                cycles: 100,
383                metadata: None,
384            }),
385            Request::issue_delegation(DelegationRequest {
386                shard_pid: p(3),
387                scopes: vec!["rpc:verify".to_string()],
388                aud: DelegationAudience::Roles(vec![CanisterRole::new("app")]),
389                ttl_secs: 60,
390                shard_public_key_sec1: vec![1, 2, 3],
391                metadata: None,
392            }),
393            Request::issue_role_attestation(RoleAttestationRequest {
394                subject: p(5),
395                role: CanisterRole::new("test"),
396                subnet_id: None,
397                audience: Some(p(6)),
398                ttl_secs: 60,
399                epoch: 0,
400                metadata: None,
401            }),
402        ]
403    }
404
405    #[test]
406    fn request_family_matches_all_variants() {
407        let families: Vec<RequestFamily> = requests_with_no_metadata()
408            .iter()
409            .map(Request::family)
410            .collect();
411        assert_eq!(
412            families,
413            vec![
414                RequestFamily::Provision,
415                RequestFamily::Upgrade,
416                RequestFamily::RecycleCanister,
417                RequestFamily::RequestCycles,
418                RequestFamily::IssueDelegation,
419                RequestFamily::IssueRoleAttestation,
420            ]
421        );
422    }
423
424    #[test]
425    fn with_metadata_and_without_metadata_cover_all_variants() {
426        let replay_meta = metadata(7);
427
428        for request in requests_with_no_metadata() {
429            let with_meta = request.clone().with_metadata(replay_meta);
430            assert_eq!(
431                with_meta.metadata(),
432                Some(replay_meta),
433                "with_metadata must set metadata for every request variant"
434            );
435
436            let without_meta = with_meta.without_metadata();
437            assert_eq!(
438                without_meta.metadata(),
439                None,
440                "without_metadata must strip metadata for every request variant"
441            );
442        }
443    }
444
445    #[test]
446    fn upgrade_request_is_only_available_for_upgrade_variant() {
447        let upgrade = Request::upgrade_canister(UpgradeCanisterRequest {
448            canister_pid: p(9),
449            metadata: Some(metadata(9)),
450        });
451        assert!(upgrade.upgrade_request().is_some());
452
453        for request in requests_with_no_metadata() {
454            if !matches!(request, Request::UpgradeCanister(_)) {
455                assert!(request.upgrade_request().is_none());
456            }
457        }
458    }
459}