onyx-sdk 0.1.0

Onyx SDK - Privacy-preserving stealth addresses for Solana
Documentation
//! Scanner for detecting incoming stealth payments
//!
//! The scanner reads announcements from on-chain and checks if any
//! of them correspond to payments for a given meta-address.

use crate::address::check_stealth_address;
use crate::error::Result;
use crate::keys::StealthMetaAddress;
use crate::spend::StealthKeypair;
use serde::{Deserialize, Serialize};
use solana_client::rpc_client::RpcClient;
use solana_sdk::pubkey::Pubkey;

/// A detected stealth payment
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DetectedPayment {
    /// The stealth address where funds are held
    pub stealth_address: Pubkey,
    /// The ephemeral public key (needed to derive spend key)
    pub ephemeral_pubkey: [u8; 32],
    /// Amount in lamports (if known)
    pub amount: Option<u64>,
    /// Timestamp of the announcement (if known)
    pub timestamp: Option<i64>,
    /// The announcement account (for reference)
    pub announcement_account: Option<Pubkey>,
}

/// An announcement read from on-chain
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Announcement {
    /// The ephemeral public key
    pub ephemeral_pubkey: [u8; 32],
    /// The stealth address
    pub stealth_address: Pubkey,
    /// Timestamp when announced
    pub timestamp: i64,
}

/// Scanner configuration
#[derive(Clone, Debug)]
pub struct ScannerConfig {
    /// The program ID for the stealth announcements
    pub program_id: Pubkey,
    /// Only scan announcements after this timestamp (optional)
    pub after_timestamp: Option<i64>,
    /// Maximum number of announcements to scan
    pub max_announcements: usize,
}

impl Default for ScannerConfig {
    fn default() -> Self {
        Self {
            program_id: Pubkey::default(), // Should be set to actual program ID
            after_timestamp: None,
            max_announcements: 1000,
        }
    }
}

/// Scanner for detecting stealth payments
pub struct Scanner {
    /// The meta-address to scan for
    meta: StealthMetaAddress,
    /// Configuration
    config: ScannerConfig,
}

impl Scanner {
    /// Create a new scanner for a meta-address
    pub fn new(meta: &StealthMetaAddress) -> Self {
        Self {
            meta: meta.clone(),
            config: ScannerConfig::default(),
        }
    }

    /// Create a scanner with custom configuration
    pub fn with_config(meta: &StealthMetaAddress, config: ScannerConfig) -> Self {
        Self {
            meta: meta.clone(),
            config,
        }
    }

    /// Set the program ID
    pub fn program_id(mut self, program_id: Pubkey) -> Self {
        self.config.program_id = program_id;
        self
    }

    /// Only scan announcements after this timestamp
    pub fn after_timestamp(mut self, timestamp: i64) -> Self {
        self.config.after_timestamp = Some(timestamp);
        self
    }

    /// Scan a list of announcements for payments to this meta-address
    pub fn scan_announcements_list(
        &self,
        announcements: &[Announcement],
    ) -> Result<Vec<DetectedPayment>> {
        let mut detected = Vec::new();

        for announcement in announcements {
            // Filter by timestamp if configured
            if let Some(after) = self.config.after_timestamp {
                if announcement.timestamp < after {
                    continue;
                }
            }

            // Check if this announcement is for us
            let is_ours = check_stealth_address(
                self.meta.viewing_key(),
                self.meta.spending_pubkey(),
                &announcement.ephemeral_pubkey,
                &announcement.stealth_address,
            )?;

            if is_ours {
                detected.push(DetectedPayment {
                    stealth_address: announcement.stealth_address,
                    ephemeral_pubkey: announcement.ephemeral_pubkey,
                    amount: None, // Will be fetched separately
                    timestamp: Some(announcement.timestamp),
                    announcement_account: None,
                });
            }
        }

        Ok(detected)
    }

    /// Scan on-chain for payments (async version)
    ///
    /// This fetches announcements from the on-chain program and checks
    /// which ones belong to this meta-address.
    pub async fn scan(&self, rpc_url: &str) -> Result<Vec<DetectedPayment>> {
        let client = RpcClient::new(rpc_url.to_string());

        // Fetch announcements from on-chain
        // This is a simplified version - actual implementation would
        // use getProgramAccounts or similar
        let announcements = self.fetch_announcements(&client)?;

        // Scan for our payments
        let mut detected = self.scan_announcements_list(&announcements)?;

        // Fetch balances for detected payments
        for payment in &mut detected {
            if let Ok(balance) = client.get_balance(&payment.stealth_address) {
                payment.amount = Some(balance);
            }
        }

        Ok(detected)
    }

