Scanner
Efficiently detects incoming stealth payments by checking announcements against your viewing key using ECDH operations.
Quick Overview
onyx_sdk::scannerCloneHow Scanning Works
For each announcement, the scanner performs an ECDH operation between your viewing key and the ephemeral public key. If the resulting stealth address matches, the payment belongs to you.
Definition
pub struct Scanner {
/// The viewing key used to detect payments
viewing_key: SecretKey,
/// The spending public key for address derivation
spending_pubkey: PublicKey,
/// Optional timestamp filter
since: Option<i64>,
/// Whether this scanner can derive spending keys
can_spend: bool,
}Constructor Methods
newpub fn new(meta: &StealthMetaAddress) -> SelfCreates a scanner with full capabilities (scan + spend) from a complete meta-address.
use onyx_sdk::prelude::*;
let meta = StealthMetaAddress::load_from_file("~/.onyx/keys.json")?;
let scanner = Scanner::new(&meta);
// This scanner can detect payments AND derive spending keys
let payments = scanner.scan(&announcements)?;
for payment in &payments {
let keypair = StealthKeypair::from_payment(&meta, payment)?;
// Can sign transactions
}new_view_onlypub fn new_view_only(view_only: &ViewOnlyMetaAddress) -> SelfCreates a view-only scanner that can detect payments but cannot derive spending keys.
use onyx_sdk::prelude::*;
// Load view-only credentials (e.g., for a watch service)
let view_only = ViewOnlyMetaAddress::load_from_file("~/.onyx/view-only.json")?;
let scanner = Scanner::new_view_only(&view_only);
// Can detect payments
let payments = scanner.scan(&announcements)?;
// But CANNOT derive spending keys
// StealthKeypair::from_payment(&view_only, &payment) -> Error!Use Case: View-only scanners are ideal for notification services, portfolio trackers, or any system that needs to detect payments without the ability to spend.
Builder Methods
sincepub fn since(self, timestamp: i64) -> SelfFilters announcements to only check those created after the given Unix timestamp.
use std::time::{SystemTime, UNIX_EPOCH};
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)?
.as_secs() as i64;
// Only scan announcements from the last hour
let one_hour_ago = now - 3600;
let payments = Scanner::new(&meta)
.since(one_hour_ago)
.scan(&announcements)?;
println!("Found {} payments in the last hour", payments.len());since_durationpub fn since_duration(self, duration: Duration) -> SelfConvenience method to filter by a duration before now.
use std::time::Duration;
// Scan last 24 hours
let payments = Scanner::new(&meta)
.since_duration(Duration::from_secs(86400))
.scan(&announcements)?;
// Scan last 7 days
let weekly_payments = Scanner::new(&meta)
.since_duration(Duration::from_secs(7 * 86400))
.scan(&announcements)?;Core Methods
scanpub fn scan(&self, announcements: &[Announcement]) -> Result<Vec<StealthPayment>>Scans a slice of announcements and returns all payments addressed to this meta-address.
let meta = StealthMetaAddress::load_from_file("~/.onyx/keys.json")?;
let scanner = Scanner::new(&meta);
// Fetch announcements from the on-chain registry
let announcements = fetch_announcements(&rpc_client)?;
// Scan for payments
let payments = scanner.scan(&announcements)?;
println!("Found {} payments", payments.len());
for payment in &payments {
println!(" {} SOL at {}", payment.sol_amount(), payment.stealth_address);
}scan_singlepub fn scan_single(&self, announcement: &Announcement) -> Option<StealthPayment>Checks a single announcement. Returns Some(payment) if it belongs to you, None otherwise.
// Useful for real-time monitoring
for announcement in new_announcements {
if let Some(payment) = scanner.scan_single(&announcement) {
println!("New payment detected: {} SOL", payment.sol_amount());
notify_user(&payment)?;
}
}check_addresspub fn check_address(&self, stealth_address: &Pubkey, ephemeral_pubkey: &[u8; 32]) -> boolChecks if a stealth address belongs to this meta-address without fetching balance.
// Quick ownership check
let is_mine = scanner.check_address(
&stealth_address,
&ephemeral_pubkey,
);
if is_mine {
println!("This address belongs to me!");
}Announcement Type
The scanner works with Announcement structs from the on-chain registry:
/// On-chain payment announcement
pub struct Announcement {
/// Sender's one-time ephemeral public key
pub ephemeral_pubkey: [u8; 32],
/// The stealth address where funds were sent
pub stealth_address: Pubkey,
/// When the announcement was created
pub timestamp: i64,
/// Optional: Token mint for SPL token payments
pub token_mint: Option<Pubkey>,
}Fetching Announcements
Use the registry client to fetch announcements from the Onyx Anchor program:
use onyx_sdk::prelude::*;
fn fetch_announcements(rpc: &RpcClient) -> Result<Vec<Announcement>> {
// Get the registry program
let registry = OnyxRegistry::new(rpc);
// Fetch all announcements
let all = registry.fetch_all()?;
// Or fetch with pagination
let page = registry.fetch_page(0, 100)?;
// Or fetch since a timestamp
let recent = registry.fetch_since(last_scan_time)?;
Ok(recent)
}
// Example with filtering
let mut scanner = Scanner::new(&meta);
let since = last_scan_timestamp();
let announcements = registry.fetch_since(since)?;
let payments = scanner.scan(&announcements)?;
// Update last scan timestamp
save_scan_timestamp(SystemTime::now())?;Performance Characteristics
Scanning is computationally efficient but scales linearly with announcement count:
| Announcements | Time | Memory |
|---|---|---|
| 1,000 | < 100ms | ~100 KB |
| 10,000 | < 1s | ~1 MB |
| 100,000 | ~5-10s | ~10 MB |
| 1,000,000 | ~1 min | ~100 MB |
Best Practices
Use Incremental Scanning
Store the timestamp of your last scan and only check new announcements. This keeps scan time constant.
let since = load_last_scan_time()?;
let payments = scanner.since(since).scan(&announcements)?;
save_last_scan_time(now())?;Batch RPC Calls
Fetch announcements in batches to reduce network overhead.
let batch_size = 1000;
for offset in (0..).step_by(batch_size) {
let batch = registry.fetch_page(offset, batch_size)?;
if batch.is_empty() { break; }
let payments = scanner.scan(&batch)?;
process_payments(payments)?;
}Handle Reorgs
For production systems, scan with some overlap to handle chain reorganizations.
// Scan with 10-block safety margin
let safe_since = last_scan_time - 10 * SLOT_DURATION;
let payments = scanner.since(safe_since).scan(&announcements)?;
// Deduplicate against already-processed paymentsComplete Example
use onyx_sdk::prelude::*;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
fn main() -> Result<()> {
// Setup
let meta = StealthMetaAddress::load_from_file("~/.onyx/keys.json")?;
let rpc = RpcClient::new("https://api.mainnet-beta.solana.com");
let registry = OnyxRegistry::new(&rpc);
// Create scanner
let scanner = Scanner::new(&meta);
// Load last scan time (or use 0 for first scan)
let last_scan = load_last_scan_time().unwrap_or(0);
// Fetch new announcements
let announcements = registry.fetch_since(last_scan)?;
println!("Checking {} new announcements...", announcements.len());
// Scan for payments
let payments = scanner.scan(&announcements)?;
if payments.is_empty() {
println!("No new payments found.");
} else {
println!("Found {} payments:", payments.len());
let mut total_sol = 0.0;
for payment in &payments {
println!(" {} SOL from {}",
payment.sol_amount(),
payment.stealth_address
);
total_sol += payment.sol_amount();
}
println!("Total: {} SOL", total_sol);
}
// Save scan time for next run
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)?
.as_secs() as i64;
save_last_scan_time(now)?;
Ok(())
}