use uuid::Uuid;
use std::{str::FromStr, sync::Arc};
use crate::{
config::ipfs::IpfsConfig,
error::RegoError,
evaluate,
mock_newton_policy_client::{INewtonPolicy::PolicyConfig, MockNewtonPolicyClient},
newton_policy::{INewtonPolicy, NewtonPolicy},
newton_prover_task_manager::{
INewtonProverTaskManager::{self, Task},
NewtonMessage::{self, Intent},
},
rego::validate_schema,
PolicyId, TaskId,
};
use alloy::{
dyn_abi::DynSolValue,
primitives::{keccak256, Address, Bytes, ChainId, B256, U256},
sol_types::SolValue,
};
use cid::Cid;
use serde::{Deserialize, Serialize};
use std::collections::BTreeSet;
// Evaluation kernel moved to `crate::eval` so the SP1 rego challenge circuit can
// share it (the kernel compiles in zkVM builds; this module does not). Re-exported
// here so existing `crate::common::task::{...}` / `crate::common::{...}` importers
// resolve unchanged.
pub use crate::eval::{decode_calldata, parse_intent, serialize_sol_value, ParsedIntent};
/// Task request
///
/// Contains task data for operators to process. Operators fetch policyTaskData
/// independently using `wasm_args` for latency improvement and decentralization.
/// The aggregator handles numeric field variance via median-based consensus.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskRequest {
/// Task ID
pub task_id: TaskId,
/// Intent
pub intent: NewtonMessage::Intent,
/// Intent signature
pub intent_signature: Option<Bytes>,
/// Policy client address
pub policy_client: Address,
/// WASM args for operators to generate policyTaskData
pub wasm_args: Option<Bytes>,
/// Quorum numbers
pub quorum_numbers: Vec<u8>,
/// Quorum threshold percentage
pub quorum_threshold_percentage: u32,
/// Task created block
pub task_created_block: u64,
/// timestamp marking the offchain ingestion of the task
pub initialization_timestamp: u64,
/// Optional IPFS CID of a TLSNotary presentation proof for zkTLS verification.
///
/// This field is off-chain only — it is NOT carried into the on-chain `Task`
/// struct (dropped by the `From<TaskRequest> for Task` conversion). The gateway
/// persists it in the off-chain task metadata and also injects the same CID into
/// `wasmArgs._newton.proof_cid` so operators can fetch and verify the proof from IPFS.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[serde(alias = "proofCid")]
pub proof_cid: Option<String>,
}
impl From<TaskRequest> for Task {
fn from(task_request: TaskRequest) -> Self {
Self {
taskId: task_request.task_id,
intent: task_request.intent,
intentSignature: task_request.intent_signature.unwrap_or_default(),
policyClient: task_request.policy_client,
taskCreatedBlock: task_request.task_created_block as u32,
wasmArgs: task_request.wasm_args.unwrap_or_default(),
quorumNumbers: task_request.quorum_numbers.into(),
quorumThresholdPercentage: task_request.quorum_threshold_percentage,
initializationTimestamp: U256::from(task_request.initialization_timestamp),
}
}
}
/// Write serialized data to buffer
/// # Arguments
/// * `buffer` - The buffer to encode to
/// * `data` - The data to encode
pub fn write_serialized(buffer: &mut Vec<u8>, data: &[u8]) -> Result<(), bincode::error::EncodeError> {
let mut input: Vec<u8> = Vec::new();
bincode::encode_into_slice(data, &mut input, bincode::config::standard())?;
buffer.extend_from_slice(&input);
Ok(())
}
/// Create a new task id using a uuid
pub fn task_id(seed: Option<&str>) -> TaskId {
let uuid = if let Some(seed) = seed {
Uuid::from_str(seed).unwrap_or_else(|_| Uuid::new_v4())
} else {
Uuid::new_v4()
};
let hash = keccak256(uuid.as_bytes());
TaskId::from(hash)
}
/// Merges multiple PolicyData entries into a single JSON object for Rego policy evaluation.
///
/// # Merge Behavior
/// - Each PolicyData's `data` field is parsed as JSON
/// - Only JSON objects are merged; non-object values (arrays, primitives) are silently ignored
/// - Keys are merged using last-write-wins semantics: if multiple PolicyData entries contain
/// the same key, the value from the later entry in the vector overwrites earlier values
///
/// # Returns
/// A single merged JSON object containing all key-value pairs from the input PolicyData entries.
///
/// # Errors
/// Returns an error if any PolicyData's `data` field contains invalid UTF-8 or invalid JSON,
/// including the policy data address that failed for debugging.
pub fn merge_task_policy_data(
policy_task_data: &NewtonMessage::PolicyTaskData,
) -> Result<serde_json::Value, RegoError> {
// Delegate the merge semantics (utf8 -> json -> extend if object) to the shared
// zkVM-safe kernel so operator and circuit agree byte-for-byte. The per-address
// diagnostic is reconstructed here to preserve this function's RegoError contract.
crate::eval::merge_policy_data(policy_task_data.policyData.iter().map(|d| d.data.as_ref())).map_err(|e| {
let address = policy_task_data
.policyData
.iter()
.map(|d| format!("{:?}", d.policyDataAddress))
.collect::<Vec<_>>()
.join(",");
RegoError::InvalidPolicyDataJson {
address,
error: e.to_string(),
}
})
}
/// Merges multiple JSON values into a single JSON object.
///
/// # Merge Behavior
/// - Only JSON objects are merged; non-object values are silently ignored
/// - Keys use last-write-wins semantics: later values overwrite earlier ones
///
/// # Returns
/// A single merged JSON object.
pub fn merge_jsons(jsons: Vec<serde_json::Value>) -> serde_json::Value {
let merged = jsons.iter().fold(serde_json::Map::new(), |mut merged, data| {
if let serde_json::Value::Object(map) = data {
merged.extend(map.iter().map(|(k, v)| (k.clone(), v.clone())));
}
merged
});
serde_json::Value::Object(merged)
}
/// Merge multiple secrets JSON schemas into a single schema.
///
/// Merge rules:
/// - `properties`: union keys (first definition wins for duplicates)
/// - `required`: union
/// - `type`: `"object"`
/// - `additionalProperties`: preserved as-is by the merged schema builder (policy-specific validation
/// should validate against each PolicyData schema, not a merged policy-level schema)
///
/// Any schema that is valid per `regorus::Schema` is considered valid here. We only special-case
/// a small subset of fields (`properties`/`required`) for merging; all other schema keywords are
/// treated as opaque and do not affect the merge result.
///
/// Invalid schema docs are ignored during merging. Concretely, a schema doc is skipped if:
/// - the schema root is not a JSON object
/// - `properties` is present and not a JSON object (and not `null`)
/// - `required` is present and not a JSON array (and not `null`)
/// - `required` is a JSON array but contains any non-string items
///
/// Rationale:
/// - Secrets schemas are external inputs (IPFS) and may drift or be malformed; skipping bad docs
/// prevents one broken PolicyData schema from blocking all clients that use the policy.
/// - `additionalProperties` is not ignored for strict secrets validation; callers that need strict
/// behavior should validate against the per-PolicyData schema directly.
/// never reject unknown keys due to `additionalProperties` constraints.
///
/// `schema_docs` is `(cid, schema_json)`; `cid` is included to aid debugging when extending this
/// logic or logging higher up the stack.
pub fn merge_secrets_schemas(schema_docs: Vec<(String, serde_json::Value)>) -> eyre::Result<serde_json::Value> {
let mut merged_properties: serde_json::Map<String, serde_json::Value> = serde_json::Map::new();
let mut merged_required: BTreeSet<String> = BTreeSet::new();
for (cid, schema_json) in schema_docs {
let Some(schema_obj) = schema_json.as_object() else {
// Ignore invalid schema docs (root must be an object).
continue;
};
// Merge properties
if let Some(props_val) = schema_obj.get("properties") {
if let Some(props_obj) = props_val.as_object() {
for (k, v) in props_obj.iter() {
// First definition wins
if !merged_properties.contains_key(k) {
merged_properties.insert(k.clone(), v.clone());
}
}
} else if !props_val.is_null() {
// Ignore invalid schema docs (`properties` must be an object if present).
continue;
}
}
// Merge required
if let Some(req_val) = schema_obj.get("required") {
if let Some(req_arr) = req_val.as_array() {
let mut local_required: Vec<String> = Vec::with_capacity(req_arr.len());
for item in req_arr {
let Some(key) = item.as_str() else {
// Ignore invalid schema docs (`required` must be an array of strings).
local_required.clear();
break;
};
local_required.push(key.to_string());
}
if local_required.is_empty() && !req_arr.is_empty() {
continue;
}
for k in local_required {
merged_required.insert(k);
}
} else if !req_val.is_null() {
// Ignore invalid schema docs (`required` must be an array if present).
continue;
}
}
}
// Build final merged schema
let mut merged = serde_json::Map::new();
merged.insert("type".to_string(), serde_json::Value::String("object".to_string()));
merged.insert("properties".to_string(), serde_json::Value::Object(merged_properties));
if !merged_required.is_empty() {
merged.insert(
"required".to_string(),
serde_json::Value::Array(merged_required.into_iter().map(serde_json::Value::String).collect()),
);
}
Ok(serde_json::Value::Object(merged))
}
/// RPC module for task evaluation
pub mod rpc {
#![cfg(feature = "rpc")]
use crate::{
common::{intent::ParsedIntent, policy::get_ipfs_url},
config::ipfs::IpfsConfig,
error::RegoError,
evaluate,
identity_registry::IdentityRegistry,
mock_newton_policy_client::MockNewtonPolicyClient,
newton_policy::{INewtonPolicy, NewtonPolicy},
newton_prover_task_manager::{INewtonProverTaskManager, NewtonMessage},
rego::validate_schema,
TaskId,
};
use alloy::primitives::{Address, Bytes};
use cid::Cid;
use newton_common::{get_provider, get_signer};
use regorus::extensions::PolicyDomainData;
use serde::{Deserialize, Serialize};
use std::str::FromStr;
/// Policy evaluation result
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PolicyEvaluationResult {
/// policy
pub policy: String,
/// parsed intent
pub parsed_intent: ParsedIntent,
/// policy params and data
pub policy_params_and_data: serde_json::Value,
/// entrypoint
pub entrypoint: String,
/// evaluation result
pub result: regorus::Value,
/// expire after
pub expire_after: u32,
}
/// public policy inputs resolved outside private evaluation.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResolvedPolicyInputs {
/// policy config for the task policy id.
pub policy_config: INewtonPolicy::PolicyConfig,
/// rego entrypoint returned by policy contract.
pub entrypoint: String,
/// schema json fetched from ipfs.
pub schema: serde_json::Value,
}
/// Parse the intent and evaluate against the policy
/// Returns the evaluation result and the expire after time
///
/// # Arguments
///
/// * `intent` - The task intent
/// * `policy_task_data` - Policy task data from the operator or consensus
/// * `rpc_url` - RPC URL for chain queries
/// * `ipfs_config` - IPFS configuration for fetching policy schemas
///
/// `additional_data`, if present, is merged into the Rego `data` namespace
/// before evaluation. This is used for transient inputs like verified TLS
/// proof data that must not mutate `policyTaskData`.
pub async fn parse_and_evaluate_task(
intent: &NewtonMessage::Intent,
policy_task_data: &NewtonMessage::PolicyTaskData,
rpc_url: &str,
ipfs_config: Option<&IpfsConfig>,
domain_data: Vec<Box<dyn PolicyDomainData>>,
additional_data: Option<serde_json::Value>,
) -> Result<PolicyEvaluationResult, RegoError> {
use crate::common::{parse_intent, task::merge_task_policy_data};
tracing::info!(
"evaluating policy for intent against policy id {}",
crate::hex!(policy_task_data.policyId)
);
let policy_id = policy_task_data.policyId;
let policy_address = policy_task_data.policyAddress;
let intent = serde_json::json!(intent);
let policy = String::from_utf8(policy_task_data.policy.to_vec()).map_err(|_| RegoError::MissingPolicy)?;
let resolved = resolve_policy_inputs(policy_address, policy_id, rpc_url, ipfs_config).await?;
evaluate_task_with_resolved_policy(intent, policy_task_data, resolved, domain_data, additional_data)
}
/// fetch public policy inputs needed before private evaluation.
pub async fn resolve_policy_inputs(
policy_address: Address,
policy_id: alloy::primitives::FixedBytes<32>,
rpc_url: &str,
ipfs_config: Option<&IpfsConfig>,
) -> Result<ResolvedPolicyInputs, RegoError> {
let provider = get_provider(rpc_url);
tracing::info!("policy_address: {} policy_id: {}", policy_address, policy_id);
let policy_contract = NewtonPolicy::new(policy_address, provider.clone());
let policy_config = policy_contract
.getPolicyConfig(policy_id)
.call()
.await
.map_err(|e| RegoError::FailedToGetPolicyConfig(e.to_string()))?;
let entrypoint = policy_contract
.getEntrypoint()
.call()
.await
.map_err(|e| RegoError::FailedToGetPolicyEntrypoint(e.to_string()))?;
let schema_cid = policy_contract
.getSchemaCid()
.call()
.await
.map_err(|e| RegoError::FailedToGetPolicySchemaCid(e.to_string()))?;
let schema = load_policy_schema_from_ipfs(&schema_cid, ipfs_config).await?;
Ok(ResolvedPolicyInputs {
policy_config,
entrypoint,
schema,
})
}
/// evaluate without chain or ipfs access.
pub fn evaluate_task_with_resolved_policy(
intent: serde_json::Value,
policy_task_data: &NewtonMessage::PolicyTaskData,
resolved: ResolvedPolicyInputs,
domain_data: Vec<Box<dyn PolicyDomainData>>,
additional_data: Option<serde_json::Value>,
) -> Result<PolicyEvaluationResult, RegoError> {
use crate::common::{parse_intent, task::merge_task_policy_data};
let policy_id = policy_task_data.policyId;
let policy_address = policy_task_data.policyAddress;
let policy = String::from_utf8(policy_task_data.policy.to_vec()).map_err(|_| RegoError::MissingPolicy)?;
let INewtonPolicy::PolicyConfig {
policyParams: policy_params,
expireAfter: expire_after,
} = resolved.policy_config;
let entrypoint = resolved.entrypoint;
let schema = resolved.schema;
let policy_rule = format!("data.{}", entrypoint);
let policy_params_str =
String::from_utf8(policy_params.to_vec()).map_err(|e| RegoError::InvalidPolicyDataUtf8 {
error: format!("invalid UTF-8 in policy params: {e}"),
address: Default::default(),
})?;
let policy_params: serde_json::Value =
serde_json::from_str(&policy_params_str).unwrap_or_else(|_| serde_json::json!({}));
// Validate the policy params against the policy schema
tracing::info!("Validating policy params against schema");
validate_schema(schema, policy_params.clone())
.map_err(|e| RegoError::FailedToValidateParamsSchema(e.to_string()))?;
let parsed_intent = parse_intent(intent).map_err(|e| RegoError::FailedToParseIntent(e.to_string()))?;
tracing::info!("parsed_intent: {}", parsed_intent);
let merged_policy_data = merge_task_policy_data(policy_task_data)?;
let parsed_intent_str: String = parsed_intent.clone().into();
let mut policy_params_and_data = serde_json::json!({
"params": policy_params,
"wasm": merged_policy_data,
});
let policy_params_and_data_str = policy_params_and_data.to_string();
let result = evaluate(
policy.clone(),
&policy_params_and_data_str,
&parsed_intent_str,
domain_data,
&policy_rule,
additional_data.as_ref(),
)
.map_err(|e| RegoError::FailedToEvaluateTask(e.to_string()))?;
// SECURITY: Do NOT merge additional_data back into policy_params_and_data here.
// additional_data may contain decrypted ephemeral privacy values that must only
// exist within the Rego evaluation sandbox (handled in rego/mod.rs). Merging
// them here would leak plaintext into PolicyEvaluationResult.policy_params_and_data,
// which flows into TaskResponse (posted on-chain).
tracing::info!("evaluation result: {}", result);
Ok(PolicyEvaluationResult {
policy,
parsed_intent,
policy_params_and_data,
entrypoint,
result,
expire_after,
})
}
/// Fetch the policy schema JSON from IPFS and parse it into a `serde_json::Value`.
///
/// When the `networking` feature is disabled, this returns
/// `RegoError::FailedToFetchPolicySchemaJson` explaining that networking is off.
async fn load_policy_schema_from_ipfs(
schema_cid: &str,
ipfs_config: Option<&IpfsConfig>,
) -> Result<serde_json::Value, RegoError> {
let schema_response = fetch_from_ipfs(schema_cid, ipfs_config)
.await
.map_err(|e| RegoError::FailedToFetchPolicySchemaJson(e.to_string()))?;
let schema_text = schema_response
.text()
.await
.map_err(|e| RegoError::FailedToDecodePolicySchemaJson(e.to_string()))?;
serde_json::from_str(&schema_text).map_err(|e| RegoError::FailedToDecodePolicySchemaJson(e.to_string()))
}
/// Fetch from IPFS.
///
/// If the IPFS config gateway is not a valid IPFS gateway, it will use the
/// fallback public IPFS gateway.
///
/// # Arguments
/// * `cid` - The CID to fetch from IPFS
/// * `config` - The IPFS config
///
/// # Returns
/// A `Result` containing the fetched response, or an error if the fetch fails.
pub async fn fetch_from_ipfs(cid: &str, config: Option<&IpfsConfig>) -> eyre::Result<reqwest::Response> {
let (uri, is_public_gateway) = get_ipfs_url(cid, config)?;
tracing::info!("Fetching from IPFS: {}", uri);
if is_public_gateway {
tracing::warn!("Using public IPFS gateway to fetch schema");
}
let result = reqwest::get(uri).await;
if result.is_ok() && result.as_ref().unwrap().status().is_success() {
return result.map_err(|e| eyre::eyre!(e.to_string()));
}
tracing::warn!("Fallback: using public IPFS gateway to fetch schema");
let uri = format!("{}{}", crate::config::ipfs::PUBLIC_IPFS_GATEWAY, cid);
reqwest::get(uri).await.map_err(|e| eyre::eyre!(e))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::newton_prover_task_manager::{INewtonPolicy::PolicyConfig, NewtonMessage};
use alloy::{json_abi::StateMutability, sol, sol_types::SolCall};
use serde_json::json;
// Sample test data based on the provided log
sol! {
// MockToken token buy contract
contract MockToken {
function mint(address account, uint256 amount) public {}
function buy(address token, uint256 amount) public {}
}
}
fn create_sample_intent() -> NewtonMessage::Intent {
let buy_call = MockToken::buyCall {
token: "0x8f86403a4de0bb5791fa46b8e795c547942fe4cf".parse().unwrap(),
amount: U256::from(200000000000u64),
};
let calldata = buy_call.abi_encode();
NewtonMessage::Intent {
from: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266".parse().unwrap(),
to: "0x8f86403a4de0bb5791fa46b8e795c547942fe4cf".parse().unwrap(),
value: U256::from(10000000000000000u64),
data: calldata.into(),
chainId: U256::from(31337),
functionSignature: "function buy(address token, uint256 amount)".as_bytes().to_vec().into(),
}
}
fn create_sample_policy_config() -> PolicyConfig {
PolicyConfig {
policyParams: Bytes::from(
r#"{
"allowed_actions": {
"31337": {
"function_signature": "function buy(address,uint256)",
"address": "0x8f86403a4de0bb5791fa46b8e795c547942fe4cf",
"max_limit": 1000000000000000000
},
"11155111": {
"function_signature": "function buy(address,uint256)",
"address": "0x8f86403a4de0bb5791fa46b8e795c547942fe4cf",
"max_limit": 1000000000000000000
}
},
"token_whitelist": {
"31337": {
"symbol": "NEWT",
"address": "0x8f86403a4de0bb5791fa46b8e795c547942fe4cf",
"max_limit": 1000000000000000000
},
"11155111": {
"symbol": "WBTC",
"address": "0x29f2D40B0605204364af54EC677bD022dA425d03",
"max_limit": 1000000000000000000
}
}
}"#
.as_bytes(),
),
expireAfter: 1000,
}
}
fn create_sample_policy_data() -> NewtonMessage::PolicyData {
NewtonMessage::PolicyData {
wasmArgs: newton_testing_utils::policy::TEST_POLICY_WASM_ARGS.as_bytes().into(),
data: Bytes::from(
r#"
{
"base_symbol":"BTC",
"quote_symbol":"USD",
"price":"10882409209030",
"confidence":"2620209030",
"exponent":-8,
"publish_time":1756572248
}
"#
.as_bytes(),
),
policyDataAddress: "0x0f6db767b6e408a8479da91b9b5513207bb3fc82".parse().unwrap(),
expireBlock: 220,
}
}
fn create_sample_policy_task_data() -> NewtonMessage::PolicyTaskData {
NewtonMessage::PolicyTaskData {
policyId: "0x4261fbbb3dfc2863eb06e5c271096d9d839bc9c41e9a8d181e1403baca5d9902".parse().unwrap(),
policyAddress: "0xed33e3a3f077bcd7c01fe5e2c1c38d5e016c012f".parse().unwrap(),
policy: "0x23204d6f636b455243323020546f6b656e2042757920506f6c6963790a23202d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d0a230a2320466f72206d6f726520696e666f726d6174696f6e207365653a0a230a23092a205265676f20636f6d70617269736f6e20746f206f746865722073797374656d733a2068747470733a2f2f7777772e6f70656e706f6c6963796167656e742e6f72672f646f63732f6c61746573742f636f6d70617269736f6e2d746f2d6f746865722d73797374656d732f0a23092a205265676f20497465726174696f6e3a2068747470733a2f2f7777772e6f70656e706f6c6963796167656e742e6f72672f646f63732f6c61746573742f23697465726174696f6e0a0a7061636b616765206d6f636b65726332300a0a232042792064656661756c742c2064656e792072657175657374732e0a64656661756c7420616c6c6f77203a3d2066616c73650a0a2320416c6c6f772061646d696e7320746f20646f20616e797468696e672e0a616c6c6f7720696620757365725f69735f61646d696e0a0a66756e6374696f6e5f7369676e6174757265203a3d20696e7075742e6465636f6465645f66756e6374696f6e5f7369676e61747572650a746f6b656e203a3d20696e7075742e6465636f6465645f66756e6374696f6e5f617267756d656e74735b305d0a616d6f756e745f6f7574203a3d20696e7075742e6465636f6465645f66756e6374696f6e5f617267756d656e74735b315d0a616c6c6f7765645f616374696f6e203a3d20646174612e706172616d732e616c6c6f7765645f616374696f6e735b666f726d61745f696e7428696e7075742e636861696e5f69642c203130295d0a626173655f73796d626f6c203a3d20646174612e646174612e626173655f73796d626f6c0a7072696365203a3d20646174612e646174612e70726963650a71756f74655f73796d626f6c203a3d20646174612e646174612e71756f74655f73796d626f6c0a0a2320416c6c6f772074686520616374696f6e206966207468652075736572206973206772616e746564207065726d697373696f6e20746f20706572666f726d2074686520616374696f6e2e0a616c6c6f77206966207b0a09626173655f73796d626f6c203d3d2022425443220a09746f5f6e756d62657228707269636529203e20616d6f756e745f6f75740a0971756f74655f73796d626f6c203d3d2022555344220a090a09616c6c6f7765645f616374696f6e2e61646472657373203d3d20746f6b656e0a20202020616c6c6f7765645f616374696f6e2e6d61785f6c696d6974203e20616d6f756e745f6f75740a7d0a0a2320757365725f69735f61646d696e2069732074727565206966202261646d696e2220697320616d6f6e67207468652075736572277320726f6c65732061732070657220646174612e757365725f726f6c65730a757365725f69735f61646d696e206966202261646d696e2220696e2028646174612e706172616d732e61646d696e203d3d20696e7075742e66726f6d290a".as_bytes().to_vec().into(),
policyData: vec![
create_sample_policy_data(),
]
}
}
fn create_sample_task() -> INewtonProverTaskManager::Task {
INewtonProverTaskManager::Task {
taskId: "0x4261fbbb3dfc2863eb06e5c271096d9d839bc9c41e9a8d181e1403baca5d9902"
.parse()
.unwrap(),
policyClient: "0xed33e3a3f077bcd7c01fe5e2c1c38d5e016c012f".parse().unwrap(),
intent: create_sample_intent(),
intentSignature: Bytes::default(),
wasmArgs: newton_testing_utils::policy::TEST_POLICY_WASM_ARGS.as_bytes().into(),
taskCreatedBlock: 0,
quorumNumbers: Bytes::from([0]), // quorum number 0
quorumThresholdPercentage: 0,
initializationTimestamp: U256::ZERO,
}
}
fn create_sample_task_response() -> INewtonProverTaskManager::TaskResponse {
INewtonProverTaskManager::TaskResponse {
taskId: "0x4261fbbb3dfc2863eb06e5c271096d9d839bc9c41e9a8d181e1403baca5d9902"
.parse()
.unwrap(),
policyClient: "0xed33e3a3f077bcd7c01fe5e2c1c38d5e016c012f".parse().unwrap(),
policyId: "0x4261fbbb3dfc2863eb06e5c271096d9d839bc9c41e9a8d181e1403baca5d9902"
.parse()
.unwrap(),
policyAddress: "0xed33e3a3f077bcd7c01fe5e2c1c38d5e016c012f".parse().unwrap(),
intent: create_sample_intent(),
intentSignature: Bytes::default(),
evaluationResult: Bytes::from("true".as_bytes()),
policyTaskData: create_sample_policy_task_data(),
policyConfig: create_sample_policy_config(),
initializationTimestamp: U256::ZERO,
}
}
#[test]
fn test_decode_calldata_selector_mismatch() {
let calldata: Bytes = "0x12345678".parse().unwrap(); // Wrong selector
let function_signature: Bytes = "function _doSomething()".as_bytes().to_vec().into();
let result = decode_calldata(&calldata, &function_signature);
assert!(result.is_err());
let error = result.unwrap_err();
assert!(error.to_string().contains("Function selector mismatch"));
}
#[test]
fn test_decode_calldata_comprehensive_types() {
use alloy::{
primitives::{FixedBytes, I256},
sol,
};
// Test contract with various Solidity types
sol! {
contract TestContract {
function testFunction(
bool b,
uint256 u,
int256 i,
address a,
bytes32 fb,
bytes dynBytes,
string s,
uint256[] dynamicArray,
uint256[3] fixedArray,
(address, uint256) tuple
) public {}
}
}
// Create test call with various types
let test_call = TestContract::testFunctionCall {
b: true,
u: U256::from(123456789),
i: I256::try_from(-987654321i64).unwrap(),
a: "0x742d35Cc6634C0532925A3B8D4C9dB96C4B4d8B6".parse().unwrap(),
fb: FixedBytes::from([0x12u8; 32]),
dynBytes: Bytes::from(vec![0xab, 0xcd, 0xef]),
s: "Hello World".to_string(),
dynamicArray: vec![U256::from(1), U256::from(2), U256::from(3)],
fixedArray: [U256::from(10), U256::from(20), U256::from(30)],
tuple: (
"0x1234567890123456789012345678901234567890".parse().unwrap(),
U256::from(999),
),
};
let calldata = test_call.abi_encode();
let function_signature: Bytes = Bytes::from("function testFunction(bool,uint256,int256,address,bytes32,bytes,string,uint256[],uint256[3],(address,uint256))".as_bytes().to_vec());
let result = decode_calldata(&Bytes::from(calldata), &function_signature);
assert!(result.is_ok());
let (_func, inputs) = result.unwrap();
assert_eq!(inputs.len(), 10);
// Verify each decoded parameter
match &inputs[0] {
DynSolValue::Bool(b) => assert!(*b),
_ => panic!("Expected bool"),
}
match &inputs[1] {
DynSolValue::Uint(u, _) => assert_eq!(*u, U256::from(123456789)),
_ => panic!("Expected uint256"),
}
match &inputs[2] {
DynSolValue::Int(i, _) => assert_eq!(*i, I256::try_from(-987654321i64).unwrap()),
_ => panic!("Expected int256"),
}
match &inputs[3] {
DynSolValue::Address(a) => assert_eq!(
*a,
"0x742d35Cc6634C0532925A3B8D4C9dB96C4B4d8B6".parse::<Address>().unwrap()
),
_ => panic!("Expected address"),
}
match &inputs[4] {
DynSolValue::FixedBytes(fb, _) => assert_eq!(fb.as_slice(), &[0x12u8; 32]),
_ => panic!("Expected bytes32"),
}
match &inputs[5] {
DynSolValue::Bytes(b) => assert_eq!(b.as_slice(), &vec![0xab, 0xcd, 0xef]),
_ => panic!("Expected bytes"),
}
match &inputs[6] {
DynSolValue::String(s) => assert_eq!(s, "Hello World"),
_ => panic!("Expected string"),
}
match &inputs[7] {
DynSolValue::Array(arr) => {
assert_eq!(arr.len(), 3);
match &arr[0] {
DynSolValue::Uint(u, _) => assert_eq!(*u, U256::from(1)),
_ => panic!("Expected uint in array"),
}
}
_ => panic!("Expected array"),
}
match &inputs[8] {
DynSolValue::FixedArray(arr) => {
assert_eq!(arr.len(), 3);
match &arr[0] {
DynSolValue::Uint(u, _) => assert_eq!(*u, U256::from(10)),
_ => panic!("Expected uint in fixed array"),
}
}
_ => panic!("Expected fixed array"),
}
match &inputs[9] {
DynSolValue::Tuple(tuple) => {
assert_eq!(tuple.len(), 2);
match &tuple[0] {
DynSolValue::Address(a) => assert_eq!(
*a,
"0x1234567890123456789012345678901234567890".parse::<Address>().unwrap()
),
_ => panic!("Expected address in tuple"),
}
match &tuple[1] {
DynSolValue::Uint(u, _) => assert_eq!(*u, U256::from(999)),
_ => panic!("Expected uint in tuple"),
}
}
_ => panic!("Expected tuple"),
}
}
#[test]
fn test_decode_calldata_primitive_types() {
use alloy::{primitives::I256, sol};
sol! {
contract PrimitiveTest {
function primitiveTest(
uint8 u8,
uint16 u16,
uint32 u32,
uint64 u64,
uint128 u128,
uint256 u256,
int8 i8,
int16 i16,
int32 i32,
int64 i64,
int128 i128,
int256 i256
) public {}
}
}
let test_call = PrimitiveTest::primitiveTestCall {
u8: 255,
u16: 65535,
u32: 4294967295,
u64: 18446744073709551615,
u128: 340282366920938463463374607431768211455u128,
u256: U256::from_str_radix(
"115792089237316195423570985008687907853269984665640564039457584007913129639935",
10,
)
.unwrap(),
i8: -128,
i16: -32768,
i32: -2147483648,
i64: -9223372036854775808,
i128: -170141183460469231731687303715884105727i128,
i256: I256::try_from(-170141183460469231731687303715884105727i128).unwrap(),
};
let calldata = test_call.abi_encode();
let function_signature: Bytes = Bytes::from(
"function primitiveTest(uint8,uint16,uint32,uint64,uint128,uint256,int8,int16,int32,int64,int128,int256)"
.as_bytes()
.to_vec(),
);
let result = decode_calldata(&Bytes::from(calldata), &function_signature);
assert!(result.is_ok());
let (_, inputs) = result.unwrap();
assert_eq!(inputs.len(), 12);
// Test that all primitive types are correctly decoded
for (i, input) in inputs.iter().enumerate() {
match input {
DynSolValue::Uint(_, bits) | DynSolValue::Int(_, bits) => {
// Verify the bit width is preserved
assert!(*bits > 0 && *bits <= 256);
}
_ => panic!("Expected numeric type at index {}", i),
}
}
}
#[test]
fn test_parse_intent() {
let intent = json!(create_sample_intent());
let parsed_intent = parse_intent(intent).unwrap();
// Verify all fields are correctly parsed
assert_eq!(
parsed_intent.from,
"0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266".parse::<Address>().unwrap()
);
assert_eq!(
parsed_intent.to,
"0x8f86403a4de0bb5791fa46b8e795c547942fe4cf".parse::<Address>().unwrap()
);
assert_eq!(parsed_intent.value, U256::from(10000000000000000u64)); // 0.01 ether
assert_eq!(parsed_intent.chain_id, Some(ChainId::from(0x7a69u64)));
assert_eq!(
parsed_intent.decoded_function_signature,
Some("function buy(address token, uint256 amount)".to_string())
);
assert_eq!(parsed_intent.decoded_function_arguments.as_ref().unwrap().len(), 2);
}
#[test]
fn test_parse_intent_missing_fields() {
let incomplete_json = json!({
"from": "0xb9e89063d40f95bf2aac0c06777764d7378ead10",
"to": "0x2e2ed0cfd3ad2f1d34481277b3204d807ca2f8c2"
// Missing other required fields
});
let result = parse_intent(incomplete_json);
assert!(result.is_err());
}
#[test]
fn test_parse_intent_optional_chain_id() {
// Test with missing chainId - should set to None
let json_without_chain_id = json!({
"from": "0xb9e89063d40f95bf2aac0c06777764d7378ead10",
"to": "0x2e2ed0cfd3ad2f1d34481277b3204d807ca2f8c2",
"value": "0x0",
"data": "0x6b2305ce",
});
let result = parse_intent(json_without_chain_id);
assert!(result.is_ok());
let parsed = result.unwrap();
assert_eq!(parsed.chain_id, None);
// Test with invalid hex chainId - should set to None
let json_invalid_hex_chain_id = json!({
"from": "0xb9e89063d40f95bf2aac0c06777764d7378ead10",
"to": "0x2e2ed0cfd3ad2f1d34481277b3204d807ca2f8c2",
"value": "0x0",
"data": "0x6b2305ce",
"chainId": "0xGGGG"
});
let result = parse_intent(json_invalid_hex_chain_id);
assert!(result.is_ok());
let parsed = result.unwrap();
assert_eq!(parsed.chain_id, None);
// Test with invalid decimal chainId - should set to None
let json_invalid_decimal_chain_id = json!({
"from": "0xb9e89063d40f95bf2aac0c06777764d7378ead10",
"to": "0x2e2ed0cfd3ad2f1d34481277b3204d807ca2f8c2",
"value": "0x0",
"data": "0x6b2305ce",
"chainId": "not_a_number"
});
let result = parse_intent(json_invalid_decimal_chain_id);
assert!(result.is_ok());
let parsed = result.unwrap();
assert_eq!(parsed.chain_id, None);
// Test with valid hex chainId
let json_valid_hex_chain_id = json!({
"from": "0xb9e89063d40f95bf2aac0c06777764d7378ead10",
"to": "0x2e2ed0cfd3ad2f1d34481277b3204d807ca2f8c2",
"value": "0x0",
"data": "0x6b2305ce",
"chainId": "0x7a69"
});
let result = parse_intent(json_valid_hex_chain_id);
assert!(result.is_ok());
let parsed = result.unwrap();
assert_eq!(parsed.chain_id, Some(31337u64));
// Test with valid decimal chainId
let json_valid_decimal_chain_id = json!({
"from": "0xb9e89063d40f95bf2aac0c06777764d7378ead10",
"to": "0x2e2ed0cfd3ad2f1d34481277b3204d807ca2f8c2",
"value": "0x0",
"data": "0x6b2305ce",
"chainId": "1"
});
let result = parse_intent(json_valid_decimal_chain_id);
assert!(result.is_ok());
let parsed = result.unwrap();
assert_eq!(parsed.chain_id, Some(1u64));
}
#[test]
fn test_parse_intent_optional_function_signature() {
// Test without functionSignature - should be None
let json_without_sig = json!({
"from": "0xb9e89063d40f95bf2aac0c06777764d7378ead10",
"to": "0x2e2ed0cfd3ad2f1d34481277b3204d807ca2f8c2",
"value": "0x0",
"data": "0x6b2305ce",
"chainId": "1"
});
let result = parse_intent(json_without_sig);
assert!(result.is_ok());
let parsed = result.unwrap();
assert_eq!(parsed.function_signature, None);
assert_eq!(parsed.decoded_function_signature, None);
assert_eq!(parsed.decoded_function_arguments, None);
assert_eq!(parsed.function, None);
// Test with functionSignature but no data - decoding should fail gracefully
let json_with_sig_no_data = json!({
"from": "0xb9e89063d40f95bf2aac0c06777764d7378ead10",
"to": "0x2e2ed0cfd3ad2f1d34481277b3204d807ca2f8c2",
"value": "0x0",
"chainId": "1",
"functionSignature": "function doSomething()"
});
let result = parse_intent(json_with_sig_no_data);
assert!(result.is_ok());
let parsed = result.unwrap();
assert!(parsed.function_signature.is_some());
assert_eq!(parsed.data, None);
assert_eq!(parsed.decoded_function_signature, None);
}
#[test]
fn test_parse_intent_optional_data() {
// Test without data - should be None
let json_without_data = json!({
"from": "0xb9e89063d40f95bf2aac0c06777764d7378ead10",
"to": "0x2e2ed0cfd3ad2f1d34481277b3204d807ca2f8c2",
"value": "0x0",
"chainId": "1"
});
let result = parse_intent(json_without_data);
assert!(result.is_ok());
let parsed = result.unwrap();
assert_eq!(parsed.data, None);
// Test with data
let json_with_data = json!({
"from": "0xb9e89063d40f95bf2aac0c06777764d7378ead10",
"to": "0x2e2ed0cfd3ad2f1d34481277b3204d807ca2f8c2",
"value": "0x0",
"data": "0x6b2305ce",
"chainId": "1"
});
let result = parse_intent(json_with_data);
assert!(result.is_ok());
let parsed = result.unwrap();
assert!(parsed.data.is_some());
assert_eq!(parsed.data.unwrap(), Bytes::from(vec![0x6b, 0x23, 0x05, 0xce]));
}
#[test]
fn test_parse_intent_minimal_valid() {
// Test with only required fields (from, to, value)
let minimal_json = json!({
"from": "0xb9e89063d40f95bf2aac0c06777764d7378ead10",
"to": "0x2e2ed0cfd3ad2f1d34481277b3204d807ca2f8c2",
"value": "1000000000000000000"
});
let result = parse_intent(minimal_json);
assert!(result.is_ok());
let parsed = result.unwrap();
assert_eq!(
parsed.from,
"0xb9e89063d40f95bf2aac0c06777764d7378ead10".parse::<Address>().unwrap()
);
assert_eq!(
parsed.to,
"0x2e2ed0cfd3ad2f1d34481277b3204d807ca2f8c2".parse::<Address>().unwrap()
);
assert_eq!(parsed.value, U256::from(1000000000000000000u64));
assert_eq!(parsed.chain_id, None);
assert_eq!(parsed.data, None);
assert_eq!(parsed.function_signature, None);
assert_eq!(parsed.decoded_function_signature, None);
assert_eq!(parsed.decoded_function_arguments, None);
assert_eq!(parsed.function, None);
}
#[test]
fn test_parse_intent_full_valid() {
// Test with all fields present and valid
let buy_call = MockToken::buyCall {
token: "0x8f86403a4de0bb5791fa46b8e795c547942fe4cf".parse().unwrap(),
amount: U256::from(200000000000u64),
};
let calldata = buy_call.abi_encode();
let full_json = json!({
"from": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
"to": "0x8f86403a4de0bb5791fa46b8e795c547942fe4cf",
"value": "10000000000000000",
"data": format!("0x{}", crate::hex!(&calldata)),
"chainId": "0x7a69",
"functionSignature": "function buy(address token, uint256 amount)"
});
let result = parse_intent(full_json);
assert!(result.is_ok());
let parsed = result.unwrap();
assert!(parsed.chain_id.is_some());
assert_eq!(parsed.chain_id.unwrap(), 31337u64);
assert!(parsed.data.is_some());
assert!(parsed.function_signature.is_some());
assert!(parsed.decoded_function_signature.is_some());
assert_eq!(
parsed.decoded_function_signature.unwrap(),
"function buy(address token, uint256 amount)"
);
assert!(parsed.decoded_function_arguments.is_some());
assert_eq!(parsed.decoded_function_arguments.as_ref().unwrap().len(), 2);
assert!(parsed.function.is_some());
}
#[test]
fn test_parse_intent_invalid_address() {
let invalid_json = json!({
"from": "0x123456789012345678901234567890123456789012", // Invalid address length (too long)
"to": "0x2e2ed0cfd3ad2f1d34481277b3204d807ca2f8c2",
"value": "0x0",
"data": "0x6b2305ce",
"chainId": "0x7a69",
"functionSignature": "0x66756e6374696f6e205f646f536f6d657468696e672829"
});
let result = parse_intent(invalid_json);
assert!(result.is_err());
}
#[test]
fn test_serialize_sol_value() {
// Test different Solidity value types
let bool_val = DynSolValue::Bool(true);
let serialized_bool = serialize_sol_value(&bool_val);
assert_eq!(serialized_bool, serde_json::Value::Bool(true));
let uint_val = DynSolValue::Uint(U256::from(123), 256);
let serialized_uint = serialize_sol_value(&uint_val);
assert_eq!(serialized_uint, serde_json::Value::String("123".to_string()));
let address_val = DynSolValue::Address("0x742d35Cc6634C0532925A3B8D4C9dB96C4B4d8B6".parse().unwrap());
let serialized_address = serialize_sol_value(&address_val);
assert_eq!(
serialized_address,
serde_json::Value::String("0x742d35cc6634c0532925a3b8d4c9db96c4b4d8b6".to_string())
);
let bytes_val = DynSolValue::Bytes(vec![0x12, 0x34, 0x56]);
let serialized_bytes = serialize_sol_value(&bytes_val);
assert_eq!(serialized_bytes, serde_json::Value::String("0x123456".to_string()));
let string_val = DynSolValue::String("Hello World".to_string());
let serialized_string = serialize_sol_value(&string_val);
assert_eq!(serialized_string, serde_json::Value::String("Hello World".to_string()));
// Test fixed bytes preserving exact length
use alloy::primitives::FixedBytes;
let mut fixed_bytes_array = [0u8; 32];
fixed_bytes_array[0] = 0x12;
fixed_bytes_array[1] = 0x34;
let fixed_bytes_val = DynSolValue::FixedBytes(FixedBytes::from(fixed_bytes_array), 32);
let serialized_fixed_bytes = serialize_sol_value(&fixed_bytes_val);
assert_eq!(
serialized_fixed_bytes,
serde_json::Value::String("0x1234000000000000000000000000000000000000000000000000000000000000".to_string())
);
// Octane Warning #3: zero-tail bytes32 must serialize to full-width hex
let mut zero_tail_array = [0u8; 32];
zero_tail_array[0] = 0xaa;
zero_tail_array[1] = 0xbb;
let zero_tail_val = DynSolValue::FixedBytes(FixedBytes::from(zero_tail_array), 32);
let serialized_zero_tail = serialize_sol_value(&zero_tail_val);
assert_eq!(
serialized_zero_tail,
serde_json::Value::String("0xaabb000000000000000000000000000000000000000000000000000000000000".to_string()),
"bytes32 with trailing zeros must serialize to full 64-nibble hex"
);
// All-zero bytes32 must also be full-width
let all_zero_array = [0u8; 32];
let all_zero_val = DynSolValue::FixedBytes(FixedBytes::from(all_zero_array), 32);
let serialized_all_zero = serialize_sol_value(&all_zero_val);
assert_eq!(
serialized_all_zero,
serde_json::Value::String("0x0000000000000000000000000000000000000000000000000000000000000000".to_string()),
"all-zero bytes32 must serialize to full 64-nibble hex, not '0x'"
);
// Test array
let array_val = DynSolValue::Array(vec![
DynSolValue::Uint(U256::from(1), 256),
DynSolValue::Uint(U256::from(2), 256),
DynSolValue::Uint(U256::from(3), 256),
]);
let serialized_array = serialize_sol_value(&array_val);
let expected_array = serde_json::json!(["1", "2", "3"]);
assert_eq!(serialized_array, expected_array);
// Test tuple
let tuple_val = DynSolValue::Tuple(vec![
DynSolValue::Address("0x742d35Cc6634C0532925A3B8D4C9dB96C4B4d8B6".parse().unwrap()),
DynSolValue::Uint(U256::from(1000), 256),
]);
let serialized_tuple = serialize_sol_value(&tuple_val);
let expected_tuple = serde_json::json!(["0x742d35cc6634c0532925a3b8d4c9db96c4b4d8b6", "1000"]);
assert_eq!(serialized_tuple, expected_tuple);
}
// Test cases for parse_and_evaluate_task
// Note: These are unit tests that focus on the core logic without blockchain interactions
#[tokio::test]
#[cfg(feature = "rpc")]
async fn test_parse_and_evaluate_task_success() {
// Create a task with valid policy data
let task = create_sample_task();
// Note: This test would require mocking the blockchain interactions
// For now, we'll test the individual components that make up this function
// Test that we can parse the intent from the task
let intent_json = json!(task.intent);
let parsed_intent = parse_intent(intent_json).unwrap();
// Verify the parsed intent has the expected structure
assert_eq!(
parsed_intent.from,
"0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266".parse::<Address>().unwrap()
);
assert_eq!(
parsed_intent.to,
"0x8f86403a4de0bb5791fa46b8e795c547942fe4cf".parse::<Address>().unwrap()
);
assert_eq!(parsed_intent.value, U256::from(10000000000000000u64));
assert_eq!(
parsed_intent.decoded_function_signature,
Some("function buy(address token, uint256 amount)".to_string())
);
let args = parsed_intent.decoded_function_arguments.as_ref().unwrap();
assert_eq!(args.len(), 2);
assert_eq!(args[0], "0x8f86403a4de0bb5791fa46b8e795c547942fe4cf");
assert_eq!(args[1], "200000000000");
let func = parsed_intent.function.as_ref().unwrap();
assert_eq!(func.name, "buy");
assert_eq!(func.inputs.len(), 2);
assert_eq!(func.inputs[0].ty, "address");
assert_eq!(func.inputs[1].ty, "uint256");
assert_eq!(func.outputs.len(), 0);
assert_eq!(func.state_mutability, StateMutability::NonPayable);
// Test that we can merge the policy data
// Use sample policy task data directly for testing merge logic
let sample_policy_task_data = create_sample_policy_task_data();
let merged_policy_data =
merge_task_policy_data(&sample_policy_task_data).expect("valid policy data should merge");
assert!(merged_policy_data.is_object());
assert_eq!(merged_policy_data["base_symbol"], "BTC");
assert_eq!(merged_policy_data["quote_symbol"], "USD");
assert_eq!(merged_policy_data["price"], "10882409209030");
assert_eq!(merged_policy_data["confidence"], "2620209030");
assert_eq!(merged_policy_data["exponent"], -8);
assert_eq!(merged_policy_data["publish_time"], 1756572248);
}
#[test]
fn test_intent_round_trip_alloy_serde_to_parsed_intent() {
use crate::common::intent::RawParsedIntent;
// Pin the invariant the circuit relies on: NewtonMessage::Intent (Solidity struct)
// -> serde_json::json! -> parse_intent -> RawParsedIntent -> String
// must produce strings for Bytes/U256 fields, never null, even when empty/zero.
// Case 1: Empty calldata (plain ETH transfer)
let intent = Intent {
from: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266".parse().unwrap(),
to: "0x70997970c51812dc3a010c7d01b50e0d17dc79c8".parse().unwrap(),
value: U256::from(1000000000000000000u64), // 1 ETH
data: Bytes::new(), // empty
chainId: U256::ZERO, // no chain ID
functionSignature: Bytes::new(), // no function signature
};
let intent_json = serde_json::json!(intent);
let parsed_intent = parse_intent(intent_json).expect("should parse empty-calldata intent");
let final_json_str: String = parsed_intent.clone().into(); // From<ParsedIntent> for String
let final_json: serde_json::Value =
serde_json::from_str(&final_json_str).expect("ParsedIntent JSON should be valid");
// Bytes.to_string() emits "0x" for empty; ChainId (u64).to_string() emits decimal
assert_eq!(final_json["data"], "0x", "empty Bytes must serialize as '0x', not null");
assert_eq!(
final_json["chain_id"], "0",
"zero ChainId must serialize as '0', not null"
);
assert_eq!(
final_json["function_signature"], "0x",
"empty Bytes must serialize as '0x', not null"
);
// Case 2: Non-empty calldata
let intent_with_data = Intent {
from: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266".parse().unwrap(),
to: "0x70997970c51812dc3a010c7d01b50e0d17dc79c8".parse().unwrap(),
value: U256::from(1000000000000000000u64),
data: Bytes::from(vec![0x6b, 0x23, 0x05, 0xce]),
chainId: U256::from(1u64),
functionSignature: Bytes::from("function transfer(address,uint256)".as_bytes()),
};
let intent_json = serde_json::json!(intent_with_data);
let parsed_intent = parse_intent(intent_json).expect("should parse intent with data");
let final_json_str: String = parsed_intent.clone().into();
let final_json: serde_json::Value =
serde_json::from_str(&final_json_str).expect("ParsedIntent JSON should be valid");
assert_eq!(final_json["data"], "0x6b2305ce");
assert_eq!(final_json["chain_id"], "1"); // ChainId is decimal
assert!(final_json["function_signature"].as_str().unwrap().starts_with("0x"));
}
#[tokio::test]
#[cfg(feature = "rpc")]
async fn test_parse_and_evaluate_task_invalid_intent() {
use alloy::primitives::Bytes;
// Create a task with invalid intent data
let mut task = create_sample_task();
// Corrupt the intent data to make it invalid (not valid ABI-encoded calldata)
task.intent.data = Bytes::from("invalid_data".as_bytes());
// Test that parsing the intent succeeds but decoding fails gracefully
let intent_json = json!(task.intent);
let result = parse_intent(intent_json);
// Parsing should succeed (data is valid hex after serialization)
assert!(result.is_ok());
let parsed = result.unwrap();
// But the data is present
assert!(parsed.data.is_some());
// And decoding should have failed, so decoded fields should be None
assert!(parsed.decoded_function_signature.is_none());
assert!(parsed.decoded_function_arguments.is_none());
assert!(parsed.function.is_none());
}
#[tokio::test]
async fn test_evaluate_real_world_scenario() {
use crate::evaluate;
// Real-world data from the logs
let policy = newton_testing_utils::policy::TEST_POLICY_REGO;
let policy_params_and_data = newton_testing_utils::policy::TEST_POLICY_DATA;
let parsed_intent = newton_testing_utils::policy::TEST_POLICY_PARSED_INTENT;
let policy_rule = "data.mockerc20.allow";
// Test the evaluation
let result = evaluate(
policy.to_string(),
policy_params_and_data,
parsed_intent,
vec![],
policy_rule,
None,
);
// The evaluation should succeed
assert!(result.is_ok());
let evaluation_result = result.unwrap();
// The result should be a boolean indicating whether the action is allowed
match evaluation_result {
regorus::Value::Bool(result) => {
// The policy evaluation is working correctly
assert!(result, "Policy evaluation failed");
}
_ => panic!("Expected boolean result, got: {}", evaluation_result),
}
}
#[test]
fn merge_secrets_schemas_unions() {
let schema_a = json!({
"type": "object",
"properties": {
"COIN_GECKO_API": { "type": "string", "minLength": 1 }
},
"required": ["COIN_GECKO_API"],
"additionalProperties": false
});
// Duplicate key with different constraint should be ignored (first wins)
let schema_b = json!({
"type": "object",
"properties": {
"COIN_GECKO_API": { "type": "string", "minLength": 999 },
"WEATHER_API": { "type": "string", "minLength": 1 }
},
"required": ["WEATHER_API"]
});
let merged = merge_secrets_schemas(vec![("cid_a".to_string(), schema_a), ("cid_b".to_string(), schema_b)])
.expect("merge");
let props = merged
.get("properties")
.and_then(|v| v.as_object())
.expect("properties object");
assert!(props.contains_key("COIN_GECKO_API"));
assert!(props.contains_key("WEATHER_API"));
// First schema wins for duplicate property definitions
assert_eq!(
props.get("COIN_GECKO_API").unwrap().get("minLength").unwrap(),
&json!(1)
);
let required = merged
.get("required")
.and_then(|v| v.as_array())
.expect("required array");
assert!(required.contains(&json!("COIN_GECKO_API")));
assert!(required.contains(&json!("WEATHER_API")));
assert!(merged.get("additionalProperties").is_none());
}
#[test]
fn merge_secrets_schemas_ignores_additional_properties_false() {
let schema_a = json!({
"type": "object",
"properties": { "A": { "type": "string" } },
"additionalProperties": true
});
let schema_b = json!({
"type": "object",
"properties": { "B": { "type": "string" } },
"additionalProperties": false
});
let merged = merge_secrets_schemas(vec![("cid_a".to_string(), schema_a), ("cid_b".to_string(), schema_b)])
.expect("merge");
assert!(merged.get("additionalProperties").is_none());
}
#[test]
fn merge_secrets_schemas_ignores_non_object_schema() {
let merged = merge_secrets_schemas(vec![("cid_bad".to_string(), json!(["nope"]))]).expect("merge");
assert!(merged.get("additionalProperties").is_none());
let props = merged
.get("properties")
.and_then(|v| v.as_object())
.expect("properties object");
assert!(props.is_empty());
}
#[test]
fn merge_secrets_schemas_ignores_invalid_properties_shape() {
let schema = json!({
"type": "object",
"properties": ["not", "an", "object"]
});
let merged = merge_secrets_schemas(vec![("cid_bad".to_string(), schema)]).expect("merge");
assert!(merged.get("additionalProperties").is_none());
let props = merged
.get("properties")
.and_then(|v| v.as_object())
.expect("properties object");
assert!(props.is_empty());
}
#[test]
fn merge_secrets_schemas_ignores_invalid_required() {
let schema_not_array = json!({
"type": "object",
"properties": {},
"required": "NOPE"
});
let merged = merge_secrets_schemas(vec![("cid_bad".to_string(), schema_not_array)]).expect("merge");
assert!(merged.get("additionalProperties").is_none());
assert!(merged.get("required").is_none());
let schema_non_string = json!({
"type": "object",
"properties": {},
"required": ["OK", 123]
});
let merged = merge_secrets_schemas(vec![("cid_bad2".to_string(), schema_non_string)]).expect("merge");
assert!(merged.get("additionalProperties").is_none());
assert!(merged.get("required").is_none());
}
#[test]
fn task_request_proof_cid_serialization() {
use serde_json;
let request = TaskRequest {
task_id: B256::ZERO,
intent: NewtonMessage::Intent {
from: Address::ZERO,
to: Address::ZERO,
value: U256::ZERO,
data: Bytes::default(),
chainId: U256::from(1),
functionSignature: Bytes::default(),
},
intent_signature: None,
policy_client: Address::ZERO,
wasm_args: None,
quorum_numbers: vec![0],
quorum_threshold_percentage: 40,
task_created_block: 100,
proof_cid: Some("bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi".to_string()),
initialization_timestamp: 0,
};
let json = serde_json::to_value(&request).unwrap();
assert_eq!(
json["proof_cid"],
"bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi"
);
// proof_cid: None should be omitted from JSON
let request_no_proof = TaskRequest {
proof_cid: None,
..request
};
let json2 = serde_json::to_value(&request_no_proof).unwrap();
assert!(json2.get("proof_cid").is_none());
// Deserialize without proof_cid field should default to None
let mut json3 = json.clone();
json3.as_object_mut().unwrap().remove("proof_cid");
let deserialized: TaskRequest = serde_json::from_value(json3).unwrap();
assert!(deserialized.proof_cid.is_none());
let mut json4 = json.clone();
json4
.as_object_mut()
.unwrap()
.insert("proofCid".to_string(), serde_json::json!("bafycamelcase"));
json4.as_object_mut().unwrap().remove("proof_cid");
let deserialized_camel: TaskRequest = serde_json::from_value(json4).unwrap();
assert_eq!(deserialized_camel.proof_cid.as_deref(), Some("bafycamelcase"));
}
#[test]
fn tls_proof_data_injected_into_rego_root_namespace() {
// additional_data (tls_proof) is merged at the root level in Rego,
// giving clean namespaces: data.tls_proof.*, data.wasm.*, data.privacy.*
let tls_proof = serde_json::json!({
"server_name": "api.twitter.com",
"verified": true,
"response_body": "{\"id\":\"123\",\"name\":\"test\"}",
"request_target": "/2/users/me"
});
let policy_data = serde_json::json!({
"params": {},
"wasm": { "some_key": "some_value" },
"tls_proof": tls_proof,
});
// Verify tls_proof is accessible at data.tls_proof (root level)
assert_eq!(policy_data["tls_proof"]["server_name"], "api.twitter.com");
assert_eq!(policy_data["tls_proof"]["verified"], true);
assert_eq!(policy_data["wasm"]["some_key"], "some_value");
}
}