use uuid::Uuid;
use std::{str::FromStr, sync::Arc};
use crate::{
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;
pub use crate::eval::{decode_calldata, parse_intent, serialize_sol_value, ParsedIntent};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskRequest {
pub task_id: TaskId,
pub intent: NewtonMessage::Intent,
pub intent_signature: Option<Bytes>,
pub policy_client: Address,
pub wasm_args: Option<Bytes>,
pub quorum_numbers: Vec<u8>,
pub quorum_threshold_percentage: u32,
pub task_created_block: u64,
pub initialization_timestamp: u64,
#[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),
}
}
}
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(())
}
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)
}
pub fn merge_task_policy_data(
policy_task_data: &NewtonMessage::PolicyTaskData,
) -> Result<serde_json::Value, RegoError> {
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(),
}
})
}
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)
}
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 {
continue;
};
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() {
if !merged_properties.contains_key(k) {
merged_properties.insert(k.clone(), v.clone());
}
}
} else if !props_val.is_null() {
continue;
}
}
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 {
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() {
continue;
}
}
}
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))
}
pub mod rpc {
#![cfg(feature = "rpc")]
use crate::{
common::intent::ParsedIntent,
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_rpc_provider::{get_provider, get_signer};
use regorus::extensions::PolicyDomainData;
use serde::{Deserialize, Serialize};
use std::str::FromStr;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PolicyEvaluationResult {
pub policy: String,
pub parsed_intent: ParsedIntent,
pub policy_params_and_data: serde_json::Value,
pub entrypoint: String,
pub result: regorus::Value,
pub expire_after: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResolvedPolicyInputs {
pub policy_config: INewtonPolicy::PolicyConfig,
pub entrypoint: String,
pub schema: serde_json::Value,
}
pub async fn parse_and_evaluate_task(
intent: &NewtonMessage::Intent,
policy_task_data: &NewtonMessage::PolicyTaskData,
rpc_url: &str,
fetcher: &dyn ObjectFetcher,
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, fetcher).await?;
evaluate_task_with_resolved_policy(intent, policy_task_data, resolved, domain_data, additional_data)
}
#[async_trait::async_trait]
pub trait ObjectFetcher: Send + Sync {
async fn get_object(&self, cid: &str) -> eyre::Result<Vec<u8>>;
}
pub async fn resolve_policy_inputs(
policy_address: Address,
policy_id: alloy::primitives::FixedBytes<32>,
rpc_url: &str,
fetcher: &dyn ObjectFetcher,
) -> 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(&schema_cid, fetcher).await?;
Ok(ResolvedPolicyInputs {
policy_config,
entrypoint,
schema,
})
}
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!({}));
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()))?;
tracing::info!("evaluation result: {}", result);
Ok(PolicyEvaluationResult {
policy,
parsed_intent,
policy_params_and_data,
entrypoint,
result,
expire_after,
})
}
async fn load_policy_schema(schema_cid: &str, fetcher: &dyn ObjectFetcher) -> Result<serde_json::Value, RegoError> {
let bytes = fetcher
.get_object(schema_cid)
.await
.map_err(|e| RegoError::FailedToFetchPolicySchemaJson(e.to_string()))?;
let schema_text =
String::from_utf8(bytes).map_err(|e| RegoError::FailedToDecodePolicySchemaJson(e.to_string()))?;
serde_json::from_str(&schema_text).map_err(|e| RegoError::FailedToDecodePolicySchemaJson(e.to_string()))
}
}
#[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;
sol! {
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_static(br#"{"enabled":true}"#),
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_static(br#"{"input":{"allow":true},"secrets":{}}"#),
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: newton_testing_utils::policy::TEST_POLICY_REGO
.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]), 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(); 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,
};
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 {}
}
}
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);
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);
for (i, input) in inputs.iter().enumerate() {
match input {
DynSolValue::Uint(_, bits) | DynSolValue::Int(_, bits) => {
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();
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.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"
});
let result = parse_intent(incomplete_json);
assert!(result.is_err());
}
#[test]
fn test_parse_intent_optional_chain_id() {
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);
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);
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);
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));
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() {
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);
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() {
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);
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() {
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() {
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", "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() {
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()));
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())
);
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"
);
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'"
);
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);
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);
}
#[tokio::test]
#[cfg(feature = "rpc")]
async fn test_parse_and_evaluate_task_success() {
let task = create_sample_task();
let intent_json = json!(task.intent);
let parsed_intent = parse_intent(intent_json).unwrap();
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);
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["input"]["allow"], true);
assert_eq!(merged_policy_data["secrets"], json!({}));
}
#[test]
fn test_intent_round_trip_alloy_serde_to_parsed_intent() {
use crate::common::intent::RawParsedIntent;
let intent = Intent {
from: "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266".parse().unwrap(),
to: "0x70997970c51812dc3a010c7d01b50e0d17dc79c8".parse().unwrap(),
value: U256::from(1000000000000000000u64), data: Bytes::new(), chainId: U256::ZERO, functionSignature: Bytes::new(), };
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(); let final_json: serde_json::Value =
serde_json::from_str(&final_json_str).expect("ParsedIntent JSON should be valid");
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"
);
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"); 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;
let mut task = create_sample_task();
task.intent.data = Bytes::from("invalid_data".as_bytes());
let intent_json = json!(task.intent);
let result = parse_intent(intent_json);
assert!(result.is_ok());
let parsed = result.unwrap();
assert!(parsed.data.is_some());
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_basic_policy() {
use crate::evaluate;
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.basic.allow";
let result = evaluate(
policy.to_string(),
policy_params_and_data,
parsed_intent,
vec![],
policy_rule,
None,
);
assert!(result.is_ok());
let evaluation_result = result.unwrap();
match evaluation_result {
regorus::Value::Bool(result) => {
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
});
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"));
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"
);
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());
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() {
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,
});
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");
}
}