Skip to content

Commit 1e56d3d

Browse files
0xrinegadeclaude
andcommitted
feat(tui): Add search, copy-to-clipboard, and edge labels
**Quick Win Features:** **1. Fuzzy Wallet Search (#4 Priority)** - Press `/` to activate search mode - Type to fuzzy-search wallet addresses/labels - Press `n`/`N` to cycle through results - Press ESC to exit search - Search results highlighted in yellow - Auto-centers viewport on selected result - Shows "Search: query (X/Y results)" status bar - Search persists until manually cleared **2. Copy to Clipboard (#6 Priority)** - Press `y` to copy selected wallet address - Uses arboard crate (already in dependencies) - Shows toast notification: "Copied: 5Q544f...e4j1" - Toast auto-dismisses after 2 seconds - Toast appears centered at top of graph **3. Edge Labels (#1 Priority)** - Shows transfer amount and token on each edge - Format: "1.5 SOL", "2.5k USDC" (auto-scales) - Only renders when zoom > 0.8 (prevents clutter) - EdgeLabel struct stores amount, token, timestamp - Labels render at edge midpoint **Implementation Details:** **Graph Struct Changes:** - Added `EdgeLabel` struct with amount/token/timestamp - Changed connections from `(usize, usize, String)` → `(usize, usize, EdgeLabel)` - Added search state: query, results, result_idx, search_active - Added toast notification system with timer - `add_connection()` now accepts amount, token, timestamp params **Input Handling:** - New GraphInput variants: StartSearch, SearchChar, SearchBackspace, SearchNext, SearchPrev, Copy - Search performs case-insensitive substring matching - Auto-selects and centers first search result - n/N keys cycle through results with viewport centering **UI Enhancements:** - Search bar renders at bottom with yellow border - Shows result counter: "Search: 5Q (2/5 results)" - Toast notifications with green background - Search matches highlighted in yellow (distinct from white selection) - Edge labels use format_short() for compact display **Help Text Updates:** - Added `/` - Start search - Added `n/N` - Next/previous result - Added `y` - Copy address to clipboard **Bug Fixes:** - Added serde::Serialize to TokenVolume and TransferEvent - Fixed missing timestamp parameter in build_from_transfers() - Updated add_transfer() signature to include timestamp **Testing Workflow:** ```bash ./target/release/osvm wallet-explorer <WALLET> # Press 2 for Graph tab # Press / and type wallet substring # Press n to cycle results # Press y to copy selected address # Zoom in to see edge labels with amounts ``` 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 34162b7 commit 1e56d3d

File tree

16 files changed

+2182
-220
lines changed

16 files changed

+2182
-220
lines changed

Cargo.lock

Lines changed: 35 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/ovsm/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ proptest = "1.4"
7777
criterion = "0.5"
7878
bolero = "0.10"
7979
tokio-test = "0.4"
80+
solana_rbpf = "0.8.5"
8081

8182
[[bench]]
8283
name = "execution_bench"
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
//! Compile OVSM to sBPF and deploy to localnet
2+
use ovsm::compiler::{Compiler, CompileOptions, debug_compile};
3+
use std::process::Command;
4+
5+
fn main() {
6+
let source = std::fs::read_to_string("/tmp/hello_test.ovsm")
7+
.expect("Failed to read /tmp/hello_test.ovsm");
8+
9+
println!("=== Compiling OVSM to sBPF ===\n");
10+
11+
// Full debug output
12+
debug_compile(&source);
13+
14+
// Compile to ELF
15+
let options = CompileOptions {
16+
opt_level: 0,
17+
debug_info: true,
18+
..Default::default()
19+
};
20+
let compiler = Compiler::new(options);
21+
22+
match compiler.compile(&source) {
23+
Ok(result) => {
24+
let elf_path = "/tmp/amm_program.so";
25+
std::fs::write(elf_path, &result.elf_bytes).expect("Failed to write ELF");
26+
println!("\n✅ Wrote ELF to {}", elf_path);
27+
println!(" Size: {} bytes", result.elf_bytes.len());
28+
29+
// Generate program keypair
30+
let program_keypair = "/tmp/amm_program_keypair.json";
31+
let keygen = Command::new("solana-keygen")
32+
.args(["new", "--no-bip39-passphrase", "--outfile", program_keypair, "--force"])
33+
.output()
34+
.expect("Failed to generate keypair");
35+
36+
if !keygen.status.success() {
37+
eprintln!("Failed to generate program keypair");
38+
return;
39+
}
40+
41+
// Get program ID
42+
let pubkey_output = Command::new("solana-keygen")
43+
.args(["pubkey", program_keypair])
44+
.output()
45+
.expect("Failed to get pubkey");
46+
let program_id = String::from_utf8_lossy(&pubkey_output.stdout).trim().to_string();
47+
println!(" Program ID: {}", program_id);
48+
49+
println!("\n=== Deploying to localnet ===\n");
50+
51+
// Deploy
52+
let deploy = Command::new("solana")
53+
.args([
54+
"program", "deploy",
55+
"--keypair", "/tmp/test-deploy-keypair.json",
56+
"--program-id", program_keypair,
57+
"--url", "http://localhost:8899",
58+
elf_path,
59+
])
60+
.output()
61+
.expect("Failed to deploy");
62+
63+
println!("Deploy stdout: {}", String::from_utf8_lossy(&deploy.stdout));
64+
println!("Deploy stderr: {}", String::from_utf8_lossy(&deploy.stderr));
65+
66+
if deploy.status.success() {
67+
println!("\n🎉 Program deployed successfully!");
68+
println!(" Program ID: {}", program_id);
69+
70+
// Verify deployment
71+
let account = Command::new("solana")
72+
.args([
73+
"program", "show",
74+
"--url", "http://localhost:8899",
75+
&program_id,
76+
])
77+
.output();
78+
79+
if let Ok(output) = account {
80+
println!("\n=== Program Info ===");
81+
println!("{}", String::from_utf8_lossy(&output.stdout));
82+
}
83+
} else {
84+
println!("\n❌ Deployment failed");
85+
println!(" This is expected - our minimal ELF may not pass full BPF loader verification");
86+
}
87+
}
88+
Err(e) => {
89+
eprintln!("❌ Compilation failed: {:?}", e);
90+
}
91+
}
92+
}

crates/ovsm/examples/debug_amm.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
use ovsm::compiler::debug_compile;
2+
3+
fn main() {
4+
let amm = r#"
5+
;; AMM Constant Product Swap
6+
;; Load pool reserves from account 0
7+
(define pool_account (get accounts 0))
8+
(define reserves_x (mem-load pool_account 0))
9+
(define reserves_y (mem-load pool_account 8))
10+
11+
;; Load swap amount from instruction data
12+
(define swap_amount (mem-load instruction-data 0))
13+
14+
;; Constants
15+
(define FEE_BPS 30)
16+
(define BPS_DENOMINATOR 10000)
17+
18+
;; Calculate fee
19+
(define fee (/ (* swap_amount FEE_BPS) BPS_DENOMINATOR))
20+
(define amount_in_after_fee (- swap_amount fee))
21+
22+
;; Constant product formula: dy = y * dx / (x + dx)
23+
(define new_reserves_x (+ reserves_x amount_in_after_fee))
24+
(define amount_out (/ (* reserves_y amount_in_after_fee) new_reserves_x))
25+
26+
;; Update pool state
27+
(mem-store pool_account 0 new_reserves_x)
28+
(mem-store pool_account 8 (- reserves_y amount_out))
29+
30+
;; Return output amount
31+
amount_out
32+
"#;
33+
34+
debug_compile(amm);
35+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Debug ELF parsing to find exact failure point
2+
use solana_rbpf::elf_parser::{consts::*, types::*, Elf64Parser};
3+
4+
fn main() {
5+
let elf_path = std::env::args()
6+
.nth(1)
7+
.unwrap_or_else(|| "/tmp/hello_final.so".to_string());
8+
9+
println!("📂 Loading ELF: {}", elf_path);
10+
let elf_bytes = std::fs::read(&elf_path).expect("Failed to read ELF");
11+
println!(" Size: {} bytes\n", elf_bytes.len());
12+
13+
println!("🔍 Parsing ELF...");
14+
15+
match Elf64Parser::parse(&elf_bytes) {
16+
Ok(parser) => {
17+
println!("✅ ELF parsed successfully!\n");
18+
19+
// Check dynamic table
20+
println!("📊 Dynamic Table:");
21+
if let Some(dynsym) = parser.dynamic_symbol_table() {
22+
println!(" Dynamic symbols: {} entries", dynsym.len());
23+
} else {
24+
println!(" No dynamic symbol table");
25+
}
26+
27+
if let Some(relocs) = parser.dynamic_relocations_table() {
28+
println!(" Dynamic relocations: {} entries", relocs.len());
29+
} else {
30+
println!(" No dynamic relocations");
31+
}
32+
}
33+
Err(e) => {
34+
println!("❌ ELF parsing failed!");
35+
println!("\n🔍 Error: {:?}", e);
36+
}
37+
}
38+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
//! Validate ELF with solana_rbpf
2+
use solana_rbpf::{
3+
elf::Executable,
4+
program::{BuiltinProgram, SBPFVersion},
5+
verifier::RequisiteVerifier,
6+
};
7+
8+
#[derive(Debug)]
9+
struct TestContext {
10+
remaining: u64,
11+
}
12+
13+
impl solana_rbpf::vm::ContextObject for TestContext {
14+
fn trace(&mut self, _state: [u64; 12]) {}
15+
fn consume(&mut self, amount: u64) { self.remaining = self.remaining.saturating_sub(amount); }
16+
fn get_remaining(&self) -> u64 { self.remaining }
17+
}
18+
19+
fn main() {
20+
let elf_bytes = std::fs::read("/tmp/amm_program.so").expect("read elf");
21+
println!("ELF size: {} bytes", elf_bytes.len());
22+
23+
let loader = std::sync::Arc::new(BuiltinProgram::<TestContext>::new_mock());
24+
25+
match Executable::<TestContext>::from_elf(&elf_bytes, loader) {
26+
Ok(exe) => {
27+
println!("✅ Valid sBPF ELF (loaded)!");
28+
println!(" Entry offset: {}", exe.get_entrypoint_instruction_offset());
29+
30+
// Verify bytecode
31+
match exe.verify::<RequisiteVerifier>() {
32+
Ok(()) => println!(" Verification: ✅ PASS"),
33+
Err(e) => println!(" Verification: ❌ {:?}", e),
34+
}
35+
}
36+
Err(e) => {
37+
println!("❌ Invalid ELF: {:?}", e);
38+
}
39+
}
40+
}

0 commit comments

Comments
 (0)