What changed, and why it matters
This firmware update for the Keystone 3 hardware wallet fixes several security and safety issues found during an internal AI-assisted review. The most important changes are: (1) the signing slider is now disabled until a transaction is fully parsed, so a malformed transaction cannot be accidentally approved; (2) Arweave data-item transactions are checked for tampered tag counts before they reach the confirmation screen; (3) Avalanche transactions are fully re-parsed during the safety check so a modified transaction is rejected; (4) Bitcoin-like fee warnings now use network-specific thresholds, so Dogecoin/Litecoin users are not falsely warned or misled by Bitcoin defaults; and (5) Cosmos no longer shows a duplicated/confusing 'Max Fee' field, and Solana Squads multi-sig memos are attached to the correct instruction. Most other changes are UI clean-up.
Treat this commit as a security-hardening patch. Review the new pre-check logic for Arweave and Avalanche with targeted fuzzing, verify that the disabled signing slider cannot be bypassed through refresh or password-success paths, and confirm the new LargeFeePolicy thresholds match each network's documented conventional fee ranges. Because the patch is large and spans many chains, regression-test transaction parsing and UI rendering for all affected cryptocurrencies before release.
Security signals we found
Signing slider disabled until parse success prevents approval of malformed transactions
Arweave DataItem tag-count mismatch rejected in pre-check before confirmation page
Avalanche transaction re-validated by type during fingerprint check
Bitcoin-family large-fee policy made network-specific (BTC/LTC/DOGE/DASH/BCH/Zcash)
Cosmos duplicate Max Fee field removed to avoid misleading fee display
Solana Squads memo correctly attributed to VaultTransactionCreate instead of ProposalCreate
Evidence from the diff
The commit is a broad defensive patch across Rust transaction parsers and the C UI layer. Security-relevant diffs include: disabling the signing slider (g_signSlider) by default and on parse failure, and only re-enabling it after successful parse plus GuiCheckIsTransactionSign(); ar_check_tx now calls parse_data_item for SignType::DataItem during the pre-check, rejecting tag-count mismatches before display; avax_check_transaction now invokes validate_transaction_by_type to re-validate the serialized transaction against the type ID, with a test showing it rejects a replaced first output asset ID; parsed_tx.rs replaces hard-coded Bitcoin large-fee constants with per-network LargeFeePolicy; Cosmos FeeDetail loses the redundant max_fee field; Solana parser moves the SquadsV4 memo from ProposalCreate to VaultTransactionCreate and exposes it in the general overview struct. UI changes update the corresponding display code. No CVE, advisory, or external attribution is present in the supplied materials.
Changed components
rust/apps/arweave/src/ao_transaction.rsrust/apps/arweave/src/data_item.rsrust/apps/bitcoin/src/network.rsrust/apps/bitcoin/src/transactions/parsed_tx.rsrust/apps/cosmos/src/proto_wrapper/fee.rsrust/apps/cosmos/src/transaction/mod.rsrust/apps/cosmos/src/transaction/structs.rsrust/apps/solana/src/parser/mod.rsrust/apps/solana/src/parser/overview.rsrust/rust_c/src/arweave/mod.rsrust/rust_c/src/avalanche/mod.rsrust/rust_c/src/solana/structs.rssrc/ui/gui_widgets/gui_transaction_detail_widgets.csrc/ui/gui_chain/multi/web3/gui_ar.csrc/ui/gui_chain/multi/web3/gui_avax.csrc/ui/gui_chain/multi/web3/gui_cosmos.csrc/ui/gui_chain/multi/web3/gui_sol.cInspect 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 61/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.