loop-agent-sdk 0.1.0

Trustless agent SDK for Loop Protocol — intent-based execution on Solana.
Documentation
//! Loop Agent SDK - Perception Interface
//! 
//! Event-driven triggers that wake up Lambda agents.
//! Uses AWS EventBridge as the "central nervous system."

use serde::{Deserialize, Serialize};
use solana_sdk::pubkey::Pubkey;

/// Event types that can trigger agent execution
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", content = "payload")]
pub enum PerceptionEvent {
    /// Transaction detected from POS integration (Square/Stripe/Clover)
    TransactionDetected(TransactionEvent),
    
    /// User entered geofenced merchant location
    LocationHit(LocationEvent),
    
    /// ZK proof submitted by user (manual capture)
    ProofSubmitted(ProofEvent),
    
    /// Staking position approaching unlock
    PositionUnlocking(UnlockEvent),
    
    /// Yield threshold reached (for auto-compound)
    YieldThreshold(YieldEvent),
    
    /// Scheduled optimization check
    ScheduledCheck(ScheduledEvent),
}

/// Transaction from POS webhook
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransactionEvent {
    /// Source processor
    pub processor: Processor,
    /// Processor's transaction ID
    pub processor_txn_id: String,
    /// Merchant pubkey in Loop registry
    pub merchant_id: String,
    /// Location ID if multi-location merchant
    pub location_id: Option<String>,
    /// Amount in cents
    pub amount_cents: u64,
    /// Card last 4 for matching
    pub card_last4: String,
    /// Card brand
    pub card_brand: String,
    /// Card fingerprint for dedup
    pub card_fingerprint: String,
    /// When transaction occurred
    pub occurred_at: i64,
    /// Raw webhook payload for audit
    pub raw_payload: serde_json::Value,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum Processor {
    Square,
    Stripe,
    Clover,
    Fidel,
}

/// User location event from mobile app
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LocationEvent {
    /// User's pubkey
    pub user_pubkey: String,
    /// Merchant they're near
    pub merchant_id: String,
    /// Location coordinates
    pub latitude: f64,
    pub longitude: f64,
    /// Accuracy in meters
    pub accuracy_m: f32,
    /// Entry or exit
    pub event_type: GeoEventType,
    /// Timestamp
    pub timestamp: i64,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum GeoEventType {
    Enter,
    Exit,
    Dwell,  // Stayed > 5 minutes
}

/// User-submitted proof event
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProofEvent {
    pub user_pubkey: String,
    pub proof_type: String,  // "reclaim", "zktls", etc.
    pub proof_data: String,  // Base64 encoded
    pub claimed_amount_cents: u64,
    pub claimed_merchant: Option<String>,
    pub submitted_at: i64,
}

/// Staking position unlocking soon
#[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,
    /// Hours until unlock
    pub hours_until_unlock: u16,
}

/// Yield threshold reached
#[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>,
}

/// Scheduled check (cron-triggered)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScheduledEvent {
    pub check_type: ScheduledCheckType,
    pub batch_id: String,
    pub user_pubkeys: Vec<String>,  // Batch of users to process
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum ScheduledCheckType {
    /// Daily yield optimization check
    DailyOptimization,
    /// Weekly position health check
    WeeklyHealth,
    /// Process pending captures
    PendingCaptures,
}

/// EventBridge rule configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EventBridgeRule {
    pub name: String,
    pub event_pattern: serde_json::Value,
    pub target_lambda: String,
    pub description: String,
}

/// Generate EventBridge rules for Loop agent
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(),
        },
    ]
}

/// Trait for perception handlers
/// Implement this in your Lambda to handle events
pub trait PerceptionHandler {
    /// Handle incoming perception event
    /// Returns true if action was taken
    fn on_event(&self, event: PerceptionEvent) -> Result<bool, PerceptionError>;
    
    /// Filter events before processing (cost optimization)
    fn should_process(&self, event: &PerceptionEvent) -> bool {
        true  // Default: process all
    }
}

#[derive(Debug, Clone)]
pub enum PerceptionError {
    /// Event data malformed
    InvalidEvent(String),
    /// User not found in system
    UserNotFound,
    /// Merchant not found
    MerchantNotFound,
    /// Duplicate event (already processed)
    DuplicateEvent,
    /// Rate limited
    RateLimited,
    /// Internal error
    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"),
        }
    }
}