Merge pull request #2264 from KeystoneHQ/regular-review-fix
What changed, and why it matters
This firmware update is a routine 'regular review fix' that hardens how the Keystone 3 hardware wallet checks and displays several cryptocurrency transactions before signing. The most important security changes are: (1) the sign button is now disabled until transaction parsing succeeds, so a malformed transaction cannot be accidentally approved; (2) Arweave/AO data-item transactions are rejected during the pre-check if their tag metadata is malformed; (3) Avalanche transactions are fully re-parsed in the pre-check, so tampered fields such as a changed output asset ID are caught; (4) Bitcoin-like fee warnings now use network-specific thresholds, so normal Dogecoin/Litecoin fees no longer trigger misleading 'large fee' alerts; and (5) Cosmos no longer shows a duplicated 'Max Fee' line, and Solana Squads proposal memos are displayed on the correct instruction. Most other changes are UI layout and cleanup.
Treat as a security-hardening patch and include in the next firmware release. Regression-test: (a) malformed Arweave DataItem and Avalanche transactions are blocked before the confirmation screen, (b) the sign slider cannot be triggered during/after parse failure, (c) Dogecoin/Litecoin transactions with typical fees do not show large-fee warnings, (d) Cosmos transactions display only one fee line, and (e) Solana Squads proposal overviews still show the intended memo. No CVE or advisory is referenced; consider requesting a security note from the vendor if one is desired.
Security signals we found
Sign slider disabled until parse success prevents premature approval
Arweave DataItem tag-count mismatch now rejected in pre-check
Avalanche transaction re-validated by type in pre-check, tampered asset ID rejected
Bitcoin-family large-fee thresholds made network-specific, reducing warning fatigue and alert bypass risk
Cosmos duplicate Max Fee field removed from data model and UI
Solana Squads memo attribution moved from ProposalCreate to VaultTransactionCreate
Parse-failure path now sets g_needSign=false and keeps slider disabled
Evidence from the diff
The commit is a multi-file defensive patch. Key security-relevant diffs: gui_transaction_detail_widgets.c now disables the signing slider at init, keeps it disabled if parsing fails (g_needSign=false), and only re-enables it after successful parse plus GuiCheckIsTransactionSign(); this closes a race/UX path where a user could confirm before parsing completed. arweave/mod.rs adds a DataItem parse in ar_check_tx when sign_type is DataItem, returning an error C pointer before the UI is reached. data_item.rs adds a test for header-vs-actual tag count mismatch. avalanche/mod.rs introduces validate_transaction_by_type which re-parses the full transaction by type ID inside avax_check_transaction, with a test showing a replaced first output asset ID is rejected. bitcoin/network.rs and parsed_tx.rs replace hard-coded BTC fee thresholds with a per-network LargeFeePolicy, including Dogecoin/Litecoin/Dash/BCH/Zcash-specific values and tests. solana parser changes stop pairing ProposalCreate memos by index and instead attach the memo to the VaultTransactionCreate instruction that actually owns it, plus expose the memo through the C FFI structs. cosmos changes remove the redundant max_fee field and associated UI label. UI changes for AR/AVAX/Cosmos/Solana are mostly display/layout improvements.
Changed components
Transaction signing UI flow (src/ui/gui_widgets/gui_transaction_detail_widgets.c)Arweave/AO transaction parser and pre-check (rust/apps/arweave, rust/rust_c/src/arweave)Avalanche transaction pre-check (rust/rust_c/src/avalanche)Bitcoin-family fee policy (rust/apps/bitcoin)Cosmos fee display (rust/apps/cosmos, src/ui/gui_chain/multi/web3/gui_cosmos)Solana Squads overview/FFI (rust/apps/solana, rust/rust_c/src/solana)Arweave/Avalanche/Ethereum/Solana UI layout filesInspect captured patch +623 / −204
### rust/apps/arweave/src/ao_transaction.rs
@@ -39,10 +39,7 @@ impl TryFrom<DataItem> for AOTransferTransaction {
}
let token_id = value.get_target();
let rest_tags = tags.iter().filter(|v| {
- v.name().ne("Data-Protocol")
- && v.name().ne("Action")
- && v.name().ne("Recipient")
- && v.name().ne("Quantity")
+ v.name().ne("Action") && v.name().ne("Recipient") && v.name().ne("Quantity")
});
if let Some(token_id) = token_id {
let from = value.get_owner();
@@ -107,6 +104,10 @@ mod tests {
);
assert_eq!(ao_transfer.quantity, "0.01 AR");
assert_eq!(ao_transfer.token_id, "Wrapped AR");
+ assert!(ao_transfer
+ .other_info
+ .iter()
+ .any(|tag| tag.name() == "Data-Protocol" && tag.value() == "ao"));
let mut tags = result.get_tags();
let mut tag_data = tags.get_data();
### rust/apps/arweave/src/data_item.rs
@@ -272,6 +272,8 @@ impl DataItem {
mod tests {
use super::{DataItem, Tags};
+ use alloc::string::ToString;
+ use alloc::vec::Vec;
use hex;
@@ -309,6 +311,23 @@ mod tests {
assert!(Tags::deserialize(&[0x00, 0x02]).is_err());
}
+ #[test]
+ fn test_reject_header_tag_count_mismatch() {
+ let raw_tags = [0x02, 0x02, b'A', 0x02, b'B', 0x00];
+ let mut binary = Vec::new();
+ binary.extend_from_slice(&1u16.to_le_bytes());
+ binary.extend_from_slice(&[0u8; 512]);
+ binary.extend_from_slice(&[0u8; 512]);
+ binary.push(0); // no target
+ binary.push(0); // no anchor
+ binary.extend_from_slice(&2u64.to_le_bytes()); // header claims two tags
+ binary.extend_from_slice(&(raw_tags.len() as u64).to_le_bytes());
+ binary.extend_from_slice(&raw_tags); // Avro payload contains one tag
+
+ let error = DataItem::deserialize(&binary).unwrap_err();
+ assert!(error.to_string().contains("tags count mismatch"));
+ }
+
#[test]
fn test_parse_data_item() {
//01000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a999bac8b7906c0bc94f7d163ea9e7fe6ef34045b6a27035e5298aaaddeea05355c50efd30da262c97a68b5aa7219726754bf8501818429e60b9f8175ed66a23349757dc8b3f126abc199272c91174bdb96a9a13aad43b9b6195583188c222002d29b105169dc237dccb0e371895aa10b9263e0b6fbe2d03d3a0073fa7f278ecfa890e75a3fe812ca86eb44f134a7edaa664a5582e72fa43b7accdfeb03f0492c118235b9ff7784106ca1a2f6e7bc4bcc6e1ed98775b7c023a1ae1e332f42e3183ab17c43c58e6605353a47331452ebf659fb267d27492b961ecdafcde9657a0a623aec761f6b3130f89ff7136cae26ebc58aaaa0c6c2264d8e0aa7c78cb46b5210cd69be2ffca64fd3cb0990116034c582828dd22d0235edf9ad999ef0b25afbcab802330d03e9653eff2dbee7f9e0a695a63e04d2aaef73152c255a1d8e5f9cc525cbcfd796ffff337f21d846ae7091037e2bfd06efaf262375100323335e62c79ca63aa31226e3655acab5f2861913630be567210d3d0d5b0f0a6bdc7edfc986e9c14b28b9d32deab5041872a26f8b95341a8cdf6326207d0c2f728ef85554f18c9e285c9f3e01e1d1cb1adf2546eeb9ddfc81a51b0fdf94c9f9116adcd5878815d21038968cbef2b51cc4a27fb1911008c6d1d587830645aca9ca775cf1d67dd9901aadb830a1e8abe0548a47619b8d80083316a645c646820640067653101c54f73164ab75f6650ea8970355bebd6f5162237379174d6afbc4a403e9d875d000800000000000000b100000000000000100c416374696f6e105472616e7366657212526563697069656e745671667a34427465626f714d556f4e536c74457077394b546462663736665252446667783841693644474a77105175616e746974791631303030303030303030301a446174612d50726f746f636f6c04616f0e56617269616e740e616f2e544e2e3108547970650e4d6573736167650653444b12616f636f6e6e65637418436f6e74656e742d5479706514746578742f706c61696e0037373037
### rust/apps/bitcoin/src/network.rs
@@ -5,6 +5,16 @@ use core::str::FromStr;
pub trait NetworkT {
fn get_unit(&self) -> String;
fn normalize(&self) -> String;
+ fn large_fee_policy(&self) -> LargeFeePolicy;
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub struct LargeFeePolicy {
+ /// Absolute fee threshold in the network's smallest unit.
+ pub absolute_threshold: u64,
+ /// Fee-rate threshold in the network's smallest unit per virtual byte.
+ /// Some networks use a fee policy that is not meaningfully byte based.
+ pub rate_threshold_per_vbyte: Option<u64>,
}
#[derive(Debug, Clone)]
@@ -46,6 +56,41 @@ impl NetworkT for Network {
}
.to_string()
}
+
+ fn large_fee_policy(&self) -> LargeFeePolicy {
+ match self {
+ // BTC-like fee market: 0.05 BTC or 100 sat/vB.
+ Network::Bitcoin | Network::BitcoinTestnet | Network::AvaxBtcBridge => LargeFeePolicy {
+ absolute_threshold: 5_000_000,
+ rate_threshold_per_vbyte: Some(100),
+ },
+ // Litecoin's normal relay/wallet fee scale is higher in litoshi/vB.
+ Network::Litecoin => LargeFeePolicy {
+ absolute_threshold: 10_000_000,
+ rate_threshold_per_vbyte: Some(1_000),
+ },
+ // Dogecoin Core recommends 0.01 DOGE/kB. Small transactions can
+ // therefore legitimately be several thousand koinu/vB.
+ Network::Dogecoin => LargeFeePolicy {
+ absolute_threshold: 100_000_000,
+ rate_threshold_per_vbyte: Some(10_000),
+ },
+ Network::Dash => LargeFeePolicy {
+ absolute_threshold: 10_000_000,
+ rate_threshold_per_vbyte: Some(100),
+ },
+ Network::BitcoinCash => LargeFeePolicy {
+ absolute_threshold: 10_000_000,
+ rate_threshold_per_vbyte: Some(100),
+ },
+ // Zcash conventional fees are action based rather than a simple
+ // sat/vB-style market, so only use the absolute safety threshold.
+ Network::Zcash => LargeFeePolicy {
+ absolute_threshold: 10_000_000,
+ rate_threshold_per_vbyte: None,
+ },
+ }
+ }
}
impl Network {
@@ -105,4 +150,11 @@ impl NetworkT for CustomNewNetwork {
}
.to_string()
}
+
+ fn large_fee_policy(&self) -> LargeFeePolicy {
+ LargeFeePolicy {
+ absolute_threshold: 5_000_000,
+ rate_threshold_per_vbyte: Some(100),
+ }
+ }
}
### rust/apps/bitcoin/src/transactions/parsed_tx.rs
@@ -234,14 +234,14 @@ pub trait TxParser {
has_unlocked_outputs || (has_anyone_can_pay && total_input_value < total_output_value);
let fee_is_lower_bound = has_anyone_can_pay && !fee_is_unknown;
let fee = total_input_value.saturating_sub(total_output_value);
- const LARGE_FEE_SATS: u64 = 5_000_000;
- const LARGE_FEE_RATE_SAT_PER_VBYTE: u64 = 100;
+ let large_fee_policy = network.large_fee_policy();
let is_large_fee = !fee_is_unknown
- && (fee > LARGE_FEE_SATS
- || estimated_signed_vbytes
- .filter(|vbytes| *vbytes > 0)
- .is_some_and(|vbytes| {
- fee > LARGE_FEE_RATE_SAT_PER_VBYTE.saturating_mul(vbytes)
+ && (fee > large_fee_policy.absolute_threshold
+ || large_fee_policy
+ .rate_threshold_per_vbyte
+ .zip(estimated_signed_vbytes.filter(|vbytes| *vbytes > 0))
+ .is_some_and(|(rate_threshold, vbytes)| {
+ fee > rate_threshold.saturating_mul(vbytes)
}));
let fee_amount = Self::format_amount(fee, network);
let fee_sat = Self::format_sat(fee);
@@ -545,4 +545,42 @@ mod tests {
.unwrap();
assert!(!exact_limits.overview.is_large_fee);
}
+
+ #[test]
+ fn test_dogecoin_large_fee_uses_dogecoin_policy() {
+ // Dogecoin Core's recommended 0.01 DOGE/kB fee must not be compared
+ // against Bitcoin's 100 sat/vB warning threshold.
+ let normal_doge_fee = DummyParser
+ .normalize(
+ vec![build_input_with_value(101_000_000, 0x01)],
+ vec![build_output(100_000_000)],
+ &Network::Dogecoin,
+ false,
+ Some(1_000),
+ )
+ .unwrap();
+ assert!(!normal_doge_fee.overview.is_large_fee);
+
+ let high_doge_rate = DummyParser
+ .normalize(
+ vec![build_input_with_value(110_000_001, 0x01)],
+ vec![build_output(100_000_000)],
+ &Network::Dogecoin,
+ false,
+ Some(1_000),
+ )
+ .unwrap();
+ assert!(high_doge_rate.overview.is_large_fee);
+
+ let high_doge_absolute_fee = DummyParser
+ .normalize(
+ vec![build_input_with_value(200_000_001, 0x01)],
+ vec![build_output(100_000_000)],
+ &Network::Dogecoin,
+ false,
+ Some(20_000),
+ )
+ .unwrap();
+ assert!(high_doge_absolute_fee.overview.is_large_fee);
+ }
}
### rust/apps/cosmos/src/proto_wrapper/fee.rs
@@ -133,7 +133,6 @@ pub fn format_fee_from_value(data: serde_json::Value) -> Result<FeeDetail> {
}
let formatted_fee = fee.join(",");
return Ok(FeeDetail {
- max_fee: formatted_fee.clone(),
fee: formatted_fee,
gas_limit,
});
@@ -154,7 +153,6 @@ mod tests {
}))
.unwrap();
assert_eq!("0.002583 ATOM", fee.fee);
- assert_eq!("0.002583 ATOM", fee.max_fee);
assert_eq!("103301", fee.gas_limit);
}
### rust/apps/cosmos/src/transaction/mod.rs
@@ -137,7 +137,6 @@ mod tests {
"common": {
"Network": "Cosmos Hub",
"Chain ID": "cosmoshub-4",
- "Max Fee": "0.002583 ATOM",
"Fee": "0.002583 ATOM",
"Gas Limit": "103301"
},
@@ -187,7 +186,6 @@ mod tests {
"Network": "Evmos Testnet",
"Chain ID": "evmos_9000-4",
"Fee": "2625000000000000 atevmos",
- "Max Fee": "2625000000000000 atevmos",
"Gas Limit": "105000",
},
"kind": [
@@ -234,7 +232,6 @@ mod tests {
"Chain ID": "evmos_9000-4",
"Fee": "8750000000000000 atevmos",
"Gas Limit": "350000",
- "Max Fee": "8750000000000000 atevmos",
},
"kind": [
{
@@ -276,7 +273,6 @@ mod tests {
"Chain ID": "osmo-test-5",
"Fee": "4625 uosmo",
"Gas Limit": "184991",
- "Max Fee": "4625 uosmo",
},
"kind": [
{
@@ -318,7 +314,6 @@ mod tests {
"Network": "Evmos Testnet",
"Chain ID": "evmos_9000-4",
"Fee": "7500000000000000 atevmos",
- "Max Fee": "7500000000000000 atevmos",
"Gas Limit": "300000"
},
"kind": [
@@ -360,7 +355,6 @@ mod tests {
"Network": "Unknown Network",
"Chain ID": "osmo-test-5",
"Fee": "9512 uosmo",
- "Max Fee": "9512 uosmo",
"Gas Limit": "237788"
},
"kind": [
@@ -403,7 +397,6 @@ mod tests {
"Network": "Evmos Testnet",
"Chain ID": "evmos_9000-4",
"Fee": "37500000000000000 atevmos",
- "Max Fee": "37500000000000000 atevmos",
"Gas Limit": "1500000"
},
"kind": [
@@ -447,7 +440,6 @@ mod tests {
"Chain ID": "osmo-test-5",
"Fee": "8164 uosmo",
"Gas Limit": "326559",
- "Max Fee": "8164 uosmo"
},
"kind": [
{
@@ -489,7 +481,6 @@ mod tests {
"Network": "Evmos Testnet",
"Chain ID": "evmos_9000-4",
"Fee": "8750000000000000 atevmos",
- "Max Fee": "8750000000000000 atevmos",
"Gas Limit": "350000"
},
"kind": [
@@ -529,7 +520,6 @@ mod tests {
"Network": "Evmos Testnet",
"Chain ID": "evmos_9000-4",
"Fee": "100 ucosm",
- "Max Fee": "100 ucosm",
"Gas Limit": "250",
"Memo": "Some memo"
},
@@ -572,7 +562,6 @@ mod tests {
"Network": "Evmos Testnet",
"Chain ID": "evmos_9000-4",
"Fee": "100 ucosm",
- "Max Fee": "100 ucosm",
"Gas Limit": "250",
"Memo": "Some memo"
},
@@ -616,7 +605,6 @@ mod tests {
"Network": "Osmosis",
"Chain ID": "osmosis-1",
"Fee": "5333 uosmo",
- "Max Fee": "5333 uosmo",
"Gas Limit": "213305"
},
"kind": [
@@ -667,7 +655,6 @@ mod tests {
"Network": "Osmosis",
"Chain ID": "osmosis-1",
"Fee": "2000 uosmo",
- "Max Fee": "2000 uosmo",
"Gas Limit": "12345"
},
"kind": [
@@ -710,7 +697,6 @@ mod tests {
"Network": "Osmosis",
"Chain ID": "osmosis-1",
"Fee": "1946 uosmo",
- "Max Fee": "1946 uosmo",
"Gas Limit": "77814"
},
"kind": [
@@ -750,7 +736,6 @@ mod tests {
"Network": "Osmosis",
"Chain ID": "osmosis-1",
"Fee": "2000 uosmo",
- "Max Fee": "2000 uosmo",
"Gas Limit": "12345"
},
"kind": [
### rust/apps/cosmos/src/transaction/structs.rs
@@ -53,11 +53,6 @@ pub struct ParsedCosmosTx {
#[derive(Clone, Debug, Serialize)]
pub struct FeeDetail {
- #[serde(
- skip_serializing_if = "String::is_empty",
- rename(serialize = "Max Fee")
- )]
- pub max_fee: String,
#[serde(skip_serializing_if = "String::is_empty", rename(serialize = "Fee"))]
pub fee: String,
#[serde(
### rust/apps/solana/src/parser/mod.rs
@@ -509,6 +509,7 @@ impl ParsedSolanaTx {
instruction_index: index + 1,
program: d.common.program.to_string(),
method: d.common.method.to_string(),
+ memo: String::new(),
value: String::new(),
from: String::new(),
to: String::new(),
@@ -551,6 +552,9 @@ impl ParsedSolanaTx {
Self::has_unusual_token_decimals(&value.mint, value.decimals);
item.decimals = value.decimals;
}
+ ProgramDetail::SquadsV4VaultTransactionCreate(value) => {
+ item.memo = value.memo.clone().unwrap_or_default();
+ }
_ => {}
}
overview.push(item)
@@ -578,7 +582,6 @@ impl ParsedSolanaTx {
}
fn build_squads_v4_proposal_overview(details: &[SolanaDetail]) -> Result<SolanaOverview> {
let mut proposal_overview_vec: Vec<ProgramOverviewProposal> = Vec::new();
- let mut proposal_index = 0usize;
for d in details {
let kind = &d.kind;
match kind {
@@ -591,31 +594,10 @@ impl ParsedSolanaTx {
});
}
ProgramDetail::SquadsV4ProposalCreate(v) => {
- // `ProposalCreateArgs` does not contain a memo. Squads stores
- // the proposal description on the corresponding
- // `VaultTransactionCreate` instruction in the same transaction.
- // Pair them by instruction order so the proposal card can show
- // the text the user is actually approving.
- let memo = details
- .iter()
- .filter_map(|detail| {
- if let ProgramDetail::SquadsV4VaultTransactionCreate(vault) =
- &detail.kind
- {
- Some(vault.memo.as_deref())
- } else {
- None
- }
- })
- .nth(proposal_index)
- .flatten()
- .filter(|memo| !memo.is_empty())
- .map(ToString::to_string);
- proposal_index += 1;
proposal_overview_vec.push(ProgramOverviewProposal {
program: "Squads".to_string(),
method: "ProposalCreate".to_string(),
- memo,
+ memo: None,
data: serde_json::to_string(v).ok(),
});
}
@@ -1215,13 +1197,19 @@ mod tests {
}
#[test]
- fn squads_proposal_create_overview_shows_corresponding_memo() {
+ fn squads_vault_transaction_create_overview_keeps_its_memo() {
use crate::solana_lib::squads_v4::instructions::{
ProposalCreateArgs, VaultTransactionCreateArgs,
};
+ const QA_MEMO: &str = concat!(
+ "Keystone offers seamless compatibility with leading wallets such as MetaMask, ",
+ "OKX Wallet, Tonkeeper, Solflare, Backpack, Blue Wallet, Keplr, Eternl and others, ",
+ "ensuring top-tier security for a wide range of cryptocurrencies, including ",
+ "Bitcoin and Ethereum."
+ );
let mut details = Vec::new();
- for (transaction_index, memo) in [(7, "Treasury payment"), (8, "Add new member")] {
+ for (transaction_index, memo) in [(7, QA_MEMO), (8, "Add new member")] {
details.push(SolanaDetail {
common: CommonDetail {
program: "SquadsV4".to_string(),
@@ -1246,19 +1234,30 @@ mod tests {
});
}
- let overview = ParsedSolanaTx::build_squads_v4_proposal_overview(&details).unwrap();
+ let display_type = ParsedSolanaTx::detect_display_type(&details);
+ assert!(matches!(display_type, SolanaTxDisplayType::SquadsV4));
+ let overview = ParsedSolanaTx::build_overview(&display_type, &details).unwrap();
let SolanaOverview::SquadsV4Proposal(items) = overview else {
panic!("expected Squads proposal overview");
};
- let proposal_memos = items
+ let vault_memos = items
.iter()
- .filter(|item| item.method == "ProposalCreate")
+ .filter(|item| item.method == "VaultTransactionCreate")
.map(|item| item.memo.as_deref())
.collect::<Vec<_>>();
- assert_eq!(
- proposal_memos,
- vec![Some("Treasury payment"), Some("Add new member")]
- );
+ assert_eq!(vault_memos, vec![Some(QA_MEMO), Some("Add new member")]);
+ assert!(items
+ .iter()
+ .filter(|item| item.method == "ProposalCreate")
+ .all(|item| item.memo.is_none()));
+
+ let general_items = ParsedSolanaTx::build_general_items(&details, None).unwrap();
+ let general_vault_memos = general_items
+ .iter()
+ .filter(|item| item.method == "VaultTransactionCreate")
+ .map(|item| item.memo.as_str())
+ .collect::<Vec<_>>();
+ assert_eq!(general_vault_memos, vec![QA_MEMO, "Add new member"]);
}
#[test]
### rust/apps/solana/src/parser/overview.rs
@@ -36,6 +36,7 @@ pub struct ProgramOverviewGeneral {
pub instruction_index: usize,
pub program: String,
pub method: String,
+ pub memo: String,
pub value: String,
pub from: String,
pub to: String,
### rust/rust_c/src/arweave/mod.rs
@@ -192,6 +192,14 @@ pub unsafe extern "C" fn ar_check_tx(
if let Ok(mfp) = mfp.try_into() as Result<[u8; 4], _> {
if hex::encode(mfp) == hex::encode(ur_mfp) {
+ // DataItem headers are part of the signed payload. Reject malformed
+ // tag metadata during the pre-check so an invalid request never
+ // reaches the transaction confirmation page.
+ if matches!(sign_request.get_sign_type(), SignType::DataItem) {
+ if let Err(e) = parse_data_item(&sign_request.get_sign_data()) {
+ return TransactionCheckResult::from(e).c_ptr();
+ }
+ }
return TransactionCheckResult::new().c_ptr();
} else {
return TransactionCheckResult::from(RustCError::MasterFingerprintMismatch).c_ptr();
### rust/rust_c/src/avalanche/mod.rs
@@ -44,6 +44,37 @@ use {
},
};
+fn validate_transaction_by_type(tx_data: Vec<u8>) -> Result<(), AvaxError> {
+ let type_id = get_avax_tx_type_id(tx_data.clone())?;
+
+ macro_rules! validate_tx {
+ ($tx_type:ty) => {
+ parse_avax_tx::<$tx_type>(tx_data).map(|_| ())
+ };
+ }
+
+ match type_id {
+ TypeId::BaseTx => {
+ let header = get_avax_tx_header(tx_data.clone())?;
+ if header.get_blockchain_id() == C_BLOCKCHAIN_ID
+ || header.get_blockchain_id() == C_TEST_BLOCKCHAIN_ID
+ {
+ validate_tx!(CchainImportTx)
+ } else {
+ validate_tx!(BaseTx)
+ }
+ }
+ TypeId::PchainExportTx | TypeId::XchainExportTx => validate_tx!(ExportTx),
+ TypeId::XchainImportTx | TypeId::PchainImportTx => validate_tx!(ImportTx),
+ TypeId::CchainExportTx => validate_tx!(CchainExportTx),
+ TypeId::AddPermissionlessValidator => validate_tx!(AddPermissionlessValidatorTx),
+ TypeId::AddPermissionlessDelegator => validate_tx!(AddPermissionlessDelegatorTx),
+ _ => Err(AvaxError::UnsupportedTransaction(format!(
+ "{type_id:?} not support"
+ ))),
+ }
+}
+
#[no_mangle]
pub unsafe extern "C" fn avax_parse_transaction(
ptr: PtrUR,
@@ -297,7 +328,32 @@ pub unsafe extern "C" fn avax_check_transaction(
};
match first_path.get_source_fingerprint() {
- Some(fingerprint) if fingerprint == mfp => TransactionCheckResult::new().c_ptr(),
+ Some(fingerprint) if fingerprint == mfp => {
+ match validate_transaction_by_type(avax_tx.get_tx_data()) {
+ Ok(()) => TransactionCheckResult::new().c_ptr(),
+ Err(e) => TransactionCheckResult::from(e).c_ptr(),
+ }
+ }
_ => TransactionCheckResult::from(RustCError::MasterFingerprintMismatch).c_ptr(),
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ const FUJI_BASE_TX: &str = "00000000000000000005ab68eb1ee142a05cfe768c36e11f0b596db5a3c6c77aabe665dad9e638ca94f7000000023d9bdac0ed1d761330cf680efdeb1a42159eb387d6d2950c96f7d28f61bbe2aa000000070000000001312d00000000000000000000000001000000018771921301d5bffff592dae86695a615bdb4a4413d9bdac0ed1d761330cf680efdeb1a42159eb387d6d2950c96f7d28f61bbe2aa000000070000000004b571c0000000000000000000000001000000010969ea62e2bb30e66d82e82fe267edf6871ea5f7000000019eae34633c2103aaee5253bb3ca3046c2ab4718a109ffcdb77b51d0427be6bb7000000003d9bdac0ed1d761330cf680efdeb1a42159eb387d6d2950c96f7d28f61bbe2aa000000050000000005f5e100000000010000000000000000";
+
+ #[test]
+ fn check_rejects_replaced_first_output_asset_id() {
+ let valid_tx = hex::decode(FUJI_BASE_TX).unwrap();
+ assert!(validate_transaction_by_type(valid_tx.clone()).is_ok());
+
+ let mut replaced_asset_tx = valid_tx;
+ // codec + type + network + blockchain id + outputs count
+ const FIRST_OUTPUT_ASSET_OFFSET: usize = 2 + 4 + 4 + 32 + 4;
+ replaced_asset_tx[FIRST_OUTPUT_ASSET_OFFSET..FIRST_OUTPUT_ASSET_OFFSET + 32].fill(0xaa);
+
+ assert!(validate_transaction_by_type(replaced_asset_tx).is_err());
+ }
+}
### rust/rust_c/src/solana/structs.rs
@@ -27,6 +27,7 @@ pub struct DisplaySolanaTxOverviewGeneral {
pub instruction_index: usize,
pub program: PtrString,
pub method: PtrString,
+ pub memo: PtrString,
pub value: PtrString,
pub from: PtrString,
pub to: PtrString,
@@ -46,6 +47,7 @@ impl Free for DisplaySolanaTxOverviewGeneral {
unsafe fn free(&self) {
free_str_ptr!(self.program);
free_str_ptr!(self.method);
+ free_str_ptr!(self.memo);
free_str_ptr!(self.value);
free_str_ptr!(self.from);
free_str_ptr!(self.to);
@@ -65,6 +67,7 @@ impl From<&ProgramOverviewGeneral> for DisplaySolanaTxOverviewGeneral {
instruction_index: value.instruction_index,
program: convert_c_char(value.program.to_string()),
method: convert_c_char(value.method.to_string()),
+ memo: convert_c_char(value.memo.to_string()),
value: convert_c_char(value.value.to_string()),
from: convert_c_char(value.from.to_string()),
to: convert_c_char(value.to.to_string()),
### rust/rust_c/src/zcash/mod.rs
@@ -940,10 +940,8 @@ pub unsafe extern "C" fn rust_aes256_cbc_decrypt(
match Aes256CbcDec::new(key, iv).decrypt_padded_vec_mut::<Pkcs7>(&data) {
Ok(pt) => match String::from_utf8(pt) {
Ok(pt_str) => SimpleResponse::success(convert_c_char(pt_str)).simple_c_ptr(),
- Err(_) => {
- SimpleResponse::from(RustCError::InvalidHex("invalid plaintext".to_string()))
- .simple_c_ptr()
- }
+ Err(_) => SimpleResponse::from(RustCError::InvalidHex("invalid plaintext".to_string()))
+ .simple_c_ptr(),
},
Err(_e) => SimpleResponse::from(RustCError::InvalidHex("decrypt failed".to_string()))
.simple_c_ptr(),
@@ -1020,10 +1018,8 @@ pub unsafe extern "C" fn rust_decrypt_ufvk_blob(
match Aes256CbcDec::new(key, iv).decrypt_padded_vec_mut::<Pkcs7>(&data) {
Ok(pt) => match String::from_utf8(pt) {
Ok(pt_str) => SimpleResponse::success(convert_c_char(pt_str)).simple_c_ptr(),
- Err(_) => {
- SimpleResponse::from(RustCError::InvalidHex("invalid plaintext".to_string()))
- .simple_c_ptr()
- }
+ Err(_) => SimpleResponse::from(RustCError::InvalidHex("invalid plaintext".to_string()))
+ .simple_c_ptr(),
},
Err(_e) => SimpleResponse::from(RustCError::InvalidHex("decrypt failed".to_string()))
.simple_c_ptr(),
@@ -1341,11 +1337,8 @@ mod tests {
let mut data = b"hello world";
let seed = hex::decode("5eb00bbddcf069084889a8ab9155568165f5c453ccb85e70811aaed6f6da5fc19a5ac40b389cd370d086206dec8aa6c43daea6690f20ad3d8d48b2d2ce9e38e4").unwrap();
// AES key derived from the seed at the dedicated path.
- let key_bytes = get_private_key_by_seed(
- &seed,
- &"m/44'/1557192335'/0'/3'/0'".to_string(),
- )
- .unwrap();
+ let key_bytes =
+ get_private_key_by_seed(&seed, &"m/44'/1557192335'/0'/3'/0'".to_string()).unwrap();
let mut iv_bytes = [0u8; 16];
iv_bytes.copy_from_slice(&hex::decode("73e6ca87d5cd5622cdc747367905efe7").unwrap());
let iv = GenericArray::from_slice(&iv_bytes);
### src/ui/gui_analyze/multi/web3/gui_general_analyze.c
@@ -457,8 +457,6 @@ static GetLabelDataFunc GuiCosmosTextFuncGet(char *type)
return GetCosmosAddress2Label;
} else if (!strcmp(type, "GetCosmosAddress2Value")) {
return GetCosmosAddress2Value;
- } else if (!strcmp(type, "GetCosmosMaxFee")) {
- return GetCosmosMaxFee;
} else if (!strcmp(type, "GetCosmosFee")) {
return GetCosmosFee;
} else if (!strcmp(type, "GetCosmosGasLimit")) {
### src/ui/gui_analyze/multi/web3/gui_general_analyze.h
@@ -48,7 +48,7 @@
},\
{\
REMAPVIEW_COSMOS,\
- "{\"table\":{\"tx\":{\"name\":\"cosmos_tx_page\",\"type\":\"tabview\",\"pos\":[36,0],\"size\":[408,530],\"bg_color\":0,\"children\":[{\"type\":\"tabview_child\",\"index\":1,\"tab_name\":\"Overview\",\"font\":\"openSansEnIllustrate\",\"children\":[{\"type\":\"custom_container\",\"bg_color\":0,\"bg_opa\":0,\"pos\":[0,12],\"custom_show_func\":\"GuiCosmosTxOverview\"}]},{\"type\":\"tabview_child\",\"index\":2,\"tab_name\":\"Details\",\"font\":\"openSansEnIllustrate\",\"children\":[{\"type\":\"custom_container\",\"bg_color\":0,\"bg_opa\":0,\"pos\":[0,12],\"custom_show_func\":\"GuiCosmosTxDetails\"}]}]},\"unknown\":{\"name\":\"cosmos_unknown_page\",\"type\":\"container\",\"pos\":[36,0],\"size\":[408,600],\"bg_color\":0,\"children\":[{\"type\":\"container\",\"pos\":[0,80],\"size\":[408,170],\"bg_opa\":31,\"radius\":24,\"children\":[{\"type\":\"label\",\"text\":\"Max Fee\",\"pos\":[24,16],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosDetailItemValue\",\"text_key\":\"Max Fee\",\"pos\":[118,16],\"text_width\":2000,\"font\":\"openSansEnIllustrate\"},{\"type\":\"label\",\"text\":\" · Max Fee Price * Gas Limit\",\"pos\":[24,54],\"font\":\"openSansDesc\",\"text_opa\":144},{\"type\":\"label\",\"text\":\"Fee\",\"pos\":[24,86],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosDetailItemValue\",\"text_key\":\"Fee\",\"pos\":[73,86],\"text_width\":2000,\"font\":\"openSansEnIllustrate\"},{\"type\":\"label\",\"text\":\"Gas Limit\",\"pos\":[24,124],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosDetailItemValue\",\"text_key\":\"Gas Limit\",\"pos\":[127,124],\"font\":\"openSansEnIllustrate\"}]},{\"type\":\"container\",\"pos\":[0,16],\"size\":[408,62],\"bg_opa\":31,\"radius\":24,\"align\":13,\"align_to\":-2,\"children\":[{\"type\":\"label\",\"text\":\"Network\",\"pos\":[24,16],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosDetailItemValue\",\"text_key\":\"Network\",\"pos\":[120,16],\"font\":\"openSansEnIllustrate\"}]},{\"type\":\"container\",\"pos\":[0,16],\"size\":[408,62],\"bg_opa\":31,\"radius\":24,\"align\":13,\"align_to\":-2,\"children\":[{\"type\":\"label\",\"text\":\"Message\",\"pos\":[24,16],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosDetailItemValue\",\"text_key\":\"Message\",\"pos\":[120,16],\"font\":\"openSansEnIllustrate\",\"text_color\":16105777}]}]},\"msg\":{\"name\":\"cosmos_msg_page\",\"type\":\"container\",\"pos\":[36,0],\"size\":[408,600],\"bg_color\":0,\"children\":[{\"type\":\"container\",\"pos\":[0,16],\"size\":[408,62],\"bg_opa\":31,\"radius\":24,\"children\":[{\"type\":\"label\",\"text\":\"Network\",\"pos\":[24,16],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosDetailItemValue\",\"text_key\":\"Network\",\"pos\":[120,16],\"font\":\"openSansEnIllustrate\"}]},{\"type\":\"container\",\"pos\":[0,16],\"size\":[408,130],\"bg_opa\":31,\"radius\":24,\"align\":13,\"align_to\":-2,\"children\":[{\"type\":\"label\",\"text\":\"Signer\",\"pos\":[24,16],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosDetailItemValue\",\"text_key\":\"Signer\",\"text_width\":360,\"pos\":[24,54],\"font\":\"openSansEnIllustrate\"}]},{\"type\":\"container\",\"pos\":[0,16],\"size\":[408,250],\"bg_opa\":31,\"radius\":24,\"align\":13,\"align_to\":-2,\"children\":[{\"type\":\"label\",\"text\":\"Message\",\"pos\":[24,16],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosDetailItemValue\",\"text_key\":\"Message\",\"pos\":[24,54],\"font\":\"openSansEnIllustrate\",\"text_width\":360}]}]}}}", \
+ "{\"table\":{\"tx\":{\"name\":\"cosmos_tx_page\",\"type\":\"tabview\",\"pos\":[36,0],\"size\":[408,530],\"bg_color\":0,\"children\":[{\"type\":\"tabview_child\",\"index\":1,\"tab_name\":\"Overview\",\"font\":\"openSansEnIllustrate\",\"children\":[{\"type\":\"custom_container\",\"bg_color\":0,\"bg_opa\":0,\"pos\":[0,12],\"custom_show_func\":\"GuiCosmosTxOverview\"}]},{\"type\":\"tabview_child\",\"index\":2,\"tab_name\":\"Details\",\"font\":\"openSansEnIllustrate\",\"children\":[{\"type\":\"custom_container\",\"bg_color\":0,\"bg_opa\":0,\"pos\":[0,12],\"custom_show_func\":\"GuiCosmosTxDetails\"}]}]},\"unknown\":{\"name\":\"cosmos_unknown_page\",\"type\":\"container\",\"pos\":[36,0],\"size\":[408,600],\"bg_color\":0,\"children\":[{\"type\":\"container\",\"pos\":[0,80],\"size\":[408,100],\"bg_opa\":31,\"radius\":24,\"children\":[{\"type\":\"label\",\"text\":\"Fee\",\"pos\":[24,16],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosDetailItemValue\",\"text_key\":\"Fee\",\"pos\":[73,16],\"text_width\":2000,\"font\":\"openSansEnIllustrate\"},{\"type\":\"label\",\"text\":\"Gas Limit\",\"pos\":[24,54],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosDetailItemValue\",\"text_key\":\"Gas Limit\",\"pos\":[127,54],\"font\":\"openSansEnIllustrate\"}]},{\"type\":\"container\",\"pos\":[0,16],\"size\":[408,62],\"bg_opa\":31,\"radius\":24,\"align\":13,\"align_to\":-2,\"children\":[{\"type\":\"label\",\"text\":\"Network\",\"pos\":[24,16],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosDetailItemValue\",\"text_key\":\"Network\",\"pos\":[120,16],\"font\":\"openSansEnIllustrate\"}]},{\"type\":\"container\",\"pos\":[0,16],\"size\":[408,62],\"bg_opa\":31,\"radius\":24,\"align\":13,\"align_to\":-2,\"children\":[{\"type\":\"label\",\"text\":\"Message\",\"pos\":[24,16],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosDetailItemValue\",\"text_key\":\"Message\",\"pos\":[120,16],\"font\":\"openSansEnIllustrate\",\"text_color\":16105777}]}]},\"msg\":{\"name\":\"cosmos_msg_page\",\"type\":\"container\",\"pos\":[36,0],\"size\":[408,600],\"bg_color\":0,\"children\":[{\"type\":\"container\",\"pos\":[0,16],\"size\":[408,62],\"bg_opa\":31,\"radius\":24,\"children\":[{\"type\":\"label\",\"text\":\"Network\",\"pos\":[24,16],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosDetailItemValue\",\"text_key\":\"Network\",\"pos\":[120,16],\"font\":\"openSansEnIllustrate\"}]},{\"type\":\"container\",\"pos\":[0,16],\"size\":[408,130],\"bg_opa\":31,\"radius\":24,\"align\":13,\"align_to\":-2,\"children\":[{\"type\":\"label\",\"text\":\"Signer\",\"pos\":[24,16],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosDetailItemValue\",\"text_key\":\"Signer\",\"text_width\":360,\"pos\":[24,54],\"font\":\"openSansEnIllustrate\"}]},{\"type\":\"container\",\"pos\":[0,16],\"size\":[408,250],\"bg_opa\":31,\"radius\":24,\"align\":13,\"align_to\":-2,\"children\":[{\"type\":\"label\",\"text\":\"Message\",\"pos\":[24,16],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosDetailItemValue\",\"text_key\":\"Message\",\"pos\":[24,54],\"font\":\"openSansEnIllustrate\",\"text_width\":360}]}]}}}", \
GuiGetCosmosData,\
GuiGetCosmosTmpType,\
FreeCosmosMemory,\
### src/ui/gui_chain/multi/web3/gui_ar.c
@@ -33,7 +33,9 @@ static bool g_isAoTransfer = false;
static void ParseRequestType();
static void GuiArPrepareComponentParent(lv_obj_t *parent, uint16_t height);
-static lv_obj_t *GuiArCreateScrollableItemView(lv_obj_t *parent, const char *title, const char *value, lv_obj_t *lastView);
+static lv_obj_t *GuiArCreatePagedMessageView(lv_obj_t *parent, const char *title, const char *value, bool utf8, lv_obj_t *lastView);
+static lv_obj_t *GuiArCreateTxDetailSummary(lv_obj_t *parent, DisplayArweaveTx *txData);
+static void GuiArCreateTxTagsCard(lv_obj_t *parent, cJSON *root, lv_obj_t *lastView);
static void ParseRequestType()
{
@@ -158,49 +160,105 @@ void GuiArTxDetails(lv_obj_t *parent, void *totalData)
DisplayArweaveTx *txData = (DisplayArweaveTx *)totalData;
GuiArPrepareComponentParent(parent, 444);
- lv_obj_t *lastView = NULL;
- lastView = CreateTransactionItemViewWithWidth(parent, _("Value"), txData->value, lastView, AR_COMPONENT_WIDTH);
- lastView = CreateTransactionItemViewWithWidth(parent, _("Fee"), txData->fee, lastView, AR_COMPONENT_WIDTH);
- lastView = CreateTransactionItemViewWithWidth(parent, _("From"), txData->from, lastView, AR_COMPONENT_WIDTH);
- lastView = CreateTransactionItemViewWithWidth(parent, _("Destination"), txData->to, lastView, AR_COMPONENT_WIDTH);
+ lv_obj_t *lastView = GuiArCreateTxDetailSummary(parent, txData);
cJSON *root = txData->detail == NULL ? NULL : cJSON_Parse((const char *)txData->detail);
if (!cJSON_IsArray(root)) {
cJSON_Delete(root);
return;
}
+ GuiArCreateTxTagsCard(parent, root, lastView);
+ cJSON_Delete(root);
+}
+
+static lv_obj_t *GuiArCreateDetailLabel(lv_obj_t *parent, const char *text, int16_t x, int16_t y, lv_opa_t opa)
+{
+ lv_obj_t *label = GuiCreateIllustrateLabel(parent, text == NULL ? "" : text);
+ lv_obj_align(label, LV_ALIGN_TOP_LEFT, x, y);
+ lv_obj_set_style_text_opa(label, opa, LV_PART_MAIN);
+ return label;
+}
+
+static lv_obj_t *GuiArCreateTxDetailSummary(lv_obj_t *parent, DisplayArweaveTx *txData)
+{
+ lv_obj_t *card = CreateRelativeTransactionContentContainer(parent, AR_COMPONENT_WIDTH, 358, NULL);
+ lv_obj_t *section = GuiArCreateDetailLabel(card, "#1", 24, 16, LV_OPA_COVER);
+ lv_obj_set_style_text_color(section, lv_color_hex(16090890), LV_PART_MAIN);
+
+ lv_obj_t *title = GuiArCreateDetailLabel(card, _("Value"), 24, 62, LV_OPA_64);
+ lv_obj_t *value = GuiArCreateDetailLabel(card, txData->value, 0, 62, LV_OPA_COVER);
+ lv_obj_set_style_text_color(value, lv_color_hex(16090890), LV_PART_MAIN);
+ lv_obj_align_to(value, title, LV_ALIGN_OUT_RIGHT_MID, 16, 0);
+
+ title = GuiArCreateDetailLabel(card, _("Fee"), 24, 100, LV_OPA_64);
+ value = GuiArCreateDetailLabel(card, txData->fee, 0, 100, LV_OPA_COVER);
+ lv_obj_align_to(value, title, LV_ALIGN_OUT_RIGHT_MID, 16, 0);
+
+ GuiArCreateDetailLabel(card, _("From"), 24, 138, LV_OPA_64);
+ value = GuiArCreateDetailLabel(card, txData->from, 24, 176, LV_OPA_COVER);
+ lv_obj_set_width(value, AR_COMPONENT_CONTENT_WIDTH);
+ lv_label_set_long_mode(value, LV_LABEL_LONG_WRAP);
+
+ GuiArCreateDetailLabel(card, _("To"), 24, 244, LV_OPA_64);
+ value = GuiArCreateDetailLabel(card, txData->to, 24, 282, LV_OPA_COVER);
+ lv_obj_set_width(value, AR_COMPONENT_CONTENT_WIDTH);
+ lv_label_set_long_mode(value, LV_LABEL_LONG_WRAP);
+ return card;
+}
+
+static void GuiArCreateTxTagsCard(lv_obj_t *parent, cJSON *root, lv_obj_t *lastView)
+{
int size = cJSON_GetArraySize(root);
+ if (size <= 0) {
+ return;
+ }
+
+ lv_obj_t *card = CreateRelativeTransactionContentContainer(parent, AR_COMPONENT_WIDTH, 62, lastView);
+ lv_obj_t *section = GuiArCreateDetailLabel(card, "#2", 24, 16, LV_OPA_COVER);
+ lv_obj_set_style_text_color(section, lv_color_hex(16090890), LV_PART_MAIN);
+ int16_t y = 62;
+
for (int i = 0; i < size; i++) {
cJSON *item = cJSON_GetArrayItem(root, i);
cJSON *name = cJSON_GetObjectItemCaseSensitive(item, "name");
cJSON *value = cJSON_GetObjectItemCaseSensitive(item, "value");
if (!cJSON_IsString(name) || !cJSON_IsString(value)) {
continue;
}
- lastView = CreateTransactionItemViewWithWidth(parent, name->valuestring, value->valuestring, lastView, AR_COMPONENT_WIDTH);
+
+ GuiArCreateDetailLabel(card, _("Name"), 24, y, LV_OPA_64);
+ lv_obj_t *text = GuiArCreateDetailLabel(card, name->valuestring, 96, y, LV_OPA_COVER);
+ lv_obj_set_style_text_color(text, lv_color_hex(16090890), LV_PART_MAIN);
+ lv_obj_set_width(text, AR_COMPONENT_WIDTH - 120);
+ lv_label_set_long_mode(text, LV_LABEL_LONG_WRAP);
+ lv_obj_update_layout(text);
+ y += LV_MAX(30, lv_obj_get_height(text)) + 8;
+
+ GuiArCreateDetailLabel(card, _("Value"), 24, y, LV_OPA_64);
+ text = GuiArCreateDetailLabel(card, value->valuestring, 96, y, LV_OPA_COVER);
+ lv_obj_set_width(text, AR_COMPONENT_WIDTH - 120);
+ lv_label_set_long_mode(text, LV_LABEL_LONG_WRAP);
+ lv_obj_update_layout(text);
+ y += LV_MAX(30, lv_obj_get_height(text)) + 16;
}
- cJSON_Delete(root);
+
+ lv_obj_set_height(card, y);
}
-static lv_obj_t *GuiArCreateScrollableItemView(lv_obj_t *parent, const char *title, const char *value, lv_obj_t *lastView)
+static lv_obj_t *GuiArCreatePagedMessageView(lv_obj_t *parent, const char *title, const char *value, bool utf8, lv_obj_t *lastView)
{
- lv_obj_t *container = CreateRelativeTransactionContentContainer(parent, AR_COMPONENT_WIDTH, 260, lastView);
+ lv_obj_t *container = CreateRelativeTransactionContentContainer(parent, AR_COMPONENT_WIDTH, 420, lastView);
lv_obj_t *titleLabel = GuiCreateIllustrateLabel(container, title);
lv_obj_align(titleLabel, LV_ALIGN_TOP_LEFT, 24, 16);
- lv_obj_set_style_text_opa(titleLabel, LV_OPA_64, LV_PART_MAIN);
+ lv_obj_set_style_text_color(titleLabel, lv_color_hex(16090890), LV_PART_MAIN);
+ lv_obj_set_style_text_opa(titleLabel, LV_OPA_COVER, LV_PART_MAIN);
- lv_obj_t *content = GuiCreateContainerWithParent(container, AR_COMPONENT_CONTENT_WIDTH, 190);
+ lv_obj_t *content = GuiCreateContainerWithParent(container, AR_COMPONENT_CONTENT_WIDTH, 350);
lv_obj_align(content, LV_ALIGN_TOP_LEFT, 24, 54);
lv_obj_set_style_bg_opa(content, LV_OPA_TRANSP, LV_PART_MAIN);
- lv_obj_add_flag(content, LV_OBJ_FLAG_SCROLLABLE);
- lv_obj_add_flag(content, LV_OBJ_FLAG_CLICKABLE);
-
- lv_obj_t *valueLabel = GuiCreateIllustrateLabel(content, value == NULL ? "" : value);
- lv_obj_set_width(valueLabel, AR_COMPONENT_CONTENT_WIDTH - 24);
- lv_label_set_long_mode(valueLabel, LV_LABEL_LONG_WRAP);
- lv_obj_align(valueLabel, LV_ALIGN_TOP_LEFT, 0, 0);
+ GuiShowPagedMessageText(content, value, utf8, NULL, NULL);
return container;
}
@@ -214,8 +272,8 @@ void GuiArMessageOverview(lv_obj_t *parent, void *totalData)
lv_obj_t *lastView = NULL;
lastView = CreateTransactionItemViewWithWidth(parent, _("Address"), address, lastView, AR_COMPONENT_WIDTH);
- lastView = GuiArCreateScrollableItemView(parent, _("Message (UTF-8)"), messageData->message, lastView);
- GuiArCreateScrollableItemView(parent, _("Raw Message"), messageData->raw_message, lastView);
+ lastView = GuiArCreatePagedMessageView(parent, _("Message (UTF-8)"), messageData->message, true, lastView);
+ GuiArCreatePagedMessageView(parent, _("Raw Message"), messageData->raw_message, false, lastView);
}
UREncodeResult *GuiGetArweaveSignQrCodeData(void)
### src/ui/gui_chain/multi/web3/gui_avax.c
@@ -91,6 +91,51 @@ static void GuiAvaxPrepareComponentParent(lv_obj_t *parent)
lv_obj_clear_flag(parent, LV_OBJ_FLAG_SCROLL_ELASTIC);
}
+static lv_obj_t *GuiAvaxCreateDetailsAddressCard(
+ lv_obj_t *parent,
+ lv_obj_t *lastView,
+ const char *title,
+ const DisplayUtxoFromTo *item)
+{
+ lv_obj_t *card = CreateRelativeTransactionContentContainer(
+ parent, AVAX_COMPONENT_WIDTH, 0, lastView);
+ uint16_t height = 16;
+
+ lv_obj_t *label = GuiCreateIllustrateLabel(card, title);
+ lv_obj_align(label, LV_ALIGN_TOP_LEFT, 24, height);
+ lv_obj_set_style_text_opa(label, LV_OPA_64, LV_PART_MAIN);
+ height += 34;
+
+ label = GuiCreateIllustrateLabel(card, item->amount);
+ lv_obj_set_width(label, AVAX_COMPONENT_WIDTH - 48);
+ lv_label_set_long_mode(label, LV_LABEL_LONG_WRAP);
+ lv_obj_set_style_text_color(label, ORANGE_COLOR, LV_PART_MAIN);
+ lv_obj_align(label, LV_ALIGN_TOP_LEFT, 24, height);
+ lv_obj_update_layout(label);
+ height += lv_obj_get_height(label) + 4;
+
+ label = GuiCreateIllustrateLabel(card, item->address);
+ lv_obj_set_width(label, AVAX_COMPONENT_WIDTH - 48);
+ lv_label_set_long_mode(label, LV_LABEL_LONG_WRAP);
+ lv_obj_align(label, LV_ALIGN_TOP_LEFT, 24, height);
+ lv_obj_update_layout(label);
+ height += lv_obj_get_height(label);
+
+ if (item->path != NULL && item->path[0] != '\0') {
+ height += 4;
+ label = GuiCreateNoticeLabel(card, item->path);
+ lv_obj_set_width(label, AVAX_COMPONENT_WIDTH - 48);
+ lv_label_set_long_mode(label, LV_LABEL_LONG_WRAP);
+ lv_obj_align(label, LV_ALIGN_TOP_LEFT, 24, height);
+ lv_obj_update_layout(label);
+ height += lv_obj_get_height(label);
+ }
+
+ lv_obj_set_height(card, height + 16);
+ lv_obj_update_layout(card);
+ return card;
+}
+
static lv_obj_t *GuiAvaxAppendAddresses(
lv_obj_t *parent,
lv_obj_t *lastView,
@@ -111,13 +156,7 @@ static lv_obj_t *GuiAvaxAppendAddresses(
}
if (showDetails) {
- lastView = CreateTransactionItemViewWithHintAndWidth(
- parent,
- title,
- ptr[i].address,
- lastView,
- ptr[i].amount,
- AVAX_COMPONENT_WIDTH);
+ lastView = GuiAvaxCreateDetailsAddressCard(parent, lastView, title, &ptr[i]);
} else {
lastView = CreateTransactionItemViewWithWidth(
parent,
@@ -126,15 +165,6 @@ static lv_obj_t *GuiAvaxAppendAddresses(
lastView,
AVAX_COMPONENT_WIDTH);
}
-
- if (showDetails && ptr[i].path != NULL && strlen(ptr[i].path) > 0) {
- lastView = CreateTransactionItemViewWithWidth(
- parent,
- _("Path"),
- ptr[i].path,
- lastView,
- AVAX_COMPONENT_WIDTH);
- }
}
return lastView;
}
### src/ui/gui_chain/multi/web3/gui_cosmos.c
@@ -65,7 +65,9 @@ static lv_obj_t *CreateCosmosHighlightedJsonItem(lv_obj_t *parent, const char *k
const cJSON *value, lv_obj_t *lastView);
static lv_obj_t *CreateCosmosJsonFields(lv_obj_t *parent, const cJSON *object, lv_obj_t *lastView,
bool overview, bool showMessageIndex);
-static lv_obj_t *CreateCosmosCommonFields(lv_obj_t *parent, const cJSON *common, lv_obj_t *lastView, bool overview);
+static lv_obj_t *CreateCosmosDetailsMessageCard(
+ lv_obj_t *parent, const cJSON *object, lv_obj_t *lastView);
+static lv_obj_t *CreateCosmosOverviewCommonFields(lv_obj_t *parent, const cJSON *common, lv_obj_t *lastView);
static lv_obj_t *CreateCosmosBlindSignView(lv_obj_t *parent, const cJSON *object, lv_obj_t *lastView);
static lv_obj_t *CreateCosmosMemoView(lv_obj_t *parent, const cJSON *memo, lv_obj_t *lastView);
static lv_obj_t *CreateCosmosSingleOverview(lv_obj_t *parent, const cJSON *message, const cJSON *common);
@@ -263,7 +265,7 @@ void GuiCosmosTxOverview(lv_obj_t *parent, void *totalData)
lastView = CreateCosmosJsonFields(parent, message, lastView, true, messageCount > 1);
}
}
- lastView = CreateCosmosCommonFields(parent, common, lastView, true);
+ lastView = CreateCosmosOverviewCommonFields(parent, common, lastView);
lv_obj_update_layout(parent);
}
@@ -468,7 +470,10 @@ void GuiCosmosTxDetails(lv_obj_t *parent, void *totalData)
lastView = CreateCosmosJsonFields(parent, message, lastView, false, messageCount > 1);
}
}
- lastView = CreateCosmosCommonFields(parent, common, lastView, false);
+ lastView = CreateCosmosFeeDetails(parent, common, lastView);
+ cJSON *memo = cJSON_IsObject(common) ? cJSON_GetObjectItem(common, "Memo") : NULL;
+ lastView = CreateCosmosMemoView(parent, memo, lastView);
+ CreateCosmosNetworkDetails(parent, common, lastView);
lv_obj_update_layout(parent);
}
@@ -517,26 +522,18 @@ static lv_obj_t *CreateCosmosVoteDetails(lv_obj_t *parent, const cJSON *message,
static lv_obj_t *CreateCosmosFeeDetails(lv_obj_t *parent, const cJSON *common, lv_obj_t *lastView)
{
- const char *maxFee = GetCosmosJsonString(common, "Max Fee");
const char *fee = GetCosmosJsonString(common, "Fee");
const char *gasLimit = GetCosmosJsonString(common, "Gas Limit");
- if (maxFee == NULL && fee == NULL && gasLimit == NULL) {
+ if (fee == NULL && gasLimit == NULL) {
return lastView;
}
- lv_obj_t *container = CreateContentContainer(parent, 408, 170);
+ lv_obj_t *container = CreateContentContainer(parent, 408, 100);
if (lastView != NULL) {
lv_obj_align_to(container, lastView, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 16);
}
- CreateCosmosDetailInlineValue(container, "Max Fee", maxFee, 16, false);
-
- lv_obj_t *description = GuiCreateLabelWithFont(
- container, " \xE2\x80\xA2 Max Fee Price * Gas Limit", &openSansDesc);
- lv_obj_set_style_text_opa(description, LV_OPA_64, LV_PART_MAIN);
- lv_obj_align(description, LV_ALIGN_TOP_LEFT, 24, 54);
-
- CreateCosmosDetailInlineValue(container, "Fee", fee, 86, false);
- CreateCosmosDetailInlineValue(container, "Gas Limit", gasLimit, 124, false);
+ CreateCosmosDetailInlineValue(container, "Fee", fee, 16, false);
+ CreateCosmosDetailInlineValue(container, "Gas Limit", gasLimit, 54, false);
return container;
}
@@ -605,6 +602,133 @@ static lv_obj_t *CreateCosmosHighlightedJsonItem(lv_obj_t *parent, const char *k
return view;
}
+static bool IsCosmosInlineDetailField(const char *key)
+{
+ return strcmp(key, "Value") == 0 || strcmp(key, "Method") == 0 ||
+ strcmp(key, "Proposal") == 0 || strcmp(key, "Voted") == 0 ||
+ strcmp(key, "Source Channel") == 0;
+}
+
+static bool IsCosmosHighlightedDetailField(const char *key)
+{
+ return strcmp(key, "Value") == 0 || strcmp(key, "Proposal") == 0 ||
+ strcmp(key, "Voted") == 0;
+}
+
+static bool AppendCosmosDetailsField(lv_obj_t *container, const cJSON *field, uint16_t *y)
+{
+ if (field == NULL || field->string == NULL || !cJSON_IsString(field) ||
+ field->valuestring == NULL || field->valuestring[0] == '\0') {
+ return false;
+ }
+
+ lv_obj_t *title = GuiCreateIllustrateLabel(container, _(field->string));
+ lv_obj_set_style_text_opa(title, LV_OPA_64, LV_PART_MAIN);
+ lv_obj_align(title, LV_ALIGN_TOP_LEFT, 24, *y);
+ lv_obj_update_layout(title);
+
+ lv_obj_t *value = GuiCreateIllustrateLabel(container, field->valuestring);
+ lv_label_set_long_mode(value, LV_LABEL_LONG_WRAP);
+ if (IsCosmosHighlightedDetailField(field->string)) {
+ lv_obj_set_style_text_color(value, ORANGE_COLOR, LV_PART_MAIN);
+ }
+
+ if (IsCosmosInlineDetailField(field->string)) {
+ uint16_t titleWidth = lv_obj_get_width(title);
+ uint16_t valueX = 24 + titleWidth + 16;
+ uint16_t valueWidth = valueX < 384 ? 384 - valueX : 0;
+ if (valueWidth >= 96) {
+ lv_obj_set_width(value, valueWidth);
+ lv_obj_align(value, LV_ALIGN_TOP_LEFT, valueX, *y);
+ lv_obj_update_layout(value);
+ uint16_t titleHeight = lv_obj_get_height(title);
+ uint16_t valueHeight = lv_obj_get_height(value);
+ *y += (titleHeight > valueHeight ? titleHeight : valueHeight) + 16;
+ return true;
+ }
+ }
+
+ *y += lv_obj_get_height(title) + 8;
+ lv_obj_set_width(value, 360);
+ lv_obj_align(value, LV_ALIGN_TOP_LEFT, 24, *y);
+ lv_obj_update_layout(value);
+ *y += lv_obj_get_height(value) + 16;
+ return true;
+}
+
+static bool IsCosmosPreferredDetailField(
+ const char *key, const char *const *preferredKeys, size_t preferredCount)
+{
+ for (size_t i = 0; i < preferredCount; i++) {
+ if (strcmp(key, preferredKeys[i]) == 0) {
+ return true;
+ }
+ }
+ return false;
+}
+
+static lv_obj_t *CreateCosmosDetailsMessageCard(
+ lv_obj_t *parent, const cJSON *object, lv_obj_t *lastView)
+{
+ lv_obj_t *container = CreateContentContainer(parent, 408, 0);
+ if (lastView != NULL) {
+ lv_obj_align_to(container, lastView, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 16);
+ }
+
+ uint16_t y = 16;
+ bool hasField = false;
+ const char *preferredKeys[6] = {"Value", "Method", NULL, NULL, NULL, NULL};
+ size_t preferredCount = 2;
+ const char *method = GetCosmosJsonString(object, "Method");
+ if (method != NULL && strcmp(method, "Send") == 0) {
+ preferredKeys[2] = "From";
+ preferredKeys[3] = "To";
+ preferredCount = 4;
+ } else if (method != NULL && strcmp(method, "IBC Transfer") == 0) {
+ preferredKeys[2] = "From";
+ preferredKeys[3] = "To";
+ preferredKeys[4] = "Source Channel";
+ preferredCount = 5;
+ } else if (method != NULL && strcmp(method, "Delegate") == 0) {
+ preferredKeys[2] = "Delegator";
+ preferredKeys[3] = "Validator";
+ preferredCount = 4;
+ } else if (method != NULL && strcmp(method, "Undelegate") == 0) {
+ preferredKeys[2] = "Validator";
+ preferredKeys[3] = "To";
+ preferredCount = 4;
+ } else if (method != NULL && strcmp(method, "Re-delegate") == 0) {
+ preferredKeys[2] = "To";
+ preferredKeys[3] = "Old Validator";
+ preferredKeys[4] = "New Validator";
+ preferredCount = 5;
+ } else if (method != NULL && strcmp(method, "Withdraw Reward") == 0) {
+ preferredKeys[0] = "Method";
+ preferredKeys[1] = "To";
+ preferredKeys[2] = "Validator";
+ preferredCount = 3;
+ }
+
+ for (size_t i = 0; i < preferredCount; i++) {
+ hasField |= AppendCosmosDetailsField(
+ container, cJSON_GetObjectItem(object, preferredKeys[i]), &y);
+ }
+ for (const cJSON *field = object->child; field != NULL; field = field->next) {
+ if (field->string == NULL ||
+ IsCosmosPreferredDetailField(field->string, preferredKeys, preferredCount)) {
+ continue;
+ }
+ hasField |= AppendCosmosDetailsField(container, field, &y);
+ }
+
+ if (!hasField) {
+ lv_obj_del(container);
+ return lastView;
+ }
+ lv_obj_set_height(container, y);
+ return container;
+}
+
static lv_obj_t *CreateCosmosJsonFields(lv_obj_t *parent, const cJSON *object, lv_obj_t *lastView,
bool overview, bool showMessageIndex)
{
@@ -639,10 +763,7 @@ static lv_obj_t *CreateCosmosJsonFields(lv_obj_t *parent, const cJSON *object, l
return lastView;
}
- for (cJSON *field = object->child; field != NULL; field = field->next) {
- lastView = CreateCosmosJsonItem(parent, field->string, field, lastView);
- }
- return lastView;
+ return CreateCosmosDetailsMessageCard(parent, object, lastView);
}
static bool IsCosmosBlindSignMessage(const cJSON *object)
@@ -714,29 +835,19 @@ static lv_obj_t *CreateCosmosMemoView(lv_obj_t *parent, const cJSON *memo, lv_ob
return container;
}
-static lv_obj_t *CreateCosmosCommonFields(lv_obj_t *parent, const cJSON *common, lv_obj_t *lastView, bool overview)
+static lv_obj_t *CreateCosmosOverviewCommonFields(
+ lv_obj_t *parent, const cJSON *common, lv_obj_t *lastView)
{
- static const char *detailKeys[] = {"Max Fee", "Gas Limit", "Network", "Chain ID"};
if (!cJSON_IsObject(common)) {
return lastView;
}
- if (overview) {
- lastView = CreateCosmosMemoView(parent, cJSON_GetObjectItem(common, "Memo"), lastView);
- cJSON *network = cJSON_GetObjectItem(common, "Network");
- lastView = CreateCosmosJsonItem(parent, "Network", network, lastView);
- if (cJSON_IsString(network) && network->valuestring != NULL &&
- strcmp(network->valuestring, "Unknown Network") == 0) {
- lastView = CreateCosmosJsonItem(parent, "Chain ID",
- cJSON_GetObjectItem(common, "Chain ID"), lastView);
- }
- return lastView;
- }
- for (size_t i = 0; i < 2; i++) {
- lastView = CreateCosmosJsonItem(parent, detailKeys[i], cJSON_GetObjectItem(common, detailKeys[i]), lastView);
- }
lastView = CreateCosmosMemoView(parent, cJSON_GetObjectItem(common, "Memo"), lastView);
- for (size_t i = 2; i < NUMBER_OF_ARRAYS(detailKeys); i++) {
- lastView = CreateCosmosJsonItem(parent, detailKeys[i], cJSON_GetObjectItem(common, detailKeys[i]), lastView);
+ cJSON *network = cJSON_GetObjectItem(common, "Network");
+ lastView = CreateCosmosJsonItem(parent, "Network", network, lastView);
+ if (cJSON_IsString(network) && network->valuestring != NULL &&
+ strcmp(network->valuestring, "Unknown Network") == 0) {
+ lastView = CreateCosmosJsonItem(parent, "Chain ID",
+ cJSON_GetObjectItem(common, "Chain ID"), lastView);
}
return lastView;
}
@@ -878,11 +989,6 @@ void GetCosmosDetailCommon(void *indata, void *param, const char* key, uint32_t
}
}
-void GetCosmosMaxFee(void *indata, void *param, uint32_t maxLen)
-{
- GetCosmosDetailCommon(indata, param, "Max Fee", maxLen);
-}
-
void GetCosmosFee(void *indata, void *param, uint32_t maxLen)
{
GetCosmosDetailCommon(indata, param, "Fee", maxLen);
@@ -1141,16 +1247,16 @@ uint8_t GuiGetCosmosTxChain(void)
return CHAIN_ATOM;
}
char chain_id[BUFFER_SIZE_64] = {0};
- if (strcmp(parseResult->data->overview->display_type, GuiGetCosmosTxTypeName(COSMOS_MESSAGE)) == 0 || strcmp(parseResult->data->overview->display_type, GuiGetCosmosTxTypeName(COSMOS_TX_UNKNOWN)) == 0) {
- cJSON* root = GetCosmosParsedDetailRoot(parseResult->data);
- cJSON* value = root == NULL ? NULL : cJSON_GetObjectItem(root, "Chain ID");
- if (value == NULL) {
- return CHAIN_ATOM;
- }
- snprintf_s(chain_id, BUFFER_SIZE_64, "%s", value->valuestring);
- } else {
- GetCosmosDetailCommon(chain_id, parseResult->data, "Chain ID", BUFFER_SIZE_64);
+ cJSON *root = GetCosmosParsedDetailRoot(parseResult->data);
+ cJSON *common = root == NULL ? NULL : cJSON_GetObjectItem(root, "common");
+ cJSON *value = cJSON_IsObject(common) ? cJSON_GetObjectItem(common, "Chain ID") : NULL;
+ if (!cJSON_IsString(value)) {
+ value = root == NULL ? NULL : cJSON_GetObjectItem(root, "Chain ID");
+ }
+ if (!cJSON_IsString(value) || value->valuestring == NULL || value->valuestring[0] == '\0') {
+ return CHAIN_ATOM;
}
+ snprintf_s(chain_id, BUFFER_SIZE_64, "%s", value->valuestring);
for (uint8_t i = 0; i < COSMOS_CHAINS_LEN; i++) {
if (strcmp(chain_id, g_cosmosChains[i].chainId) == 0) {
return g_cosmosChains[i].index;
### src/ui/gui_chain/multi/web3/gui_cosmos.h
@@ -42,7 +42,6 @@ void GetCosmosAddress1Value(void *indata, void *param, uint32_t maxLen);
void GetCosmosAddress1Label(void *indata, void *param, uint32_t maxLen);
void GetCosmosAddress2Value(void *indata, void *param, uint32_t maxLen);
void GetCosmosAddress2Label(void *indata, void *param, uint32_t maxLen);
-void GetCosmosMaxFee(void *indata, void *param, uint32_t maxLen);
void GetCosmosFee(void *indata, void *param, uint32_t maxLen);
void GetCosmosGasLimit(void *indata, void *param, uint32_t maxLen);
void GetCosmosChainId(void *indata, void *param, uint32_t maxLen);
### src/ui/gui_chain/multi/web3/gui_eth.c
@@ -1541,6 +1541,7 @@ static lv_obj_t *CreateEthDetailsRawDataButton(lv_obj_t *parent, lv_obj_t *lastV
lv_obj_set_style_text_color(button, lv_color_hex(0x1BE0C6), LV_PART_MAIN);
if (label != NULL) {
lv_obj_set_style_text_color(label, lv_color_hex(0x1BE0C6), LV_PART_MAIN);
+ lv_obj_align(label, LV_ALIGN_LEFT_MID, 24, 0);
}
if (lastView != NULL) {
lv_obj_align_to(button, lastView, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 16);
@@ -1663,6 +1664,7 @@ static lv_obj_t *CreateEthParsedContractView(
lv_obj_set_style_text_color(rawDataButton, lv_color_hex(0x1BE0C6), LV_PART_MAIN);
if (rawDataLabel != NULL) {
lv_obj_set_style_text_color(rawDataLabel, lv_color_hex(0x1BE0C6), LV_PART_MAIN);
+ lv_obj_align(rawDataLabel, LV_ALIGN_LEFT_MID, 0, 0);
}
lv_obj_align(rawDataButton, LV_ALIGN_TOP_LEFT, 24, y);
lv_obj_add_event_cb(rawDataButton, EthContractCheckRawData, LV_EVENT_CLICKED, NULL);
### src/ui/gui_chain/multi/web3/gui_sol.c
@@ -903,13 +903,23 @@ static lv_obj_t *GuiShowSolTxGeneralOverview(
const char *method = strlen(general->data[i].method) > 0
? general->data[i].method
: "Unknown";
- lv_obj_t *actionCard = CreateTransactionOverviewCardWithWidth(
- parent,
- order,
- program,
- _("Method"),
- method,
- SOL_COMPONENT_WIDTH);
+ lv_obj_t *actionCard;
+ if (strcmp(method, "VaultTransactionCreate") == 0 &&
+ strlen(general->data[i].memo) > 0) {
+ // Preserve the established Squads review card even when a mixed
+ // transaction reaches the General/addon path. The memo belongs
+ // to VaultTransactionCreate, not ProposalCreate.
+ actionCard = CreateSolanaSquadsProposalOverviewCard(
+ parent, "Squads", method, general->data[i].memo, "");
+ } else {
+ actionCard = CreateTransactionOverviewCardWithWidth(
+ parent,
+ order,
+ program,
+ _("Method"),
+ method,
+ SOL_COMPONENT_WIDTH);
+ }
if (lastView != NULL) {
lv_obj_align_to(actionCard, lastView, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 16);
}
@@ -982,6 +992,41 @@ static lv_obj_t *GuiGetSolTxBottomView(lv_obj_t *parent)
return bottomView;
}
+static bool GuiHasSolTxAdditionalUnknownPrograms(
+ PtrT_DisplaySolanaTxOverview overviewData)
+{
+ PtrT_VecFFI_PtrString programs = overviewData->additional_unknown_programs;
+ return programs != NULL && programs->size > 0;
+}
+
+static lv_obj_t *GuiAppendSolTxAdditionalUnknownProgramCards(
+ lv_obj_t *parent,
+ PtrT_DisplaySolanaTxOverview overviewData,
+ lv_obj_t *lastView)
+{
+ PtrT_VecFFI_PtrString programs = overviewData->additional_unknown_programs;
+ if (programs == NULL) {
+ return lastView;
+ }
+
+ 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);
+ if (lastView != NULL) {
+ lv_obj_align_to(programCard, lastView, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 16);
+ }
+ lastView = programCard;
+ }
+ return lastView;
+}
+
static void GuiShowSolTxAdditionalUnknownPrograms(
lv_obj_t *parent,
PtrT_DisplaySolanaTxOverview overviewData)
@@ -1015,19 +1060,7 @@ static void GuiShowSolTxAdditionalUnknownPrograms(
}
}
- 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;
- }
+ GuiAppendSolTxAdditionalUnknownProgramCards(parent, overviewData, lastView);
}
static void GuiShowSolTxUnknownOverview(lv_obj_t *parent)
{
@@ -1163,7 +1196,7 @@ static void GuiShowSolTxMultiSigCreateDetail(lv_obj_t *parent, PtrT_DisplaySolan
lv_obj_set_style_pad_all(memberItem, 0, LV_PART_MAIN); // remove all default padding
lv_obj_set_style_pad_left(memberItem, 24, LV_PART_MAIN);
lv_obj_set_style_pad_top(memberItem, 16, LV_PART_MAIN);
- lv_obj_add_flag(memberItem, LV_OBJ_FLAG_SCROLLABLE);
+ lv_obj_clear_flag(memberItem, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t *memberItemLabel = lv_label_create(memberItem);
char memberText[64];
snprintf(memberText, sizeof(memberText), "Member %d", i + 1);
@@ -1446,12 +1479,20 @@ void GuiShowSolTxOverview(lv_obj_t *parent, void *totalData)
lv_obj_add_flag(parent, LV_OBJ_FLAG_CLICKABLE);
DisplaySolanaTx *txData = (DisplaySolanaTx*)totalData;
PtrT_DisplaySolanaTxOverview overviewData = txData->overview;
+ bool unknownProgramsRendered = false;
if (0 == strcmp(overviewData->display_type, "Transfer")) {
GuiShowSolTxTransferOverview(parent, overviewData);
} else if (0 == strcmp(overviewData->display_type, "Vote")) {
GuiShowSolTxVoteOverview(parent, overviewData);
} else if (0 == strcmp(overviewData->display_type, "General")) {
- GuiShowSolTxGeneralOverview(parent, overviewData, NULL);
+ lv_obj_t *lastView = NULL;
+ if (GuiHasSolTxAdditionalUnknownPrograms(overviewData)) {
+ lastView = GuiCreateWarningCard(parent);
+ lv_obj_align(lastView, LV_ALIGN_TOP_LEFT, 0, 0);
+ }
+ lastView = GuiShowSolTxGeneralOverview(parent, overviewData, lastView);
+ GuiAppendSolTxAdditionalUnknownProgramCards(parent, overviewData, lastView);
+ unknownProgramsRendered = true;
} else if (0 == strcmp(overviewData->display_type, "squads_multisig_create")) {
GuiShowSolTxMultiSigCreateOverview(parent, overviewData);
} else if (0 == strcmp(overviewData->display_type, "TokenTransfer")) {
@@ -1475,12 +1516,18 @@ void GuiShowSolTxOverview(lv_obj_t *parent, void *totalData)
0 != strcmp(overviewData->display_type, "jupiterv6_swap")) {
GuiShowSolTxGeneralOverview(parent, overviewData, GuiGetSolTxBottomView(parent));
}
- GuiShowSolTxAdditionalUnknownPrograms(parent, overviewData);
+ if (!unknownProgramsRendered) {
+ GuiShowSolTxAdditionalUnknownPrograms(parent, overviewData);
+ }
lv_obj_update_layout(parent);
lv_obj_scroll_to_y(parent, 0, LV_ANIM_OFF);
}
-static void GuiShowSolTxRawDetailCard(lv_obj_t *parent, PtrString txDetail, lv_obj_t *lastView)
+static void GuiShowSolTxRawDetailCard(
+ lv_obj_t *parent,
+ PtrString txDetail,
+ lv_obj_t *lastView,
+ bool useParentScroll)
{
lv_obj_t *cont = lv_obj_create(parent);
lv_obj_set_size(cont, SOL_COMPONENT_WIDTH, 444);
@@ -1495,13 +1542,18 @@ static void GuiShowSolTxRawDetailCard(lv_obj_t *parent, PtrString txDetail, lv_o
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_align(cont, LV_ALIGN_TOP_LEFT, 0, 0);
- lv_obj_add_flag(cont, LV_OBJ_FLAG_SCROLLABLE);
- lv_obj_add_flag(cont, LV_OBJ_FLAG_CLICKABLE);
+ if (useParentScroll) {
+ lv_obj_clear_flag(cont, LV_OBJ_FLAG_SCROLLABLE);
+ lv_obj_clear_flag(cont, LV_OBJ_FLAG_CLICKABLE);
+ } else {
+ lv_obj_add_flag(cont, LV_OBJ_FLAG_SCROLLABLE);
+ lv_obj_add_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;
- cJSON *root = cJSON_Parse(rawDetail);
+ cJSON *root = useParentScroll ? NULL : cJSON_Parse(rawDetail);
char *retStr = root == NULL ? NULL : cJSON_PrintBuffered(root, BUFFER_SIZE_1024, false);
lv_label_set_text(label, retStr == NULL ? rawDetail : retStr);
EXT_FREE(retStr);
@@ -1510,6 +1562,10 @@ static void GuiShowSolTxRawDetailCard(lv_obj_t *parent, PtrString txDetail, lv_o
lv_obj_set_width(label, SOL_COMPONENT_CONTENT_WIDTH);
SetTitleLabelStyle(label);
lv_obj_align(label, LV_ALIGN_TOP_LEFT, 0, 0);
+ if (useParentScroll) {
+ lv_obj_update_layout(label);
+ lv_obj_set_height(cont, lv_obj_get_height(label) + 32);
+ }
if (lastView == NULL) {
lv_obj_align(cont, LV_ALIGN_TOP_LEFT, 0, 0);
} else {
@@ -1532,10 +1588,11 @@ void GuiShowSolTxDetail(lv_obj_t *parent, void *totalData)
lv_obj_set_scrollbar_mode(parent, LV_SCROLLBAR_MODE_OFF);
GuiShowSolTxMultiSigCreateDetail(parent, overviewData);
lv_obj_update_layout(parent);
- GuiShowSolTxRawDetailCard(parent, txData->detail, GuiGetSolTxBottomView(parent));
+ GuiShowSolTxRawDetailCard(
+ parent, txData->detail, GuiGetSolTxBottomView(parent), true);
lv_obj_update_layout(parent);
lv_obj_scroll_to_y(parent, 0, LV_ANIM_OFF);
return;
}
- GuiShowSolTxRawDetailCard(parent, txData->detail, NULL);
+ GuiShowSolTxRawDetailCard(parent, txData->detail, NULL, false);
}
### src/ui/gui_widgets/gui_transaction_detail_widgets.c
@@ -166,8 +166,11 @@ void GuiTransactionDetailInit(uint8_t viewType)
g_pageWidget = CreatePageWidget();
g_needSign = true;
GuiTransactionDetailNavBarInit();
- ParseTransaction(g_viewType);
g_signSlider = GuiCreateConfirmSlider(g_pageWidget->contentZone, CheckSliderProcessHandler);
+ // A transaction must never be signable before parsing completes.
+ lv_obj_add_state(g_signSlider, LV_STATE_DISABLED);
+ lv_obj_set_style_bg_img_src(g_signSlider, &imgDenySign, LV_PART_KNOB);
+ ParseTransaction(g_viewType);
g_fingerSignCount = 0;
GuiPendingHintBoxMoveToTargetParent(lv_scr_act());
}
@@ -190,6 +193,11 @@ void GuiTransactionDetailDeInit()
//should get error cod here
void GuiTransactionParseFailed()
{
+ g_needSign = false;
+ if (g_signSlider != NULL) {
+ lv_obj_add_state(g_signSlider, LV_STATE_DISABLED);
+ lv_obj_set_style_bg_img_src(g_signSlider, &imgDenySign, LV_PART_KNOB);
+ }
#ifndef BTC_ONLY
if (GetCurrentTransactionMode() == TRANSACTION_MODE_USB) {
const char *data = "UR parsing failed";
@@ -208,7 +216,10 @@ void GuiTransactionDetailRefresh()
static void ThrowError(int32_t errorCode)
{
- g_parseErrorHintBox = GuiCreateErrorCodeWindow(errorCode, &g_parseErrorHintBox, NULL);
+ g_parseErrorHintBox = GuiCreateErrorCodeWindow(
+ errorCode,
+ &g_parseErrorHintBox,
+ (ErrorWindowCallback)GuiCloseCurrentWorkingView);
}
void GuiTransactionDetailParseSuccess(void *param)
@@ -218,15 +229,21 @@ void GuiTransactionDetailParseSuccess(void *param)
if (!g_needSign) {
GuiCreateErrorCodeWindow(ERR_MULTISIG_TRANSACTION_ALREADY_SIGNED, NULL, (ErrorWindowCallback)GuiCloseCurrentWorkingView);
}
- bool isCanSign = GuiCheckIsTransactionSign();
+ bool isCanSign = g_needSign && GuiCheckIsTransactionSign();
if (!isCanSign) {
lv_obj_add_state(g_signSlider, LV_STATE_DISABLED);
lv_obj_set_style_bg_img_src(g_signSlider, &imgDenySign, LV_PART_KNOB);
+ } else {
+ lv_obj_clear_state(g_signSlider, LV_STATE_DISABLED);
+ lv_obj_set_style_bg_img_src(g_signSlider, &imgConfirmSlider, LV_PART_KNOB);
}
}
void GuiTransactionDetailVerifyPasswordSuccess(void)
{
+ if (!g_needSign) {
+ return;
+ }
GUI_DEL_OBJ(g_fingerSingContainer)
GuiDeleteKeyboardWidget(g_keyboardWidget);
#ifdef BTC_ONLY
@@ -343,6 +360,10 @@ static void CheckSliderProcessHandler(lv_event_t *e)
{
lv_event_code_t code = lv_event_get_code(e);
if (code == LV_EVENT_RELEASED) {
+ if (!g_needSign) {
+ lv_slider_set_value(lv_event_get_target(e), 0, LV_ANIM_OFF);
+ return;
+ }
int32_t value = lv_slider_get_value(lv_event_get_target(e));
if (value >= QRCODE_CONFIRM_SIGN_PROCESS) {
if ((GetCurrentAccountIndex() < 3) && GetFingerSignFlag() && g_fingerSignCount < 3) {
### ui_simulator/simulator_model.c
@@ -23,7 +23,7 @@ bool g_reboot = false;
bool g_otpProtect = false;
// Comment out this macro if you need to retrieve data from the file
-#define GET_QR_DATA_FROM_SCREEN
+// #define GET_QR_DATA_FROM_SCREEN
void OTP_PowerOn(void)
{Why this scored 63/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.