use serde::{Deserialize, Serialize};
use solana_sdk::pubkey::Pubkey;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", content = "payload")]
pub enum PerceptionEvent {
TransactionDetected(TransactionEvent),
LocationHit(LocationEvent),
ProofSubmitted(ProofEvent),
PositionUnlocking(UnlockEvent),
YieldThreshold(YieldEvent),
ScheduledCheck(ScheduledEvent),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransactionEvent {
pub processor: Processor,
pub processor_txn_id: String,
pub merchant_id: String,
pub location_id: Option<String>,
pub amount_cents: u64,
pub card_last4: String,
pub card_brand: String,
pub card_fingerprint: String,
pub occurred_at: i64,
pub raw_payload: serde_json::Value,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum Processor {
Square,
Stripe,
Clover,
Fidel,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LocationEvent {
pub user_pubkey: String,
pub merchant_id: String,
pub latitude: f64,
pub longitude: f64,
pub accuracy_m: f32,
pub event_type: GeoEventType,
pub timestamp: i64,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum GeoEventType {
Enter,
Exit,
Dwell, }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProofEvent {
pub user_pubkey: String,
pub proof_type: String, pub proof_data: String, pub claimed_amount_cents: u64,
pub claimed_merchant: Option<String>,
pub submitted_at: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UnlockEvent {
pub user_pubkey: String,
pub position_id: String,
pub unlock_time: i64,
pub amount: u64,
pub accrued_yield: u64,
pub hours_until_unlock: u16,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct YieldEvent {
pub user_pubkey: String,
pub total_pending_yield: u64,
pub threshold_reached: u64,
pub positions: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScheduledEvent {
pub check_type: ScheduledCheckType,
pub batch_id: String,
pub user_pubkeys: Vec<String>, }
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum ScheduledCheckType {
DailyOptimization,
WeeklyHealth,
PendingCaptures,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EventBridgeRule {
pub name: String,
pub event_pattern: serde_json::Value,
pub target_lambda: String,
pub description: String,
}
pub fn generate_eventbridge_rules(lambda_arn: &str) -> Vec<EventBridgeRule> {
vec![
EventBridgeRule {
name: "loop-transaction-detected".to_string(),
event_pattern: serde_json::json!({
"source": ["loop.pos"],
"detail-type": ["TransactionDetected"]
}),
target_lambda: lambda_arn.to_string(),
description: "Trigger agent on POS transaction webhook".to_string(),
},
EventBridgeRule {
name: "loop-location-hit".to_string(),
event_pattern: serde_json::json!({
"source": ["loop.mobile"],
"detail-type": ["LocationHit"]
}),
target_lambda: lambda_arn.to_string(),
description: "Trigger agent on user geofence entry".to_string(),
},
EventBridgeRule {
name: "loop-proof-submitted".to_string(),
event_pattern: serde_json::json!({
"source": ["loop.capture"],
"detail-type": ["ProofSubmitted"]
}),
target_lambda: lambda_arn.to_string(),
description: "Trigger agent on ZK proof submission".to_string(),
},
EventBridgeRule {
name: "loop-position-unlocking".to_string(),
event_pattern: serde_json::json!({
"source": ["loop.staking"],
"detail-type": ["PositionUnlocking"]
}),
target_lambda: lambda_arn.to_string(),
description: "Notify agent 24h before position unlock".to_string(),
},
EventBridgeRule {
name: "loop-yield-threshold".to_string(),
event_pattern: serde_json::json!({
"source": ["loop.staking"],
"detail-type": ["YieldThreshold"]
}),
target_lambda: lambda_arn.to_string(),
description: "Trigger auto-compound when yield threshold reached".to_string(),
},
EventBridgeRule {
name: "loop-daily-optimization".to_string(),
event_pattern: serde_json::json!({
"source": ["aws.scheduler"],
"detail-type": ["Scheduled Event"],
"detail": {
"check_type": ["DailyOptimization"]
}
}),
target_lambda: lambda_arn.to_string(),
description: "Daily yield optimization batch (6 AM UTC)".to_string(),
},
]
}
pub trait PerceptionHandler {
fn on_event(&self, event: PerceptionEvent) -> Result<bool, PerceptionError>;
fn should_process(&self, event: &PerceptionEvent) -> bool {
true }
}
#[derive(Debug, Clone)]
pub enum PerceptionError {
InvalidEvent(String),
UserNotFound,
MerchantNotFound,
DuplicateEvent,
RateLimited,
Internal(String),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn can_deserialize_transaction_event() {
let json = r#"{
"type": "TransactionDetected",
"payload": {
"processor": "Square",
"processor_txn_id": "abc123",
"merchant_id": "5A7E...YKTZ",
"location_id": null,
"amount_cents": 1599,
"card_last4": "1234",
"card_brand": "VISA",
"card_fingerprint": "fp_abc123",
"occurred_at": 1711468800,
"raw_payload": {}
}
}"#;
let event: PerceptionEvent = serde_json::from_str(json).unwrap();
match event {
PerceptionEvent::TransactionDetected(tx) => {
assert_eq!(tx.amount_cents, 1599);
}
_ => panic!("Wrong event type"),
}
}
}