    /// Fetch announcements from on-chain
    fn fetch_announcements(&self, _client: &RpcClient) -> Result<Vec<Announcement>> {
        // TODO: Implement actual fetching from the program
        // For now, return empty list
        // In a real implementation, this would:
        // 1. Use getProgramAccounts to fetch all announcement accounts
        // 2. Deserialize them into Announcement structs
        // 3. Return the list

        Ok(Vec::new())
    }

    /// Derive the spend keypair for a detected payment
    pub fn derive_spend_keypair(&self, payment: &DetectedPayment) -> Result<StealthKeypair> {
        StealthKeypair::derive(&self.meta, &payment.ephemeral_pubkey)
    }
}

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

    #[test]
    fn test_scan_detects_payment() {
        let meta = StealthMetaAddress::generate();
        let public_meta = meta.public_meta_address();

        // Create a payment
        let payment = StealthPayment::create(&public_meta, 1_000_000_000).unwrap();

        // Create an announcement from the payment
        let announcement = Announcement {
            ephemeral_pubkey: payment.ephemeral_pubkey,
            stealth_address: payment.stealth_address,
            timestamp: 12345,
        };

        // Scanner should detect this
        let scanner = Scanner::new(&meta);
        let detected = scanner.scan_announcements_list(&[announcement]).unwrap();

        assert_eq!(detected.len(), 1);
        assert_eq!(detected[0].stealth_address, payment.stealth_address);
    }

    #[test]
    fn test_scan_ignores_other_payments() {
        let alice = StealthMetaAddress::generate();
        let bob = StealthMetaAddress::generate();

        // Create a payment to Alice
        let payment = StealthPayment::create(&alice.public_meta_address(), 1_000_000_000).unwrap();

        let announcement = Announcement {
            ephemeral_pubkey: payment.ephemeral_pubkey,
            stealth_address: payment.stealth_address,
            timestamp: 12345,
        };

        // Bob's scanner should NOT detect this
        let scanner = Scanner::new(&bob);
        let detected = scanner.scan_announcements_list(&[announcement]).unwrap();

        assert_eq!(detected.len(), 0, "Bob should not detect Alice's payment");
    }

    #[test]
    fn test_scan_multiple_payments() {
        let meta = StealthMetaAddress::generate();
        let public_meta = meta.public_meta_address();

        // Create multiple payments
        let payments: Vec<_> = (0..5)
            .map(|i| StealthPayment::create(&public_meta, (i + 1) * 1_000_000_000).unwrap())
            .collect();

        let announcements: Vec<_> = payments
            .iter()
            .enumerate()
            .map(|(i, p)| Announcement {
                ephemeral_pubkey: p.ephemeral_pubkey,
                stealth_address: p.stealth_address,
                timestamp: i as i64,
            })
            .collect();

        let scanner = Scanner::new(&meta);
        let detected = scanner.scan_announcements_list(&announcements).unwrap();

        assert_eq!(detected.len(), 5);
    }

    #[test]
    fn test_timestamp_filtering() {
        let meta = StealthMetaAddress::generate();
        let public_meta = meta.public_meta_address();

        let payments: Vec<_> = (0..5)
            .map(|i| StealthPayment::create(&public_meta, (i + 1) * 1_000_000_000).unwrap())
            .collect();

        let announcements: Vec<_> = payments
            .iter()
            .enumerate()
            .map(|(i, p)| Announcement {
                ephemeral_pubkey: p.ephemeral_pubkey,
                stealth_address: p.stealth_address,
                timestamp: (i * 100) as i64, // 0, 100, 200, 300, 400
            })
            .collect();

        // Only scan after timestamp 200
        let scanner = Scanner::new(&meta).after_timestamp(200);
        let detected = scanner.scan_announcements_list(&announcements).unwrap();

        assert_eq!(detected.len(), 3); // 200, 300, 400
    }
}