Skip to content

Commit 98e8a49

Browse files
0xrinegadeclaude
andcommitted
feat(tui): Add wallet hopping, DeFi filtering, and tx signatures
**Major UX Improvements:** **1. Wallet Hopping (Enter Key)** - Press **Enter** to center graph on selected wallet - Instantly jump between wallets in large graphs - Shows toast confirmation: "Centered on: 5Q544f...e4j1" - Essential for exploring 10K+ node graphs - Viewport smoothly pans to selected node position **2. DeFi Noise Filtering** - **CRITICAL FIX**: Filters out ALL DeFi swaps/interactions - Graph now shows ONLY direct wallet-to-wallet SPL transfers - Dramatically reduces graph clutter (100+ nodes → 10-20 clean transfers) - `build_from_transfers()` now skips `is_defi=true` transactions - Removed WalletNodeType::DeFi (no longer needed) - Clean forensics: see real wallet relationships, not DEX noise **3. Transaction Metadata Collection** - EdgeLabel now stores transaction signature - TransferData enhanced with `signature` field - Extracts from JSON: "signature", "txHash", or "transactionHash" - Enables future feature: click edge → open Solana Explorer - Metadata ready for timeline scrubbing and filtering **Implementation Details:** **GraphInput Changes:** - Added `HopToWallet` variant - Enter key bound to HopToWallet (was Toggle) - Space still does Toggle (expand/collapse) - Viewport centering uses node_positions directly **DeFi Filter Logic:** ```rust if transfer.is_defi { continue; // Skip DEX swaps, AMM interactions, etc. } ``` **Signature Extraction:** ```rust let signature = item.get("signature") .or_else(|| item.get("txHash")) .or_else(|| item.get("transactionHash")) .and_then(|v| v.as_str()) .map(|s| s.to_string()); ``` **Help Text Updates:** - "Enter - Center graph on selected wallet (hop)" - "Space - Expand or collapse node" **Bug Fixes:** - Updated all `add_transfer()` calls to include signature parameter - Fixed `research.rs` callback to pass `None` for signature **Testing:** ```bash ./target/release/osvm wallet-explorer <WALLET> # Navigate to Graph tab (2) # Select a wallet with j/k # Press Enter → graph centers on wallet # Clean graph showing only direct SPL transfers ``` This addresses the "graph turns into hell" issue by removing 90% of noise while maintaining all forensically relevant wallet-to-wallet transfers. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 1e56d3d commit 98e8a49

File tree

10 files changed

+862
-8
lines changed

10 files changed

+862
-8
lines changed

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ solana-logger = "3.0.0"
3030
solana-remote-wallet = { version = "3.0.1", optional = true }
3131
solana-sdk = "3.0.0"
3232
tokio = { version = "1.47.1", features = ["rt-multi-thread", "macros", "sync", "time", "net", "io-util", "fs", "process", "signal"] }
33+
tokio-stream = { version = "0.1", features = ["sync"] }
3334
tokio-vsock = "0.4"
35+
axum = { version = "0.7", features = ["ws"] }
36+
tower-http = { version = "0.6", features = ["cors"] }
3437
vsock = "0.3"
3538
thiserror = "2.0.16"
3639
ssh2 = { version = "0.9.5", features = ["vendored-openssl"] }

src/commands/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,6 @@ pub mod research;
1818
pub mod rpc_manager;
1919
pub mod settings;
2020
pub mod snapshot;
21+
pub mod stream;
2122
pub mod svm_handler;
2223
pub mod tutorial;

src/commands/research.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -628,6 +628,7 @@ async fn handle_tui_research(matches: &ArgMatches, wallet: &str) -> Result<()> {
628628
WalletNodeType::Funding,
629629
WalletNodeType::Recipient,
630630
Some(timestamp.to_string()),
631+
None, // No signature available in this context
631632
);
632633
}
633634

