1use serde::{Deserialize, Serialize};
7use solana_sdk::pubkey::Pubkey;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11#[serde(tag = "type", content = "payload")]
12pub enum PerceptionEvent {
13 TransactionDetected(TransactionEvent),
15
16 LocationHit(LocationEvent),
18
19 ProofSubmitted(ProofEvent),
21
22 PositionUnlocking(UnlockEvent),
24
25 YieldThreshold(YieldEvent),
27
28 ScheduledCheck(ScheduledEvent),
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct TransactionEvent {
35 pub processor: Processor,
37 pub processor_txn_id: String,
39 pub merchant_id: String,
41 pub location_id: Option<String>,
43 pub amount_cents: u64,
45 pub card_last4: String,
47 pub card_brand: String,
49 pub card_fingerprint: String,
51 pub occurred_at: i64,
53 pub raw_payload: serde_json::Value,
55}
56
57#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
58pub enum Processor {
59 Square,
60 Stripe,
61 Clover,
62 Fidel,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct LocationEvent {
68 pub user_pubkey: String,
70 pub merchant_id: String,
72 pub latitude: f64,
74 pub longitude: f64,
75 pub accuracy_m: f32,
77 pub event_type: GeoEventType,
79 pub timestamp: i64,
81}
82
83#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
84pub enum GeoEventType {
85 Enter,
86 Exit,
87 Dwell, }
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct ProofEvent {
93 pub user_pubkey: String,
94 pub proof_type: String, pub proof_data: String, pub claimed_amount_cents: u64,
97 pub claimed_merchant: Option<String>,
98 pub submitted_at: i64,
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct UnlockEvent {
104 pub user_pubkey: String,
105 pub position_id: String,
106 pub unlock_time: i64,
107 pub amount: u64,
108 pub accrued_yield: u64,
109 pub hours_until_unlock: u16,
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct YieldEvent {
116 pub user_pubkey: String,
117 pub total_pending_yield: u64,
118 pub threshold_reached: u64,
119 pub positions: Vec<String>,
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct ScheduledEvent {
125 pub check_type: ScheduledCheckType,
126 pub batch_id: String,
127 pub user_pubkeys: Vec<String>, }
129
130#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
131pub enum ScheduledCheckType {
132 DailyOptimization,
134 WeeklyHealth,
136 PendingCaptures,
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct EventBridgeRule {
143 pub name: String,
144 pub event_pattern: serde_json::Value,
145 pub target_lambda: String,
146 pub description: String,
147}
148
149pub fn generate_eventbridge_rules(lambda_arn: &str) -> Vec<EventBridgeRule> {
151 vec![
152 EventBridgeRule {
153 name: "loop-transaction-detected".to_string(),
154 event_pattern: serde_json::json!({
155 "source": ["loop.pos"],
156 "detail-type": ["TransactionDetected"]
157 }),
158 target_lambda: lambda_arn.to_string(),
159 description: "Trigger agent on POS transaction webhook".to_string(),
160 },
161 EventBridgeRule {
162 name: "loop-location-hit".to_string(),
163 event_pattern: serde_json::json!({
164 "source": ["loop.mobile"],
165 "detail-type": ["LocationHit"]
166 }),
167 target_lambda: lambda_arn.to_string(),
168 description: "Trigger agent on user geofence entry".to_string(),
169 },
170 EventBridgeRule {
171 name: "loop-proof-submitted".to_string(),
172 event_pattern: serde_json::json!({
173 "source": ["loop.capture"],
174 "detail-type": ["ProofSubmitted"]
175 }),
176 target_lambda: lambda_arn.to_string(),
177 description: "Trigger agent on ZK proof submission".to_string(),
178 },
179 EventBridgeRule {
180 name: "loop-position-unlocking".to_string(),
181 event_pattern: serde_json::json!({
182 "source": ["loop.staking"],
183 "detail-type": ["PositionUnlocking"]
184 }),
185 target_lambda: lambda_arn.to_string(),
186 description: "Notify agent 24h before position unlock".to_string(),
187 },
188 EventBridgeRule {
189 name: "loop-yield-threshold".to_string(),
190 event_pattern: serde_json::json!({
191 "source": ["loop.staking"],
192 "detail-type": ["YieldThreshold"]
193 }),
194 target_lambda: lambda_arn.to_string(),
195 description: "Trigger auto-compound when yield threshold reached".to_string(),
196 },
197 EventBridgeRule {
198 name: "loop-daily-optimization".to_string(),
199 event_pattern: serde_json::json!({
200 "source": ["aws.scheduler"],
201 "detail-type": ["Scheduled Event"],
202 "detail": {
203 "check_type": ["DailyOptimization"]
204 }
205 }),
206 target_lambda: lambda_arn.to_string(),
207 description: "Daily yield optimization batch (6 AM UTC)".to_string(),
208 },
209 ]
210}
211
212pub trait PerceptionHandler {
215 fn on_event(&self, event: PerceptionEvent) -> Result<bool, PerceptionError>;
218
219 fn should_process(&self, event: &PerceptionEvent) -> bool {
221 true }
223}
224
225#[derive(Debug, Clone)]
226pub enum PerceptionError {
227 InvalidEvent(String),
229 UserNotFound,
231 MerchantNotFound,
233 DuplicateEvent,
235 RateLimited,
237 Internal(String),
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244
245 #[test]
246 fn can_deserialize_transaction_event() {
247 let json = r#"{
248 "type": "TransactionDetected",
249 "payload": {
250 "processor": "Square",
251 "processor_txn_id": "abc123",
252 "merchant_id": "5A7E...YKTZ",
253 "location_id": null,
254 "amount_cents": 1599,
255 "card_last4": "1234",
256 "card_brand": "VISA",
257 "card_fingerprint": "fp_abc123",
258 "occurred_at": 1711468800,
259 "raw_payload": {}
260 }
261 }"#;
262
263 let event: PerceptionEvent = serde_json::from_str(json).unwrap();
264 match event {
265 PerceptionEvent::TransactionDetected(tx) => {
266 assert_eq!(tx.amount_cents, 1599);
267 }
268 _ => panic!("Wrong event type"),
269 }
270 }
271}