What changed, and why it matters
This firmware update adds support for Solana's new V1 transaction format and changes how oversized or malformed transaction details are displayed. The code introduces a dedicated parser for V1 messages with strict size and structure checks, and it prevents malformed V1 messages from being silently treated as ordinary text messages. It also paginates long transaction detail screens so users can scroll through large transactions instead of the UI breaking or truncating.
Treat this as a routine feature/security-hardening release. Review the new V1 parser's bounds and test coverage for edge cases such as zero-length instructions, maximum account indexes, and boundary sizes including signatures. Verify that the pagination UI correctly displays all instructions and does not hide attacker-controlled fields. No immediate incident response is indicated by the diff alone.
Security signals we found
New parser enforces hard limits on V1 message fields and rejects malformed 0x81-prefix payloads instead of falling back to message signing
Compute-budget instructions in V1 are explicitly ignored to prevent fee-display spoofing
UI pagination change for large transaction details reduces truncation risk
Error message generalization obscures the previous 'hidden trailing data' wording
Evidence from the diff
The commit merges Keystone 3 firmware release v3.0.6. The main security-relevant changes are in the Solana parser: a new message_v1.rs module implements SIMD-0385 V1 message parsing with explicit bounds on accounts (<=64), instructions (<=64), required signatures (<=12), heap size (32-256 KB, 1 KB aligned), and total wire size (<=4096 bytes including trailing signatures). The classify_payload function now returns MalformedTransaction for any payload starting with 0x81 that fails parsing, preventing fallback to arbitrary message signing. Compute-budget instructions inside V1 messages are rendered as no-ops so their fee data cannot override the displayed V1 transaction config. The C UI layer now paginates large Solana transaction detail cards. Error messages for MalformedTransaction were generalized from ‘transaction contains hidden trailing data’ to ‘invalid or unsupported transaction data’.
Changed components
rust/apps/solana/src/lib.rsrust/apps/solana/src/message.rsrust/apps/solana/src/message_v1.rsrust/apps/solana/src/parser/detail.rsrust/apps/solana/src/parser/mod.rsrust/rust_c/src/solana/mod.rssrc/ui/gui_chain/multi/web3/gui_sol.cInspect captured patch +564 / −28
### CHANGELOG-ZH.md
@@ -1,3 +1,11 @@
+## 3.0.6 (2026-09-14)
+
+**Web3:**
+
+### 新增
+1. 支持 Solana V1 交易格式
+
+
## 3.0.4 (2026-8-10)
**Web3:**
### CHANGELOG.md
@@ -1,3 +1,11 @@
+## 3.0.6 (2026-09-14)
+
+**Web3:**
+
+### What's new
+1. Added support for Solana v1 transactions
+
+
## 3.0.4 (2026-08-10)
**Web3:**
### rust/apps/solana/src/lib.rs
@@ -23,6 +23,7 @@ mod compact;
pub mod errors;
mod instruction;
pub mod message;
+mod message_v1;
pub mod parser;
pub mod read;
#[cfg_attr(coverage_nightly, coverage(off))]
@@ -52,6 +53,11 @@ pub fn classify_payload(data: &[u8]) -> SolanaPayloadType {
return SolanaPayloadType::Transaction;
}
+ // A malformed V1 message must not fall back to arbitrary message signing.
+ if data.first() == Some(&0x81) {
+ return SolanaPayloadType::MalformedTransaction;
+ }
+
let has_transaction_prefix = {
let mut candidate = data.to_vec();
message::Message::has_valid_prefix(&mut candidate)
### rust/apps/solana/src/message.rs
@@ -9,6 +9,7 @@ use bitcoin::base58;
use crate::compact::Compact;
use crate::errors::{Result, SolanaError};
use crate::instruction::Instruction;
+pub use crate::message_v1::TransactionConfig;
use crate::parser::detail::{CommonDetail, ProgramDetail, ProgramDetailInstruction, SolanaDetail};
use crate::read::Read;
@@ -61,6 +62,7 @@ impl Read<BlockHash> for BlockHash {
#[derive(Clone)]
pub struct Message {
+ pub transaction_config: Option<TransactionConfig>,
pub is_versioned: bool,
pub header: MessageHeader,
pub accounts: Vec<Account>,
@@ -71,6 +73,9 @@ pub struct Message {
impl Read<Message> for Message {
fn read(raw: &mut Vec<u8>) -> Result<Message> {
+ if raw.first() == Some(&0x81) {
+ return Self::read_v1(raw);
+ }
let first_byte = raw.first().copied();
let is_versioned = match first_byte {
Some(0x80) => true,
@@ -94,6 +99,7 @@ impl Read<Message> for Message {
false => None,
};
let message = Message {
+ transaction_config: None,
is_versioned,
header,
accounts,
@@ -135,7 +141,7 @@ impl Message {
))
}
- fn validate_structure(&self) -> Result<()> {
+ pub(crate) fn validate_structure(&self) -> Result<()> {
let static_account_count = self.accounts.len();
let required_signatures = self.header.num_required_signatures as usize;
let readonly_signed = self.header.num_readonly_signed_accounts as usize;
@@ -191,7 +197,8 @@ impl Message {
pub fn to_program_details(&self) -> Result<Vec<SolanaDetail>> {
let resolved_accounts = self.prepare_accounts();
- self.instructions
+ let mut details = self
+ .instructions
.iter()
.map(|instruction| {
let instruction_accounts = instruction
@@ -212,6 +219,23 @@ impl Message {
)
})?
.to_string();
+ // V1 compute-budget instructions are successful no-ops, even
+ // when their data is invalid. Never display them as active fees.
+ if self.transaction_config.is_some()
+ && program_account == "ComputeBudget111111111111111111111111111111"
+ {
+ return Ok(SolanaDetail {
+ common: CommonDetail {
+ program: "ComputeBudget".to_string(),
+ method: "IgnoredInV1".to_string(),
+ },
+ kind: ProgramDetail::Instruction(ProgramDetailInstruction {
+ data: base58::encode(&instruction.data),
+ accounts: instruction_accounts,
+ program_account,
+ }),
+ });
+ }
// parse instruction data
match instruction.parse(&program_account, instruction_accounts.clone()) {
Ok(value) => Ok(value),
@@ -228,7 +252,11 @@ impl Message {
}),
}
})
- .collect::<Result<Vec<SolanaDetail>>>()
+ .collect::<Result<Vec<SolanaDetail>>>()?;
+ if let Some(config) = &self.transaction_config {
+ details.push(config.to_detail());
+ }
+ Ok(details)
}
pub fn validate(raw: &mut Vec<u8>) -> bool {
### rust/apps/solana/src/message_v1.rs
@@ -0,0 +1,422 @@
+//! SIMD-0385 signable messages (version prefix included, signatures excluded).
+//! https://github.com/solana-foundation/solana-improvement-documents/blob/main/proposals/0385-transaction-v1.md
+use alloc::{string::ToString, vec::Vec};
+
+use crate::errors::{Result, SolanaError};
+use crate::instruction::Instruction;
+use crate::message::{Account, BlockHash, Message, MessageHeader};
+use crate::parser::detail::{
+ CommonDetail, ProgramDetail, ProgramDetailComputeBudget, SolanaDetail,
+};
+
+#[derive(Clone, Debug)]
+pub struct TransactionConfig {
+ pub priority_fee_lamports: u64,
+ pub compute_unit_limit: u32,
+ pub loaded_accounts_data_size_limit: u32,
+ pub heap_frame_bytes: u32,
+}
+
+impl TransactionConfig {
+ pub(crate) fn to_detail(&self) -> SolanaDetail {
+ SolanaDetail {
+ common: CommonDetail {
+ program: "TransactionConfig".to_string(),
+ method: "V1".to_string(),
+ },
+ kind: ProgramDetail::ComputeBudget(ProgramDetailComputeBudget {
+ priority_fee_lamports: self.priority_fee_lamports.to_string(),
+ compute_unit_limit: self.compute_unit_limit.to_string(),
+ loaded_accounts_data_size_limit: self.loaded_accounts_data_size_limit.to_string(),
+ heap_frame_bytes: self.heap_frame_bytes.to_string(),
+ ..Default::default()
+ }),
+ }
+ }
+}
+
+fn invalid(reason: &str) -> SolanaError {
+ SolanaError::InvalidData(reason.to_string())
+}
+
+// Borrow the input while checking every length; allocate only validated fields.
+struct Cursor<'a>(&'a [u8]);
+impl<'a> Cursor<'a> {
+ fn take(&mut self, size: usize) -> Result<&'a [u8]> {
+ if size > self.0.len() {
+ return Err(invalid("truncated V1 message"));
+ }
+ let (value, rest) = self.0.split_at(size);
+ self.0 = rest;
+ Ok(value)
+ }
+
+ fn byte(&mut self) -> Result<u8> {
+ Ok(self.take(1)?[0])
+ }
+
+ fn u32(&mut self) -> Result<u32> {
+ let bytes = self.take(4)?;
+ Ok(u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
+ }
+}
+
+impl Message {
+ pub(crate) fn read_v1(raw: &mut Vec<u8>) -> Result<Self> {
+ // The 4096-byte limit includes the trailing signatures on the wire.
+ if raw.len() > 4096 {
+ return Err(invalid("V1 transaction exceeds size limit"));
+ }
+ let mut cursor = Cursor(raw);
+ cursor.byte()?; // 0x81, checked by Message::read
+ let header = MessageHeader {
+ num_required_signatures: cursor.byte()?,
+ num_readonly_signed_accounts: cursor.byte()?,
+ num_readonly_unsigned_accounts: cursor.byte()?,
+ };
+ if header.num_required_signatures > 12
+ || header.num_readonly_signed_accounts >= header.num_required_signatures
+ {
+ return Err(invalid("invalid V1 signature header"));
+ }
+ let mask = cursor.u32()?;
+ if mask & !0x1f != 0 || matches!(mask & 3, 1 | 2) {
+ return Err(invalid("unsupported V1 transaction config mask"));
+ }
+ let block_hash = BlockHash {
+ value: cursor.take(32)?.to_vec(),
+ };
+ let instruction_count = cursor.byte()? as usize;
+ let account_count = cursor.byte()? as usize;
+ if instruction_count > 64 || account_count > 64 {
+ return Err(invalid("V1 account or instruction count exceeds limit"));
+ }
+ let mut accounts: Vec<Account> = Vec::with_capacity(account_count);
+ for _ in 0..account_count {
+ let value = cursor.take(32)?;
+ if accounts.iter().any(|account| account.value == value) {
+ return Err(invalid("duplicate V1 account"));
+ }
+ accounts.push(Account {
+ value: value.to_vec(),
+ });
+ }
+ let priority_fee_lamports = if mask & 3 == 3 {
+ let low = cursor.u32()? as u64;
+ low | ((cursor.u32()? as u64) << 32)
+ } else {
+ 0
+ };
+ let config = TransactionConfig {
+ priority_fee_lamports,
+ compute_unit_limit: if mask & 4 != 0 { cursor.u32()? } else { 0 },
+ loaded_accounts_data_size_limit: if mask & 8 != 0 { cursor.u32()? } else { 0 },
+ heap_frame_bytes: if mask & 16 != 0 { cursor.u32()? } else { 32768 },
+ };
+ if !(32768..=262144).contains(&config.heap_frame_bytes)
+ || config.heap_frame_bytes % 1024 != 0
+ {
+ return Err(invalid("invalid V1 heap size"));
+ }
+ // All instruction headers precede all instruction payloads.
+ let headers = cursor.take(instruction_count * 4)?;
+ let mut instructions = Vec::with_capacity(instruction_count);
+ for header in headers.chunks_exact(4) {
+ let program_index = header[0];
+ if program_index == 0 {
+ return Err(invalid("V1 program cannot be the fee payer"));
+ }
+ let account_indexes = cursor.take(header[1] as usize)?.to_vec();
+ let data_len = u16::from_le_bytes([header[2], header[3]]) as usize;
+ let data = cursor.take(data_len)?.to_vec();
+ instructions.push(Instruction {
+ program_index,
+ account_indexes,
+ data,
+ });
+ }
+ let consumed = raw.len() - cursor.0.len();
+ if consumed + header.num_required_signatures as usize * 64 > 4096 {
+ return Err(invalid(
+ "V1 transaction exceeds size limit including signatures",
+ ));
+ }
+ let message = Self {
+ transaction_config: Some(config),
+ is_versioned: true,
+ header,
+ accounts,
+ block_hash,
+ instructions,
+ address_table_lookups: None,
+ };
+ message.validate_structure()?;
+ raw.drain(..consumed);
+ Ok(message)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::{classify_payload, parse, parse_for_signer, read::Read, SolanaPayloadType};
+ use alloc::vec;
+
+ fn encode(
+ mask: u32,
+ config: &[u8],
+ accounts: &[[u8; 32]],
+ instructions: &[Instruction],
+ ) -> Vec<u8> {
+ let mut raw = vec![0x81, 1, 0, 1];
+ raw.extend(mask.to_le_bytes());
+ raw.extend([7; 32]);
+ raw.extend([instructions.len() as u8, accounts.len() as u8]);
+ for account in accounts {
+ raw.extend(account);
+ }
+ raw.extend(config);
+ for instruction in instructions {
+ raw.extend([
+ instruction.program_index,
+ instruction.account_indexes.len() as u8,
+ ]);
+ raw.extend((instruction.data.len() as u16).to_le_bytes());
+ }
+ for instruction in instructions {
+ raw.extend(&instruction.account_indexes);
+ raw.extend(&instruction.data);
+ }
+ raw
+ }
+
+ fn transfer() -> Instruction {
+ let mut data = 2u32.to_le_bytes().to_vec();
+ data.extend(123456789u64.to_le_bytes());
+ Instruction {
+ program_index: 2,
+ account_indexes: vec![0, 1],
+ data,
+ }
+ }
+
+ fn sample() -> Vec<u8> {
+ encode(0, &[], &[[1; 32], [2; 32], [0; 32]], &[transfer()])
+ }
+
+ #[test]
+ fn transfer_and_defaults_are_reviewed() {
+ let raw = sample();
+ assert_eq!(classify_payload(&raw), SolanaPayloadType::Transaction);
+ let parsed = parse_for_signer(&raw, &[1; 32]).unwrap();
+ assert_eq!(parsed.display_type.to_string(), "Transfer");
+ assert!(parsed.detail.contains("0.123456789 SOL"));
+ let config = &parsed.additional_overviews[0];
+ assert_eq!(config.instruction_index, 0);
+ assert!(config.memo.contains("Priority Fee: 0 lamports"));
+ assert!(config.memo.contains("Heap Size: 32768 bytes"));
+ assert!(parse_for_signer(&raw, &[2; 32]).is_err());
+ assert!(parse_for_signer(&raw, &[9; 32]).is_err());
+ }
+
+ #[test]
+ fn every_config_mask_and_full_u64_fee() {
+ for mask in 0u32..32 {
+ if matches!(mask & 3, 1 | 2) {
+ continue;
+ }
+ let mut values = Vec::new();
+ if mask & 3 == 3 {
+ values.extend(u64::MAX.to_le_bytes());
+ }
+ if mask & 4 != 0 {
+ values.extend(200000u32.to_le_bytes());
+ }
+ if mask & 8 != 0 {
+ values.extend(65536u32.to_le_bytes());
+ }
+ if mask & 16 != 0 {
+ values.extend(65536u32.to_le_bytes());
+ }
+ let mut raw = encode(mask, &values, &[[1; 32], [2; 32], [0; 32]], &[transfer()]);
+ let message = Message::read_exact(&mut raw).unwrap();
+ let config = message.transaction_config.unwrap();
+ assert_eq!(
+ config.priority_fee_lamports,
+ if mask & 3 == 3 { u64::MAX } else { 0 }
+ );
+ assert_eq!(
+ config.compute_unit_limit,
+ if mask & 4 != 0 { 200000 } else { 0 }
+ );
+ assert_eq!(
+ config.loaded_accounts_data_size_limit,
+ if mask & 8 != 0 { 65536 } else { 0 }
+ );
+ assert_eq!(
+ config.heap_frame_bytes,
+ if mask & 16 != 0 { 65536 } else { 32768 }
+ );
+ }
+ }
+
+ #[test]
+ fn split_headers_and_compute_budget_noop() {
+ let budget: [u8; 32] = bs58::decode("ComputeBudget111111111111111111111111111111")
+ .into_vec()
+ .unwrap()
+ .try_into()
+ .unwrap();
+ for data in [
+ vec![],
+ vec![255],
+ vec![3, 255, 255, 255, 255, 255, 255, 255, 255],
+ ] {
+ let raw = encode(
+ 3,
+ &42u64.to_le_bytes(),
+ &[[1; 32], [2; 32], [0; 32], budget],
+ &[
+ transfer(),
+ Instruction {
+ program_index: 3,
+ account_indexes: vec![],
+ data,
+ },
+ transfer(),
+ ],
+ );
+ let parsed = parse(&raw).unwrap();
+ assert!(parsed.detail.contains("IgnoredInV1"));
+ assert!(!parsed.detail.contains("compute_unit_price_micro_lamports"));
+ assert!(parsed.detail.contains("\"priority_fee_lamports\":\"42\""));
+ let message = Message::read_exact(&mut raw.clone()).unwrap();
+ assert_eq!(message.instructions[0].data, message.instructions[2].data);
+ }
+ }
+
+ fn rejected(raw: &[u8]) {
+ assert!(Message::read_exact(&mut raw.to_vec()).is_err());
+ assert_eq!(
+ classify_payload(raw),
+ SolanaPayloadType::MalformedTransaction
+ );
+ assert!(parse(&raw.to_vec()).is_err());
+ }
+
+ #[test]
+ fn truncations_and_trailing_signatures_never_fall_back_to_message() {
+ let raw = sample();
+ for len in 1..raw.len() {
+ rejected(&raw[..len]);
+ }
+ for suffix in [vec![0], vec![0; 64], vec![0; 128]] {
+ let mut invalid = raw.clone();
+ invalid.extend(suffix);
+ rejected(&invalid);
+ }
+ }
+
+ #[test]
+ fn rejects_invalid_headers_masks_accounts_and_indices() {
+ for (offset, value) in [
+ (1, 0),
+ (1, 13),
+ (2, 1),
+ (3, 3),
+ (4, 1),
+ (4, 2),
+ (4, 32),
+ (7, 128),
+ (40, 65),
+ (41, 65),
+ (138, 0),
+ (138, 3),
+ (142, 3),
+ ] {
+ let mut raw = sample();
+ raw[offset] = value;
+ rejected(&raw);
+ }
+ let mut raw = sample();
+ raw[74..106].fill(1);
+ rejected(&raw);
+ for heap in [0u32, 32767, 32769, 262145] {
+ rejected(&encode(16, &heap.to_le_bytes(), &[[1; 32], [0; 32]], &[]));
+ }
+ }
+
+ #[test]
+ fn enforces_size_including_signatures_and_accepts_large_instruction() {
+ // Header 42 + two addresses 64 + instruction header 4 + signature 64.
+ for (data_len, valid) in [(1233, true), (3922, true), (3923, false), (4096, false)] {
+ let raw = encode(
+ 0,
+ &[],
+ &[[1; 32], [2; 32]],
+ &[Instruction {
+ program_index: 1,
+ account_indexes: vec![],
+ data: vec![3; data_len],
+ }],
+ );
+ assert_eq!(Message::read_exact(&mut raw.clone()).is_ok(), valid);
+ if !valid {
+ rejected(&raw);
+ }
+ }
+ }
+ #[test]
+ fn official_sdk_message_and_independent_signature_match() {
+ let fixture: serde_json::Value =
+ serde_json::from_str(include_str!("../tests/fixtures/v1-transfer.json")).unwrap();
+ let raw = hex::decode(fixture["message"].as_str().unwrap()).unwrap();
+ let seed = hex::decode(fixture["seed"].as_str().unwrap()).unwrap();
+ let path = fixture["path"].as_str().unwrap().to_string();
+ let signer = crate::get_public_key(&seed, &path).unwrap();
+ let parsed = parse_for_signer(&raw, &signer).unwrap();
+ assert_eq!(parsed.display_type.to_string(), "Transfer");
+ assert!(parsed.additional_overviews[0]
+ .memo
+ .contains("9876543210 lamports"));
+ let signature = crate::sign(raw.clone(), &path, &seed).unwrap();
+ assert_eq!(
+ hex::encode(signature),
+ fixture["signature"].as_str().unwrap()
+ );
+ let message = Message::read_exact(&mut raw.clone()).unwrap();
+ assert!(message.address_table_lookups.is_none());
+ assert_eq!(message.instructions[0].data, transfer().data);
+ assert_eq!(message.instructions[0].account_indexes, vec![0, 1]);
+ assert_eq!(
+ message.transaction_config.unwrap().compute_unit_limit,
+ 200000
+ );
+ }
+
+ #[test]
+ fn accepts_max_counts_and_readonly_cosigner() {
+ let accounts: Vec<[u8; 32]> = (0..64).map(|i| [i; 32]).collect();
+ let instructions = vec![Instruction {
+ program_index: 63,
+ account_indexes: vec![0; 255],
+ data: vec![],
+ }];
+ let mut raw = encode(0, &[], &accounts, &instructions);
+ raw[1] = 12;
+ raw[2] = 11;
+ let message = Message::read_exact(&mut raw).unwrap();
+ assert!(message.validate_signer(&[11; 32]).is_ok());
+ assert!(message.validate_signer(&[12; 32]).is_err());
+ let instructions = vec![
+ Instruction {
+ program_index: 1,
+ account_indexes: vec![],
+ data: vec![],
+ };
+ 64
+ ];
+ let raw = encode(0, &[], &[[1; 32], [2; 32]], &instructions);
+ assert!(Message::read_exact(&mut raw.clone()).is_ok());
+ }
+}
### rust/apps/solana/src/parser/detail.rs
@@ -30,6 +30,8 @@ pub struct ProgramDetailSystemTransfer {
#[derive(Debug, Clone, Default, Serialize)]
pub struct ProgramDetailComputeBudget {
+ #[serde(skip_serializing_if = "String::is_empty")]
+ pub priority_fee_lamports: String,
#[serde(skip_serializing_if = "String::is_empty")]
pub compute_unit_limit: String,
#[serde(skip_serializing_if = "String::is_empty")]
### rust/apps/solana/src/parser/mod.rs
@@ -525,6 +525,17 @@ impl ParsedSolanaTx {
to_lookup_table_reference: String::new(),
};
match &d.kind {
+ ProgramDetail::ComputeBudget(value) if d.common.program == "TransactionConfig" => {
+ // This is message metadata, not an additional instruction.
+ item.instruction_index = 0;
+ item.memo = format!(
+ "Priority Fee: {} lamports\nCompute Unit Limit: {}\nLoaded Accounts Data Limit: {} bytes\nHeap Size: {} bytes",
+ value.priority_fee_lamports,
+ value.compute_unit_limit,
+ value.loaded_accounts_data_size_limit,
+ value.heap_frame_bytes,
+ );
+ }
ProgramDetail::SystemTransfer(value) => {
item.value = value.value.clone();
item.from = value.from.clone();
### rust/apps/solana/tests/fixtures/generate-v1-reference.rs
@@ -0,0 +1,53 @@
+// Standalone SDK/Ed25519 fixture generator; not part of the firmware build.
+use ed25519_dalek::{Signer, SigningKey};
+use hmac::{Hmac, Mac};
+use sha2::Sha512;
+use solana_address::Address;
+use solana_hash::Hash;
+use solana_message::{
+ compiled_instruction::CompiledInstruction,
+ v1::{Message, TransactionConfig},
+ MessageHeader, VersionedMessage,
+};
+fn main() {
+ let seed = hex::decode("5eb00bbddcf069084889a8ab9155568165f5c453ccb85e70811aaed6f6da5fc19a5ac40b389cd370d086206dec8aa6c43daea6690f20ad3d8d48b2d2ce9e38e4").unwrap();
+ let mut h = Hmac::<Sha512>::new_from_slice(b"ed25519 seed").unwrap();
+ h.update(&seed);
+ let mut key = h.finalize().into_bytes().to_vec();
+ for index in [44u32, 501, 0] {
+ let mut h = Hmac::<Sha512>::new_from_slice(&key[32..]).unwrap();
+ h.update(&[0]);
+ h.update(&key[..32]);
+ h.update(&(index | 0x80000000).to_be_bytes());
+ key = h.finalize().into_bytes().to_vec();
+ }
+ let signing = SigningKey::from_bytes(key[..32].try_into().unwrap());
+ let mut data = 2u32.to_le_bytes().to_vec();
+ data.extend(123456789u64.to_le_bytes());
+ let m = Message {
+ header: MessageHeader {
+ num_required_signatures: 1,
+ num_readonly_signed_accounts: 0,
+ num_readonly_unsigned_accounts: 1,
+ },
+ config: TransactionConfig::empty()
+ .with_priority_fee(9876543210)
+ .with_compute_unit_limit(200000)
+ .with_loaded_accounts_data_size_limit(65536)
+ .with_heap_size(65536),
+ lifetime_specifier: Hash::new_from_array([7; 32]),
+ account_keys: vec![
+ Address::new_from_array(signing.verifying_key().to_bytes()),
+ Address::new_from_array([2; 32]),
+ Address::new_from_array([0; 32]),
+ ],
+ instructions: vec![CompiledInstruction {
+ program_id_index: 2,
+ accounts: vec![0, 1],
+ data,
+ }],
+ };
+ let raw = VersionedMessage::V1(m).serialize();
+ println!("message={}", hex::encode(&raw));
+ println!("signature={}", hex::encode(signing.sign(&raw).to_bytes()));
+}
### rust/apps/solana/tests/fixtures/v1-transfer.json
@@ -0,0 +1,7 @@
+{
+ "message": "810100011f00000007070707070707070707070707070707070707070707070707070707070707070103e9b6062841bb977ad21de71ec961900633c26f21384e015b014a637a6149954702020202020202020202020202020202020202020202020202020202020202020000000000000000000000000000000000000000000000000000000000000000ea16b04c02000000400d0300000001000000010002020c0000010200000015cd5b0700000000",
+ "signature": "c960dc18e9d6682e2a7a7353450255b5e2e84db836c840bef39d5531aef4fd55b41ad1947a8dabd827a51327bd8918e5a976df66f9ce14ea3eae20f86842ab0d",
+ "seed": "5eb00bbddcf069084889a8ab9155568165f5c453ccb85e70811aaed6f6da5fc19a5ac40b389cd370d086206dec8aa6c43daea6690f20ad3d8d48b2d2ce9e38e4",
+ "path": "m/44'/501'/0'",
+ "source": "solana-message 4.6.0 (wincode), ed25519-dalek 2.2.0; SLIP-0010 using hmac 0.12 / sha2 0.10"
+}
### rust/rust_c/src/solana/mod.rs
@@ -24,7 +24,7 @@ unsafe fn build_sign_result(ptr: PtrUR, seed: &[u8]) -> Result<SolSignature, Sol
app_solana::SolanaPayloadType::Message => false,
app_solana::SolanaPayloadType::MalformedTransaction => {
return Err(SolanaError::InvalidData(
- "transaction contains hidden trailing data".to_string(),
+ "invalid or unsupported transaction data".to_string(),
));
}
};
@@ -78,7 +78,7 @@ pub unsafe extern "C" fn solana_check(
app_solana::SolanaPayloadType::MalformedTransaction
) {
return TransactionCheckResult::from(SolanaError::InvalidData(
- "transaction contains hidden trailing data".to_string(),
+ "invalid or unsupported transaction data".to_string(),
))
.c_ptr();
}
@@ -109,7 +109,7 @@ pub unsafe extern "C" fn solana_parse_tx(
app_solana::SolanaPayloadType::MalformedTransaction
) {
return TransactionParseResult::from(SolanaError::InvalidData(
- "transaction contains hidden trailing data".to_string(),
+ "invalid or unsupported transaction data".to_string(),
))
.c_ptr();
}
@@ -131,7 +131,7 @@ pub unsafe extern "C" fn solana_parse_tx_with_pubkey(
app_solana::SolanaPayloadType::MalformedTransaction
) {
return TransactionParseResult::from(SolanaError::InvalidData(
- "transaction contains hidden trailing data".to_string(),
+ "invalid or unsupported transaction data".to_string(),
))
.c_ptr();
}
@@ -199,7 +199,7 @@ pub unsafe extern "C" fn solana_parse_message(
}
app_solana::SolanaPayloadType::MalformedTransaction => {
return TransactionParseResult::from(SolanaError::InvalidData(
- "transaction contains hidden trailing data".to_string(),
+ "invalid or unsupported transaction data".to_string(),
))
.c_ptr();
}
### src/config/version.h
@@ -7,7 +7,7 @@
#define SOFTWARE_VERSION_MAJOR 13
#define SOFTWARE_VERSION_MAJOR_OFFSET 10
#define SOFTWARE_VERSION_MINOR 0
-#define SOFTWARE_VERSION_BUILD 4
+#define SOFTWARE_VERSION_BUILD 6
#define SOFTWARE_VERSION_BETA 0
#define SOFTWARE_VERSION (SOFTWARE_VERSION_MAJOR * 10000 + SOFTWARE_VERSION_MINOR * 100 + SOFTWARE_VERSION_BUILD)
#ifdef WEB3_VERSION
### src/ui/gui_chain/multi/web3/gui_sol.c
@@ -907,6 +907,12 @@ static lv_obj_t *GuiShowSolTxGeneralOverview(
for (int i = 0; i < general->size; i++) {
char *program = general->data[i].program;
+ if (strcmp(program, "TransactionConfig") == 0) {
+ lastView = CreateTransactionItemViewWithWidth(
+ parent, "Transaction Config (V1)", general->data[i].memo,
+ lastView, SOL_COMPONENT_WIDTH);
+ continue;
+ }
char order[BUFFER_SIZE_16] = {0};
snprintf_s(order, BUFFER_SIZE_16, "#%u", (unsigned int)general->data[i].instruction_index);
const char *method = strlen(general->data[i].method) > 0
@@ -1538,31 +1544,16 @@ static void GuiShowSolTxRawDetailCard(
lv_obj_t *lastView)
{
lv_obj_t *cont = lv_obj_create(parent);
- lv_obj_set_size(cont, SOL_COMPONENT_WIDTH, LV_SIZE_CONTENT);
- lv_obj_set_style_border_width(cont, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
- lv_obj_set_style_clip_corner(cont, 0, 0);
- lv_obj_set_style_radius(cont, 24, LV_PART_MAIN | LV_STATE_DEFAULT);
- lv_obj_set_style_pad_all(cont, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
- lv_obj_set_style_bg_color(cont, WHITE_COLOR, LV_PART_MAIN | LV_STATE_DEFAULT);
- lv_obj_set_style_bg_opa(cont, 30, LV_PART_MAIN | LV_STATE_DEFAULT);
- lv_obj_set_style_pad_top(cont, 16, LV_PART_MAIN | LV_STATE_DEFAULT);
- lv_obj_set_style_pad_bottom(cont, 16, LV_PART_MAIN | LV_STATE_DEFAULT);
- lv_obj_set_style_pad_left(cont, 24, LV_PART_MAIN | LV_STATE_DEFAULT);
- lv_obj_set_style_pad_right(cont, 24, LV_PART_MAIN | LV_STATE_DEFAULT);
+ lv_obj_remove_style_all(cont);
+ // Reuse the release pager: long pages remain scrollable within the viewport.
+ lv_obj_set_size(cont, SOL_COMPONENT_WIDTH, 420);
lv_obj_align(cont, LV_ALIGN_TOP_LEFT, 0, 0);
lv_obj_clear_flag(cont, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_clear_flag(cont, LV_OBJ_FLAG_CLICKABLE);
lv_obj_set_scrollbar_mode(cont, LV_SCROLLBAR_MODE_OFF);
- lv_obj_t *label = lv_label_create(cont);
const char *rawDetail = txDetail == NULL ? "" : txDetail;
- lv_label_set_text(label, rawDetail);
- lv_label_set_long_mode(label, LV_LABEL_LONG_WRAP);
- lv_obj_set_width(label, SOL_COMPONENT_CONTENT_WIDTH);
- SetTitleLabelStyle(label);
- lv_obj_align(label, LV_ALIGN_TOP_LEFT, 0, 0);
- lv_obj_update_layout(label);
- lv_obj_set_height(cont, lv_obj_get_height(label) + 32);
+ GuiShowPagedMessageText(cont, rawDetail, true, NULL, NULL);
if (lastView == NULL) {
lv_obj_align(cont, LV_ALIGN_TOP_LEFT, 0, 0);
} else {Why this scored 47/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.