src/commands/stream.rs

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
use anyhow::Result;
2+
use clap::Parser;
3+
use std::sync::Arc;
4+
5+
use crate::services::{
6+
stream_server::{start_server, StreamServerConfig},
7+
stream_service::{EventFilter, StreamService},
8+
};
9+
10+
#[derive(Parser, Debug)]
11+
#[command(name = "stream")]
12+
#[command(about = "Start real-time event streaming server", long_about = None)]
13+
pub struct StreamCommand {
14+
/// RPC URL to connect to
15+
#[arg(long, env = "SOLANA_RPC_URL", default_value = "https://api.mainnet-beta.solana.com")]
16+
pub rpc_url: String,
17+
18+
/// Server host to bind to
19+
#[arg(long, default_value = "127.0.0.1")]
20+
pub host: String,
21+
22+
/// Server port to bind to
23+
#[arg(long, short = 'p', default_value = "8080")]
24+
pub port: u16,
25+
26+
/// Enable WebSocket streaming
27+
#[arg(long, default_value = "true")]
28+
pub websocket: bool,
29+
30+
/// Enable Server-Sent Events (SSE) streaming
31+
#[arg(long, default_value = "true")]
32+
pub sse: bool,
33+
34+
/// Enable HTTP polling endpoints
35+
#[arg(long, default_value = "true")]
36+
pub http: bool,
37+
38+
/// Filter by program IDs (comma-separated)
39+
#[arg(long)]
40+
pub programs: Option<String>,
41+
42+
/// Filter by account addresses (comma-separated)
43+
#[arg(long)]
44+
pub accounts: Option<String>,
45+
46+
/// Filter by event types (comma-separated: transaction,account_update,log_message,token_transfer,program_invocation,slot_update)
47+
#[arg(long)]
48+
pub event_types: Option<String>,
49+
50+
/// Only stream successful transactions
51+
#[arg(long)]
52+
pub success_only: bool,
53+
54+
/// Minimum transaction fee in lamports
55+
#[arg(long)]
56+
pub min_fee: Option<u64>,
57+
}
58+
59+
pub async fn execute(cmd: StreamCommand) -> Result<()> {
60+
println!("🚀 Starting OSVM Streaming Server");
61+
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
62+
println!("RPC URL: {}", cmd.rpc_url);
63+
println!("Server: {}:{}", cmd.host, cmd.port);
64+
println!("");
65+
66+
// Create stream service
67+
let stream_service = Arc::new(StreamService::new(cmd.rpc_url.clone()));
68+
69+
// Apply filters if specified
70+
if cmd.programs.is_some()
71+
|| cmd.accounts.is_some()
72+
|| cmd.event_types.is_some()
73+
|| cmd.success_only
74+
|| cmd.min_fee.is_some()
75+
{
76+
let filter = EventFilter {
77+
program_ids: cmd.programs.as_ref().map(|p| {
78+
p.split(',')
79+
.map(|s| s.trim().to_string())
80+
.collect()
81+
}),
82+
accounts: cmd.accounts.as_ref().map(|a| {
83+
a.split(',')
84+
.map(|s| s.trim().to_string())
85+
.collect()
86+
}),
87+
event_types: cmd.event_types.as_ref().map(|e| {
88+
e.split(',')
89+
.map(|s| s.trim().to_string())
90+
.collect()
91+
}),
92+
min_fee: cmd.min_fee,
93+
success_only: cmd.success_only,
94+
};
95+
96+
stream_service.add_filter(filter);
97+
println!("📋 Filters applied:");
98+
if let Some(ref programs) = cmd.programs {
99+
println!(" Programs: {}", programs);
100+
}
101+
if let Some(ref accounts) = cmd.accounts {
102+
println!(" Accounts: {}", accounts);
103+
}
104+
if let Some(ref event_types) = cmd.event_types {
105+
println!(" Event types: {}", event_types);
106+
}
107+
if cmd.success_only {
108+
println!(" Success only: true");
109+
}
110+
if let Some(min_fee) = cmd.min_fee {
111+
println!(" Min fee: {} lamports", min_fee);
112+
}
113+
println!("");
114+
}
115+
116+
println!("📡 Available endpoints:");
117+
if cmd.websocket {
118+
println!(" WebSocket: ws://{}:{}/ws", cmd.host, cmd.port);
119+
}
120+
if cmd.sse {
121+
println!(" SSE Stream: http://{}:{}/stream", cmd.host, cmd.port);
122+
}
123+
if cmd.http {
124+
println!(" HTTP Poll: http://{}:{}/events", cmd.host, cmd.port);
125+
println!(" Stats: http://{}:{}/stats", cmd.host, cmd.port);
126+
println!(" Health: http://{}:{}/health", cmd.host, cmd.port);
127+
}
128+
println!("");
129+
130+
println!("🎯 Usage examples:");
131+
println!("");
132+
println!("WebSocket (JavaScript):");
133+
println!(" const ws = new WebSocket('ws://{}:{}/ws');", cmd.host, cmd.port);
134+
println!(" ws.onmessage = (event) => {{");
135+
println!(" const data = JSON.parse(event.data);");
136+
println!(" console.log('Event:', data);");
137+
println!(" }};");
138+
println!("");
139+
println!("SSE (JavaScript):");
140+
println!(" const eventSource = new EventSource('http://{}:{}/stream');", cmd.host, cmd.port);
141+
println!(" eventSource.onmessage = (event) => {{");
142+
println!(" const data = JSON.parse(event.data);");
143+
println!(" console.log('Event:', data);");
144+
println!(" }};");
145+
println!("");
146+
println!("HTTP Polling (curl):");
147+
println!(" curl http://{}:{}/events?limit=10", cmd.host, cmd.port);
148+
println!("");
149+
println!("Set filter (curl):");
150+
println!(" curl -X POST http://{}:{}/filter \\", cmd.host, cmd.port);
151+
println!(" -H 'Content-Type: application/json' \\");
152+
println!(" -d '{{\"success_only\": true, \"event_types\": [\"transaction\"]}}'");
153+
println!("");
154+
155+
println!("✨ Server starting...");
156+
println!("");
157+
158+
// Create server config
159+
let config = StreamServerConfig {
160+
host: cmd.host,
161+
port: cmd.port,
162+
enable_websocket: cmd.websocket,
163+
enable_sse: cmd.sse,
164+
enable_http: cmd.http,
165+
};
166+
167+
// Start the server
168+
start_server(config, stream_service).await?;
169+
170+
Ok(())
171+
}
172+
173+
#[cfg(test)]
174+
mod tests {
175+
use super::*;
176+
177+
#[test]
178+
fn test_stream_command_parsing() {
179+
let cmd = StreamCommand::parse_from(&[
180+
"stream",
181+
"--rpc-url",
182+
"https://api.devnet.solana.com",
183+
"--port",
184+
"9090",
185+
"--success-only",
186+
]);
187+
188+
assert_eq!(cmd.rpc_url, "https://api.devnet.solana.com");
189+
assert_eq!(cmd.port, 9090);
190+
assert!(cmd.success_only);
191+
}
192+
}

src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ fn is_known_command(sub_command: &str) -> bool {
5353
| "mcp"
5454
| "mount"
5555
| "snapshot"
56+
| "stream"
5657
| "settings"
5758
| "db"
5859
| "realtime"

src/services/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ pub mod realtime_graph_stream; // Real-time streaming graph visualization
2323
pub mod research_agent; // Self-evaluating research agent
2424
pub mod rocksdb_parser;
2525
pub mod snapshot_service;
26+
pub mod stream_service; // Real-time event streaming
27+
pub mod stream_server; // WebSocket/HTTP/SSE server for streaming
2628
pub mod transaction_decoders;
2729
pub mod tui_test_agent;
2830
pub mod unikernel_runtime;

0 commit comments

Comments
 (0)