Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions src/chain_parsers/visualsign-solana/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ spl-stake-pool = "2.0.2"
solana-system-interface = "1.0"

[dev-dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
jupiter-swap-api-client = "0.2.0"
base64 = "0.22.1"
bs58 = "0.5"
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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));
Copy link
Copy Markdown
Contributor

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


Ok(JupiterSwapInstruction::SharedAccountsRoute {
in_token,
Expand Down Expand Up @@ -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
]);
}

Expand Down Expand Up @@ -759,4 +758,5 @@ mod tests {
);
println!("✅ Platform Fee field present in expanded fields");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm okay with these emojis in tests but we should avoid println! in non test code

Copy link
Copy Markdown
Contributor Author

@hassan-anchor hassan-anchor Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is an exisitng test

Copy link
Copy Markdown
Contributor Author

@hassan-anchor hassan-anchor Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All the println! in mod.rs are in tests

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I saw this comment of yours in the template file.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// 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`

If I remove this, the test doesn't get included in the suite

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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");
}
}
Loading