loop-agent-sdk 0.1.0

Trustless agent SDK for Loop Protocol — intent-based execution on Solana.
Documentation
//! Loop Agent SDK - Action Interface
//! 
//! Intent-based execution layer. Agents request actions, smart contracts execute.
//! The agent NEVER holds keys or directly moves funds.

use solana_sdk::pubkey::Pubkey;
use solana_sdk::signature::Signature;
use borsh::{BorshDeserialize, BorshSerialize};
use serde::{Serialize, Deserialize};

/// Serde helper for Signature (base58 string)
mod signature_serde {
    use solana_sdk::signature::Signature;
    use serde::{Serializer, Deserializer, Deserialize};
    use std::str::FromStr;
    
    pub fn serialize<S>(sig: &Signature, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        serializer.serialize_str(&sig.to_string())
    }
    
    pub fn deserialize<'de, D>(deserializer: D) -> Result<Signature, D::Error>
    where
        D: Deserializer<'de>,
    {
        let s = String::deserialize(deserializer)?;
        Signature::from_str(&s).map_err(serde::de::Error::custom)
    }
}

/// Session key with scoped permissions
/// Similar to EIP-7702 / Solana session keys
#[derive(Debug, Clone, BorshSerialize, BorshDeserialize)]
pub struct SessionKey {
    /// The session key pubkey (not the user's main key)
    pub key: Pubkey,
    /// Scoped permissions granted
    pub scopes: Vec<PermissionScope>,
    /// Expiration timestamp (Unix seconds)
    pub expires_at: i64,
    /// User vault this session is authorized for
    pub vault: Pubkey,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
pub enum PermissionScope {
    /// Read vault state, positions, rates
    Read,
    /// Submit purchase proofs to capture value
    Capture,
    /// Stake/unstake Cred, claim yield
    Stake,
}

/// Zero-knowledge proof for purchase verification
#[derive(Debug, Clone, BorshSerialize, BorshDeserialize)]
pub struct ZkProof {
    pub proof_type: ProofType,
    /// Base64-encoded proof payload
    pub data: Vec<u8>,
    /// When the purchase occurred
    pub timestamp: i64,
    /// Purchase amount in cents
    pub amount_cents: u64,
    /// Optional: card fingerprint for dedup
    pub card_fingerprint: Option<String>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
pub enum ProofType {
    /// Reclaim Protocol ZK proof
    Reclaim,
    /// zkTLS proof (e.g., from bank statement)
    ZkTls,
    /// Fidel card-linked transaction
    Fidel,
    /// Square POS webhook (trusted merchant)
    SquarePos,
    /// Stripe Connect (trusted merchant)
    StripeConnect,
}

/// Result of a capture operation
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CaptureResult {
    /// Transaction signature on Solana (base58 string)
    #[serde(with = "signature_serde")]
    pub signature: Signature,
    /// Total Cred minted
    pub cred_minted: u64,
    /// User's share (80%)
    pub user_amount: u64,
    /// Treasury share (14%)
    pub treasury_amount: u64,
    /// Staker pool share (6%)
    pub staker_amount: u64,
    /// Merchant that received attribution
    pub merchant: Pubkey,
}

/// Staking position details
#[derive(Debug, Clone, BorshSerialize, BorshDeserialize)]
pub struct StakingPosition {
    pub position_id: Pubkey,
    pub amount: u64,
    pub start_time: i64,
    pub duration_days: u16,
    pub unlock_time: i64,
    pub apy_bps: u16,  // Basis points (1000 = 10%)
    pub auto_compound: bool,
    pub accrued_yield: u64,
}

/// Yield optimization recommendation
#[derive(Debug, Clone, BorshSerialize, BorshDeserialize)]
pub struct YieldRecommendation {
    pub current_apy: u16,
    pub recommended_action: RecommendedAction,
    pub projected_apy: u16,
    pub reasoning: String,
}

#[derive(Debug, Clone, BorshSerialize, BorshDeserialize)]
pub enum RecommendedAction {
    /// Keep current positions
    Hold,
    /// Stake idle Cred
    Stake { amount: u64, duration_days: u16 },
    /// Consolidate positions
    Consolidate { from_positions: Vec<Pubkey> },
    /// Extend existing stake for better rate
    Extend { position: Pubkey, additional_days: u16 },
}

/// The Action Interface trait
/// 
/// Implementations handle the actual Solana transaction building.
/// Lambda invokes these, but the SESSION KEY (not Lambda) signs.
pub trait VaultAction {
    /// Capture value from a verified purchase
    /// 
    /// # Security
    /// - Requires `Capture` scope in session key
    /// - Contract enforces 80/14/6 split
    /// - Agent cannot modify distribution
    fn capture_value(
        &self,
        session: &SessionKey,
        merchant_id: Pubkey,
        proof: ZkProof,
    ) -> Result<CaptureResult, ActionError>;

