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// Tests
314
315#[cfg(test)]
316mod tests {
317    use super::*;
318
319    fn p(id: u8) -> Principal {
320        Principal::from_slice(&[id; 29])
321    }
322
323    fn metadata(id: u8) -> RootRequestMetadata {
324        RootRequestMetadata {
325            request_id: [id; 32],
326            ttl_ns: 60_000_000_000,
327        }
328    }
329
330    fn requests_with_no_metadata() -> Vec<Request> {
331        vec![
332            Request::create_canister(CreateCanisterRequest {
333                canister_role: CanisterRole::new("app"),
334                parent: CreateCanisterParent::Root,
335                extra_arg: None,
336                metadata: None,
337            }),
338            Request::upgrade_canister(UpgradeCanisterRequest {
339                canister_pid: p(2),
340                metadata: None,
341            }),
342            Request::recycle_canister(RecycleCanisterRequest {
343                canister_pid: p(7),
344                metadata: None,
345            }),
346            Request::cycles(CyclesRequest {
347                cycles: 100,
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::RecycleCanister,
365                RequestFamily::RequestCycles,
366            ]
367        );
368    }
369
370    #[test]
371    fn with_metadata_and_without_metadata_cover_all_variants() {
372        let replay_meta = metadata(7);
373
374        for request in requests_with_no_metadata() {
375            let with_meta = request.clone().with_metadata(replay_meta);
376            assert_eq!(
377                with_meta.metadata(),
378                Some(replay_meta),
379                "with_metadata must set metadata for every request variant"
380            );
381
382            let without_meta = with_meta.without_metadata();
383            assert_eq!(
384                without_meta.metadata(),
385                None,
386                "without_metadata must strip metadata for every request variant"
387            );
388        }
389    }
390
391    #[test]
392    fn upgrade_request_is_only_available_for_upgrade_variant() {
393        let upgrade = Request::upgrade_canister(UpgradeCanisterRequest {
394            canister_pid: p(9),
395            metadata: Some(metadata(9)),
396        });
397        assert!(upgrade.upgrade_request().is_some());
398
399        for request in requests_with_no_metadata() {
400            if !matches!(request, Request::UpgradeCanister(_)) {
401                assert!(request.upgrade_request().is_none());
402            }
403        }
404    }
405}