Skip to main content

Solana链上交易监控最佳实践:从logsSubscribe到全方位监控

·2 mins

Solana链上交易监控最佳实践:从logsSubscribe到全方位监控 #

背景介绍 #

在Solana链上开发中,实时监控特定账户的交易活动是一个常见需求,特别是在构建跟单机器人这类对时效性要求较高的应用场景中。最初,我们可能会想到使用Solana提供的logsSubscribe WebSocket API来实现这个功能,因为它看起来是最直接的解决方案。然而,在实际应用中,我们发现这种方案存在一些限制和问题。

问题发现 #

在使用logsSubscribe进行账户监控时,我们发现一个关键问题:某些确实发生的交易并没有被我们的监控系统捕获到。这个问题的发现促使我们深入研究Solana的交易日志机制,并最终设计了一个更全面的监控方案。

为什么会遗漏交易? #

  1. 日志记录机制的局限性

    • 程序可能不会在日志中明确记录所有涉及的账户地址
    • 交易可能使用了PDA(Program Derived Address)或其他派生地址
    • 某些DEX采用内部账户映射,而不是直接记录用户地址
  2. mentions过滤器的限制

    • mentions匹配的是交易账户列表(accountKeys)中包含目标地址的交易,而非日志文本中出现的地址,因此只有当目标地址作为交易的显式参与账户时才会被捕获
    • 无法捕获通过间接方式(如CPI调用中未在顶层accountKeys中列出)影响目标账户的交易

解决方案 #

针对上述问题,我们设计了一个多维度监控方案,通过组合多种订阅方式来确保不会遗漏任何相关交易。

1. 三重订阅机制 #

pub struct EnhancedTradeWatcher {
    target_account: Pubkey,
    ws_client: WebSocketClient,
}

impl EnhancedTradeWatcher {
    async fn setup_comprehensive_monitoring(&mut self) -> Result<()> {
        // 1. logsSubscribe - 捕获显式提及
        let logs_sub = json!({
            "jsonrpc": "2.0",
            "method": "logsSubscribe",
            "params": [
                {
                    "mentions": [self.target_account.to_string()],
                },
                {
                    "commitment": "processed"
                }
            ]
        });

        // 2. programSubscribe - 监控DEX程序
        let dex_program_sub = json!({
            "jsonrpc": "2.0",
            "method": "programSubscribe",
            "params": [
                DEX_PROGRAM_ID,
                {
                    "encoding": "jsonParsed",
                    "commitment": "processed"
                }
            ]
        });

        // 3. accountSubscribe - 监控账户变更
        let account_sub = json!({
            "jsonrpc": "2.0",
            "method": "accountSubscribe",
            "params": [
                self.target_account.to_string(),
                {
                    "encoding": "jsonParsed",
                    "commitment": "processed"
                }
            ]
        });

        // 发送所有订阅请求
        self.ws_client.send(logs_sub).await?;
        self.ws_client.send(dex_program_sub).await?;
        self.ws_client.send(account_sub).await?;
    }
}

2. 交易去重机制 #

为了避免多个订阅渠道导致的重复处理,我们实现了基于交易签名的去重机制:

async fn handle_all_events(&mut self) -> Result<()> {
    let mut transactions_seen = HashSet::new();

    while let Some(event) = self.ws_client.next_message().await? {
        if !transactions_seen.contains(&event.signature) {
            self.process_transaction(&event).await?;
            transactions_seen.insert(event.signature);
        }
    }
    Ok(())
}

3. 相关账户检查 #

为了确保捕获所有相关交易,我们实现了全面的账户关联检查:

fn check_related_accounts(&self, program_info: &ProgramInfo) -> bool {
    // 检查Token账户
    let token_accounts = self.get_associated_token_accounts(&self.target_account);
    
    // 检查OpenOrders账户
    let open_orders = self.get_open_orders_accounts(&self.target_account);
    
    // 检查PDA
    let pdas = self.get_related_pdas(&self.target_account);
    
    program_info.accounts.iter().any(|acc| 
        token_accounts.contains(acc) 
        || open_orders.contains(acc)
        || pdas.contains(acc)
    )
}

为什么选择这种方案? #

  1. 完整性保证

    • 多维度监控确保不会遗漏任何相关交易
    • 通过检查关联账户捕获间接交易
  2. 性能优化

    • 使用缓存减少RPC调用
    • 实现交易去重避免重复处理
    • 采用processed提交级别获得最低延迟
  3. 可扩展性

    • 方案设计支持添加新的DEX监控
    • 可以根据具体需求调整监控策略
  4. 可靠性

    • 多渠道数据源提供数据冗余
    • 降低单点故障风险

性能考虑 #

虽然这种多维度监控方案会带来一些额外的系统开销,但在跟单场景中,准确性和完整性的重要性远大于少量的性能损耗。为了优化性能,我们实现了以下机制:

struct AccountCache {
    token_accounts: LruCache<Pubkey, Vec<Pubkey>>,
    open_orders: LruCache<Pubkey, Vec<Pubkey>>,
    last_update: HashMap<Pubkey, Instant>,
}

结论 #

在Solana链上开发中,单一的监控方式往往无法满足复杂业务场景的需求。通过结合多种订阅方式,并配合合理的缓存策略和去重机制,我们可以构建一个既可靠又高效的交易监控系统。这个方案虽然实现较为复杂,但能够提供更好的可靠性和完整性保证,特别适合对实时性和准确性要求较高的跟单场景。

未来展望 #

  1. 支持更多DEX协议
  2. 优化缓存策略
  3. 添加更多性能监控指标
  4. 实现自动化失败重试机制

希望这篇文章能够帮助大家在实现Solana链上监控时避免一些常见陷阱,构建更可靠的监控系统。