aleph-types 0.13.1

Definitions for the most commonly used types in the Aleph Cloud network.
Documentation
use crate::chain::Address;
use crate::cid::Cid;
use crate::item_hash::{AlephItemHash, ItemHash};
use crate::message::execution::base::{Payment, PaymentType};
use memsizes::Bytes;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt::{Display, Formatter};

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "item_type", rename_all = "lowercase")]
pub enum StorageBackend {
    Ipfs { item_hash: Cid },
    Storage { item_hash: AlephItemHash },
}

/// User's choice of storage backend for file uploads.
///
/// Distinct from [`StorageBackend`], which pairs the engine choice with the
/// file hash and is used for serde of [`StoreContent`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StorageEngine {
    Storage,
    Ipfs,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
/// File reference, as deserialized in STORE messages. Does not contain
/// information about the owner.
pub enum RawFileRef {
    ItemHash(ItemHash),
    UserDefined(String),
}

impl Display for RawFileRef {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            RawFileRef::ItemHash(hash) => write!(f, "{}", hash),
            RawFileRef::UserDefined(name) => write!(f, "{}", name),
        }
    }
}

#[derive(Debug, Clone, PartialEq)]
/// Explicit file reference, tagged with the owner address for the user-defined case.
/// This is required to perform unambiguous queries on user-defined file references
/// as multiple users may use the same references.
pub enum FileRef {
    ItemHash(ItemHash),
    UserDefined { owner: Address, reference: String },
}

impl Display for FileRef {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        match self {
            FileRef::ItemHash(item_hash) => write!(f, "{}", item_hash),
            FileRef::UserDefined { owner, reference } => write!(f, "{}/{}", owner, reference),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct StoreContent {
    #[serde(flatten)]
    /// A combination of the `item_hash` and `item_type` fields, deserialized together to detect
    /// inconsistencies. Use the `file_hash()` method to access the file hash.
    file_hash: StorageBackend,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    /// Size of the file. Generated by CCNs upon processing.
    pub size: Option<Bytes>,
    /// Generated by CCNs upon processing.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub content_type: Option<String>,
    #[serde(rename = "ref", default, skip_serializing_if = "Option::is_none")]
    pub reference: Option<RawFileRef>,
    /// Metadata of the VM.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub metadata: Option<HashMap<String, serde_json::Value>>,
    /// Payment information for storage. Only `hold` and `credit` types are supported.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub payment: Option<Payment>,
}

impl StoreContent {
    pub fn new(
        file_hash: StorageBackend,
        reference: Option<RawFileRef>,
        metadata: Option<HashMap<String, serde_json::Value>>,
        payment: Option<Payment>,
    ) -> Self {
        Self {
            file_hash,
            size: None,
            content_type: None,
            reference,
            metadata,
            payment,
        }
    }

    /// Returns `true` if the payment type is valid for a STORE message.
    /// Only `hold` and `credit` are supported; `superfluid` is not allowed.
    pub fn has_valid_payment(&self) -> bool {
        match &self.payment {
            None => true,
            Some(p) => !matches!(p.payment_type, PaymentType::Superfluid),
        }
    }

