use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserContext {
pub pubkey: String,
pub vault: VaultState,
pub preferences: UserPreferences,
pub recent_events: Vec<ContextEvent>,
pub agent_memory: HashMap<String, serde_json::Value>,
pub updated_at: i64,
pub version: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VaultState {
pub cred_balance: u64,
pub oxo_balance: u64,
pub total_staked: u64,
pub pending_yield: u64,
pub active_positions: u16,
pub snapshot_at: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserPreferences {
pub auto_stake: bool,
pub default_duration_days: u16,
pub auto_compound: bool,
pub compound_threshold: u64,
pub risk_tolerance: String,
pub notifications: NotificationPrefs,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NotificationPrefs {
pub on_capture: bool,
pub on_stake: bool,
pub on_yield: bool,
pub on_unlock: bool,
pub min_notify_amount: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContextEvent {
pub event_type: String,
pub timestamp: i64,
pub summary: String,
pub amount: Option<u64>,
}
impl Default for UserPreferences {
fn default() -> Self {
Self {
auto_stake: true,
default_duration_days: 90,
auto_compound: true,
compound_threshold: 10_000_000_000, risk_tolerance: "balanced".to_string(),
notifications: NotificationPrefs {
on_capture: true,
on_stake: false,
on_yield: true,
on_unlock: true,
min_notify_amount: 1_000_000_000, },
}
}
}
pub trait StateStore {
fn context_fetch(&self, user_pubkey: &str) -> Result<UserContext, StateError>;
fn context_push(&self, context: UserContext) -> Result<(), StateError>;
fn context_batch_fetch(&self, user_pubkeys: &[String]) -> Result<Vec<UserContext>, StateError>;
fn context_patch(
&self,
user_pubkey: &str,
path: &str,
value: serde_json::Value,
) -> Result<(), StateError>;
fn push_event(&self, user_pubkey: &str, event: ContextEvent) -> Result<(), StateError>;
fn get_or_init(&self, user_pubkey: &str) -> Result<UserContext, StateError>;
}
#[derive(Debug, Clone)]
pub enum StateError {
NotFound,
VersionConflict { expected: u64, actual: u64 },
StorageError(String),
SerializationError(String),
Timeout,
}
#[derive(Debug, Clone)]
pub struct DynamoStateConfig {
pub table_name: String,
pub region: String,
pub vault_cache_ttl: u32,
pub max_events: usize,
}
impl Default for DynamoStateConfig {
fn default() -> Self {
Self {
table_name: "loop-agent-state".to_string(),
region: "us-east-1".to_string(),
vault_cache_ttl: 60, max_events: 10,
}
}
}
pub fn init_user_context(pubkey: &str) -> UserContext {
UserContext {
pubkey: pubkey.to_string(),
vault: VaultState {
cred_balance: 0,
oxo_balance: 0,
total_staked: 0,
pending_yield: 0,
active_positions: 0,
snapshot_at: 0,
},
preferences: UserPreferences::default(),
recent_events: Vec::new(),
agent_memory: HashMap::new(),
updated_at: chrono::Utc::now().timestamp(),
version: 1,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_preferences_sensible() {
let prefs = UserPreferences::default();
assert!(prefs.auto_stake);
assert!(prefs.auto_compound);
assert_eq!(prefs.default_duration_days, 90);
}
#[test]
fn context_serializes() {
let ctx = init_user_context("5A7ETEST");
let json = serde_json::to_string(&ctx).unwrap();
let parsed: UserContext = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.pubkey, "5A7ETEST");
}
}