rave_engine 0.4.0

A secure and efficient JSON Schema validation and Rhai script execution engine
Documentation
use super::utils::*;
use crate::{
    helper_fn::inner_get_agent_app_entries_activity,
    rhai_engine::RhaiEngine,
    types::{
        entries::{
            parked_link::{ParkedLink, ParkedLinkType},
            DataBlob,
        },
        Ledger, UnitMap,
    },
};
use hdi::prelude::{debug, trace, wasm_error, Action, ActionHash, ExternResult, Timestamp};
use rhai::{Array, Dynamic, EvalAltResult, Map, Position};
use serde_json::Value;
use std::time::Duration;
use zfuel::fuel::ZFuel;

/// get spend links author
pub fn get_spend_links_author(spend_link_hash: String) -> Result<Dynamic, Box<EvalAltResult>> {
    let spend_link_hash = ActionHash::try_from(spend_link_hash).map_err(|e| {
        Box::new(EvalAltResult::ErrorRuntime(
            format!("Failed to parse ActionHash: {e}").into(),
            Position::NONE,
        ))
    })?;
    let a = ParkedLink::must_get(&spend_link_hash).map_err(|e| {
        Box::new(EvalAltResult::ErrorRuntime(
            format!("Failed to get ParkedLink: {e}").into(),
            Position::NONE,
        ))
    })?;
    Ok(Dynamic::from(a.creator.to_string()))
}

// todo: we can break this down to move most of the logic into the rhai script
pub fn check_cool_down_period(
    most_recent_spend_link_hash: String,
    agreement_id: String,
    executed_timestamp: String,
    cool_down_period: String,
    withdrawing_unit_index: String,
) -> Result<Dynamic, Box<EvalAltResult>> {
    let most_recent_spend_link_hash =
        ActionHash::try_from(most_recent_spend_link_hash).map_err(|e| {
            Box::new(EvalAltResult::ErrorRuntime(
                format!("Failed to parse ActionHash: {e}").into(),
                Position::NONE,
            ))
        })?;
    let agreement_id = ActionHash::try_from(agreement_id).map_err(|e| {
        Box::new(EvalAltResult::ErrorRuntime(
            format!("Failed to parse ActionHash: {e}").into(),
            Position::NONE,
        ))
    })?;
    let current_link = ParkedLink::must_get(&most_recent_spend_link_hash).map_err(|e| {
        Box::new(EvalAltResult::ErrorRuntime(
            format!("Failed to get ParkedLink: {e}").into(),
            Position::NONE,
        ))
    })?;
    let executed_timestamp = Timestamp::try_from(executed_timestamp).map_err(|e| {
        Box::new(EvalAltResult::ErrorRuntime(
            format!("Failed to parse Timestamp: {e}").into(),
            Position::NONE,
        ))
    })?;
    let until_time = (executed_timestamp
        - Duration::from_secs(cool_down_period.parse::<u64>().map_err(|e| {
            Box::new(EvalAltResult::ErrorRuntime(
                format!("Failed to parse cool_down_period: {e}").into(),
                Position::NONE,
            ))
        })?))
    .expect("Failed to subtract seconds from timestamp");

    let author = current_link.creator;

    let (agents_chain_entry_records, _agent_activity) = inner_get_agent_app_entries_activity(
        author.to_owned(),
        most_recent_spend_link_hash.to_owned(),
    )
    .map_err(|e| {
        Box::new(EvalAltResult::ErrorRuntime(
            e.to_string().into(),
            Position::NONE,
        ))
    })?;
    // get the ledger state would be at a specific time
    // use the highest timestamp and filter out the ones that are older than the minutes
    let filtered_agents_chain_entry_record_before_timestamp = agents_chain_entry_records
        .clone()
        .into_iter()
        .filter(|r| r.action().timestamp() < until_time)
        .collect::<Vec<_>>();
    let ledger_at_time =
        Ledger::calculate_from(&filtered_agents_chain_entry_record_before_timestamp);

    // get the spend links since the last 6 hours
    let all_links: Vec<ParkedLink> = agents_chain_entry_records
        .iter()
        .filter(|s| s.action().timestamp() > until_time)
        .filter_map(|r| r.try_into().ok())
        .filter(|s: &ParkedLink| s.ea_id == agreement_id)
        .collect();
    // calculate the total spend for the last 6 hours
    let total_spend_amounts = all_links.iter().filter_map(|s| s.amount()).collect();
    let total_spends = UnitMap::sum_vec(total_spend_amounts).unwrap_or_default();

    let ledger_balance_for_unit_6_hours_ago =
        ledger_at_time.balance.get_safe(&withdrawing_unit_index);
    let total_spend_amounts_for_unit_6_hours_ago = total_spends.get_safe(&withdrawing_unit_index);
    let difference = (ledger_balance_for_unit_6_hours_ago
        - total_spend_amounts_for_unit_6_hours_ago)
        .map_err(|e| {
            Box::new(EvalAltResult::ErrorRuntime(
                e.to_string().into(),
                Position::NONE,
            ))
        })?;
    if difference < ZFuel::zero() {
        return Ok(Dynamic::from(false));
    }
    Ok(Dynamic::from(true))
}

