fix: address Solana review findings
What changed, and why it matters
This commit improves how Keystone 3's Solana app recognizes and displays certain transaction types. It adds support for the Solana Compute Budget program so those instructions are no longer labeled as 'Unknown,' and it adds parsing for Solana off-chain message signing so those messages are shown as human-readable text instead of being mistaken for raw transactions. The UI now shows unknown program addresses separately after the main transaction overview, and USB error messages can display more specific failure text. The commit title says it addresses 'Solana review findings,' but the commit message itself does not explicitly call these security fixes.
Treat this as a hardening and usability improvement rather than an urgent vulnerability fix. Firmware maintainers should verify that the new Compute Budget parser rejects all malformed or out-of-spec instructions, that off-chain message parsing cannot be confused with a real transaction, and that the new unknown-programs UI section is consistently rendered across all Solana transaction flows. End users should update when a release containing this commit is available, especially if they sign Solana transactions with compute-budget instructions or off-chain messages.
Security signals we found
Previously unknown Compute Budget instructions are now parsed and labeled, reducing the chance a user approves a transaction they cannot understand.
Off-chain signing messages are parsed with format and length validation, so wallet-connect-style sign-in messages are shown as text rather than misinterpreted as transactions.
Unknown program accounts are surfaced explicitly in the UI instead of being hidden inside a generic 'Unknown' instruction list.
USB signing failures can now show a specific backend error message instead of a generic unknown-error title.
No explicit security advisory, CVE, or researcher attribution is present in the commit or supplied references.
Evidence from the diff
The patch makes several coordinated changes in the Solana transaction parser and UI layer. In rust/apps/solana, it adds a new SupportedProgram variant for ComputeBudget111111111111111111111111111111 with a strict instruction parser for the five known compute-budget instruction layouts. It also introduces parse_offchain_message_v0 to detect Solana off-chain message v0 packets (domain-prefixed, length-prefixed, restricted-ASCII/UTF8 formats) and route them through parse_message as displayable text rather than as transaction bytes. The parser now excludes ComputeBudget and Unknown details when deciding the transaction display type, collects unknown program account addresses into a new unknown_programs field, and passes them to the C UI. The C UI removes the old ‘Unknown’ warning card from the general overview and instead appends a dedicated ‘Additional Unknown Programs’ section after the primary overview. Finally, the USB transport error fallback now prefers a concrete error_message when one is provided.
Changed components
rust/apps/solana/src/instruction.rsrust/apps/solana/src/lib.rsrust/apps/solana/src/parser/detail.rsrust/apps/solana/src/parser/mod.rsrust/apps/solana/src/parser/structs.rsrust/rust_c/src/solana/structs.rssrc/ui/gui_chain/multi/web3/gui_sol.csrc/ui/gui_views/multi/gui_key_derivation_request_view.csrc/ui/gui_widgets/multi/gui_usb_transport_widgets.cui_simulator/simulator_model.c.github/workflows/rust-solana-checks.ymlInspect captured patch +343 / −57
diff --git a/.github/workflows/rust-solana-checks.yml b/.github/workflows/rust-solana-checks.yml
index 867cde3..452dea3 100644
--- a/.github/workflows/rust-solana-checks.yml
+++ b/.github/workflows/rust-solana-checks.yml
@@ -27,4 +27,4 @@ jobs:
uses: taiki-e/install-action@cargo-llvm-cov
- name: Run rust/apps/solana
- run: cd rust/apps/solana && cargo +$RUST_TOOLCHAIN llvm-cov --fail-under-regions 73 --fail-under-functions 73 --fail-under-lines 71 --ignore-filename-regex 'keystore/*|utils/*'
+ run: cd rust/apps/solana && cargo +$RUST_TOOLCHAIN llvm-cov --fail-under-regions 74 --fail-under-functions 73 --fail-under-lines 72 --ignore-filename-regex 'keystore/*|utils/*'
diff --git a/rust/apps/solana/src/instruction.rs b/rust/apps/solana/src/instruction.rs
index 34093aa..d7a3ca4 100644
--- a/rust/apps/solana/src/instruction.rs
+++ b/rust/apps/solana/src/instruction.rs
@@ -4,7 +4,9 @@ use alloc::vec::Vec;
use crate::compact::Compact;
use crate::errors::SolanaError::ProgramError;
use crate::errors::{Result, SolanaError};
-use crate::parser::detail::SolanaDetail;
+use crate::parser::detail::{
+ CommonDetail, ProgramDetail, ProgramDetailComputeBudget, SolanaDetail,
+};
use crate::resolvers;
use crate::solana_lib::solana_program::stake::instruction::StakeInstruction;
use crate::solana_lib::solana_program::system_instruction::SystemInstruction;
@@ -44,6 +46,7 @@ enum SupportedProgram {
TokenLendingProgram,
SquadsProgramV4,
JupiterProgramV6,
+ ComputeBudgetProgram,
}
impl SupportedProgram {
@@ -62,6 +65,9 @@ impl SupportedProgram {
}
"SQDS4ep65T869zMMBKyuUq6aD6EgTu8psMjkvj52pCf" => Ok(SupportedProgram::SquadsProgramV4),
"JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4" => Ok(SupportedProgram::JupiterProgramV6),
+ "ComputeBudget111111111111111111111111111111" => {
+ Ok(SupportedProgram::ComputeBudgetProgram)
+ }
x => Err(SolanaError::UnsupportedProgram(x.to_string())),
}
}
@@ -128,7 +134,76 @@ impl Instruction {
.map_err(|e| ProgramError(e.to_string()))?;
resolvers::jupiter_v6::resolve(instruction, accounts)
}
+ SupportedProgram::ComputeBudgetProgram => {
+ Self::parse_compute_budget_instruction(self.data.as_slice(), accounts)
+ }
+ }
+ }
+
+ fn parse_compute_budget_instruction(
+ data: &[u8],
+ accounts: Vec<String>,
+ ) -> Result<SolanaDetail> {
+ if !accounts.is_empty() || data.is_empty() {
+ return Err(SolanaError::InvalidData(
+ "invalid compute budget instruction".to_string(),
+ ));
}
+ let mut detail = ProgramDetailComputeBudget::default();
+ let method = match data[0] {
+ 0 if data.len() == 9 => {
+ detail.compute_unit_limit =
+ u32::from_le_bytes(data[1..5].try_into().map_err(|_| {
+ SolanaError::InvalidData("invalid compute unit limit".to_string())
+ })?)
+ .to_string();
+ detail.additional_fee_lamports =
+ u32::from_le_bytes(data[5..9].try_into().map_err(|_| {
+ SolanaError::InvalidData("invalid additional fee".to_string())
+ })?)
+ .to_string();
+ "RequestUnitsDeprecated"
+ }
+ 1 if data.len() == 5 => {
+ detail.heap_frame_bytes = Self::read_compute_budget_u32(&data[1..5])?.to_string();
+ "RequestHeapFrame"
+ }
+ 2 if data.len() == 5 => {
+ detail.compute_unit_limit = Self::read_compute_budget_u32(&data[1..5])?.to_string();
+ "SetComputeUnitLimit"
+ }
+ 3 if data.len() == 9 => {
+ detail.compute_unit_price_micro_lamports =
+ u64::from_le_bytes(data[1..9].try_into().map_err(|_| {
+ SolanaError::InvalidData("invalid compute unit price".to_string())
+ })?)
+ .to_string();
+ "SetComputeUnitPrice"
+ }
+ 4 if data.len() == 5 => {
+ detail.loaded_accounts_data_size_limit =
+ Self::read_compute_budget_u32(&data[1..5])?.to_string();
+ "SetLoadedAccountsDataSizeLimit"
+ }
+ _ => {
+ return Err(SolanaError::InvalidData(
+ "unsupported compute budget instruction".to_string(),
+ ));
+ }
+ };
+ Ok(SolanaDetail {
+ common: CommonDetail {
+ program: "ComputeBudget".to_string(),
+ method: method.to_string(),
+ },
+ kind: ProgramDetail::ComputeBudget(detail),
+ })
+ }
+
+ fn read_compute_budget_u32(data: &[u8]) -> Result<u32> {
+ Ok(u32::from_le_bytes(data.try_into().map_err(|_| {
+ SolanaError::InvalidData("invalid compute budget value".to_string())
+ })?))
}
fn parse_native_program_instruction<T: for<'de> serde::de::Deserialize<'de>>(
@@ -148,7 +223,7 @@ impl Instruction {
mod tests {
use super::*;
- const PROGRAM_IDS: [&str; 8] = [
+ const PROGRAM_IDS: [&str; 9] = [
"11111111111111111111111111111111",
"Vote111111111111111111111111111111111111111",
"Stake11111111111111111111111111111111111111",
@@ -157,6 +232,7 @@ mod tests {
"LendZqTs7gn5CTSJU1jWKhKuVpjJGom45nnwPb2AMTi",
"SQDS4ep65T869zMMBKyuUq6aD6EgTu8psMjkvj52pCf",
"JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4",
+ "ComputeBudget111111111111111111111111111111",
];
#[test]
@@ -185,4 +261,29 @@ mod tests {
assert!(instruction.parse(program_id, vec![]).is_err());
}
}
+
+ #[test]
+ fn parses_compute_budget_instructions_strictly() {
+ let cases = [
+ (vec![0, 64, 13, 3, 0, 7, 0, 0, 0], "RequestUnitsDeprecated"),
+ (vec![1, 0, 0, 4, 0], "RequestHeapFrame"),
+ (vec![2, 64, 13, 3, 0], "SetComputeUnitLimit"),
+ (vec![3, 5, 0, 0, 0, 0, 0, 0, 0], "SetComputeUnitPrice"),
+ (vec![4, 0, 0, 16, 0], "SetLoadedAccountsDataSizeLimit"),
+ ];
+ for (data, method) in cases {
+ let parsed = Instruction::parse_compute_budget_instruction(&data, vec![]).unwrap();
+ assert_eq!("ComputeBudget", parsed.common.program);
+ assert_eq!(method, parsed.common.method);
+ }
+
+ assert!(
+ Instruction::parse_compute_budget_instruction(&[2, 1, 0, 0, 0, 0], vec![]).is_err()
+ );
+ assert!(Instruction::parse_compute_budget_instruction(
+ &[2, 1, 0, 0, 0],
+ vec!["unexpected".to_string()]
+ )
+ .is_err());
+ }
}
diff --git a/rust/apps/solana/src/lib.rs b/rust/apps/solana/src/lib.rs
index fe825ab..89ad9cf 100644
--- a/rust/apps/solana/src/lib.rs
+++ b/rust/apps/solana/src/lib.rs
@@ -33,15 +33,58 @@ mod resolvers;
mod solana_lib;
pub mod structs;
pub mod utils;
+
+const OFFCHAIN_MESSAGE_DOMAIN: &[u8; 16] = b"\xffsolana offchain";
+
+fn parse_offchain_message_v0(data: &[u8]) -> Option<&str> {
+ const PREAMBLE_LEN: usize = 20;
+ const RESTRICTED_ASCII: u8 = 0;
+ const UTF8_1232_BYTES_MAX: u8 = 1;
+ const UTF8_65535_BYTES_MAX: u8 = 2;
+
+ if data.len() < PREAMBLE_LEN
+ || &data[..OFFCHAIN_MESSAGE_DOMAIN.len()] != OFFCHAIN_MESSAGE_DOMAIN
+ {
+ return None;
+ }
+ let version = data[16];
+ let format = data[17];
+ let message_len = u16::from_le_bytes([data[18], data[19]]) as usize;
+ let message = &data[PREAMBLE_LEN..];
+ if version != 0 || message.is_empty() || message.len() != message_len {
+ return None;
+ }
+ match format {
+ RESTRICTED_ASCII => {
+ if message_len > 1232
+ || !message
+ .iter()
+ .all(|byte| *byte == b'\n' || (0x20..=0x7e).contains(byte))
+ {
+ return None;
+ }
+ }
+ UTF8_1232_BYTES_MAX if message_len <= 1232 => {}
+ UTF8_65535_BYTES_MAX => {}
+ _ => return None,
+ }
+ core::str::from_utf8(message).ok()
+}
+
pub fn parse_message(tx_hex: Vec<u8>, from_key: &String) -> errors::Result<SolanaMessage> {
// Keep only the representation that is displayed. Retaining both the
// UTF-8 string and its hex encoding doubles peak memory for long messages.
- let (raw_message, utf8_message) = match String::from_utf8(tx_hex) {
- Ok(message) if !message.as_bytes().contains(&0) && !app_utils::is_cjk(&message) => {
- (String::new(), message)
- }
- Ok(message) => (hex::encode(message.as_bytes()), String::new()),
- Err(error) => (hex::encode(error.as_bytes()), String::new()),
+ let offchain_message = parse_offchain_message_v0(&tx_hex).map(ToString::to_string);
+ let (raw_message, utf8_message) = match offchain_message {
+ Some(message) if !app_utils::is_cjk(&message) => (String::new(), message),
+ Some(_) => (hex::encode(tx_hex), String::new()),
+ None => match String::from_utf8(tx_hex) {
+ Ok(message) if !message.as_bytes().contains(&0) && !app_utils::is_cjk(&message) => {
+ (String::new(), message)
+ }
+ Ok(message) => (hex::encode(message.as_bytes()), String::new()),
+ Err(error) => (hex::encode(error.as_bytes()), String::new()),
+ },
};
SolanaMessage::from(raw_message, utf8_message, from_key)
}
@@ -114,6 +157,34 @@ mod tests {
assert_eq!("GWZVzcS2MXfFqHmP782Qx3RzkkQX2KfgchZPLp3AEZrm", parsed.from);
}
+ #[test]
+ fn test_solana_offchain_message_is_not_treated_as_transaction() {
+ let data = hex::decode("ff736f6c616e61206f6666636861696e000127026d616769636564656e2e696f2077616e747320796f7520746f207369676e20696e207769746820796f757220536f6c616e61206163636f756e743a0a4841676b31344a704d514c6774367256677637634251464a5746746f3544717869343732755433444b70716b0a0a57656c636f6d6520746f204d61676963204564656e2e205369676e696e6720697320746865206f6e6c79207761792077652063616e207472756c79206b6e6f77207468617420796f752061726520746865206f776e6572206f66207468652077616c6c657420796f752061726520636f6e6e656374696e672e205369676e696e67206973206120736166652c206761732d6c657373207472616e73616374696f6e207468617420646f6573206e6f7420696e20616e79207761792067697665204d61676963204564656e207065726d697373696f6e20746f20706572666f726d20616e79207472616e73616374696f6e73207769746820796f75722077616c6c65742e0a0a5552493a2068747470733a2f2f6d616769636564656e2e696f2f6d61726b6574706c6163652f73636f6f6279647269700a56657273696f6e3a20310a4e6f6e63653a2037326131633866623062393134376165383631363130303632393330383563620a4973737565642041743a20323032362d30382d30335430373a30373a33382e3234335a0a526571756573742049443a2063313331346235622d656365382d346234662d613837392d333839346464613336346534").unwrap();
+ assert_eq!(571, data.len());
+ assert!(!validate_tx(&mut data.clone()));
+ assert!(!has_tx_prefix(&mut data.clone()));
+
+ let parsed = parse_message(data.clone(), &String::new()).unwrap();
+ assert!(parsed.raw_message.is_empty());
+ assert!(parsed
+ .utf8_message
+ .starts_with("magiceden.io wants you to sign in"));
+ assert!(parsed
+ .utf8_message
+ .ends_with("c1314b5b-ece8-4b4f-a879-3894dda364e4"));
+
+ let seed = hex::decode("5eb00bbddcf069084889a8ab9155568165f5c453ccb85e70811aaed6f6da5fc19a5ac40b389cd370d086206dec8aa6c43daea6690f20ad3d8d48b2d2ce9e38e4").unwrap();
+ assert!(sign(data, &"m/44'/501'/0'".to_string(), &seed).is_ok());
+ }
+
+ #[test]
+ fn malformed_offchain_message_falls_back_to_raw_hex() {
+ let data = b"\xffsolana offchain\x00\x01\x05\x00test".to_vec();
+ let parsed = parse_message(data.clone(), &String::new()).unwrap();
+ assert!(parsed.utf8_message.is_empty());
+ assert_eq!(hex::encode(data), parsed.raw_message);
+ }
+
#[test]
fn test_solana_version_message() {
let mut buffer = hex::decode("8001000308ad7dba70a9559a85961a00a0d00e7caf6967ff28c5861360cd8e22abcbb9b3eb080a39da2d35d3c666ff3d5195bb9065ed97a2208b4245bc70e69ff4b1d2b73fff21b6871e314c96e0848ba15117ec8cef8433b5a594ff2b88798b7e6d37b84e0a0cc6b26d4216a826e48cbb719136037e76cc273efb63ec73a6f2cb56cfa6015f376f7759fe1a4270a9b568d86110869cb073c8580adc986a178c876c18ee3d0306466fe5211732ffecadba72c39be7bc8ce5bbc5f7126b2c439b3a4000000004b2acb11258cce3682c418ba872ff3df91102712f15af12b6be69b3435b0008eaa020c61cc479712813461ce153894a96a6c00b21ed0cfc2798d1f9a9e9c94a5b6bfd38a1d64b54c65490c10b1be53496be5ede8d18086335aade667915e9e90705000502400d03000500090310010a00000000000606080d0706060e0802da8aeb4fc9196606030d010808218493e497c04859060b0001090802030d0f101112098c90fd150a4af80300060c00010d09080a0b0c04061113104b5d5ddc2296dac4ffffffffffffffff060b0001090802030d0f101112098c90fd150a4af803000110aaf654f15c22d8c9aa80622becb4872b0f5a52cc8f467d8a0da58b19197d690540024544420701757403060704").unwrap();
diff --git a/rust/apps/solana/src/parser/detail.rs b/rust/apps/solana/src/parser/detail.rs
index 7b3212c..d2755b6 100644
--- a/rust/apps/solana/src/parser/detail.rs
+++ b/rust/apps/solana/src/parser/detail.rs
@@ -28,6 +28,20 @@ pub struct ProgramDetailSystemTransfer {
pub to: String,
}
+#[derive(Debug, Clone, Default, Serialize)]
+pub struct ProgramDetailComputeBudget {
+ #[serde(skip_serializing_if = "String::is_empty")]
+ pub compute_unit_limit: String,
+ #[serde(skip_serializing_if = "String::is_empty")]
+ pub compute_unit_price_micro_lamports: String,
+ #[serde(skip_serializing_if = "String::is_empty")]
+ pub heap_frame_bytes: String,
+ #[serde(skip_serializing_if = "String::is_empty")]
+ pub loaded_accounts_data_size_limit: String,
+ #[serde(skip_serializing_if = "String::is_empty")]
+ pub additional_fee_lamports: String,
+}
+
#[derive(Debug, Clone, Default, Serialize)]
pub struct ProgramDetailSystemTransferWithSeed {
pub from: String,
@@ -1101,6 +1115,8 @@ pub struct JupiterV6SharedAccountsExactOutRouteDetail {
#[derive(Debug, Clone, Serialize)]
#[serde(untagged)]
pub enum ProgramDetail {
+ // compute budget
+ ComputeBudget(ProgramDetailComputeBudget),
// system
SystemTransfer(ProgramDetailSystemTransfer),
SystemTransferWithSeed(ProgramDetailSystemTransferWithSeed),
@@ -1236,6 +1252,7 @@ mod tests {
#[test]
fn serializes_representative_program_details() {
let details = vec![
+ ProgramDetail::ComputeBudget(ProgramDetailComputeBudget::default()),
ProgramDetail::SystemTransfer(ProgramDetailSystemTransfer::default()),
ProgramDetail::SystemTransferWithSeed(ProgramDetailSystemTransferWithSeed::default()),
ProgramDetail::SystemCreateAccount(ProgramDetailSystemCreateAccount::default()),
diff --git a/rust/apps/solana/src/parser/mod.rs b/rust/apps/solana/src/parser/mod.rs
index 66d953c..409f872 100644
--- a/rust/apps/solana/src/parser/mod.rs
+++ b/rust/apps/solana/src/parser/mod.rs
@@ -58,10 +58,12 @@ impl ParsedSolanaTx {
let raw_details = message.to_program_details()?;
let display_type = Self::detect_display_type(&raw_details);
let parsed_overview = Self::build_overview(&display_type, &raw_details)?;
+ let unknown_programs = Self::collect_additional_unknown_programs(&raw_details);
let parsed_detail = Self::build_detail(&display_type, &raw_details, &message)?;
Ok(Self {
display_type,
overview: parsed_overview,
+ unknown_programs,
detail: parsed_detail,
network: "Solana Mainnet".to_string(),
})
@@ -75,16 +77,20 @@ impl ParsedSolanaTx {
if unknown_count == details.len() {
return SolanaTxDisplayType::Unknown;
}
- if unknown_count > 0 {
- return SolanaTxDisplayType::General;
- }
+ let primary_details = details
+ .iter()
+ .filter(|detail| {
+ !Self::is_unknown_detail(&detail.common)
+ && !Self::is_compute_budget_detail(&detail.common)
+ })
+ .collect::<Vec<_>>();
let squads = details
.iter()
.filter(|d| Self::is_sqauds_v4_detail(&d.common))
.collect::<Vec<&SolanaDetail>>();
if !squads.is_empty()
- && details.iter().all(|detail| {
+ && primary_details.iter().all(|detail| {
Self::is_sqauds_v4_detail(&detail.common)
|| Self::is_system_transfer_detail(&detail.common)
})
@@ -96,7 +102,7 @@ impl ParsedSolanaTx {
.iter()
.filter(|d| Self::is_jupiter_v6_detail(&d.common))
.collect::<Vec<&SolanaDetail>>();
- if jupiter.len() == 1 && details.len() == 1 {
+ if jupiter.len() == 1 && primary_details.len() == 1 {
return SolanaTxDisplayType::JupiterV6;
}
@@ -104,7 +110,7 @@ impl ParsedSolanaTx {
.iter()
.filter(|d| Self::is_system_transfer_detail(&d.common))
.collect::<Vec<&SolanaDetail>>();
- if transfer.len() == 1 && details.len() == 1 {
+ if transfer.len() == 1 && primary_details.len() == 1 {
return SolanaTxDisplayType::Transfer;
}
// if contains token transfer check
@@ -112,7 +118,7 @@ impl ParsedSolanaTx {
.iter()
.filter(|d| Self::is_token_transfer_checked_detail(&d.common))
.collect::<Vec<&SolanaDetail>>();
- if token_transfer.len() == 1 && details.len() == 1 {
+ if token_transfer.len() == 1 && primary_details.len() == 1 {
return SolanaTxDisplayType::TokenTransfer;
}
@@ -120,7 +126,7 @@ impl ParsedSolanaTx {
.iter()
.filter(|d| Self::is_vote_detail(&d.common))
.collect::<Vec<&SolanaDetail>>();
- if vote.len() == 1 && details.len() == 1 {
+ if vote.len() == 1 && primary_details.len() == 1 {
return SolanaTxDisplayType::Vote;
}
SolanaTxDisplayType::General
@@ -142,6 +148,10 @@ impl ParsedSolanaTx {
common.program.eq("Unknown") && common.method.is_empty()
}
+ fn is_compute_budget_detail(common: &CommonDetail) -> bool {
+ common.program.eq("ComputeBudget")
+ }
+
fn is_instructions_detail(common: &CommonDetail) -> bool {
common.program.eq("Instructions") && common.method.is_empty()
}
@@ -451,6 +461,9 @@ impl ParsedSolanaTx {
fn build_general_overview(details: &[SolanaDetail]) -> Result<SolanaOverview> {
let mut overview = Vec::new();
for d in details {
+ if Self::is_unknown_detail(&d.common) {
+ continue;
+ }
let mut item = ProgramOverviewGeneral {
program: d.common.program.to_string(),
method: d.common.method.to_string(),
@@ -941,6 +954,29 @@ impl ParsedSolanaTx {
}))
}
+ fn collect_additional_unknown_programs(details: &[SolanaDetail]) -> Vec<String> {
+ if details
+ .iter()
+ .all(|detail| Self::is_unknown_detail(&detail.common))
+ {
+ return Vec::new();
+ }
+ details
+ .iter()
+ .filter_map(|detail| {
+ if !Self::is_unknown_detail(&detail.common) {
+ return None;
+ }
+ match &detail.kind {
+ ProgramDetail::Instruction(value) => Some(value.program_account.clone()),
+ ProgramDetail::Unknown(value) => Some(value.program_account.clone()),
+ ProgramDetail::RawUnknown(value) => Some(value.program_account.clone()),
+ _ => None,
+ }
+ })
+ .collect()
+ }
+
fn build_overview(
display_type: &SolanaTxDisplayType,
details: &[SolanaDetail],
@@ -999,7 +1035,15 @@ mod tests {
detail("System", "Transfer"),
detail("Unknown", "")
]),
- SolanaTxDisplayType::General
+ SolanaTxDisplayType::Transfer
+ ));
+ assert!(matches!(
+ ParsedSolanaTx::detect_display_type(&[
+ detail("ComputeBudget", "SetComputeUnitLimit"),
+ detail("ComputeBudget", "SetComputeUnitPrice"),
+ detail("JupiterV6", "SharedAccountsRoute")
+ ]),
+ SolanaTxDisplayType::JupiterV6
));
for (program, method, expected) in [
("System", "Transfer", SolanaTxDisplayType::Transfer),
@@ -1269,15 +1313,13 @@ mod tests {
let data = "0200050a06852df21778a462ea79aae81500eae98a935dcca05f8b899ca8b41021a79980acc933a10d87058ad3131361cd345fe95eb7598ad52d972ee559f1ea3f8deb452bb2df65fdf1ad0514f549457e4338bb71e6885354aa5ed87969ef14f5fc736772295dfa0330919867f6f90f2e334d1a56a2203ec3d4086151aab0171ca13c74b626da01ca1cb62be1bbbf9927dd0de251964d351736fd36100bb0e06f728b4100000000000000000000000000000000000000000000000000000000000000008c97258f4e2489f1bb3d1029148e0d830b5a1399daff1084048e7bd8dbe9f8590b7065b1e3d17c45389d527f6b04c3cd58b86c731aa0fdb549b6d1bc03f8294606a7d517192c5c51218cc94c3d4af17f58daee089ba1fd44e3dbd98a0000000006ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a92865a919afcfd4d57cf8f69e11990c98a55e4cc4389ba43c7d32184ca652adb406050200013400000000604d160000000000520000000000000006ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a90902010843000006852df21778a462ea79aae81500eae98a935dcca05f8b899ca8b41021a799800106852df21778a462ea79aae81500eae98a935dcca05f8b899ca8b41021a799800707030100000005087b0012000000536e65616b65722023313830333539303435000000003200000068747470733a2f2f6170692e737465706e2e636f6d2f72756e2f6e66746a736f6e2f3130332f3130363036313531353732319001010100000006852df21778a462ea79aae81500eae98a935dcca05f8b899ca8b41021a799800164010607000400010509080009030104000907010000000000000007090201000000030905080a0a010000000000000000";
let transaction = Vec::from_hex(data).unwrap();
let parsed = ParsedSolanaTx::build(&transaction).unwrap();
+ assert_eq!(3, parsed.unknown_programs.len());
match parsed.overview {
SolanaOverview::General(overview) => {
let expected = [
("System", "CreateAccount"),
("Token", "InitializeMint"),
- ("Unknown", ""),
- ("Unknown", ""),
("Token", "MintTo"),
- ("Unknown", ""),
];
assert_eq!(expected.len(), overview.len());
for (item, (program, method)) in overview.iter().zip(expected) {
@@ -1485,13 +1527,13 @@ mod tests {
let data = "0301070faa30697d8ea2d14ce506c401ad5f1bd33476ebcb8a8b5cea89fa0aafb7c04f3925070f12913aa23553bfaf08f0a6f293aadb24dd66711db239c9b0ccca751b05dcc3a6c16cb67f59d67085e174cd7469f3e00b99a63d6c8d95337096f9e8437d8c17a1e64eba64bb7238d33b21461db8824508c879f91199d9eff9309ff63952baf04e4356057aaea057a6d744e1a0dbb99091448d6c2807114f0a016c9f2c39df8b1e991b87277d51b2ee23b63496ff1a54ab30e61eb4c4572e52fb99af9421e636e5095d76cede0e72ff2a024a07652d423fb7f3978a4663a278d13e1c1ba10deb821d34b39060c73598d3dd86ecf853df3b2f38b02991ad2ccfa51306e01600000000000000000000000000000000000000000000000000000000000000003f5877e18f96dea58c638a21d2be860ba96f0e21d1d84c6a94dba44e2be81f0e494500f4fdcbc9ad22814e250c0d6763266f6ca9169e12662f477601991e1a36be49a1eeb81bf889c158fd8b7496ff9141d4aa433eae3948d0d8488f78951b78069b8857feab8184fb687f634618c035dac439dc1aeb3b5598a0f0000000000106a7d517192c5c51218cc94c3d4af17f58daee089ba1fd44e3dbd98a0000000006ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a954c495b382bac905bb70f97bc252b2ee3b796b2836a3287ed5787c70eb2484120608020001340000000030266d0500000000a50000000000000006ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a90e04010c000d01010e03010200090440084e05000000000b0a090a020106030504070e110140084e05000000005d4d3700000000000e02010001050e030100000109";
let transaction = Vec::from_hex(data).unwrap();
let parsed = ParsedSolanaTx::build(&transaction).unwrap();
+ assert_eq!(1, parsed.unknown_programs.len());
match parsed.overview {
SolanaOverview::General(overview) => {
let expected = [
("System", "CreateAccount"),
("Token", "InitializeAccount"),
("Token", "Approve"),
- ("Unknown", ""),
("Token", "Revoke"),
("Token", "CloseAccount"),
];
@@ -1574,30 +1616,16 @@ mod tests {
let parsed = ParsedSolanaTx::build(&transaction).unwrap();
let detail_tx = parsed.detail;
let parsed_detail: Value = serde_json::from_str(detail_tx.as_str()).unwrap();
- let expect_data = json!({
- "accounts": [
- "NjordRPSzFs8XQUKMjGrhPcmGo9yfC9HP3VHmh8xZpZ",
- "ComputeBudget111111111111111111111111111111",
- "YmirFH6wUrtUMUmfRPZE7TcnszDw689YNWYrMgyB55N"
- ],
- "block_hash": "EBkkMFBA3ArcDiCkQtFyP7omptVXKoK23joaJoDUDqTF",
- "header": {
- "num_readonly_signed_accounts": 0,
- "num_readonly_unsigned_accounts": 2,
- "num_required_signatures": 1
- },
- "instructions": [
+ let expect_data = json!([
{
- "accounts": [],
- "data": "Hg27aP",
- "program": "Unknown",
- "program_account": "ComputeBudget111111111111111111111111111111"
+ "compute_unit_limit": "542092",
+ "method": "SetComputeUnitLimit",
+ "program": "ComputeBudget"
},
{
- "accounts": [],
- "data": "3Ju5f2HYNk7Z",
- "program": "Unknown",
- "program_account": "ComputeBudget111111111111111111111111111111"
+ "compute_unit_price_micro_lamports": "99872",
+ "method": "SetComputeUnitPrice",
+ "program": "ComputeBudget"
},
{
"accounts": [
@@ -1645,8 +1673,7 @@ mod tests {
"program": "Unknown",
"program_account": "YmirFH6wUrtUMUmfRPZE7TcnszDw689YNWYrMgyB55N"
}
- ]
- });
+ ]);
assert_eq!(expect_data, parsed_detail);
}
diff --git a/rust/apps/solana/src/parser/structs.rs b/rust/apps/solana/src/parser/structs.rs
index 4d4002e..e7da1f4 100644
--- a/rust/apps/solana/src/parser/structs.rs
+++ b/rust/apps/solana/src/parser/structs.rs
@@ -1,11 +1,14 @@
use alloc::string::{String, ToString};
+use alloc::vec::Vec;
+
use crate::parser::overview::SolanaOverview;
#[derive(Clone, Debug)]
pub struct ParsedSolanaTx {
pub display_type: SolanaTxDisplayType,
pub overview: SolanaOverview,
+ pub unknown_programs: Vec<String>,
pub detail: String,
pub network: String,
}
diff --git a/rust/rust_c/src/solana/structs.rs b/rust/rust_c/src/solana/structs.rs
index de54191..9e40c58 100644
--- a/rust/rust_c/src/solana/structs.rs
+++ b/rust/rust_c/src/solana/structs.rs
@@ -229,6 +229,8 @@ pub struct DisplaySolanaTxOverview {
pub general: PtrT<VecFFI<DisplaySolanaTxOverviewGeneral>>,
// instructions
pub unknown_instructions: PtrT<DisplaySolanaTxOverviewUnknownInstructions>,
+ // Unknown programs appended after a successfully parsed primary overview.
+ pub additional_unknown_programs: PtrT<VecFFI<PtrString>>,
// squads_v4
pub squads_multisig_create: PtrT<DisplaySolanaTxOverviewSquadsV4MultisigCreate>,
@@ -323,6 +325,7 @@ impl Default for DisplaySolanaTxOverview {
vote_account: null_mut(),
general: null_mut(),
unknown_instructions: null_mut(),
+ additional_unknown_programs: null_mut(),
squads_multisig_create: null_mut(),
squads_proposal: null_mut(),
spl_token_transfer: null_mut(),
@@ -367,6 +370,11 @@ impl Free for DisplaySolanaTxOverview {
let x = Box::from_raw(self.unknown_instructions);
x.free();
}
+ if !self.additional_unknown_programs.is_null() {
+ let value = Box::from_raw(self.additional_unknown_programs);
+ let programs = Vec::from_raw_parts(value.data, value.size, value.cap);
+ programs.iter().for_each(|program| free_str_ptr!(*program));
+ }
if !self.squads_multisig_create.is_null() {
let x = Box::from_raw(self.squads_multisig_create);
x.free();
@@ -388,9 +396,20 @@ impl Free for DisplaySolanaTxOverview {
impl From<ParsedSolanaTx> for DisplaySolanaTx {
fn from(value: ParsedSolanaTx) -> Self {
+ let mut overview = DisplaySolanaTxOverview::from(&value);
+ if !value.unknown_programs.is_empty() {
+ overview.additional_unknown_programs = VecFFI::from(
+ value
+ .unknown_programs
+ .iter()
+ .map(|program| convert_c_char(program.to_string()))
+ .collect_vec(),
+ )
+ .c_ptr();
+ }
DisplaySolanaTx {
network: convert_c_char(value.network.to_string()),
- overview: DisplaySolanaTxOverview::from(&value).c_ptr(),
+ overview: overview.c_ptr(),
detail: convert_c_char(value.detail),
}
}
diff --git a/src/ui/gui_chain/multi/web3/gui_sol.c b/src/ui/gui_chain/multi/web3/gui_sol.c
index a59cd8b..827a8ab 100644
--- a/src/ui/gui_chain/multi/web3/gui_sol.c
+++ b/src/ui/gui_chain/multi/web3/gui_sol.c
@@ -1061,13 +1061,6 @@ static void GuiShowSolTxGeneralOverview(lv_obj_t *parent, PtrT_DisplaySolanaTxOv
PtrT_VecFFI_DisplaySolanaTxOverviewGeneral general = overviewData->general;
lv_obj_t *lastView = NULL;
- for (int i = 0; i < general->size; i++) {
- if (0 == strcmp(general->data[i].program, "Unknown")) {
- lastView = GuiCreateWarningCard(parent);
- break;
- }
- }
-
for (int i = 0; i < general->size; i++) {
char *program = general->data[i].program;
char order[BUFFER_SIZE_16] = {0};
@@ -1129,6 +1122,48 @@ static void GuiShowSolTxGeneralOverview(lv_obj_t *parent, PtrT_DisplaySolanaTxOv
}
}
}
+
+static void GuiShowSolTxAdditionalUnknownPrograms(
+ lv_obj_t *parent,
+ PtrT_DisplaySolanaTxOverview overviewData)
+{
+ PtrT_VecFFI_PtrString programs = overviewData->additional_unknown_programs;
+ if (programs == NULL || programs->size == 0) {
+ return;
+ }
+
+ int32_t contentChildCount = lv_obj_get_child_cnt(parent);
+ lv_obj_t *warningCard = GuiCreateWarningCard(parent);
+ lv_obj_align(warningCard, LV_ALIGN_TOP_LEFT, 0, 0);
+ lv_obj_update_layout(warningCard);
+ int32_t contentOffset = lv_obj_get_height(warningCard) + 16;
+
+ lv_obj_t *lastView = warningCard;
+ int32_t lastBottom = lv_obj_get_height(warningCard);
+ for (int32_t i = 0; i < contentChildCount; i++) {
+ lv_obj_t *child = lv_obj_get_child(parent, i);
+ lv_obj_set_y(child, lv_obj_get_y(child) + contentOffset);
+ int32_t childBottom = lv_obj_get_y(child) + lv_obj_get_height(child);
+ if (childBottom > lastBottom) {
+ lastBottom = childBottom;
+ lastView = child;
+ }
+ }
+
+ for (int i = 0; i < programs->size; i++) {
+ char order[BUFFER_SIZE_16] = {0};
+ snprintf_s(order, BUFFER_SIZE_16, "#%d", i + 1);
+ lv_obj_t *programCard = CreateTransactionOverviewCardWithWidth(
+ parent,
+ order,
+ "Unknown Program",
+ "Program Address",
+ programs->data[i],
+ SOL_COMPONENT_WIDTH);
+ lv_obj_align_to(programCard, lastView, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 16);
+ lastView = programCard;
+ }
+}
static void GuiShowSolTxUnknownOverview(lv_obj_t *parent)
{
uint16_t height = 177;
@@ -1563,7 +1598,9 @@ void GuiShowSolTxOverview(lv_obj_t *parent, void *totalData)
GuiShowJupiterV6SwapOverview(parent, overviewData);
} else {
GuiShowSolTxInstructionsOverview(parent, overviewData);
+ return;
}
+ GuiShowSolTxAdditionalUnknownPrograms(parent, overviewData);
}
void GuiShowSolTxDetail(lv_obj_t *parent, void *totalData)
diff --git a/src/ui/gui_views/multi/gui_key_derivation_request_view.c b/src/ui/gui_views/multi/gui_key_derivation_request_view.c
index a999d2a..0f1e943 100644
--- a/src/ui/gui_views/multi/gui_key_derivation_request_view.c
+++ b/src/ui/gui_views/multi/gui_key_derivation_request_view.c
@@ -4,6 +4,7 @@
#include "gui_key_derivation_request_widgets.h"
#include "gui_keyboard_hintbox.h"
#include "gui_lock_widgets.h"
+#include "gui_usb_transport_widgets.h"
int32_t GuiKeyDerivationRequestViewEventProcess(void *self, uint16_t usEvent, void *param, uint16_t usLen)
{
@@ -70,4 +71,4 @@ GUI_VIEW g_keyDerivationRequestView = {
.isActive = false,
.optimization = false,
.pEvtHandler = GuiKeyDerivationRequestViewEventProcess,
-};
\ No newline at end of file
+};
diff --git a/src/ui/gui_widgets/multi/gui_usb_transport_widgets.c b/src/ui/gui_widgets/multi/gui_usb_transport_widgets.c
index 85cfc52..a391e39 100644
--- a/src/ui/gui_widgets/multi/gui_usb_transport_widgets.c
+++ b/src/ui/gui_widgets/multi/gui_usb_transport_widgets.c
@@ -65,8 +65,13 @@ static ResolveUrInfo_t CalcResolveUrPageInfo()
info.buttonText = _("OK");
break;
default:
- info.title = _("usb_transport_sign_unkown_error_title");
- info.subTitle = _("usb_transport_sign_unkown_error_message");
+ if (g_param->error_message != NULL && g_param->error_message[0] != '\0') {
+ info.title = _("usb_transport_sign_failed_title");
+ info.subTitle = g_param->error_message;
+ } else {
+ info.title = _("usb_transport_sign_unkown_error_title");
+ info.subTitle = _("usb_transport_sign_unkown_error_message");
+ }
info.buttonText = _("OK");
break;
}
@@ -234,4 +239,4 @@ static void RejectButtonHandler(lv_event_t *e)
ExportAddressReject();
GuiCloseCurrentWorkingView();
}
-#endif
\ No newline at end of file
+#endif
diff --git a/ui_simulator/simulator_model.c b/ui_simulator/simulator_model.c
index 61f91b4..e912b5a 100644
--- a/ui_simulator/simulator_model.c
+++ b/ui_simulator/simulator_model.c
@@ -205,6 +205,11 @@ uint16_t GetCurrentUSParsingRequestID()
void HandleURResultViaUSBFunc(const void *data, uint32_t data_len, uint16_t requestID, StatusEnum status)
{
}
+
+void HandleURResultViaUSBAsyncFunc(const void *data, uint32_t data_len, uint16_t requestID, StatusEnum status)
+{
+ HandleURResultViaUSBFunc(data, data_len, requestID, status);
+}
#endif
uint32_t GetBatteryMilliVolt(void)
Why this scored 49/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.