Skip to content
Draft
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
5 changes: 5 additions & 0 deletions proto/parser/parser.proto
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ message ParsedTransactionPayload {
string metadata_digest = 3;
// Legacy field. Will be removed, please do not use!
string signable_payload = 4;
// Chain-specific intermediate output for downstream policy evaluation.
// Borsh-serialized; schema is defined per chain in Rust. Unset for chains
// that do not produce one. Solana uses
// `visualsign_solana::intermediate::SolanaIntermediateOutput`.
optional bytes intermediate_output = 5;
}

message ParsedTransaction {
Expand Down
69 changes: 68 additions & 1 deletion src/Cargo.lock

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

18 changes: 12 additions & 6 deletions src/chain_parsers/visualsign-ethereum/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use visualsign::{
encodings::SupportedEncodings,
registry::LayeredRegistry,
vsptrait::{
DeveloperConfig, Transaction, TransactionParseError, VisualSignConverter,
ConversionResult, DeveloperConfig, Transaction, TransactionParseError, VisualSignConverter,
VisualSignConverterFromString, VisualSignError, VisualSignOptions,
},
};
Expand Down Expand Up @@ -253,8 +253,10 @@ impl VisualSignConverter<EthereumTransactionWrapper> for EthereumVisualSignConve
&self,
transaction_wrapper: EthereumTransactionWrapper,
options: VisualSignOptions,
) -> Result<SignablePayload, VisualSignError> {
self.convert_transaction_inner(transaction_wrapper.inner().clone(), options)
) -> Result<ConversionResult, VisualSignError> {
let payload =
self.convert_transaction_inner(transaction_wrapper.inner().clone(), options)?;
Ok(ConversionResult::new(payload))
}
}