// /// verify if the record is a Spend or a ParkedSpend
// pub fn verify_valid_spend_records(spend: Map) -> Result<Dynamic, Box<EvalAltResult>> {
//     let spend_record = try_into_record(spend.clone()).map_err(|e| {
//         Box::new(EvalAltResult::ErrorRuntime(
//             format!("Failed to parse Spend: {spend:?} Error: {e} End").into(),
//             Position::NONE,
//         ))
//     })?;
//     if let Ok(spend) = Spend::try_from(&spend_record) {
//         if spend.amount.is_zero() {
//             return Ok(Dynamic::from(false));
//         }
//         Ok(Dynamic::from(true))
//     } else if let Ok(parked_spend) = ParkedLink::try_from(&spend_record) {
//         if let ParkedLinkType::ParkedSpendData(parked_spend_tag) = parked_spend.tag {
//             if parked_spend_tag.amount.is_zero() {
//                 return Ok(Dynamic::from(false));
//             }
//             return Ok(Dynamic::from(true));
//         } else {
//             return Ok(Dynamic::from(false));
//         }
//     } else {
//         return Ok(Dynamic::from(false));
//     }
// }

fn get_parked_link_from_source(allocation: Map) -> ExternResult<(ParkedLink, ActionHash)> {
    // todo: better error handling
    let source = allocation["source"].to_string();

    let source_action = ActionHash::try_from(source)
        .map_err(|e| wasm_error!("Failed to parse ActionHash: {}", e))?;

    let parked_link = ParkedLink::must_get(&source_action)?;

    Ok((parked_link, source_action))
}
/// sort allocation based on timestamp of the sources
pub fn acceding_sort_allocation(allocation: Array) -> Result<Array, Box<EvalAltResult>> {
    let parked_links: Vec<(ParkedLink, ActionHash)> = allocation
        .into_iter()
        .filter_map(|a| {
            a.try_cast::<Map>()
                .and_then(|map| get_parked_link_from_source(map).ok())
        })
        .collect();

    let mut sorted_parked_links = parked_links;
    sorted_parked_links.sort_by_key(|(a, _)| a.timestamp);

    // convert Vec<ParkedLink> to A Vec<Map>
    let sorted_parked_links_map: Vec<Map> = sorted_parked_links
        .into_iter()
        .map(|(a, id)| {
            let mut map = Map::new();
            let amount: Map = if let Some(amount) = a.amount() {
                amount.to_map()
            } else {
                UnitMap::new().to_map()
            };
            map.insert("amount".into(), Dynamic::from(amount));
            map.insert("source".into(), Dynamic::from(id.to_string()));
            map.insert("spender".into(), Dynamic::from(a.creator.to_string()));
            map
        })
        .collect();

    Ok(sorted_parked_links_map
        .into_iter()
        .map(Dynamic::from)
        .collect())
}

/// get data blob
pub fn get_data_blob(hash: String) -> Result<Dynamic, Box<EvalAltResult>> {
    let blob_hash_id = ActionHash::try_from(hash).map_err(|e| {
        Box::new(EvalAltResult::ErrorRuntime(
            format!("Failed to parse ActionHash: {e}").into(),
            Position::NONE,
        ))
    })?;

    let blob = DataBlob::must_get(&blob_hash_id).map_err(|e| {
        Box::new(EvalAltResult::ErrorRuntime(
            format!("Failed to get DataBlob: {e}").into(),
            Position::NONE,
        ))
    })?;

    let blob_bytes = (blob.0).data;
    let value: Value = rmp_serde::from_slice(&blob_bytes).map_err(|e| {
        Box::new(EvalAltResult::ErrorRuntime(
            format!("Failed to parse DataBlob: {blob_bytes:?} Error: {e} End").into(),
            Position::NONE,
        ))
    })?;

    // convert value to dynamic
    let dynamic = RhaiEngine::json_to_rhai_dynamic(&value);
    debug!("value: {:?}", dynamic);
    Ok(dynamic)
}

/// Functions for handling ParkedSpend data in the DNA context.
/// This module provides utilities for parsing and handling parked spends
/// within the Holochain DNA environment.
pub fn parse_record_to_parked_amount_and_source(
    record: Map,
) -> Result<Dynamic, Box<EvalAltResult>> {
    // Convert the input Map to a Record type
    let record = try_into_record(record)?;
    let record_id = record.action_address();
    let action = record.action();

    // Ensure the action is a CreateLink and extract it
    let create_link = match action {
        Action::CreateLink(create_link) => create_link,
        _ => {
            return Err(Box::new(EvalAltResult::ErrorRuntime(
                "Expected CreateLink action".into(),
                Position::NONE,
            )))
        }
    };

    // Parse the link tag into a ParkedL
    let tag = ParkedLinkType::from_create_link(create_link).map_err(|e| {
        Box::new(EvalAltResult::ErrorRuntime(
            format!("Failed to parse ParkedData: {e}").into(),
            Position::NONE,
        ))
    })?;

    // Convert allocations to Rhai format
    // Each allocation is converted into a map with amount, agent, and source fields
    if let ParkedLinkType::ParkedSpendBalance(parked_spend_tag) = tag {
        trace!("parked_spend_tag: {:?}", parked_spend_tag);
        let mut map = Map::new();
        // Convert amount to array of strings
        map.insert(
            "amount".into(),
            Dynamic::from(parked_spend_tag.amount.to_map()),
        );
        map.insert("source".into(), Dynamic::from(record_id.to_string()));

        trace!("parse_record_to_parked_amount_and_source: {:?}", map);
        Ok(Dynamic::from(map))
    } else if let ParkedLinkType::ParkedSpendCredit(parked_spend_tag) = tag {
        trace!("parked_spend_credit_tag: {:?}", parked_spend_tag);
        let mut map = Map::new();
        // Convert amount to array of strings
        map.insert(
            "amount".into(),
            Dynamic::from(parked_spend_tag.amount.to_map()),
        );
        map.insert("source".into(), Dynamic::from(record_id.to_string()));

        trace!("parse_record_to_parked_amount_and_source: {:?}", map);
        Ok(Dynamic::from(map))
    } else {
        Err(Box::new(EvalAltResult::ErrorRuntime(
            "Expected ParkedSpendData".into(),
            Position::NONE,
        )))
    }
}