1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct UserContext {
12 pub pubkey: String,
14
15 pub vault: VaultState,
17
18 pub preferences: UserPreferences,
20
21 pub recent_events: Vec<ContextEvent>,
23
24 pub agent_memory: HashMap<String, serde_json::Value>,
26
27 pub updated_at: i64,
29
30 pub version: u64,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct VaultState {
36 pub cred_balance: u64,
37 pub oxo_balance: u64,
38 pub total_staked: u64,
39 pub pending_yield: u64,
40 pub active_positions: u16,
41 pub snapshot_at: i64,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct UserPreferences {
47 pub auto_stake: bool,
49 pub default_duration_days: u16,
51 pub auto_compound: bool,
53 pub compound_threshold: u64,
55 pub risk_tolerance: String,
57 pub notifications: NotificationPrefs,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct NotificationPrefs {
63 pub on_capture: bool,
64 pub on_stake: bool,
65 pub on_yield: bool,
66 pub on_unlock: bool,
67 pub min_notify_amount: u64,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct ContextEvent {
73 pub event_type: String,
74 pub timestamp: i64,
75 pub summary: String,
76 pub amount: Option<u64>,
77}
78
79impl Default for UserPreferences {
80 fn default() -> Self {
81 Self {
82 auto_stake: true,
83 default_duration_days: 90,
84 auto_compound: true,
85 compound_threshold: 10_000_000_000, risk_tolerance: "balanced".to_string(),
87 notifications: NotificationPrefs {
88 on_capture: true,
89 on_stake: false,
90 on_yield: true,
91 on_unlock: true,
92 min_notify_amount: 1_000_000_000, },
94 }
95 }
96}
97
98pub trait StateStore {
101 fn context_fetch(&self, user_pubkey: &str) -> Result<UserContext, StateError>;
103
104 fn context_push(&self, context: UserContext) -> Result<(), StateError>;
106
107 fn context_batch_fetch(&self, user_pubkeys: &[String]) -> Result<Vec<UserContext>, StateError>;
109
110 fn context_patch(
112 &self,
113 user_pubkey: &str,
114 path: &str,
115 value: serde_json::Value,
116 ) -> Result<(), StateError>;
117
118 fn push_event(&self, user_pubkey: &str, event: ContextEvent) -> Result<(), StateError>;
120
121 fn get_or_init(&self, user_pubkey: &str) -> Result<UserContext, StateError>;
123}
124
125#[derive(Debug, Clone)]
126pub enum StateError {
127 NotFound,
129 VersionConflict { expected: u64, actual: u64 },
131 StorageError(String),
133 SerializationError(String),
135 Timeout,
137}
138
139#[derive(Debug, Clone)]
141pub struct DynamoStateConfig {
142 pub table_name: String,
143 pub region: String,
144 pub vault_cache_ttl: u32,
146 pub max_events: usize,
148}
149
150impl Default for DynamoStateConfig {
151 fn default() -> Self {
152 Self {
153 table_name: "loop-agent-state".to_string(),
154 region: "us-east-1".to_string(),
155 vault_cache_ttl: 60, max_events: 10,
157 }
158 }
159}
160
161pub fn init_user_context(pubkey: &str) -> UserContext {
187 UserContext {
188 pubkey: pubkey.to_string(),
189 vault: VaultState {
190 cred_balance: 0,
191 oxo_balance: 0,
192 total_staked: 0,
193 pending_yield: 0,
194 active_positions: 0,
195 snapshot_at: 0,
196 },
197 preferences: UserPreferences::default(),
198 recent_events: Vec::new(),
199 agent_memory: HashMap::new(),
200 updated_at: chrono::Utc::now().timestamp(),
201 version: 1,
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208
209 #[test]
210 fn default_preferences_sensible() {
211 let prefs = UserPreferences::default();
212 assert!(prefs.auto_stake);
213 assert!(prefs.auto_compound);
214 assert_eq!(prefs.default_duration_days, 90);
215 }
216
217 #[test]
218 fn context_serializes() {
219 let ctx = init_user_context("5A7ETEST");
220 let json = serde_json::to_string(&ctx).unwrap();
221 let parsed: UserContext = serde_json::from_str(&json).unwrap();
222 assert_eq!(parsed.pubkey, "5A7ETEST");
223 }
224}