miracle-api 0.6.0

Miracle is a pay2e protocol for sovereign individuals living in Mirascape Horizon.
Documentation
use serde::{Deserialize, Serialize};
use steel::*;

// External dependencies: solana_program::hash::hashv and hex for string conversion

/// DailyParticipantData represents aggregated payment data for a single participant in a daily epoch.
///
/// This structure is used off-chain by the oracle to build Merkle trees for daily payment snapshots.
/// It aggregates all payments for a single participant (customer or merchant) within a 24-hour epoch.
///
/// ## Key Features
/// - **Aggregation**: Combines multiple payments per participant per day
/// - **Fair Rewards**: Uses transaction count and total amount for reward calculation
/// - **Efficient Storage**: Off-chain only, reduces on-chain storage costs
/// - **Flexible**: Supports both customer and merchant participants
/// - **Simple Design**: Only necessary fields included (Simple is Best)
/// - **Transaction Verification**: Includes short transaction signatures for on-chain verification
///
/// ## Data Structure
/// - `participant_id`: Unique identifier for the participant (customer/merchant)
/// - `first_payment_timestamp`: Timestamp of first payment in the day
/// - `last_payment_timestamp`: Timestamp of last payment in the day
/// - `first_payment_tx_sig_short`: First 8 characters of first payment transaction signature
/// - `last_payment_tx_sig_short`: First 8 characters of last payment transaction signature
/// - `first_payment_tx_sig_short`: First 8 characters of first payment transaction signature
/// - `last_payment_tx_sig_short`: First 8 characters of last payment transaction signature
/// - `payment_count`: Number of transactions for the day
/// - `participant_type`: Whether this is a customer (0) or merchant (1)
///
/// ## Merkle Leaf Construction
/// The Merkle leaf hash is computed as:
/// ```rust
/// use miracle_api::prelude::DailyParticipantData;
///
/// let participant_data = DailyParticipantData::new(
///     [0u8; 32],           // participant_id
///     1723680000,         // first_payment_timestamp
///     1723766400,         // last_payment_timestamp
///     [0u8; 8],           // first_payment_tx_sig_short
///     [0u8; 8],           // last_payment_tx_sig_short
///     5,                  // payment_count
///     0,                   // participant_type (customer)
/// );
/// let leaf_hash = participant_data.compute_leaf_hash();
/// ```
///
/// ## Security Features
/// - **Timestamp Range**: Provides audit trail and prevents stale data
/// - **Participant Type**: Distinguishes between customers and merchants for fair allocation
/// - **Aggregation**: Reduces Merkle tree size while maintaining fairness
/// - **Uniqueness**: participant_id + daily timestamps provide natural uniqueness
/// - **Deployment Isolation**: Handled by program ID, not data structure
/// - **Transaction Verification**: Short signatures enable on-chain payment verification
/// - **Transaction Verification**: Short signatures enable on-chain verification of payment authenticity
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable, Serialize, Deserialize)]
pub struct DailyParticipantData {
    /// Unique identifier for the participant (customer or merchant).
    /// This should be a deterministic identifier that can be reproduced
    /// by both the oracle and the claiming user.
    pub participant_id: [u8; 32],

    /// Timestamp of the first payment in the daily epoch (Unix timestamp).
    /// Used for audit trail and to prevent stale data replay.
    pub first_payment_timestamp: i64,

    /// Timestamp of the last payment in the daily epoch (Unix timestamp).
    /// Used for audit trail and to ensure data freshness.
    pub last_payment_timestamp: i64,

    /// First 8 characters of the first payment transaction signature.
    /// Used for on-chain verification of payment authenticity.
    /// Provides cost-effective verification without storing full signatures.
    pub first_payment_tx_sig_short: [u8; 8],

    /// First 8 characters of the last payment transaction signature.
    /// Used for on-chain verification of payment authenticity.
    /// Provides cost-effective verification without storing full signatures.
    pub last_payment_tx_sig_short: [u8; 8],

    /// Number of transactions for this participant in the daily epoch.
    /// Used for activity-based reward calculation and community metrics.
    pub payment_count: u32,

    /// Participant type: 0 for customer, 1 for merchant.
    /// Used to determine reward allocation between customer and merchant pools.
    pub participant_type: u8,

    /// Padding for future extensibility.
    pub _padding: [u8; 3],
}

impl DailyParticipantData {
    /// Create a new DailyParticipantData instance.
    ///
    /// ## Parameters
    /// - `participant_id`: Unique identifier for the participant
    /// - `first_payment_timestamp`: Timestamp of first payment
    /// - `last_payment_timestamp`: Timestamp of last payment
    /// - `first_payment_tx_sig_short`: First 8 characters of first payment transaction signature
    /// - `last_payment_tx_sig_short`: First 8 characters of last payment transaction signature
    /// - `payment_count`: Number of transactions
    /// - `participant_type`: 0 for customer, 1 for merchant
    ///
    /// ## Returns
    /// - New DailyParticipantData instance
    pub fn new(
        participant_id: [u8; 32],
        first_payment_timestamp: i64,
        last_payment_timestamp: i64,
        first_payment_tx_sig_short: [u8; 8],
        last_payment_tx_sig_short: [u8; 8],
        payment_count: u32,
        participant_type: u8,
    ) -> Self {
        Self {
            participant_id,
            first_payment_timestamp,
            last_payment_timestamp,
            first_payment_tx_sig_short,
            last_payment_tx_sig_short,
            payment_count,
            participant_type,
            _padding: [0u8; 3],
        }
    }

