chio-settle 0.1.2

Settlement runtime for Chio web3 escrow and bond execution
Documentation
use chio_core::economic_continuity::{
    EconomicContentV1, EconomicResourceHeadV1, EconomicResourceKeyV1, VerifiedEconomicStateView,
};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};

use super::validation::{validate_digest, validate_positive, I_JSON_MAX_SAFE_INTEGER};
use super::{ChannelError, ChannelEscrowReferenceV1};

pub const CHANNEL_LIFECYCLE_SCHEMA: &str = "chio.channel.lifecycle.v1";
pub const CHANNEL_LIFECYCLE_RESOURCE_FAMILY: &str = "channel_lifecycle";
pub const CHANNEL_ESCROW_RESERVATION_RESOURCE_FAMILY: &str = "channel_escrow_reservation";

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ChannelLifecycleStatusV1 {
    Open,
    ClosePending,
    Closing,
    Released,
    Refunded,
    Incident,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ChannelLifecycleViewV1 {
    pub schema: String,
    pub channel_id: String,
    pub status: ChannelLifecycleStatusV1,
    pub latest_state_digest: String,
    pub latest_sequence: u64,
    pub state_version: u64,
    pub lifecycle_fence: u64,
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        deserialize_with = "super::validation::deserialize_present_option"
    )]
    pub pending_close_body_digest: Option<String>,
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        deserialize_with = "super::validation::deserialize_present_option"
    )]
    pub admitted_dispute_digest: Option<String>,
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        deserialize_with = "super::validation::deserialize_present_option"
    )]
    pub live_reservation_id: Option<String>,
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        deserialize_with = "super::validation::deserialize_present_option"
    )]
    pub operation_id: Option<String>,
}

impl ChannelLifecycleViewV1 {
    pub fn validate(&self) -> Result<(), ChannelError> {
        if self.schema != CHANNEL_LIFECYCLE_SCHEMA {
            return Err(ChannelError::InvalidField("channel_lifecycle_schema"));
        }
        validate_digest("lifecycle_channel_id", &self.channel_id)?;
        validate_digest("lifecycle_latest_state_digest", &self.latest_state_digest)?;
        validate_positive("lifecycle_state_version", self.state_version)?;
        validate_positive("lifecycle_fence", self.lifecycle_fence)?;
        if self.latest_sequence > I_JSON_MAX_SAFE_INTEGER {
            return Err(ChannelError::InvalidField("lifecycle_latest_sequence"));
        }
        match (&self.live_reservation_id, &self.operation_id) {
            (Some(reservation_id), Some(operation_id)) => {
                validate_digest("lifecycle_live_reservation_id", reservation_id)?;
                validate_digest("lifecycle_operation_id", operation_id)?;
                if self.status != ChannelLifecycleStatusV1::Open {
                    return Err(ChannelError::IllegalTransition);
                }
            }
            (None, None) => {}
            _ => return Err(ChannelError::InvalidField("lifecycle_live_reservation")),
        }
        match (self.status, &self.pending_close_body_digest) {
            (
                ChannelLifecycleStatusV1::ClosePending | ChannelLifecycleStatusV1::Closing,
                Some(digest),
            ) => {
                validate_digest("lifecycle_pending_close_body_digest", digest)?;
            }
            (ChannelLifecycleStatusV1::ClosePending | ChannelLifecycleStatusV1::Closing, None) => {
                return Err(ChannelError::InvalidField("lifecycle_pending_close"));
            }
            (_, None) => {}
            (_, Some(_)) => return Err(ChannelError::InvalidField("lifecycle_pending_close")),
        }
        if let Some(digest) = &self.admitted_dispute_digest {
            validate_digest("lifecycle_admitted_dispute_digest", digest)?;
            if self.status != ChannelLifecycleStatusV1::ClosePending {
                return Err(ChannelError::InvalidField(
                    "lifecycle_admitted_dispute_digest",
                ));
            }
        }
        Ok(())
    }

    const fn lifecycle_state(&self) -> &'static str {
        match self.status {
            ChannelLifecycleStatusV1::Open => "open",
            ChannelLifecycleStatusV1::ClosePending => "close_pending",
            ChannelLifecycleStatusV1::Closing => "closing",
            ChannelLifecycleStatusV1::Released => "released",
            ChannelLifecycleStatusV1::Refunded => "refunded",
            ChannelLifecycleStatusV1::Incident => "incident",
        }
    }
}

