1use chio_core::economic_continuity::{
2 EconomicContentV1, EconomicResourceHeadV1, EconomicResourceKeyV1, VerifiedEconomicStateView,
3};
4use serde::de::DeserializeOwned;
5use serde::{Deserialize, Serialize};
6
7use super::validation::{validate_digest, validate_positive, I_JSON_MAX_SAFE_INTEGER};
8use super::{ChannelError, ChannelEscrowReferenceV1};
9
10pub const CHANNEL_LIFECYCLE_SCHEMA: &str = "chio.channel.lifecycle.v1";
11pub const CHANNEL_LIFECYCLE_RESOURCE_FAMILY: &str = "channel_lifecycle";
12pub const CHANNEL_ESCROW_RESERVATION_RESOURCE_FAMILY: &str = "channel_escrow_reservation";
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(rename_all = "snake_case")]
16pub enum ChannelLifecycleStatusV1 {
17 Open,
18 ClosePending,
19 Closing,
20 Released,
21 Refunded,
22 Incident,
23}
24
25#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(rename_all = "camelCase", deny_unknown_fields)]
27pub struct ChannelLifecycleViewV1 {
28 pub schema: String,
29 pub channel_id: String,
30 pub status: ChannelLifecycleStatusV1,
31 pub latest_state_digest: String,
32 pub latest_sequence: u64,
33 pub state_version: u64,
34 pub lifecycle_fence: u64,
35 #[serde(
36 default,
37 skip_serializing_if = "Option::is_none",
38 deserialize_with = "super::validation::deserialize_present_option"
39 )]
40 pub pending_close_body_digest: Option<String>,
41 #[serde(
42 default,
43 skip_serializing_if = "Option::is_none",
44 deserialize_with = "super::validation::deserialize_present_option"
45 )]
46 pub admitted_dispute_digest: Option<String>,
47 #[serde(
48 default,
49 skip_serializing_if = "Option::is_none",
50 deserialize_with = "super::validation::deserialize_present_option"
51 )]
52 pub live_reservation_id: Option<String>,
53 #[serde(
54 default,
55 skip_serializing_if = "Option::is_none",
56 deserialize_with = "super::validation::deserialize_present_option"
57 )]
58 pub operation_id: Option<String>,
59}
60
61impl ChannelLifecycleViewV1 {
62 pub fn validate(&self) -> Result<(), ChannelError> {
63 if self.schema != CHANNEL_LIFECYCLE_SCHEMA {
64 return Err(ChannelError::InvalidField("channel_lifecycle_schema"));
65 }
66 validate_digest("lifecycle_channel_id", &self.channel_id)?;
67 validate_digest("lifecycle_latest_state_digest", &self.latest_state_digest)?;
68 validate_positive("lifecycle_state_version", self.state_version)?;
69 validate_positive("lifecycle_fence", self.lifecycle_fence)?;
70 if self.latest_sequence > I_JSON_MAX_SAFE_INTEGER {
71 return Err(ChannelError::InvalidField("lifecycle_latest_sequence"));
72 }
73 match (&self.live_reservation_id, &self.operation_id) {
74 (Some(reservation_id), Some(operation_id)) => {
75 validate_digest("lifecycle_live_reservation_id", reservation_id)?;
76 validate_digest("lifecycle_operation_id", operation_id)?;
77 if self.status != ChannelLifecycleStatusV1::Open {
78 return Err(ChannelError::IllegalTransition);
79 }
80 }
81 (None, None) => {}
82 _ => return Err(ChannelError::InvalidField("lifecycle_live_reservation")),
83 }
84 match (self.status, &self.pending_close_body_digest) {
85 (
86 ChannelLifecycleStatusV1::ClosePending | ChannelLifecycleStatusV1::Closing,
87 Some(digest),
88 ) => {
89 validate_digest("lifecycle_pending_close_body_digest", digest)?;
90 }
91 (ChannelLifecycleStatusV1::ClosePending | ChannelLifecycleStatusV1::Closing, None) => {
92 return Err(ChannelError::InvalidField("lifecycle_pending_close"));
93 }
94 (_, None) => {}
95 (_, Some(_)) => return Err(ChannelError::InvalidField("lifecycle_pending_close")),
96 }
97 if let Some(digest) = &self.admitted_dispute_digest {
98 validate_digest("lifecycle_admitted_dispute_digest", digest)?;
99 if self.status != ChannelLifecycleStatusV1::ClosePending {
100 return Err(ChannelError::InvalidField(
101 "lifecycle_admitted_dispute_digest",
102 ));
103 }
104 }
105 Ok(())
106 }
107
108 const fn lifecycle_state(&self) -> &'static str {
109 match self.status {
110 ChannelLifecycleStatusV1::Open => "open",
111 ChannelLifecycleStatusV1::ClosePending => "close_pending",
112 ChannelLifecycleStatusV1::Closing => "closing",
113 ChannelLifecycleStatusV1::Released => "released",
114 ChannelLifecycleStatusV1::Refunded => "refunded",
115 ChannelLifecycleStatusV1::Incident => "incident",
116 }
117 }
118}
119
120pub const CHANNEL_ESCROW_RESERVATION_SCHEMA: &str = "chio.channel.escrow-reservation.v1";
121
122#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
123#[serde(rename_all = "snake_case")]
124pub enum ChannelEscrowReservationStatusV1 {
125 Open,
126 Closing,
127 Released,
128 Refunded,
129 Incident,
130}
131
132#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
133#[serde(rename_all = "camelCase", deny_unknown_fields)]
134pub struct ChannelEscrowReservationViewV1 {
135 pub schema: String,
136 pub channel_id: String,
137 pub open_digest: String,
138 pub escrow_reference: ChannelEscrowReferenceV1,
139 pub status: ChannelEscrowReservationStatusV1,
140 pub version: u64,
141 pub lifecycle_fence: u64,
142 #[serde(
143 default,
144 skip_serializing_if = "Option::is_none",
145 deserialize_with = "super::validation::deserialize_present_option"
146 )]
147 pub pending_close_body_digest: Option<String>,
148}
149
150impl ChannelEscrowReservationViewV1 {
151 pub fn validate(&self) -> Result<(), ChannelError> {
152 if self.schema != CHANNEL_ESCROW_RESERVATION_SCHEMA {
153 return Err(ChannelError::InvalidField(
154 "channel_escrow_reservation_schema",
155 ));
156 }
157 validate_digest("escrow_reservation_channel_id", &self.channel_id)?;
158 validate_digest("escrow_reservation_open_digest", &self.open_digest)?;
159 self.escrow_reference.validate()?;
160 validate_positive("escrow_reservation_version", self.version)?;
161 validate_positive("escrow_reservation_fence", self.lifecycle_fence)?;
162 match (self.status, &self.pending_close_body_digest) {
163 (
164 ChannelEscrowReservationStatusV1::Open | ChannelEscrowReservationStatusV1::Closing,
165 Some(digest),
166 ) => {
167 validate_digest("escrow_reservation_pending_close", digest)?;
168 }
169 (ChannelEscrowReservationStatusV1::Closing, None) => {
170 return Err(ChannelError::InvalidField(
171 "escrow_reservation_pending_close",
172 ));
173 }
174 (_, None) => {}
175 (_, Some(_)) => {
176 return Err(ChannelError::InvalidField(
177 "escrow_reservation_pending_close",
178 ));
179 }
180 }
181 Ok(())
182 }
183
184 const fn lifecycle_state(&self) -> &'static str {
185 match self.status {
186 ChannelEscrowReservationStatusV1::Open => "open",
187 ChannelEscrowReservationStatusV1::Closing => "closing",
188 ChannelEscrowReservationStatusV1::Released => "released",
189 ChannelEscrowReservationStatusV1::Refunded => "refunded",
190 ChannelEscrowReservationStatusV1::Incident => "incident",
191 }
192 }
193}
194
195#[derive(Debug, Clone)]
196pub struct VerifiedChannelLifecycleSnapshotV1 {
197 lifecycle: ChannelLifecycleViewV1,
198 escrow: ChannelEscrowReservationViewV1,
199 settlement_authority_scope_id: String,
200 checkpoint_sequence: u64,
201 checkpoint_digest: String,
202 channel_head_digest: String,
203 escrow_head_digest: String,
204 observed_at_unix_ms: u64,
205 channel_head: EconomicResourceHeadV1,
206 escrow_head: EconomicResourceHeadV1,
207}
208
209impl VerifiedChannelLifecycleSnapshotV1 {
210 #[must_use]
211 pub const fn lifecycle(&self) -> &ChannelLifecycleViewV1 {
212 &self.lifecycle
213 }
214
215 #[must_use]
216 pub const fn escrow(&self) -> &ChannelEscrowReservationViewV1 {
217 &self.escrow
218 }
219
220 #[must_use]
221 pub fn settlement_authority_scope_id(&self) -> &str {
222 &self.settlement_authority_scope_id
223 }
224
225 #[must_use]
226 pub fn checkpoint_digest(&self) -> &str {
227 &self.checkpoint_digest
228 }
229
230 #[must_use]
231 pub const fn checkpoint_sequence(&self) -> u64 {
232 self.checkpoint_sequence
233 }
234
235 #[must_use]
236 pub fn channel_head_digest(&self) -> &str {
237 &self.channel_head_digest
238 }
239
240 #[must_use]
241 pub fn escrow_head_digest(&self) -> &str {
242 &self.escrow_head_digest
243 }
244
245 #[must_use]
246 pub fn channel_predecessor_digest(&self) -> Option<&str> {
247 self.channel_head.predecessor_digest.as_deref()
248 }
249
250 #[must_use]
251 pub fn escrow_predecessor_digest(&self) -> Option<&str> {
252 self.escrow_head.predecessor_digest.as_deref()
253 }
254
255 #[must_use]
256 pub const fn observed_at_unix_ms(&self) -> u64 {
257 self.observed_at_unix_ms
258 }
259
260 pub(super) const fn channel_head(&self) -> &EconomicResourceHeadV1 {
261 &self.channel_head
262 }
263
264 pub(super) const fn escrow_head(&self) -> &EconomicResourceHeadV1 {
265 &self.escrow_head
266 }
267}
268
269pub fn verify_channel_lifecycle_snapshot(
270 current: &VerifiedEconomicStateView,
271 settlement_authority_scope_id: &str,
272 channel_id: &str,
273) -> Result<VerifiedChannelLifecycleSnapshotV1, ChannelError> {
274 validate_digest("anchored_channel_id", channel_id)?;
275 super::validation::validate_text(
276 "anchored_settlement_authority_scope_id",
277 settlement_authority_scope_id,
278 )?;
279 let channel_key = EconomicResourceKeyV1 {
280 resource_family: CHANNEL_LIFECYCLE_RESOURCE_FAMILY.to_owned(),
281 scope_id: settlement_authority_scope_id.to_owned(),
282 resource_id: channel_id.to_owned(),
283 };
284 let escrow_key = EconomicResourceKeyV1 {
285 resource_family: CHANNEL_ESCROW_RESERVATION_RESOURCE_FAMILY.to_owned(),
286 scope_id: settlement_authority_scope_id.to_owned(),
287 resource_id: channel_id.to_owned(),
288 };
289 let channel_head = current
290 .view()
291 .head(&channel_key)
292 .ok_or(ChannelError::AuthorityVerification)?;
293 let escrow_head = current
294 .view()
295 .head(&escrow_key)
296 .ok_or(ChannelError::AuthorityVerification)?;
297 let lifecycle: ChannelLifecycleViewV1 = decode_inline(channel_head)?;
298 let escrow: ChannelEscrowReservationViewV1 = decode_inline(escrow_head)?;
299 lifecycle.validate()?;
300 escrow.validate()?;
301 if lifecycle.channel_id != channel_id
302 || escrow.channel_id != channel_id
303 || channel_head.resource_version != lifecycle.state_version
304 || channel_head.lifecycle_fence != lifecycle.lifecycle_fence
305 || channel_head.lifecycle_state != lifecycle.lifecycle_state()
306 || channel_head.operation_id != lifecycle.operation_id
307 || escrow_head.resource_version != escrow.version
308 || escrow_head.lifecycle_fence != escrow.lifecycle_fence
309 || escrow_head.lifecycle_state != escrow.lifecycle_state()
310 || escrow_head.operation_id != lifecycle.operation_id
311 || lifecycle.lifecycle_fence != escrow.lifecycle_fence
312 || lifecycle.pending_close_body_digest != escrow.pending_close_body_digest
313 || !statuses_match(lifecycle.status, escrow.status)
314 || channel_head.trusted_clock_high_water > current.view().observed_at
315 || escrow_head.trusted_clock_high_water > current.view().observed_at
316 {
317 return Err(ChannelError::AuthorityVerification);
318 }
319 Ok(VerifiedChannelLifecycleSnapshotV1 {
320 lifecycle,
321 escrow,
322 settlement_authority_scope_id: settlement_authority_scope_id.to_owned(),
323 checkpoint_sequence: current.view().checkpoint_sequence,
324 checkpoint_digest: current.view().checkpoint_digest.clone(),
325 channel_head_digest: channel_head
326 .digest()
327 .map_err(|_| ChannelError::AuthorityVerification)?,
328 escrow_head_digest: escrow_head
329 .digest()
330 .map_err(|_| ChannelError::AuthorityVerification)?,
331 observed_at_unix_ms: current.view().observed_at,
332 channel_head: channel_head.clone(),
333 escrow_head: escrow_head.clone(),
334 })
335}
336
337fn decode_inline<T: DeserializeOwned>(head: &EconomicResourceHeadV1) -> Result<T, ChannelError> {
338 let EconomicContentV1::Inline { value } = &head.state else {
339 return Err(ChannelError::AuthorityVerification);
340 };
341 serde_json::from_value(value.clone()).map_err(|_| ChannelError::AuthorityVerification)
342}
343
344const fn statuses_match(
345 lifecycle: ChannelLifecycleStatusV1,
346 escrow: ChannelEscrowReservationStatusV1,
347) -> bool {
348 matches!(
349 (lifecycle, escrow),
350 (
351 ChannelLifecycleStatusV1::Open,
352 ChannelEscrowReservationStatusV1::Open
353 ) | (
354 ChannelLifecycleStatusV1::ClosePending,
355 ChannelEscrowReservationStatusV1::Open
356 ) | (
357 ChannelLifecycleStatusV1::Closing,
358 ChannelEscrowReservationStatusV1::Closing
359 ) | (
360 ChannelLifecycleStatusV1::Released,
361 ChannelEscrowReservationStatusV1::Released
362 ) | (
363 ChannelLifecycleStatusV1::Refunded,
364 ChannelEscrowReservationStatusV1::Refunded
365 ) | (
366 ChannelLifecycleStatusV1::Incident,
367 ChannelEscrowReservationStatusV1::Incident
368 )
369 )
370}