    /// Compute the Merkle leaf hash for this participant data.
    ///
    /// ## Returns
    /// - 32-byte hash that will be used as a leaf in the Merkle tree
    ///
    /// ## Security
    /// This hash includes all fields to ensure data integrity and prevent
    /// replay attacks across different epochs. The short transaction signatures
    /// are included to enable on-chain verification of payment authenticity.
    ///
    /// ## Implementation
    /// Uses Solana's standard hashv function with SHA-256 for cryptographic security.
    /// All fields are included in the hash to prevent any data manipulation.
    pub fn compute_leaf_hash(&self) -> [u8; 32] {
        use solana_program::hash::hashv;

        // Create a deterministic byte representation of all fields
        let mut data = Vec::new();

        // Add all fields in a deterministic order (same as struct field order)
        data.extend_from_slice(&self.participant_id);
        data.extend_from_slice(&self.first_payment_timestamp.to_le_bytes());
        data.extend_from_slice(&self.last_payment_timestamp.to_le_bytes());
        data.extend_from_slice(&self.first_payment_tx_sig_short);
        data.extend_from_slice(&self.last_payment_tx_sig_short);
        data.extend_from_slice(&self.payment_count.to_le_bytes());
        data.push(self.participant_type);
        // Note: _padding is not included in hash for determinism

        // Compute cryptographic hash using Solana's standard hash function
        hashv(&[&data]).to_bytes()
    }

    /// Validate the participant data for consistency.
    ///
    /// ## Returns
    /// - `Ok(())` if data is valid
    /// - `Err(&str)` with error message if validation fails
    ///
    /// ## Validation Rules
    /// - Participant type must be 0 or 1
    /// - Payment count must be greater than 0
    /// - First timestamp must be before or equal to last timestamp
    /// - Short signatures must not be all zeros (basic validation)
    pub fn validate(&self) -> Result<(), &'static str> {
        if self.participant_type > 1 {
            return Err("Invalid participant type");
        }
        if self.payment_count == 0 {
            return Err("Payment count must be greater than 0");
        }
        if self.first_payment_timestamp > self.last_payment_timestamp {
            return Err("First timestamp must be before or equal to last timestamp");
        }
        if self.first_payment_tx_sig_short == [0u8; 8] {
            return Err("First payment transaction signature short cannot be all zeros");
        }
        if self.last_payment_tx_sig_short == [0u8; 8] {
            return Err("Last payment transaction signature short cannot be all zeros");
        }

        Ok(())
    }

    /// Check if this participant is a customer.
    ///
    /// ## Returns
    /// - `true` if participant_type is 0 (customer)
    /// - `false` if participant_type is 1 (merchant)
    pub fn is_customer(&self) -> bool {
        self.participant_type == 0
    }

    /// Check if this participant is a merchant.
    ///
    /// ## Returns
    /// - `true` if participant_type is 1 (merchant)
    /// - `false` if participant_type is 0 (customer)
    pub fn is_merchant(&self) -> bool {
        self.participant_type == 1
    }

    // /// Get the first payment transaction signature short as a string.
    // ///
    // /// ## Returns
    // /// - String representation of the first payment transaction signature short
    // pub fn first_payment_tx_sig_short_str(&self) -> String {
    //     hex::encode(&self.first_payment_tx_sig_short)
    // }

    // /// Get the last payment transaction signature short as a string.
    // ///
    // /// ## Returns
    // /// - String representation of the last payment transaction signature short
    // pub fn last_payment_tx_sig_short_str(&self) -> String {
    //     hex::encode(&self.last_payment_tx_sig_short)
    // }

    /// Deserialize from bytes manually
    pub fn from_bytes(data: &[u8]) -> Result<Self, &'static str> {
        let expected_size = std::mem::size_of::<Self>();
        if data.len() < expected_size {
            return Err("Insufficient data for DailyParticipantData");
        }

        let participant_id: [u8; 32] = data[0..32]
            .try_into()
            .map_err(|_| "Invalid participant_id")?;
        let first_payment_timestamp = i64::from_le_bytes(
            data[32..40]
                .try_into()
                .map_err(|_| "Invalid first_payment_timestamp")?,
        );
        let last_payment_timestamp = i64::from_le_bytes(
            data[40..48]
                .try_into()
                .map_err(|_| "Invalid last_payment_timestamp")?,
        );
        let first_payment_tx_sig_short: [u8; 8] = data[48..56]
            .try_into()
            .map_err(|_| "Invalid first_payment_tx_sig_short")?;
        let last_payment_tx_sig_short: [u8; 8] = data[56..64]
            .try_into()
            .map_err(|_| "Invalid last_payment_tx_sig_short")?;
        let payment_count = u32::from_le_bytes(
            data[64..68]
                .try_into()
                .map_err(|_| "Invalid payment_count")?,
        );
        let participant_type = data[68];

        Ok(Self {
            participant_id,
            first_payment_timestamp,
            last_payment_timestamp,
            first_payment_tx_sig_short,
            last_payment_tx_sig_short,
            payment_count,
            participant_type,
            _padding: [0u8; 3],
        })
    }
}