1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5#[serde(rename_all = "snake_case")]
6pub enum PolicyAction {
7 Deny,
8}
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12#[serde(tag = "type", rename_all = "snake_case")]
13pub enum PolicyRule {
14 AllowedChains { chain_ids: Vec<String> },
16
17 ExpiresAt { timestamp: String },
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct Policy {
24 pub id: String,
25 pub name: String,
26 pub version: u32,
27 pub created_at: String,
28 pub rules: Vec<PolicyRule>,
29 #[serde(skip_serializing_if = "Option::is_none")]
31 pub executable: Option<String>,
32 #[serde(skip_serializing_if = "Option::is_none")]
34 pub config: Option<serde_json::Value>,
35 pub action: PolicyAction,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct PolicyContext {
41 pub chain_id: String,
42 pub wallet_id: String,
43 pub api_key_id: String,
44 pub transaction: TransactionContext,
45 pub spending: SpendingContext,
46 pub timestamp: String,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct TransactionContext {
52 #[serde(skip_serializing_if = "Option::is_none")]
54 pub to: Option<String>,
55 #[serde(skip_serializing_if = "Option::is_none")]
57 pub value: Option<String>,
58 pub raw_hex: String,
60 #[serde(skip_serializing_if = "Option::is_none")]
62 pub data: Option<String>,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct SpendingContext {
68 pub daily_total: String,
70 pub date: String,
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct PolicyResult {
77 pub allow: bool,
78 #[serde(skip_serializing_if = "Option::is_none")]
79 pub reason: Option<String>,
80 #[serde(skip_serializing_if = "Option::is_none")]
82 pub policy_id: Option<String>,
83}
84
85impl PolicyResult {
86 pub fn allowed() -> Self {
87 Self {
88 allow: true,
89 reason: None,
90 policy_id: None,
91 }
92 }
93
94 pub fn denied(policy_id: impl Into<String>, reason: impl Into<String>) -> Self {
95 Self {
96 allow: false,
97 reason: Some(reason.into()),
98 policy_id: Some(policy_id.into()),
99 }
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106
107 #[test]
108 fn test_policy_rule_serde_allowed_chains() {
109 let rule = PolicyRule::AllowedChains {
110 chain_ids: vec!["eip155:8453".into(), "eip155:84532".into()],
111 };
112 let json = serde_json::to_value(&rule).unwrap();
113 assert_eq!(json["type"], "allowed_chains");
114 assert_eq!(json["chain_ids"][0], "eip155:8453");
115
116 let deserialized: PolicyRule = serde_json::from_value(json).unwrap();
117 assert_eq!(deserialized, rule);
118 }
119
120 #[test]
121 fn test_policy_rule_serde_expires_at() {
122 let rule = PolicyRule::ExpiresAt {
123 timestamp: "2026-04-01T00:00:00Z".to_string(),
124 };
125 let json = serde_json::to_value(&rule).unwrap();
126 assert_eq!(json["type"], "expires_at");
127 assert_eq!(json["timestamp"], "2026-04-01T00:00:00Z");
128
129 let deserialized: PolicyRule = serde_json::from_value(json).unwrap();
130 assert_eq!(deserialized, rule);
131 }
132
133 #[test]
134 fn test_policy_serde_roundtrip() {
135 let policy = Policy {
136 id: "base-agent-limits".into(),
137 name: "Base Agent Safety Limits".into(),
138 version: 1,
139 created_at: "2026-03-22T10:00:00Z".into(),
140 rules: vec![
141 PolicyRule::AllowedChains {
142 chain_ids: vec!["eip155:8453".into()],
143 },
144 PolicyRule::ExpiresAt {
145 timestamp: "2026-12-31T23:59:59Z".into(),
146 },
147 ],
148 executable: None,
149 config: None,
150 action: PolicyAction::Deny,
151 };
152
153 let json = serde_json::to_string(&policy).unwrap();
154 let deserialized: Policy = serde_json::from_str(&json).unwrap();
155 assert_eq!(deserialized.id, "base-agent-limits");
156 assert_eq!(deserialized.rules.len(), 2);
157 assert!(deserialized.executable.is_none());
158 }
159
160 #[test]
161 fn test_policy_with_executable() {
162 let json = r#"{
163 "id": "sim-policy",
164 "name": "Simulation Policy",
165 "version": 1,
166 "created_at": "2026-03-22T10:00:00Z",
167 "rules": [],
168 "executable": "/usr/local/bin/simulate-tx",
169 "config": {"rpc": "https://mainnet.base.org"},
170 "action": "deny"
171 }"#;
172 let policy: Policy = serde_json::from_str(json).unwrap();
173 assert_eq!(policy.executable.unwrap(), "/usr/local/bin/simulate-tx");
174 assert!(policy.config.is_some());
175 }
176
177 #[test]
178 fn test_policy_context_serde() {
179 let ctx = PolicyContext {
180 chain_id: "eip155:8453".into(),
181 wallet_id: "3198bc9c-6672-5ab3-d995-4942343ae5b6".into(),
182 api_key_id: "7a2f1b3c-4d5e-6f7a-8b9c-0d1e2f3a4b5c".into(),
183 transaction: TransactionContext {
184 to: Some("0x742d35Cc6634C0532925a3b844Bc9e7595f2bD0C".into()),
185 value: Some("100000000000000000".into()),
186 raw_hex: "0x02f8...".into(),
187 data: None,
188 },
189 spending: SpendingContext {
190 daily_total: "50000000000000000".into(),
191 date: "2026-03-22".into(),
192 },
193 timestamp: "2026-03-22T10:35:22Z".into(),
194 };
195
196 let json = serde_json::to_string(&ctx).unwrap();
197 let deserialized: PolicyContext = serde_json::from_str(&json).unwrap();
198 assert_eq!(deserialized.chain_id, "eip155:8453");
199 assert_eq!(
200 deserialized.transaction.to.unwrap(),
201 "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD0C"
202 );
203 assert!(!json.contains("\"data\""));
205 }
206
207 #[test]
208 fn test_policy_result_allowed() {
209 let result = PolicyResult::allowed();
210 assert!(result.allow);
211 assert!(result.reason.is_none());
212 assert!(result.policy_id.is_none());
213
214 let json = serde_json::to_value(&result).unwrap();
215 assert_eq!(json["allow"], true);
216 assert!(!json.as_object().unwrap().contains_key("reason"));
217 }
218
219 #[test]
220 fn test_policy_result_denied() {
221 let result = PolicyResult::denied(
222 "spending-limit",
223 "Daily spending limit exceeded: 0.95 / 1.0 ETH",
224 );
225 assert!(!result.allow);
226 assert_eq!(
227 result.reason.as_deref(),
228 Some("Daily spending limit exceeded: 0.95 / 1.0 ETH")
229 );
230 assert_eq!(result.policy_id.as_deref(), Some("spending-limit"));
231 }
232
233 #[test]
234 fn test_policy_action_serde() {
235 let action = PolicyAction::Deny;
236 let json = serde_json::to_value(&action).unwrap();
237 assert_eq!(json, "deny");
238
239 let deserialized: PolicyAction = serde_json::from_value(json).unwrap();
240 assert_eq!(deserialized, PolicyAction::Deny);
241 }
242}