Skip to main content

loop_agent_sdk/
action.rs

1//! Loop Agent SDK - Action Interface
2//! 
3//! Intent-based execution layer. Agents request actions, smart contracts execute.
4//! The agent NEVER holds keys or directly moves funds.
5
6use solana_sdk::pubkey::Pubkey;
7use solana_sdk::signature::Signature;
8use borsh::{BorshDeserialize, BorshSerialize};
9use serde::{Serialize, Deserialize};
10
11/// Serde helper for Signature (base58 string)
12mod signature_serde {
13    use solana_sdk::signature::Signature;
14    use serde::{Serializer, Deserializer, Deserialize};
15    use std::str::FromStr;
16    
17    pub fn serialize<S>(sig: &Signature, serializer: S) -> Result<S::Ok, S::Error>
18    where
19        S: Serializer,
20    {
21        serializer.serialize_str(&sig.to_string())
22    }
23    
24    pub fn deserialize<'de, D>(deserializer: D) -> Result<Signature, D::Error>
25    where
26        D: Deserializer<'de>,
27    {
28        let s = String::deserialize(deserializer)?;
29        Signature::from_str(&s).map_err(serde::de::Error::custom)
30    }
31}
32
33/// Session key with scoped permissions
34/// Similar to EIP-7702 / Solana session keys
35#[derive(Debug, Clone, BorshSerialize, BorshDeserialize)]
36pub struct SessionKey {
37    /// The session key pubkey (not the user's main key)
38    pub key: Pubkey,
39    /// Scoped permissions granted
40    pub scopes: Vec<PermissionScope>,
41    /// Expiration timestamp (Unix seconds)
42    pub expires_at: i64,
43    /// User vault this session is authorized for
44    pub vault: Pubkey,
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
48pub enum PermissionScope {
49    /// Read vault state, positions, rates
50    Read,
51    /// Submit purchase proofs to capture value
52    Capture,
53    /// Stake/unstake Cred, claim yield
54    Stake,
55}
56
57/// Zero-knowledge proof for purchase verification
58#[derive(Debug, Clone, BorshSerialize, BorshDeserialize)]
59pub struct ZkProof {
60    pub proof_type: ProofType,
61    /// Base64-encoded proof payload
62    pub data: Vec<u8>,
63    /// When the purchase occurred
64    pub timestamp: i64,
65    /// Purchase amount in cents
66    pub amount_cents: u64,
67    /// Optional: card fingerprint for dedup
68    pub card_fingerprint: Option<String>,
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
72pub enum ProofType {
73    /// Reclaim Protocol ZK proof
74    Reclaim,
75    /// zkTLS proof (e.g., from bank statement)
76    ZkTls,
77    /// Fidel card-linked transaction
78    Fidel,
79    /// Square POS webhook (trusted merchant)
80    SquarePos,
81    /// Stripe Connect (trusted merchant)
82    StripeConnect,
83}
84
85/// Result of a capture operation
86#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct CaptureResult {
88    /// Transaction signature on Solana (base58 string)
89    #[serde(with = "signature_serde")]
90    pub signature: Signature,
91    /// Total Cred minted
92    pub cred_minted: u64,
93    /// User's share (80%)
94    pub user_amount: u64,
95    /// Treasury share (14%)
96    pub treasury_amount: u64,
97    /// Staker pool share (6%)
98    pub staker_amount: u64,
99    /// Merchant that received attribution
100    pub merchant: Pubkey,
101}
102
103/// Staking position details
104#[derive(Debug, Clone, BorshSerialize, BorshDeserialize)]
105pub struct StakingPosition {
106    pub position_id: Pubkey,
107    pub amount: u64,
108    pub start_time: i64,
109    pub duration_days: u16,
110    pub unlock_time: i64,
111    pub apy_bps: u16,  // Basis points (1000 = 10%)
112    pub auto_compound: bool,
113    pub accrued_yield: u64,
114}
115
116/// Yield optimization recommendation
117#[derive(Debug, Clone, BorshSerialize, BorshDeserialize)]
118pub struct YieldRecommendation {
119    pub current_apy: u16,
120    pub recommended_action: RecommendedAction,
121    pub projected_apy: u16,
122    pub reasoning: String,
123}
124
125#[derive(Debug, Clone, BorshSerialize, BorshDeserialize)]
126pub enum RecommendedAction {
127    /// Keep current positions
128    Hold,
129    /// Stake idle Cred
130    Stake { amount: u64, duration_days: u16 },
131    /// Consolidate positions
132    Consolidate { from_positions: Vec<Pubkey> },
133    /// Extend existing stake for better rate
134    Extend { position: Pubkey, additional_days: u16 },
135}
136
137/// The Action Interface trait
138/// 
139/// Implementations handle the actual Solana transaction building.
140/// Lambda invokes these, but the SESSION KEY (not Lambda) signs.
141pub trait VaultAction {
142    /// Capture value from a verified purchase
143    /// 
144    /// # Security
145    /// - Requires `Capture` scope in session key
146    /// - Contract enforces 80/14/6 split
147    /// - Agent cannot modify distribution
148    fn capture_value(
149        &self,
150        session: &SessionKey,
151        merchant_id: Pubkey,
152        proof: ZkProof,
153    ) -> Result<CaptureResult, ActionError>;
154
155    /// Stake Cred for yield
156    /// 
157    /// # Security
158    /// - Requires `Stake` scope in session key
159    /// - Funds move from user vault to staking pool
160    /// - Position NFT minted to user
161    fn stake_cred(
162        &self,
163        session: &SessionKey,
164        amount: u64,
165        duration_days: u16,
166        auto_compound: bool,
167    ) -> Result<StakingPosition, ActionError>;
168
169    /// Claim yield from positions
170    /// 
171    /// # Security
172    /// - Requires `Stake` scope
173    /// - Can only claim to user's own vault
174    fn claim_yield(
175        &self,
176        session: &SessionKey,
177        position_id: Option<Pubkey>,  // None = all positions
178        restake: bool,
179    ) -> Result<u64, ActionError>;  // Returns amount claimed
180
181    /// Get optimization recommendation (read-only)
182    /// 
183    /// # Security
184    /// - Requires `Read` scope
185    /// - No state modification
186    fn request_yield_optimization(
187        &self,
188        session: &SessionKey,
189        risk_tolerance: RiskTolerance,
190    ) -> Result<YieldRecommendation, ActionError>;
191}
192
193#[derive(Debug, Clone, Copy)]
194pub enum RiskTolerance {
195    /// Shorter locks, lower yield, more liquidity
196    Conservative,
197    /// Balanced approach
198    Balanced,
199    /// Longer locks, higher yield
200    Aggressive,
201}
202
203#[derive(Debug, Clone)]
204pub enum ActionError {
205    /// Session key expired or invalid
206    InvalidSession,
207    /// Session key doesn't have required scope
208    InsufficientScope(PermissionScope),
209    /// Proof verification failed
210    InvalidProof(String),
211    /// Merchant not registered
212    MerchantNotFound,
213    /// Insufficient balance for operation
214    InsufficientBalance { required: u64, available: u64 },
215    /// Position is still locked
216    PositionLocked { unlock_time: i64 },
217    /// Solana transaction failed
218    TransactionFailed(String),
219    /// Rate limiting
220    RateLimited { retry_after_ms: u64 },
221}
222
223/// Current yield rates by duration
224pub const YIELD_RATES_BPS: [(u16, u16); 4] = [
225    (30, 800),    // 30 days = 8% APY
226    (90, 1200),   // 90 days = 12% APY
227    (180, 1600),  // 180 days = 16% APY
228    (365, 2000),  // 365 days = 20% APY
229];
230
231/// Distribution policy (enforced by smart contract, not agent)
232pub const DISTRIBUTION: Distribution = Distribution {
233    user_bps: 8000,      // 80%
234    treasury_bps: 1400,  // 14%
235    staker_bps: 600,     // 6%
236};
237
238pub struct Distribution {
239    pub user_bps: u16,
240    pub treasury_bps: u16,
241    pub staker_bps: u16,
242}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247
248    #[test]
249    fn distribution_adds_to_100() {
250        assert_eq!(
251            DISTRIBUTION.user_bps + DISTRIBUTION.treasury_bps + DISTRIBUTION.staker_bps,
252            10000
253        );
254    }
255
256    #[test]
257    fn yield_rates_increase_with_duration() {
258        for i in 1..YIELD_RATES_BPS.len() {
259            assert!(YIELD_RATES_BPS[i].1 > YIELD_RATES_BPS[i-1].1);
260        }
261    }
262}