pub const CHANNEL_ESCROW_RESERVATION_SCHEMA: &str = "chio.channel.escrow-reservation.v1";

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ChannelEscrowReservationStatusV1 {
    Open,
    Closing,
    Released,
    Refunded,
    Incident,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ChannelEscrowReservationViewV1 {
    pub schema: String,
    pub channel_id: String,
    pub open_digest: String,
    pub escrow_reference: ChannelEscrowReferenceV1,
    pub status: ChannelEscrowReservationStatusV1,
    pub version: u64,
    pub lifecycle_fence: u64,
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        deserialize_with = "super::validation::deserialize_present_option"
    )]
    pub pending_close_body_digest: Option<String>,
}

impl ChannelEscrowReservationViewV1 {
    pub fn validate(&self) -> Result<(), ChannelError> {
        if self.schema != CHANNEL_ESCROW_RESERVATION_SCHEMA {
            return Err(ChannelError::InvalidField(
                "channel_escrow_reservation_schema",
            ));
        }
        validate_digest("escrow_reservation_channel_id", &self.channel_id)?;
        validate_digest("escrow_reservation_open_digest", &self.open_digest)?;
        self.escrow_reference.validate()?;
        validate_positive("escrow_reservation_version", self.version)?;
        validate_positive("escrow_reservation_fence", self.lifecycle_fence)?;
        match (self.status, &self.pending_close_body_digest) {
            (
                ChannelEscrowReservationStatusV1::Open | ChannelEscrowReservationStatusV1::Closing,
                Some(digest),
            ) => {
                validate_digest("escrow_reservation_pending_close", digest)?;
            }
            (ChannelEscrowReservationStatusV1::Closing, None) => {
                return Err(ChannelError::InvalidField(
                    "escrow_reservation_pending_close",
                ));
            }
            (_, None) => {}
            (_, Some(_)) => {
                return Err(ChannelError::InvalidField(
                    "escrow_reservation_pending_close",
                ));
            }
        }
        Ok(())
    }

    const fn lifecycle_state(&self) -> &'static str {
        match self.status {
            ChannelEscrowReservationStatusV1::Open => "open",
            ChannelEscrowReservationStatusV1::Closing => "closing",
            ChannelEscrowReservationStatusV1::Released => "released",
            ChannelEscrowReservationStatusV1::Refunded => "refunded",
            ChannelEscrowReservationStatusV1::Incident => "incident",
        }
    }
}

#[derive(Debug, Clone)]
pub struct VerifiedChannelLifecycleSnapshotV1 {
    lifecycle: ChannelLifecycleViewV1,
    escrow: ChannelEscrowReservationViewV1,
    settlement_authority_scope_id: String,
    checkpoint_sequence: u64,
    checkpoint_digest: String,
    channel_head_digest: String,
    escrow_head_digest: String,
    observed_at_unix_ms: u64,
    channel_head: EconomicResourceHeadV1,
    escrow_head: EconomicResourceHeadV1,
}

impl VerifiedChannelLifecycleSnapshotV1 {
    #[must_use]
    pub const fn lifecycle(&self) -> &ChannelLifecycleViewV1 {
        &self.lifecycle
    }

    #[must_use]
    pub const fn escrow(&self) -> &ChannelEscrowReservationViewV1 {
        &self.escrow
    }

    #[must_use]
    pub fn settlement_authority_scope_id(&self) -> &str {
        &self.settlement_authority_scope_id
    }

    #[must_use]
    pub fn checkpoint_digest(&self) -> &str {
        &self.checkpoint_digest
    }

    #[must_use]
    pub const fn checkpoint_sequence(&self) -> u64 {
        self.checkpoint_sequence
    }

    #[must_use]
    pub fn channel_head_digest(&self) -> &str {
        &self.channel_head_digest
    }

    #[must_use]
    pub fn escrow_head_digest(&self) -> &str {
        &self.escrow_head_digest
    }

    #[must_use]
    pub fn channel_predecessor_digest(&self) -> Option<&str> {
        self.channel_head.predecessor_digest.as_deref()
    }

    #[must_use]
    pub fn escrow_predecessor_digest(&self) -> Option<&str> {
        self.escrow_head.predecessor_digest.as_deref()
    }

    #[must_use]
    pub const fn observed_at_unix_ms(&self) -> u64 {
        self.observed_at_unix_ms
    }

    pub(super) const fn channel_head(&self) -> &EconomicResourceHeadV1 {
        &self.channel_head
    }

    pub(super) const fn escrow_head(&self) -> &EconomicResourceHeadV1 {
        &self.escrow_head
    }
}

