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::str::FromStr;
use std::time::Duration;
use zfuel::fuel::ZFuel;
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()))
}
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,
))
})?;
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);
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();
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))
}
fn get_parked_link_from_source(allocation: Map) -> ExternResult<(ParkedLink, ActionHash)> {
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))
}
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);
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())
}
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,
))
})?;
let dynamic = RhaiEngine::json_to_rhai_dynamic(&value);
debug!("value: {:?}", dynamic);
Ok(dynamic)
}
pub fn parse_record_to_parked_amount_and_source(
record: Map,
) -> Result<Dynamic, Box<EvalAltResult>> {
let record = try_into_record(record)?;
let record_id = record.action_address();
let action = record.action();
let create_link = match action {
Action::CreateLink(create_link) => create_link,
_ => {
return Err(Box::new(EvalAltResult::ErrorRuntime(
"Expected CreateLink action".into(),
Position::NONE,
)))
}
};
let tag = ParkedLinkType::from_create_link(create_link).map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
format!("Failed to parse ParkedData: {e}").into(),
Position::NONE,
))
})?;
if let ParkedLinkType::ParkedSpendBalance(parked_spend_tag) = tag {
trace!("parked_spend_tag: {:?}", parked_spend_tag);
let mut map = Map::new();
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();
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,
)))
}
}
pub fn consume_allocations(
allocations: &mut Array,
unit_index: String,
needed: String,
) -> Result<Map, Box<EvalAltResult>> {
fn err(msg: String) -> Box<EvalAltResult> {
Box::new(EvalAltResult::ErrorRuntime(msg.into(), Position::NONE))
}
fn amount_at(amount: &Map, unit_index: &str, i: usize) -> Result<ZFuel, Box<EvalAltResult>> {
match amount.get(unit_index) {
None => Ok(ZFuel::zero()),
Some(v) => {
let s = v.to_string();
ZFuel::from_str(&s).map_err(|e| {
err(format!(
"consume_allocations: allocation {i} amount \"{s}\" is not a valid fuel amount: {e:?}"
))
})
}
}
}
fn amount_of(alloc: &Dynamic, unit_index: &str, i: usize) -> Result<ZFuel, Box<EvalAltResult>> {
let alloc_map = alloc
.read_lock::<Map>()
.ok_or_else(|| err(format!("consume_allocations: allocation {i} is not a map")))?;
let data = alloc_map
.get("data")
.and_then(|d| d.read_lock::<Map>())
.ok_or_else(|| {
err(format!(
"consume_allocations: allocation {i} has no data map"
))
})?;
let amount = data
.get("amount")
.and_then(|a| a.read_lock::<Map>())
.ok_or_else(|| {
err(format!(
"consume_allocations: allocation {i} has no data.amount map"
))
})?;
amount_at(&amount, unit_index, i)
}
fn parts(
alloc: &Dynamic,
unit_index: &str,
i: usize,
) -> Result<(Map, Map, Map, ZFuel), Box<EvalAltResult>> {
let alloc_map = alloc
.clone()
.try_cast::<Map>()
.ok_or_else(|| err(format!("consume_allocations: allocation {i} is not a map")))?;
let data = alloc_map
.get("data")
.and_then(|d| d.clone().try_cast::<Map>())
.ok_or_else(|| {
err(format!(
"consume_allocations: allocation {i} has no data map"
))
})?;
let amount = data
.get("amount")
.and_then(|a| a.clone().try_cast::<Map>())
.ok_or_else(|| {
err(format!(
"consume_allocations: allocation {i} has no data.amount map"
))
})?;
let amt = amount_at(&amount, unit_index, i)?;
Ok((alloc_map, data, amount, amt))
}
let needed_fuel = ZFuel::from_str(&needed).map_err(|e| {
err(format!(
"consume_allocations: needed amount \"{needed}\" is not a valid fuel amount: {e:?}"
))
})?;
let zero = ZFuel::zero();
if needed_fuel < zero {
return Err(err(format!(
"consume_allocations: needed amount \"{needed}\" must not be negative"
)));
}
let mut total = ZFuel::zero();
for (i, alloc) in allocations.iter().enumerate() {
let amt = amount_of(alloc, &unit_index, i)?;
if amt > zero {
total = (total + amt).map_err(|e| err(format!("consume_allocations: {e:?}")))?;
}
}
let mut result = Map::new();
if total < needed_fuel {
result.insert("covered".into(), Dynamic::from(false));
result.insert("sources".into(), Dynamic::from(Array::new()));
return Ok(result);
}
let mut remaining = needed_fuel;
let mut sources = Array::new();
for (i, slot) in allocations.iter_mut().enumerate() {
if remaining <= zero {
break;
}
let (mut alloc_map, mut data, mut amount, amt) = parts(slot, &unit_index, i)?;
if amt <= zero {
continue;
}
let source = data.get("source").cloned().ok_or_else(|| {
err(format!(
"consume_allocations: allocation {i} has no data.source"
))
})?;
let drawn = if amt >= remaining { remaining } else { amt };
let left = (amt - drawn).map_err(|e| err(format!("consume_allocations: {e:?}")))?;
remaining = (remaining - drawn).map_err(|e| err(format!("consume_allocations: {e:?}")))?;
amount.insert(unit_index.clone().into(), left.to_string().into());
data.insert("amount".into(), Dynamic::from(amount));
alloc_map.insert("data".into(), Dynamic::from(data));
*slot = Dynamic::from(alloc_map);
sources.push(source);
}
result.insert("covered".into(), Dynamic::from(true));
result.insert("sources".into(), Dynamic::from(sources));
Ok(result)
}
#[cfg(test)]
mod consume_allocations_tests {
use super::*;
fn alloc(amount: &str, source: &str) -> Dynamic {
let mut amt = Map::new();
amt.insert("0".into(), Dynamic::from(amount.to_string()));
let mut data = Map::new();
data.insert("amount".into(), Dynamic::from(amt));
data.insert("source".into(), Dynamic::from(source.to_string()));
let mut m = Map::new();
m.insert("data".into(), Dynamic::from(data));
Dynamic::from(m)
}
fn amount_left(arr: &Array, i: usize) -> String {
arr[i]
.clone()
.try_cast::<Map>()
.unwrap()
.get("data")
.unwrap()
.clone()
.try_cast::<Map>()
.unwrap()
.get("amount")
.unwrap()
.clone()
.try_cast::<Map>()
.unwrap()
.get("0")
.unwrap()
.to_string()
}
fn sources_of(result: &Map) -> Vec<String> {
result
.get("sources")
.unwrap()
.clone()
.try_cast::<Array>()
.unwrap()
.into_iter()
.map(|s| s.to_string())
.collect()
}
fn covered(result: &Map) -> bool {
result
.get("covered")
.unwrap()
.clone()
.try_cast::<bool>()
.unwrap()
}
#[test]
fn splits_across_sources_when_no_single_one_covers() {
let mut arr: Array = vec![alloc("5", "s1"), alloc("5", "s2")];
let result = consume_allocations(&mut arr, "0".into(), "7".into()).unwrap();
assert!(covered(&result));
assert_eq!(sources_of(&result), vec!["s1", "s2"]);
assert_eq!(amount_left(&arr, 0), "0");
assert_eq!(amount_left(&arr, 1), "3");
}
#[test]
fn insufficient_total_consumes_nothing() {
let mut arr: Array = vec![alloc("5", "s1"), alloc("5", "s2")];
let result = consume_allocations(&mut arr, "0".into(), "11".into()).unwrap();
assert!(!covered(&result));
assert!(sources_of(&result).is_empty());
assert_eq!(amount_left(&arr, 0), "5");
assert_eq!(amount_left(&arr, 1), "5");
}
#[test]
fn exact_total_drains_every_source() {
let mut arr: Array = vec![alloc("5", "s1"), alloc("5", "s2")];
let result = consume_allocations(&mut arr, "0".into(), "10".into()).unwrap();
assert!(covered(&result));
assert_eq!(sources_of(&result), vec!["s1", "s2"]);
assert_eq!(amount_left(&arr, 0), "0");
assert_eq!(amount_left(&arr, 1), "0");
}
#[test]
fn zero_needed_is_covered_drawing_nothing() {
let mut arr: Array = vec![alloc("5", "s1")];
let result = consume_allocations(&mut arr, "0".into(), "0".into()).unwrap();
assert!(covered(&result));
assert!(sources_of(&result).is_empty());
assert_eq!(amount_left(&arr, 0), "5");
}
#[test]
fn skips_drained_sources_and_handles_mixed_precision() {
let mut arr: Array = vec![alloc("0", "s1"), alloc("20", "s2")];
let result = consume_allocations(&mut arr, "0".into(), "19.5".into()).unwrap();
assert!(covered(&result));
assert_eq!(sources_of(&result), vec!["s2"]);
assert_eq!(amount_left(&arr, 1), "0.5");
}
#[test]
fn garbage_amount_errors_naming_the_allocation() {
let mut arr: Array = vec![alloc("not-a-number", "s1")];
let e = consume_allocations(&mut arr, "0".into(), "1".into())
.unwrap_err()
.to_string();
assert!(e.contains("not-a-number"), "must name the value: {e}");
}
#[test]
fn negative_needed_errors_instead_of_trivially_covering() {
let mut arr: Array = vec![alloc("5", "s1")];
let e = consume_allocations(&mut arr, "0".into(), "-7".into())
.unwrap_err()
.to_string();
assert!(e.contains("must not be negative"), "got: {e}");
assert_eq!(amount_left(&arr, 0), "5");
}
}