-
Notifications
You must be signed in to change notification settings - Fork 13
Update token addresses parsing + add Jupiter parser testing with a real route fixture #88
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a9dc4dc
481169a
0f56f43
ba3fa77
f162a3d
cd0075f
9c2e178
98c2dd2
a757358
dd492ff
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -201,7 +201,7 @@ fn parse_route_instruction( | |
| JupiterSwapInstruction::parse_amounts_and_slippage_from_data(data)?; | ||
|
|
||
| let in_token = accounts.first().map(|addr| get_token_info(addr, in_amount)); | ||
| let out_token = accounts.get(1).map(|addr| get_token_info(addr, out_amount)); | ||
| let out_token = accounts.get(5).map(|addr| get_token_info(addr, out_amount)); | ||
|
|
||
| Ok(JupiterSwapInstruction::Route { | ||
| in_token, | ||
|
|
@@ -219,7 +219,7 @@ fn parse_exact_out_route_instruction( | |
| JupiterSwapInstruction::parse_amounts_and_slippage_from_data(data)?; | ||
|
|
||
| let in_token = accounts.first().map(|addr| get_token_info(addr, in_amount)); | ||
| let out_token = accounts.get(1).map(|addr| get_token_info(addr, out_amount)); | ||
| let out_token = accounts.get(5).map(|addr| get_token_info(addr, out_amount)); | ||
|
|
||
| Ok(JupiterSwapInstruction::ExactOutRoute { | ||
| in_token, | ||
|
|
@@ -237,7 +237,7 @@ fn parse_shared_accounts_route_instruction( | |
| JupiterSwapInstruction::parse_amounts_and_slippage_from_data(data)?; | ||
|
|
||
| let in_token = accounts.first().map(|addr| get_token_info(addr, in_amount)); | ||
| let out_token = accounts.get(1).map(|addr| get_token_info(addr, out_amount)); | ||
| let out_token = accounts.get(5).map(|addr| get_token_info(addr, out_amount)); | ||
|
|
||
| Ok(JupiterSwapInstruction::SharedAccountsRoute { | ||
| in_token, | ||
|
|
@@ -349,8 +349,7 @@ fn create_jupiter_swap_expanded_fields( | |
| .map_err(|e| VisualSignError::ConversionError(e.to_string()))?, | ||
| create_text_field("Input Token Name", &token.name) | ||
| .map_err(|e| VisualSignError::ConversionError(e.to_string()))?, | ||
| create_text_field("Input Token Address", &token.address) | ||
| .map_err(|e| VisualSignError::ConversionError(e.to_string()))?, | ||
| // TODO: Add back Input Token Address | ||
| ]); | ||
| } | ||
|
|
||
|
|
@@ -759,4 +758,5 @@ mod tests { | |
| ); | ||
| println!("✅ Platform Fee field present in expanded fields"); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm okay with these emojis in tests but we should avoid
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is an exisitng test
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All the println! in mod.rs are in tests
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ok, great, I was just looking at the small diff |
||
| } | ||
| mod fixture_test; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why is this here?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I saw this comment of yours in the template file.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If I remove this, the test doesn't get included in the suite
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ok, you want to put them at the top of the file or in the mod.rs |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,236 @@ | ||
| // Fixture-based tests for Jupiter Swap instruction parsing | ||
| // See /src/chain_parsers/visualsign-solana/TESTING.md for documentation | ||
| // | ||
| // To add these tests to the existing tests module in mod.rs, add this line at the end | ||
| // of the existing `mod tests` block (before the closing brace): | ||
| // | ||
| // mod fixture_tests; | ||
| // | ||
| // This file will then be compiled as `tests::fixture_tests` | ||
|
|
||
| use super::*; | ||
| use solana_sdk::{ | ||
| instruction::{AccountMeta, Instruction}, | ||
| pubkey::Pubkey, | ||
| }; | ||
| use std::str::FromStr; | ||
| use visualsign::SignablePayloadField; | ||
|
|
||
| #[derive(Debug, serde::Deserialize)] | ||
| struct TestFixture { | ||
| description: String, | ||
| source: String, | ||
| signature: String, | ||
| cluster: String, | ||
| #[serde(default)] | ||
| full_transaction_note: Option<String>, | ||
| #[allow(dead_code)] | ||
| instruction_index: usize, | ||
| instruction_data: String, | ||
| program_id: String, | ||
| accounts: Vec<TestAccount>, | ||
| expected_fields: serde_json::Map<String, serde_json::Value>, | ||
| } | ||
|
|
||
| #[derive(Debug, serde::Deserialize)] | ||
| struct TestAccount { | ||
| pubkey: String, | ||
| signer: bool, | ||
| writable: bool, | ||
| #[allow(dead_code)] | ||
| description: String, | ||
| } | ||
|
|
||
| fn load_fixture(name: &str) -> TestFixture { | ||
| let fixture_path = format!( | ||
| "{}/tests/fixtures/jupiter_swap/{}.json", | ||
| env!("CARGO_MANIFEST_DIR"), | ||
| name | ||
| ); | ||
| let fixture_content = std::fs::read_to_string(&fixture_path) | ||
| .unwrap_or_else(|e| panic!("Failed to read fixture {fixture_path}: {e}")); | ||
| serde_json::from_str(&fixture_content) | ||
| .unwrap_or_else(|e| panic!("Failed to parse fixture {fixture_path}: {e}")) | ||
| } | ||
|
|
||
| fn create_instruction_from_fixture(fixture: &TestFixture) -> Instruction { | ||
| let program_id = Pubkey::from_str(&fixture.program_id).unwrap(); | ||
| let accounts: Vec<AccountMeta> = fixture | ||
| .accounts | ||
| .iter() | ||
| .map(|acc| { | ||
| let pubkey = Pubkey::from_str(&acc.pubkey).unwrap(); | ||
| AccountMeta { | ||
| pubkey, | ||
| is_signer: acc.signer, | ||
| is_writable: acc.writable, | ||
| } | ||
| }) | ||
| .collect(); | ||
|
|
||
| // Instruction data from JSON RPC responses is base58 encoded | ||
| let data = bs58::decode(&fixture.instruction_data) | ||
| .into_vec() | ||
| .expect("Failed to decode base58 instruction data"); | ||
|
|
||
| Instruction { | ||
| program_id, | ||
| accounts, | ||
| data, | ||
| } | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_route_real_transaction() { | ||
| use crate::core::VisualizerContext; | ||
| use solana_parser::solana::structs::SolanaAccount; | ||
|
|
||
| let fixture: TestFixture = load_fixture("sample_route"); | ||
| println!("\n=== Testing Real Transaction ==="); | ||
| println!("Description: {}", fixture.description); | ||
| println!("Source: {}", fixture.source); | ||
| println!("Signature: {}", fixture.signature); | ||
| println!("Cluster: {}", fixture.cluster); | ||
| if let Some(note) = &fixture.full_transaction_note { | ||
| println!("Transaction Context: {note}"); | ||
| } | ||
| println!(); | ||
|
|
||
| let instruction = create_instruction_from_fixture(&fixture); | ||
| let instructions = vec![instruction.clone()]; | ||
|
|
||
| // Create a context - using index 0 since we only loaded the one relevant instruction | ||
| // In reality, the fixture.instruction_index would be used with all transaction instructions | ||
| let sender = SolanaAccount { | ||
| account_key: fixture.accounts.first().unwrap().pubkey.clone(), | ||
| signer: false, | ||
| writable: false, | ||
| }; | ||
| let context = VisualizerContext::new(&sender, 0, &instructions); | ||
|
|
||
| // Visualize | ||
| let visualizer = super::JupiterSwapVisualizer; | ||
| let result = visualizer | ||
| .visualize_tx_commands(&context) | ||
| .expect("Failed to visualize instruction"); | ||
|
|
||
| // Extract the preview layout | ||
| if let SignablePayloadField::PreviewLayout { | ||
| common, | ||
| preview_layout, | ||
| } = result.signable_payload_field | ||
| { | ||
| println!("\n=== Extracted Fields ==="); | ||
| println!("Label: {}", common.label); | ||
| if let Some(title) = &preview_layout.title { | ||
| println!("Title: {}", title.text); | ||
| } | ||
|
|
||
| if let Some(expanded) = &preview_layout.expanded { | ||
| println!("\nExpanded Fields:"); | ||
| for field in &expanded.fields { | ||
| match &field.signable_payload_field { | ||
| SignablePayloadField::TextV2 { common, text_v2 } => { | ||
| println!(" {}: {}", common.label, text_v2.text); | ||
| } | ||
| SignablePayloadField::Number { common, number } => { | ||
| println!(" {}: {}", common.label, number.number); | ||
| } | ||
| SignablePayloadField::AmountV2 { common, amount_v2 } => { | ||
| println!(" {}: {}", common.label, amount_v2.amount); | ||
| } | ||
| _ => {} | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Validate against expected fields | ||
| println!("\n=== Validation ==="); | ||
| for (key, expected_value) in &fixture.expected_fields { | ||
| let expected_str = expected_value | ||
| .as_str() | ||
| .unwrap_or_else(|| panic!("Expected field '{key}' is not a string")); | ||
|
|
||
| if let Some(expanded) = &preview_layout.expanded { | ||
| let found = | ||
| expanded | ||
| .fields | ||
| .iter() | ||
| .any(|field| match &field.signable_payload_field { | ||
| SignablePayloadField::TextV2 { common, text_v2 } => { | ||
| let label_normalized = | ||
| common.label.to_lowercase().replace(" ", "_"); | ||
| let key_normalized = key.to_lowercase(); | ||
| let label_matches = label_normalized == key_normalized; | ||
| let value_matches = text_v2.text == expected_str; | ||
|
|
||
| if label_matches { | ||
| if value_matches { | ||
| println!("✓ {key}: {expected_str} (matches)"); | ||
| } else { | ||
| println!( | ||
| "✗ {}: expected '{}', got '{}'", | ||
| key, expected_str, text_v2.text | ||
| ); | ||
| } | ||
| return value_matches; | ||
| } | ||
| false | ||
| } | ||
| SignablePayloadField::Number { common, number } => { | ||
| let label_normalized = | ||
| common.label.to_lowercase().replace(" ", "_"); | ||
| let key_normalized = key.to_lowercase(); | ||
| let label_matches = label_normalized == key_normalized; | ||
| let value_matches = number.number == expected_str; | ||
|
|
||
| if label_matches { | ||
| if value_matches { | ||
| println!("✓ {key}: {expected_str} (matches)"); | ||
| } else { | ||
| println!( | ||
| "✗ {}: expected '{}', got '{}'", | ||
| key, expected_str, number.number | ||
| ); | ||
| } | ||
| return value_matches; | ||
| } | ||
| false | ||
| } | ||
| SignablePayloadField::AmountV2 { common, amount_v2 } => { | ||
| let label_normalized = | ||
| common.label.to_lowercase().replace(" ", "_"); | ||
| let key_normalized = key.to_lowercase(); | ||
| let label_matches = label_normalized == key_normalized; | ||
| let value_matches = amount_v2.amount == expected_str; | ||
|
|
||
| if label_matches { | ||
| if value_matches { | ||
| println!("✓ {key}: {expected_str} (matches)"); | ||
| } else { | ||
| println!( | ||
| "✗ {}: expected '{}', got '{}'", | ||
| key, expected_str, amount_v2.amount | ||
| ); | ||
| } | ||
| return value_matches; | ||
| } | ||
| false | ||
| } | ||
| _ => false, | ||
| }); | ||
|
|
||
| if !found { | ||
| println!("✗ {key}: field not found in output"); | ||
| } | ||
|
|
||
| assert!( | ||
| found, | ||
| "Expected field '{key}' with value '{expected_str}' not found in visualization" | ||
| ); | ||
| } | ||
| } | ||
| } else { | ||
| panic!("Expected PreviewLayout field type"); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this needs a comment that this is hardcoded to 5