pub fn verify_channel_lifecycle_snapshot(
    current: &VerifiedEconomicStateView,
    settlement_authority_scope_id: &str,
    channel_id: &str,
) -> Result<VerifiedChannelLifecycleSnapshotV1, ChannelError> {
    validate_digest("anchored_channel_id", channel_id)?;
    super::validation::validate_text(
        "anchored_settlement_authority_scope_id",
        settlement_authority_scope_id,
    )?;
    let channel_key = EconomicResourceKeyV1 {
        resource_family: CHANNEL_LIFECYCLE_RESOURCE_FAMILY.to_owned(),
        scope_id: settlement_authority_scope_id.to_owned(),
        resource_id: channel_id.to_owned(),
    };
    let escrow_key = EconomicResourceKeyV1 {
        resource_family: CHANNEL_ESCROW_RESERVATION_RESOURCE_FAMILY.to_owned(),
        scope_id: settlement_authority_scope_id.to_owned(),
        resource_id: channel_id.to_owned(),
    };
    let channel_head = current
        .view()
        .head(&channel_key)
        .ok_or(ChannelError::AuthorityVerification)?;
    let escrow_head = current
        .view()
        .head(&escrow_key)
        .ok_or(ChannelError::AuthorityVerification)?;
    let lifecycle: ChannelLifecycleViewV1 = decode_inline(channel_head)?;
    let escrow: ChannelEscrowReservationViewV1 = decode_inline(escrow_head)?;
    lifecycle.validate()?;
    escrow.validate()?;
    if lifecycle.channel_id != channel_id
        || escrow.channel_id != channel_id
        || channel_head.resource_version != lifecycle.state_version
        || channel_head.lifecycle_fence != lifecycle.lifecycle_fence
        || channel_head.lifecycle_state != lifecycle.lifecycle_state()
        || channel_head.operation_id != lifecycle.operation_id
        || escrow_head.resource_version != escrow.version
        || escrow_head.lifecycle_fence != escrow.lifecycle_fence
        || escrow_head.lifecycle_state != escrow.lifecycle_state()
        || escrow_head.operation_id != lifecycle.operation_id
        || lifecycle.lifecycle_fence != escrow.lifecycle_fence
        || lifecycle.pending_close_body_digest != escrow.pending_close_body_digest
        || !statuses_match(lifecycle.status, escrow.status)
        || channel_head.trusted_clock_high_water > current.view().observed_at
        || escrow_head.trusted_clock_high_water > current.view().observed_at
    {
        return Err(ChannelError::AuthorityVerification);
    }
    Ok(VerifiedChannelLifecycleSnapshotV1 {
        lifecycle,
        escrow,
        settlement_authority_scope_id: settlement_authority_scope_id.to_owned(),
        checkpoint_sequence: current.view().checkpoint_sequence,
        checkpoint_digest: current.view().checkpoint_digest.clone(),
        channel_head_digest: channel_head
            .digest()
            .map_err(|_| ChannelError::AuthorityVerification)?,
        escrow_head_digest: escrow_head
            .digest()
            .map_err(|_| ChannelError::AuthorityVerification)?,
        observed_at_unix_ms: current.view().observed_at,
        channel_head: channel_head.clone(),
        escrow_head: escrow_head.clone(),
    })
}

fn decode_inline<T: DeserializeOwned>(head: &EconomicResourceHeadV1) -> Result<T, ChannelError> {
    let EconomicContentV1::Inline { value } = &head.state else {
        return Err(ChannelError::AuthorityVerification);
    };
    serde_json::from_value(value.clone()).map_err(|_| ChannelError::AuthorityVerification)
}

const fn statuses_match(
    lifecycle: ChannelLifecycleStatusV1,
    escrow: ChannelEscrowReservationStatusV1,
) -> bool {
    matches!(
        (lifecycle, escrow),
        (
            ChannelLifecycleStatusV1::Open,
            ChannelEscrowReservationStatusV1::Open
        ) | (
            ChannelLifecycleStatusV1::ClosePending,
            ChannelEscrowReservationStatusV1::Open
        ) | (
            ChannelLifecycleStatusV1::Closing,
            ChannelEscrowReservationStatusV1::Closing
        ) | (
            ChannelLifecycleStatusV1::Released,
            ChannelEscrowReservationStatusV1::Released
        ) | (
            ChannelLifecycleStatusV1::Refunded,
            ChannelEscrowReservationStatusV1::Refunded
        ) | (
            ChannelLifecycleStatusV1::Incident,
            ChannelEscrowReservationStatusV1::Incident
        )
    )
}