    /// Stake Cred for yield
    /// 
    /// # Security
    /// - Requires `Stake` scope in session key
    /// - Funds move from user vault to staking pool
    /// - Position NFT minted to user
    fn stake_cred(
        &self,
        session: &SessionKey,
        amount: u64,
        duration_days: u16,
        auto_compound: bool,
    ) -> Result<StakingPosition, ActionError>;

    /// Claim yield from positions
    /// 
    /// # Security
    /// - Requires `Stake` scope
    /// - Can only claim to user's own vault
    fn claim_yield(
        &self,
        session: &SessionKey,
        position_id: Option<Pubkey>,  // None = all positions
        restake: bool,
    ) -> Result<u64, ActionError>;  // Returns amount claimed

    /// Get optimization recommendation (read-only)
    /// 
    /// # Security
    /// - Requires `Read` scope
    /// - No state modification
    fn request_yield_optimization(
        &self,
        session: &SessionKey,
        risk_tolerance: RiskTolerance,
    ) -> Result<YieldRecommendation, ActionError>;
}

#[derive(Debug, Clone, Copy)]
pub enum RiskTolerance {
    /// Shorter locks, lower yield, more liquidity
    Conservative,
    /// Balanced approach
    Balanced,
    /// Longer locks, higher yield
    Aggressive,
}

#[derive(Debug, Clone)]
pub enum ActionError {
    /// Session key expired or invalid
    InvalidSession,
    /// Session key doesn't have required scope
    InsufficientScope(PermissionScope),
    /// Proof verification failed
    InvalidProof(String),
    /// Merchant not registered
    MerchantNotFound,
    /// Insufficient balance for operation
    InsufficientBalance { required: u64, available: u64 },
    /// Position is still locked
    PositionLocked { unlock_time: i64 },
    /// Solana transaction failed
    TransactionFailed(String),
    /// Rate limiting
    RateLimited { retry_after_ms: u64 },
}

/// Current yield rates by duration
pub const YIELD_RATES_BPS: [(u16, u16); 4] = [
    (30, 800),    // 30 days = 8% APY
    (90, 1200),   // 90 days = 12% APY
    (180, 1600),  // 180 days = 16% APY
    (365, 2000),  // 365 days = 20% APY
];

/// Distribution policy (enforced by smart contract, not agent)
pub const DISTRIBUTION: Distribution = Distribution {
    user_bps: 8000,      // 80%
    treasury_bps: 1400,  // 14%
    staker_bps: 600,     // 6%
};

pub struct Distribution {
    pub user_bps: u16,
    pub treasury_bps: u16,
    pub staker_bps: u16,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn distribution_adds_to_100() {
        assert_eq!(
            DISTRIBUTION.user_bps + DISTRIBUTION.treasury_bps + DISTRIBUTION.staker_bps,
            10000
        );
    }

    #[test]
    fn yield_rates_increase_with_duration() {
        for i in 1..YIELD_RATES_BPS.len() {
            assert!(YIELD_RATES_BPS[i].1 > YIELD_RATES_BPS[i-1].1);
        }
    }
}