    pub fn file_hash(&self) -> ItemHash {
        match &self.file_hash {
            StorageBackend::Ipfs { item_hash: cid } => ItemHash::Ipfs(cid.clone()),
            StorageBackend::Storage { item_hash } => ItemHash::Native(*item_hash),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::chain::{Address, Chain, Signature};
    use crate::channel::Channel;
    use crate::item_hash;
    use crate::message::base_message::{Message, MessageContentEnum};
    use crate::message::{ContentSource, MessageType};
    use crate::timestamp::Timestamp;
    use assert_matches::assert_matches;

    const STORE_IPFS_FIXTURE: &str = include_str!(concat!(
        env!("CARGO_MANIFEST_DIR"),
        "/../../fixtures/messages/store/store-ipfs.json"
    ));

    #[test]
    fn test_deserialize_store_message() {
        let message: Message = serde_json::from_str(STORE_IPFS_FIXTURE).unwrap();

        assert_eq!(
            message.sender,
            Address::from("0x238224C744F4b90b4494516e074D2676ECfC6803".to_string())
        );
        assert_eq!(message.chain, Chain::Ethereum);
        assert_eq!(
                message.signature,
                Some(Signature::from(
                    "0x9c87f5d659b9165be7cbd4b9f0bd5df5c66b9bb9a384a0a33b1277428be21244595a0731697035c4b085064cd3fc088bc5b3cddeb22159e7f462e6e5b5e7e8181c".to_string()
                ))
            );
        assert_matches!(message.message_type, MessageType::Store);
        assert_matches!(
            message.content_source,
            ContentSource::Inline { item_content: _ }
        );
        assert_eq!(
            &message.item_hash.to_string(),
            "afe106f1fd70b6b806e0452cc2f9485e518143581ffd046ae19fc64af7b6bbaa"
        );
        assert_eq!(message.time, Timestamp::from(1761047957.74837));
        assert_matches!(message.channel, Some(ref channel) if channel == &Channel::from("ALEPH-CLOUDSOLUTIONS".to_string()));

        // Check content fields
        assert_eq!(
            &message.content.address,
            &Address::from("0x238224C744F4b90b4494516e074D2676ECfC6803".to_string())
        );
        assert_eq!(&message.content.time, &Timestamp::from(1761047957.7483068));
        assert_eq!(message.sent_at(), &message.content.time);

        // Check STORE-specific fields
        match message.content() {
            MessageContentEnum::Store(store) => {
                assert_eq!(
                    store.file_hash,
                    StorageBackend::Ipfs {
                        item_hash: Cid::try_from("QmYULJoNGPDmoRq4WNWTDTUvJGJv1hosox8H6vVd1kCsY8")
                            .unwrap()
                    }
                );
                assert_eq!(
                    store.file_hash(),
                    item_hash!("QmYULJoNGPDmoRq4WNWTDTUvJGJv1hosox8H6vVd1kCsY8")
                );

                assert!(store.size.is_none());
                assert!(store.content_type.is_none());
                assert!(store.reference.is_none());
                assert!(store.metadata.is_none());
            }
            other => {
                panic!("Expected MessageContentEnum::Store, got {:?}", other)
            }
        }

        // Check confirmations
        assert!(message.confirmed());
        assert_eq!(message.confirmations.len(), 1);

        let confirmation = &message.confirmations[0];
        assert_eq!(confirmation.chain, Chain::Ethereum);
        assert_eq!(confirmation.height, 23626206);
        assert_eq!(
            confirmation.hash,
            "0x7e73ff97d7920fcfc289a899aeac4bc2898d1482a9876bd2ac4584ae876d22be"
        );
        assert!(confirmation.time.is_none());
        assert!(confirmation.publisher.is_none());

        message.verify_item_hash().unwrap();
    }

    #[test]
    fn test_deserialize_serialized_store_message() {
        let message: Message = serde_json::from_str(STORE_IPFS_FIXTURE).unwrap();
        message.verify_item_hash().unwrap();

        let serialized_message = serde_json::to_string(&message).unwrap();
        let deserialized_message: Message = serde_json::from_str(&serialized_message).unwrap();
        deserialized_message.verify_item_hash().unwrap();

        assert_eq!(message, deserialized_message);
    }

    const TEST_HASH: &str = "d281eb8a69ba1f4dda2d71aaf3ded06caa92edd690ef3d0632f41aa91167762c";

    #[test]
    fn test_store_content_without_payment() {
        let json = format!(r#"{{"item_type":"storage","item_hash":"{}"}}"#, TEST_HASH);
        let content: StoreContent = serde_json::from_str(&json).unwrap();
        assert!(content.payment.is_none());
        assert!(content.has_valid_payment());
    }

    #[test]
    fn test_store_content_with_credit_payment() {
        let json = format!(
            r#"{{"item_type":"storage","item_hash":"{}","payment":{{"type":"credit"}}}}"#,
            TEST_HASH
        );
        let content: StoreContent = serde_json::from_str(&json).unwrap();
        let payment = content.payment.as_ref().unwrap();
        assert_eq!(payment.payment_type, PaymentType::Credit);
        assert!(payment.chain.is_none());
        assert!(payment.receiver.is_none());
        assert!(content.has_valid_payment());
    }

    #[test]
    fn test_store_content_with_hold_payment() {
        let json = format!(
            r#"{{"item_type":"storage","item_hash":"{}","payment":{{"type":"hold"}}}}"#,
            TEST_HASH
        );
        let content: StoreContent = serde_json::from_str(&json).unwrap();
        assert_eq!(
            content.payment.as_ref().unwrap().payment_type,
            PaymentType::Hold
        );
        assert!(content.has_valid_payment());
    }

    #[test]
    fn test_store_content_superfluid_payment_invalid() {
        let json = format!(
            r#"{{"item_type":"storage","item_hash":"{}","payment":{{"type":"superfluid"}}}}"#,
            TEST_HASH
        );
        let content: StoreContent = serde_json::from_str(&json).unwrap();
        assert!(!content.has_valid_payment());
    }

    #[test]
    fn test_store_content_credit_payment_round_trip() {
        let json = format!(
            r#"{{"item_type":"storage","item_hash":"{}","payment":{{"type":"credit"}}}}"#,
            TEST_HASH
        );
        let content: StoreContent = serde_json::from_str(&json).unwrap();
        let serialized = serde_json::to_string(&content).unwrap();
        let deserialized: StoreContent = serde_json::from_str(&serialized).unwrap();
        assert_eq!(content, deserialized);
    }

    #[test]
    fn test_store_content_payment_not_serialized_when_none() {
        let content = StoreContent::new(
            StorageBackend::Storage {
                item_hash: AlephItemHash::from_bytes(b"test"),
            },
            None,
            None,
            None,
        );
        let json = serde_json::to_string(&content).unwrap();
        assert!(!json.contains("payment"));
    }
}