Expand All @@ -263,7 +265,7 @@ impl VisualSignConverterFromString<EthereumTransactionWrapper> for EthereumVisua
&self,
transaction_data: &str,
options: VisualSignOptions,
) -> Result<SignablePayload, VisualSignError> {
) -> Result<ConversionResult, VisualSignError> {
let wrapper = EthereumTransactionWrapper::from_string_with_options(
transaction_data,
options.developer_config.as_ref(),
Expand Down Expand Up @@ -608,15 +610,19 @@ pub fn transaction_to_visual_sign(
) -> Result<SignablePayload, VisualSignError> {
let wrapper = EthereumTransactionWrapper::new(transaction);
let converter = EthereumVisualSignConverter::new();
converter.to_visual_sign_payload(wrapper, options)
converter
.to_visual_sign_payload(wrapper, options)
.map(|r| r.payload)
}

pub fn transaction_string_to_visual_sign(
transaction_data: &str,
options: VisualSignOptions,
) -> Result<SignablePayload, VisualSignError> {
let converter = EthereumVisualSignConverter::new();
converter.to_visual_sign_payload_from_string(transaction_data, options)
converter
.to_visual_sign_payload_from_string(transaction_data, options)
.map(|r| r.payload)
}

#[cfg(test)]
Expand Down
3 changes: 2 additions & 1 deletion src/chain_parsers/visualsign-ethereum/tests/lib_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,8 @@ fn test_abi_from_metadata_decodes_function() {
let converter = EthereumVisualSignConverter::new();
let result = converter
.to_visual_sign_payload_from_string(&tx_hex, options)
.unwrap();
.unwrap()
.payload;

// The ABI from metadata should decode the function name.
// Without abi_mappings, this address is unknown and would show raw hex.
Expand Down
1 change: 1 addition & 0 deletions src/chain_parsers/visualsign-solana/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ jupiter-swap-api-client = "0.2.0"
base64 = "0.22.1"
bs58 = "0.5"
proptest = "1"
cel-interpreter = { version = "0.10.0", features = ["json"] }

[lints]
workspace = true
90 changes: 67 additions & 23 deletions src/chain_parsers/visualsign-solana/src/core/visualsign.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@ use visualsign::{
SignablePayload, SignablePayloadField, SignablePayloadFieldCommon,
encodings::SupportedEncodings,
vsptrait::{
Transaction, TransactionParseError, VisualSignConverter, VisualSignConverterFromString,
VisualSignError, VisualSignOptions,
ConversionResult, Transaction, TransactionParseError, VisualSignConverter,
VisualSignConverterFromString, VisualSignError, VisualSignOptions,
},
};

use crate::intermediate::extract_solana_intermediate_output;

/// Wrapper around Solana's transaction types that implements the Transaction trait
#[derive(Debug, Clone)]
pub enum SolanaTransactionWrapper {
Expand Down Expand Up @@ -158,26 +160,62 @@ impl VisualSignConverter<SolanaTransactionWrapper> for SolanaVisualSignConverter
&self,
transaction_wrapper: SolanaTransactionWrapper,
options: VisualSignOptions,
) -> Result<SignablePayload, VisualSignError> {
match transaction_wrapper {
) -> Result<ConversionResult, VisualSignError> {
let idl_registry = create_idl_registry_from_options(&options)?;

let (payload, message_hex) = match &transaction_wrapper {
SolanaTransactionWrapper::Legacy(transaction) => {
// Convert the legacy transaction to a VisualSign payload
convert_to_visual_sign_payload(
&transaction,
let payload = convert_to_visual_sign_payload(
transaction,
options.decode_transfers,
options.transaction_name.clone(),
&options,
)
)?;
let hex = hex::encode(transaction.message.serialize());
(payload, hex)
}
SolanaTransactionWrapper::Versioned(versioned_tx) => {
// Handle versioned transactions
convert_versioned_to_visual_sign_payload(
&versioned_tx,
let payload = convert_versioned_to_visual_sign_payload(
versioned_tx,
options.decode_transfers,
options.transaction_name.clone(),
&options,
)
)?;
let hex = hex::encode(versioned_tx.message.serialize());
(payload, hex)
}
};

let intermediate_bytes = build_intermediate_bytes(&message_hex, &idl_registry);
Ok(match intermediate_bytes {
Some(bytes) => ConversionResult::with_intermediate(payload, bytes),
None => ConversionResult::new(payload),
})
}
}

/// Build the borsh-encoded intermediate output for a Solana transaction.
///
/// Best-effort: if `solana_parser::parse_transaction_with_idls` cannot parse
/// the message (e.g. an obscure variant we still display via fallback paths)
/// we drop the intermediate output rather than fail the whole conversion. The
/// SignablePayload is still returned so visual signing keeps working; only
/// policy-engine evaluation degrades to "no metadata".
fn build_intermediate_bytes(
message_hex: &str,
idl_registry: &crate::idl::IdlRegistry,
) -> Option<Vec<u8>> {
match extract_solana_intermediate_output(message_hex, false, idl_registry) {
Ok(output) => match borsh::to_vec(&output) {
Ok(bytes) => Some(bytes),
Err(err) => {
tracing::warn!("Failed to borsh-encode Solana intermediate output: {err}");
None
}
},
Err(err) => {
tracing::warn!("Failed to extract Solana intermediate output: {err}");
None
}
}
}
Expand All @@ -191,25 +229,30 @@ pub fn transaction_to_visual_sign(
) -> Result<SignablePayload, VisualSignError> {
SolanaVisualSignConverter
.to_visual_sign_payload(SolanaTransactionWrapper::new_legacy(transaction), options)
.map(|r| r.payload)
}

/// Public API function for versioned transactions
pub fn versioned_transaction_to_visual_sign(
transaction: VersionedTransaction,
options: VisualSignOptions,
) -> Result<SignablePayload, VisualSignError> {
SolanaVisualSignConverter.to_visual_sign_payload(
SolanaTransactionWrapper::new_versioned(transaction),
options,
)
SolanaVisualSignConverter
.to_visual_sign_payload(
SolanaTransactionWrapper::new_versioned(transaction),
options,
)
.map(|r| r.payload)
}

/// Public API function for string-based transactions
pub fn transaction_string_to_visual_sign(
transaction_data: &str,
options: VisualSignOptions,
) -> Result<SignablePayload, VisualSignError> {
SolanaVisualSignConverter.to_visual_sign_payload_from_string(transaction_data, options)
SolanaVisualSignConverter
.to_visual_sign_payload_from_string(transaction_data, options)
.map(|r| r.payload)
}

/// Convert Solana transaction to visual sign payload
Expand Down Expand Up @@ -464,7 +507,7 @@ mod tests {
}
assert!(payload_result.is_ok());

let payload = payload_result.unwrap();
let payload = payload_result.unwrap().payload;

// Verify basic payload properties
assert_eq!(payload.title, "Solana Transaction");
Expand Down Expand Up @@ -547,7 +590,7 @@ mod tests {
}
assert!(payload_result.is_ok());

let payload = payload_result.unwrap();
let payload = payload_result.unwrap().payload;

// Verify basic payload properties
assert_eq!(payload.title, "V0 Transaction");
Expand Down Expand Up @@ -710,7 +753,7 @@ mod tests {
);

assert!(legacy_payload_result.is_ok());
let legacy_payload = legacy_payload_result.unwrap();
let legacy_payload = legacy_payload_result.unwrap().payload;

// Check for transfer fields in legacy transaction
let legacy_has_transfers = legacy_payload
Expand Down Expand Up @@ -754,7 +797,7 @@ mod tests {
);

assert!(v0_payload_result.is_ok());
let v0_payload = v0_payload_result.unwrap();
let v0_payload = v0_payload_result.unwrap().payload;

// Check for transfer fields in V0 transaction
let v0_has_transfers = v0_payload
Expand Down Expand Up @@ -881,7 +924,8 @@ mod tests {
);

match payload_result {
Ok(payload) => {
Ok(conversion) => {
let payload = conversion.payload;
println!(
"✅ V0 transaction conversion succeeded with {} fields",
payload.fields.len()
Expand Down Expand Up @@ -1036,7 +1080,7 @@ mod tests {
"Should convert TokenKeg transaction to payload"
);

let payload = payload_result.unwrap();
let payload = payload_result.unwrap().payload;

// Verify we have instruction fields (should not be empty)
let instruction_fields: Vec<_> = payload
Expand Down
Loading
Loading