Skip to main content

canic_core/dto/
rpc.rs

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