Merge pull request #2262 from KeystoneHQ/regular-review-fix
What changed, and why it matters
This firmware update fixes several security and safety issues found during a regular AI-assisted code review. The most important changes are: (1) Zcash wallet data is now encrypted with a key derived from the wallet seed and a fresh random IV, instead of being encrypted with a simple hash of the login password and a fixed seed-derived IV. (2) Ethereum legacy transactions now show a warning when they are not protected against replay attacks. (3) Solana transactions that contain hidden trailing bytes are now rejected before the user can review them. (4) Solana mixed transactions no longer hide unrelated instructions behind a specialized UI; sibling instructions are shown separately. (5) Tron transactions now reject multiple contracts, unsupported contract types, and ignore untrusted token metadata supplied by a host computer. (6) Cosmos delegate/undelegate/redelegate labels were corrected so users see the right addresses. (7) A new paginated message viewer helps users read long Ethereum messages before signing. The commit is described by the vendor as a 'regular AI review and fix' and does not explicitly call itself a security patch, but the changes clearly remove weaknesses that could mislead users or leak sensitive data.
Treat this as a security-relevant firmware update and ensure it is included in the next release. Users with Zcash enabled should be encouraged to update so that stored UFVK blobs are migrated to the new seed-derived-key + random-IV format. QA should verify that legacy Zcash blobs are regenerated on first unlock, that Ethereum legacy non-EIP-155 transactions show the replay warning, that Solana transactions with trailing bytes are rejected, and that mixed Solana transactions display all sibling instructions. The patch is broad and partially defensive, so a full regression of transaction parsing for Ethereum, Solana, Tron, Cosmos, and Zcash is warranted.
Security signals we found
Zcash UFVK encryption key changed from sha256(password) to seed-derived BIP32 key
Zcash UFVK IV changed from deterministic seed-derived value to fresh TRNG-generated IV per blob
Zcash UFVK storage format now uses magic-prefixed blob with embedded IV and rejects legacy blobs
Ethereum legacy transactions now expose replay_protected flag and warn when false
Solana parser no longer filters out sibling instructions for specialized display types
Solana check/parse paths reject malformed transactions with trailing bytes
Tron raw parser rejects multiple contracts and unsupported contract types
Tron payload parser ignores host-supplied token override metadata and falls back to local allowlist or raw units
Tron adds TRC-20 Approve decoding in addition to Transfer
Cosmos delegate/undelegate/redelegate labels corrected to Delegator/Validator/New Validator
New paginated message viewer for long Ethereum messages
Evidence from the diff
The commit is a large defensive patch across Rust transaction parsers and the C UI layer. Key technical changes: Zcash UFVK encryption in rust/rust_c/src/zcash/mod.rs and src/crypto/account_public_info.c / src/managers/account_manager.c replaces sha256(password) keying and a deterministic seed-derived IV with a seed-derived AES-256 key (BIP32 m/44’/1557192335’/0’/3’/0’) and a TRNG-generated IV packed into a ‘z2’ magic-prefixed blob; legacy blobs are rejected and regenerated. Ethereum legacy parsing in rust/apps/ethereum/src/legacy_transaction.rs and rust/rust_c/src/ethereum/structs.rs adds a replay_protected flag based on is_eip155_compatible(), and the UI in src/ui/gui_chain/multi/web3/gui_eth.c displays a warning for unprotected legacy transactions. Solana parsing in rust/apps/solana/src/parser/mod.rs removes the ‘primary_details’ filter that previously hid mixed-transaction siblings, adds additional_overviews for sibling instructions, and rust/rust_c/src/solana/mod.rs rejects MalformedTransaction payloads (trailing bytes) in both check and parse paths. Tron in rust/apps/tron/src/transaction/wrapped_tron.rs now rejects multiple raw contracts and unsupported contract types, decodes TRC-20 approve as well as transfer, and ignores host-supplied token override metadata in favor of a local allowlist. Cosmos in rust/apps/cosmos/src/transaction/overview.rs renames Delegate fields from From/To to Delegator/Validator, and src/ui/gui_chain/multi/web3/gui_cosmos.c updates the UI labels accordingly. A new paginated message widget in src/ui/gui_chain/gui_chain_components.c / .h is wired to Ethereum personal-message review in src/ui/gui_analyze/gui_analyze.c and src/ui/gui_chain/multi/web3/gui_eth.c. Several UI width constants and warning labels are also adjusted.
Changed components
rust/rust_c/src/zcash/mod.rssrc/crypto/account_public_info.csrc/managers/account_manager.crust/apps/ethereum/src/legacy_transaction.rsrust/apps/ethereum/src/structs.rsrust/rust_c/src/ethereum/structs.rssrc/ui/gui_chain/multi/web3/gui_eth.crust/apps/solana/src/parser/mod.rsrust/apps/solana/src/parser/overview.rsrust/apps/solana/src/parser/structs.rsrust/rust_c/src/solana/mod.rsrust/rust_c/src/solana/structs.rsrust/apps/tron/src/transaction/wrapped_tron.rsrust/apps/tron/src/transaction/parser.rsrust/apps/cosmos/src/transaction/overview.rssrc/ui/gui_chain/multi/web3/gui_cosmos.csrc/ui/gui_chain/gui_chain_components.csrc/ui/gui_chain/gui_chain_components.hsrc/ui/gui_analyze/gui_analyze.cInspect captured patch +1852 / −526
### rust/apps/cosmos/src/transaction/mod.rs
@@ -240,8 +240,8 @@ mod tests {
{
"Method": "Delegate",
"Value": "10000000000000000 atevmos",
- "From": "evmos1tqsdz785sqjnlggee0lwxjwfk6dl36ae2uf9er",
- "To": "evmosvaloper10t6kyy4jncvnevmgq6q2ntcy90gse3yxa7x2p4"
+ "Delegator": "evmos1tqsdz785sqjnlggee0lwxjwfk6dl36ae2uf9er",
+ "Validator": "evmosvaloper10t6kyy4jncvnevmgq6q2ntcy90gse3yxa7x2p4"
}
]
});
@@ -282,8 +282,8 @@ mod tests {
{
"Method": "Delegate",
"Value": "2000000 uosmo",
- "From": "osmo17u02f80vkafne9la4wypdx3kxxxxwm6fzmcgyc",
- "To": "osmovaloper1hh0g5xf23e5zekg45cmerc97hs4n2004dy2t26"
+ "Delegator": "osmo17u02f80vkafne9la4wypdx3kxxxxwm6fzmcgyc",
+ "Validator": "osmovaloper1hh0g5xf23e5zekg45cmerc97hs4n2004dy2t26"
}
]
});
### rust/apps/cosmos/src/transaction/overview.rs
@@ -44,9 +44,9 @@ pub struct OverviewDelegate {
pub method: String,
#[serde(skip_serializing_if = "String::is_empty", rename(serialize = "Value"))]
pub value: String,
- #[serde(rename(serialize = "From"))]
+ #[serde(rename(serialize = "Delegator"))]
pub from: String,
- #[serde(rename(serialize = "To"))]
+ #[serde(rename(serialize = "Validator"))]
pub to: String,
}
### rust/apps/ethereum/src/batch_tx_rules.rs
@@ -64,6 +64,7 @@ mod tests {
max_priority: None,
gas_limit: "21000".to_string(),
max_txn_fee: "0".to_string(),
+ replay_protected: true,
}
}
### rust/apps/ethereum/src/legacy_transaction.rs
@@ -277,10 +277,12 @@ pub struct ParsedLegacyTransaction {
pub(crate) input: String,
pub(crate) chain_id: u64,
pub(crate) max_txn_fee: String,
+ pub(crate) replay_protected: bool,
}
impl From<LegacyTransaction> for ParsedLegacyTransaction {
fn from(value: LegacyTransaction) -> Self {
+ let replay_protected = value.is_eip155_compatible();
Self {
nonce: value.nonce.to_string(),
gas_price: normalize_price(value.gas_price),
@@ -290,6 +292,7 @@ impl From<LegacyTransaction> for ParsedLegacyTransaction {
input: hex::encode(value.get_data()),
chain_id: value.chain_id(),
max_txn_fee: normalize_value(value.gas_price.mul(value.gas_limit)),
+ replay_protected,
}
}
}
@@ -561,6 +564,7 @@ mod tests {
"".to_string(),
);
assert_eq!(tx.chain_id(), 1); // Default chain_id for unsigned
+ assert!(!tx.is_eip155_compatible());
// Test EIP-155 compatible transaction (v > 35)
// For signed transactions, s should not be zero
### rust/apps/ethereum/src/structs.rs
@@ -65,6 +65,7 @@ pub struct ParsedEthereumTransaction {
pub gas_limit: String,
pub max_txn_fee: String,
+ pub replay_protected: bool,
}
impl ParsedEthereumTransaction {
@@ -87,6 +88,7 @@ impl ParsedEthereumTransaction {
max_priority_fee_per_gas: None,
max_fee: None,
max_priority: None,
+ replay_protected: tx.replay_protected,
})
}
@@ -108,6 +110,7 @@ impl ParsedEthereumTransaction {
max_fee: Some(tx.max_fee),
max_priority: Some(tx.max_priority),
gas_price: None,
+ replay_protected: true,
})
}
}
### rust/apps/solana/src/parser/mod.rs
@@ -58,11 +58,13 @@ impl ParsedSolanaTx {
let raw_details = message.to_program_details()?;
let display_type = Self::detect_display_type(&raw_details);
let parsed_overview = Self::build_overview(&display_type, &raw_details)?;
+ let additional_overviews = Self::build_additional_overviews(&display_type, &raw_details)?;
let unknown_programs = Self::collect_additional_unknown_programs(&raw_details);
let parsed_detail = Self::build_detail(&display_type, &raw_details, &message)?;
Ok(Self {
display_type,
overview: parsed_overview,
+ additional_overviews,
unknown_programs,
detail: parsed_detail,
network: "Solana Mainnet".to_string(),
@@ -77,56 +79,49 @@ impl ParsedSolanaTx {
if unknown_count == details.len() {
return SolanaTxDisplayType::Unknown;
}
- let primary_details = details
- .iter()
- .filter(|detail| {
- !Self::is_unknown_detail(&detail.common)
- && !Self::is_compute_budget_detail(&detail.common)
- })
- .collect::<Vec<_>>();
-
let squads = details
.iter()
.filter(|d| Self::is_sqauds_v4_detail(&d.common))
.collect::<Vec<&SolanaDetail>>();
- if !squads.is_empty()
- && primary_details.iter().all(|detail| {
- Self::is_sqauds_v4_detail(&detail.common)
- || Self::is_system_transfer_detail(&detail.common)
- })
- {
+ if !squads.is_empty() {
return SolanaTxDisplayType::SquadsV4;
}
let jupiter = details
.iter()
.filter(|d| Self::is_jupiter_v6_detail(&d.common))
.collect::<Vec<&SolanaDetail>>();
- if jupiter.len() == 1 && primary_details.len() == 1 {
+ // Keep the purpose-built Jupiter review for a single swap instruction.
+ // Every sibling instruction is separately retained in
+ // `additional_overviews`/`unknown_programs`, so this does not hide
+ // mixed-transaction actions.
+ if jupiter.len() == 1 {
return SolanaTxDisplayType::JupiterV6;
}
let transfer: Vec<&SolanaDetail> = details
.iter()
.filter(|d| Self::is_system_transfer_detail(&d.common))
.collect::<Vec<&SolanaDetail>>();
- if transfer.len() == 1 && primary_details.len() == 1 {
+ if transfer.len() == 1 {
return SolanaTxDisplayType::Transfer;
}
- // if contains token transfer check
+ // Keep both SPL Token transfer variants on the purpose-built transfer UI.
+ // A plain `Transfer` does not include mint metadata, but its source,
+ // destination, and authority roles are still known and must be shown.
let token_transfer: Vec<&SolanaDetail> = details
.iter()
- .filter(|d| Self::is_token_transfer_checked_detail(&d.common))
+ .filter(|d| Self::is_token_transfer_detail(&d.common))
.collect::<Vec<&SolanaDetail>>();
- if token_transfer.len() == 1 && primary_details.len() == 1 {
+ if token_transfer.len() == 1 {
return SolanaTxDisplayType::TokenTransfer;
}
let vote: Vec<&SolanaDetail> = details
.iter()
.filter(|d| Self::is_vote_detail(&d.common))
.collect::<Vec<&SolanaDetail>>();
- if vote.len() == 1 && primary_details.len() == 1 {
+ if vote.len() == 1 {
return SolanaTxDisplayType::Vote;
}
SolanaTxDisplayType::General
@@ -136,8 +131,9 @@ impl ParsedSolanaTx {
common.program.eq("System") && common.method.eq("Transfer")
}
- fn is_token_transfer_checked_detail(common: &CommonDetail) -> bool {
- common.program.eq("Token") && common.method.eq("TransferChecked")
+ fn is_token_transfer_detail(common: &CommonDetail) -> bool {
+ common.program.eq("Token")
+ && (common.method.eq("TransferChecked") || common.method.eq("Transfer"))
}
fn is_vote_detail(common: &CommonDetail) -> bool {
@@ -406,37 +402,58 @@ impl ParsedSolanaTx {
utils::format_token_amount(amount, decimals)
}
- fn build_token_transfer_checked_overview(details: &[SolanaDetail]) -> Result<SolanaOverview> {
+ fn build_token_transfer_overview(details: &[SolanaDetail]) -> Result<SolanaOverview> {
let detail = details
.iter()
- .find(|d| Self::is_token_transfer_checked_detail(&d.common))
+ .find(|d| Self::is_token_transfer_detail(&d.common))
.ok_or_else(|| {
SolanaError::ParseTxError(
"parse spl token transfer failed, empty transfer program".to_string(),
)
})?;
- let ProgramDetail::TokenTransferChecked(value) = &detail.kind else {
- return Err(SolanaError::ParseTxError(
- "parse spl token transfer failed, invalid transfer program".to_string(),
- ));
- };
- Self::validate_token_decimals(&value.mint, value.decimals)?;
- let amount =
- Self::format_token_amount_for_display(&value.mint, &value.amount, value.decimals)?;
- let unusual_decimals = Self::has_unusual_token_decimals(&value.mint, value.decimals);
- Ok(SolanaOverview::SplTokenTransfer(
- ProgramOverviewSplTokenTransfer {
- source: value.account.to_string(),
- destination: value.recipient.to_string(),
- authority: value.owner.to_string(),
- decimals: value.decimals,
- amount: format!("{} {}", amount, Self::find_token_info(&value.mint).0),
- token_mint_account: value.mint.clone(),
- token_symbol: Self::find_token_info(&value.mint).0,
- token_name: Self::find_token_info(&value.mint).1,
- unusual_decimals,
+ let overview = match &detail.kind {
+ ProgramDetail::TokenTransferChecked(value) => {
+ Self::validate_token_decimals(&value.mint, value.decimals)?;
+ let amount = Self::format_token_amount_for_display(
+ &value.mint,
+ &value.amount,
+ value.decimals,
+ )?;
+ let unusual_decimals =
+ Self::has_unusual_token_decimals(&value.mint, value.decimals);
+ ProgramOverviewSplTokenTransfer {
+ source: value.account.to_string(),
+ destination: value.recipient.to_string(),
+ authority: value.owner.to_string(),
+ decimals: value.decimals,
+ amount: format!("{} {}", amount, Self::find_token_info(&value.mint).0),
+ token_mint_account: value.mint.clone(),
+ token_symbol: Self::find_token_info(&value.mint).0,
+ token_name: Self::find_token_info(&value.mint).1,
+ unusual_decimals,
+ }
+ }
+ ProgramDetail::TokenTransfer(value) => ProgramOverviewSplTokenTransfer {
+ source: value.source_account.clone(),
+ destination: value.recipient.clone(),
+ authority: value.owner.clone(),
+ decimals: 0,
+ amount: format!("{} raw units", value.amount),
+ // A plain SPL Token `Transfer` does not carry mint or decimal
+ // metadata. Leave token metadata empty instead of guessing, while
+ // still reusing the established transfer review UI.
+ token_mint_account: String::new(),
+ token_symbol: String::new(),
+ token_name: String::new(),
+ unusual_decimals: false,
},
- ))
+ _ => {
+ return Err(SolanaError::ParseTxError(
+ "parse spl token transfer failed, invalid transfer program".to_string(),
+ ))
+ }
+ };
+ Ok(SolanaOverview::SplTokenTransfer(overview))
}
fn build_vote_overview(details: &[SolanaDetail]) -> Result<SolanaOverview> {
let overview: Option<SolanaOverview> = details
@@ -458,13 +475,38 @@ impl ParsedSolanaTx {
))
}
- fn build_general_overview(details: &[SolanaDetail]) -> Result<SolanaOverview> {
+ fn is_represented_by_primary_overview(
+ display_type: &SolanaTxDisplayType,
+ detail: &SolanaDetail,
+ ) -> bool {
+ match display_type {
+ SolanaTxDisplayType::Transfer => Self::is_system_transfer_detail(&detail.common),
+ SolanaTxDisplayType::TokenTransfer => Self::is_token_transfer_detail(&detail.common),
+ SolanaTxDisplayType::Vote => Self::is_vote_detail(&detail.common),
+ SolanaTxDisplayType::JupiterV6 => Self::is_jupiter_v6_detail(&detail.common),
+ SolanaTxDisplayType::SquadsV4 => {
+ Self::is_sqauds_v4_detail(&detail.common)
+ || Self::is_system_transfer_detail(&detail.common)
+ }
+ SolanaTxDisplayType::General | SolanaTxDisplayType::Unknown => false,
+ }
+ }
+
+ fn build_general_items(
+ details: &[SolanaDetail],
+ primary_display_type: Option<&SolanaTxDisplayType>,
+ ) -> Result<Vec<ProgramOverviewGeneral>> {
let mut overview = Vec::new();
- for d in details {
- if Self::is_unknown_detail(&d.common) {
+ for (index, d) in details.iter().enumerate() {
+ if Self::is_unknown_detail(&d.common)
+ || primary_display_type
+ .map(|display_type| Self::is_represented_by_primary_overview(display_type, d))
+ .unwrap_or(false)
+ {
continue;
}
let mut item = ProgramOverviewGeneral {
+ instruction_index: index + 1,
program: d.common.program.to_string(),
method: d.common.method.to_string(),
value: String::new(),
@@ -513,10 +555,30 @@ impl ParsedSolanaTx {
}
overview.push(item)
}
- Ok(SolanaOverview::General(overview))
+ Ok(overview)
+ }
+
+ fn build_general_overview(details: &[SolanaDetail]) -> Result<SolanaOverview> {
+ Ok(SolanaOverview::General(Self::build_general_items(
+ details, None,
+ )?))
+ }
+
+ fn build_additional_overviews(
+ display_type: &SolanaTxDisplayType,
+ details: &[SolanaDetail],
+ ) -> Result<Vec<ProgramOverviewGeneral>> {
+ if matches!(
+ display_type,
+ SolanaTxDisplayType::General | SolanaTxDisplayType::Unknown
+ ) {
+ return Ok(Vec::new());
+ }
+ Self::build_general_items(details, Some(display_type))
}
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 {
@@ -529,10 +591,31 @@ 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: None,
+ memo,
data: serde_json::to_string(v).ok(),
});
}
@@ -987,9 +1070,7 @@ impl ParsedSolanaTx {
SolanaTxDisplayType::General => Self::build_general_overview(details),
SolanaTxDisplayType::Unknown => Self::build_instructions_overview(details),
SolanaTxDisplayType::SquadsV4 => Self::build_squads_overview(details),
- SolanaTxDisplayType::TokenTransfer => {
- Self::build_token_transfer_checked_overview(details)
- }
+ SolanaTxDisplayType::TokenTransfer => Self::build_token_transfer_overview(details),
SolanaTxDisplayType::JupiterV6 => Self::build_jupiter_v6_overview(details),
}
}
@@ -1045,6 +1126,65 @@ mod tests {
]),
SolanaTxDisplayType::JupiterV6
));
+ let mixed_jupiter = [
+ detail("ComputeBudget", "SetComputeUnitLimit"),
+ detail("ComputeBudget", "SetComputeUnitPrice"),
+ detail("JupiterV6", "SharedAccountsRoute"),
+ detail("Token", "CloseAccount"),
+ detail("Unknown", ""),
+ ];
+ assert!(matches!(
+ ParsedSolanaTx::detect_display_type(&mixed_jupiter),
+ SolanaTxDisplayType::JupiterV6
+ ));
+ let siblings = ParsedSolanaTx::build_additional_overviews(
+ &SolanaTxDisplayType::JupiterV6,
+ &mixed_jupiter,
+ )
+ .unwrap();
+ assert_eq!(siblings.len(), 3);
+ assert_eq!(siblings[0].instruction_index, 1);
+ assert_eq!(siblings[0].program, "ComputeBudget");
+ assert_eq!(siblings[1].instruction_index, 2);
+ assert_eq!(siblings[2].instruction_index, 4);
+ assert_eq!(siblings[2].method, "CloseAccount");
+
+ for (primary, details, expected_addon) in [
+ (
+ SolanaTxDisplayType::Transfer,
+ [
+ detail("System", "Transfer"),
+ detail("ComputeBudget", "SetComputeUnitLimit"),
+ ],
+ "ComputeBudget",
+ ),
+ (
+ SolanaTxDisplayType::TokenTransfer,
+ [
+ detail("Token", "TransferChecked"),
+ detail("Token", "CloseAccount"),
+ ],
+ "Token",
+ ),
+ (
+ SolanaTxDisplayType::Vote,
+ [detail("Vote", "Vote"), detail("Memo", "Memo")],
+ "Memo",
+ ),
+ (
+ SolanaTxDisplayType::SquadsV4,
+ [
+ detail("SquadsV4", "ProposalCreate"),
+ detail("ComputeBudget", "SetComputeUnitLimit"),
+ ],
+ "ComputeBudget",
+ ),
+ ] {
+ let addons = ParsedSolanaTx::build_additional_overviews(&primary, &details).unwrap();
+ assert_eq!(addons.len(), 1);
+ assert_eq!(addons[0].program, expected_addon);
+ assert_eq!(addons[0].instruction_index, 2);
+ }
for (program, method, expected) in [
("System", "Transfer", SolanaTxDisplayType::Transfer),
(
@@ -1074,6 +1214,90 @@ mod tests {
));
}
+ #[test]
+ fn squads_proposal_create_overview_shows_corresponding_memo() {
+ use crate::solana_lib::squads_v4::instructions::{
+ ProposalCreateArgs, VaultTransactionCreateArgs,
+ };
+
+ let mut details = Vec::new();
+ for (transaction_index, memo) in [(7, "Treasury payment"), (8, "Add new member")] {
+ details.push(SolanaDetail {
+ common: CommonDetail {
+ program: "SquadsV4".to_string(),
+ method: "VaultTransactionCreate".to_string(),
+ },
+ kind: ProgramDetail::SquadsV4VaultTransactionCreate(VaultTransactionCreateArgs {
+ vault_index: 0,
+ ephemeral_signers: 0,
+ transaction_message: Vec::new(),
+ memo: Some(memo.to_string()),
+ }),
+ });
+ details.push(SolanaDetail {
+ common: CommonDetail {
+ program: "SquadsV4".to_string(),
+ method: "ProposalCreate".to_string(),
+ },
+ kind: ProgramDetail::SquadsV4ProposalCreate(ProposalCreateArgs {
+ transaction_index,
+ draft: false,
+ }),
+ });
+ }
+
+ let overview = ParsedSolanaTx::build_squads_v4_proposal_overview(&details).unwrap();
+ let SolanaOverview::SquadsV4Proposal(items) = overview else {
+ panic!("expected Squads proposal overview");
+ };
+ let proposal_memos = items
+ .iter()
+ .filter(|item| item.method == "ProposalCreate")
+ .map(|item| item.memo.as_deref())
+ .collect::<Vec<_>>();
+ assert_eq!(
+ proposal_memos,
+ vec![Some("Treasury payment"), Some("Add new member")]
+ );
+ }
+
+ #[test]
+ fn plain_token_transfer_reuses_specialized_transfer_overview() {
+ use crate::parser::detail::ProgramDetailTokenTransfer;
+
+ let details = [SolanaDetail {
+ common: CommonDetail {
+ program: "Token".to_string(),
+ method: "Transfer".to_string(),
+ },
+ kind: ProgramDetail::TokenTransfer(ProgramDetailTokenTransfer {
+ source_account: "source-token-account".to_string(),
+ recipient: "destination-token-account".to_string(),
+ owner: "transfer-authority".to_string(),
+ signers: None,
+ amount: "1000000".to_string(),
+ }),
+ }];
+
+ assert!(matches!(
+ ParsedSolanaTx::detect_display_type(&details),
+ SolanaTxDisplayType::TokenTransfer
+ ));
+ let SolanaOverview::SplTokenTransfer(overview) =
+ ParsedSolanaTx::build_token_transfer_overview(&details).unwrap()
+ else {
+ panic!("expected specialized SPL Token transfer overview");
+ };
+ assert_eq!(overview.amount, "1000000 raw units");
+ assert_eq!(overview.source, "source-token-account");
+ assert_eq!(overview.destination, "destination-token-account");
+ assert_eq!(overview.authority, "transfer-authority");
+ assert!(overview.token_mint_account.is_empty());
+ assert!(overview.token_symbol.is_empty());
+ assert!(overview.token_name.is_empty());
+ assert!(!overview.unusual_decimals);
+ }
+
#[test]
fn token_metadata_and_decimal_safety_helpers() {
let known_mints = [
@@ -1140,7 +1364,7 @@ mod tests {
fn overview_builders_reject_missing_required_details() {
let empty = &[];
assert!(ParsedSolanaTx::build_transfer_overview(empty).is_err());
- assert!(ParsedSolanaTx::build_token_transfer_checked_overview(empty).is_err());
+ assert!(ParsedSolanaTx::build_token_transfer_overview(empty).is_err());
assert!(ParsedSolanaTx::build_vote_overview(empty).is_err());
let _ = ParsedSolanaTx::build_squads_v4_proposal_overview(empty);
let _ = ParsedSolanaTx::build_squads_v4_multisig_overview(empty);
### rust/apps/solana/src/parser/overview.rs
@@ -33,6 +33,7 @@ pub struct ProgramOverviewVote {
#[derive(Debug, Clone)]
pub struct ProgramOverviewGeneral {
+ pub instruction_index: usize,
pub program: String,
pub method: String,
pub value: String,
### rust/apps/solana/src/parser/structs.rs
@@ -2,12 +2,16 @@ use alloc::string::{String, ToString};
use alloc::vec::Vec;
-use crate::parser::overview::SolanaOverview;
+use crate::parser::overview::{ProgramOverviewGeneral, SolanaOverview};
#[derive(Clone, Debug)]
pub struct ParsedSolanaTx {
pub display_type: SolanaTxDisplayType,
pub overview: SolanaOverview,
+ /// Parsed sibling instructions which must be shown after a specialized
+ /// overview such as Jupiter. This keeps the richer primary UI without
+ /// hiding any other instruction committed by the signature.
+ pub additional_overviews: Vec<ProgramOverviewGeneral>,
pub unknown_programs: Vec<String>,
pub detail: String,
pub network: String,
### rust/apps/tron/src/transaction/parser.rs
@@ -176,7 +176,7 @@ mod tests {
"TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t".to_string(),
parsed_tx.detail.contract_address
);
- assert!(parsed_tx.detail.token.is_empty());
+ assert_eq!("USDT".to_string(), parsed_tx.detail.token);
}
{
let hex = "1f8b08000000000000036590bb4ec2500086158d212c4ae3409848638231697aaeeda993012f040a166c8074316d4f1b01a1da969b2383a3cfe0ec03b0bac81b38b8b8bab8b9b85a66bf7cc33f7ef9d32961b7199603ee158c30880337b8cdaf52e99490a648254405585ca632db66ebb2211c4080d6320901db9708f76dc9810c4b34c157180714a07cb62e135294a1468b32489461e1ebf169b580873ba5ef4d4134cbe7ba4ef98c5a555e1b75c65ed49df560db98f6f5f6b06359732d2f9aa7837034ad3f443066fde0a27d17e90a19d6303c0b5bb8177771248a666512c57a496db6260ebfbf31c60d7b6e065a87f6355c1f20ab3239da40fb10fce7b89139819e9ff47ac4610e5320f21dc401c19801e062c23d9ff89ae203e051c55535d725d4b63972806d3305a8d4657b9f3fcb5ceee3e5fd6d014bd9cc56f5ca14d2eb97ae9395132acfbf4ae1d5f803c61a369f5e010000";
### rust/apps/tron/src/transaction/wrapped_tron.rs
@@ -34,6 +34,7 @@ pub struct WrappedTron {
pub(crate) from: String,
pub(crate) to: String,
pub(crate) value: String,
+ pub(crate) method: String,
pub(crate) token_short_name: Option<String>,
pub(crate) divider: f64,
pub(crate) fee_limit: u64,
@@ -90,6 +91,13 @@ const KNOWN_TOKENS: &[(&str, &str, f64)] = &[
), // 18 decimals
];
+fn known_token_metadata(contract_address: &str) -> Option<(&'static str, f64)> {
+ KNOWN_TOKENS
+ .iter()
+ .find(|token| token.0 == contract_address)
+ .map(|token| (token.1, token.2))
+}
+
impl WrappedTron {
pub fn from_raw_transaction(raw_tx: Transaction, path: String) -> Result<Self> {
let mut instance = Self {
@@ -102,6 +110,7 @@ impl WrappedTron {
from: String::new(),
to: String::new(),
value: "0".to_string(),
+ method: String::new(),
divider: DIVIDER,
token_short_name: None,
fee_limit: 0,
@@ -112,10 +121,19 @@ impl WrappedTron {
if let Some(raw) = &instance.tron_tx.raw_data {
instance.fee_limit = raw.fee_limit as u64;
instance.memo = String::from_utf8_lossy(&raw.data).to_string();
+ if raw.contract.len() > 1 {
+ return Err(TronError::InvalidRawTxCryptoBytes(
+ "multiple contracts require a complete review".to_string(),
+ ));
+ }
if let Some(contract) = raw.contract.get(0) {
use crate::pb::protocol::transaction::contract::ContractType;
- let c_type = ContractType::from_i32(contract.r#type)
- .unwrap_or(ContractType::TransferContract);
+ let c_type = ContractType::from_i32(contract.r#type).ok_or(
+ TronError::InvalidRawTxCryptoBytes(format!(
+ "unsupported contract type {}",
+ contract.r#type
+ )),
+ )?;
if let Some(param) = &contract.parameter {
match c_type {
@@ -148,6 +166,7 @@ impl WrappedTron {
bitcoin::base58::encode_check(&ct.contract_address);
if ct.data.len() >= 68 && &ct.data[0..4] == &[0xa9, 0x05, 0x9c, 0xbb] {
+ instance.method = "TRC-20 Transfer".to_string();
let mut to_addr_bytes = vec![0x41u8];
to_addr_bytes.extend_from_slice(&ct.data[16..36]);
instance.to = bitcoin::base58::encode_check(&to_addr_bytes);
@@ -157,16 +176,39 @@ impl WrappedTron {
ethabi::ethereum_types::U256::from_big_endian(amount_bytes)
.to_string();
- if let Some(token_info) = KNOWN_TOKENS
- .iter()
- .find(|t| t.0 == instance.contract_address)
+ if let Some((token, divider)) =
+ known_token_metadata(&instance.contract_address)
{
- instance.token = token_info.1.to_string();
- instance.divider = token_info.2;
+ instance.token = token.to_string();
+ instance.divider = divider;
} else {
- instance.token = "TRC20 Token".to_string();
- instance.divider = 10u64.pow(6) as f64;
+ // Unknown token metadata must stay in raw units. A
+ // hard-coded decimal count would make the displayed
+ // amount look authoritative even though the contract
+ // has not been verified.
+ instance.token = "TRC20 Token (raw)".to_string();
+ instance.divider = 1.0;
}
+ } else if ct.data.len() >= 68
+ && &ct.data[0..4] == &[0x09, 0x5e, 0xa7, 0xb3]
+ {
+ instance.method = "TRC-20 Approve".to_string();
+ let mut spender_bytes = vec![0x41u8];
+ spender_bytes.extend_from_slice(&ct.data[16..36]);
+ instance.to = bitcoin::base58::encode_check(&spender_bytes);
+ instance.value =
+ U256::from_big_endian(&ct.data[36..68]).to_string();
+ if let Some((token, divider)) =
+ known_token_metadata(&instance.contract_address)
+ {
+ instance.token = token.to_string();
+ instance.divider = divider;
+ } else {
+ instance.token = "TRC20 Token (raw)".to_string();
+ instance.divider = 1.0;
+ }
+ } else {
+ instance.method = "Contract Call".to_string();
}
}
@@ -184,7 +226,12 @@ impl WrappedTron {
instance.token = String::from_utf8_lossy(&ct.asset_name).to_string();
instance.divider = DIVIDER;
}
- _ => {}
+ _ => {
+ return Err(TronError::InvalidRawTxCryptoBytes(format!(
+ "unsupported contract type {}",
+ contract.r#type
+ )))
+ }
}
}
}
@@ -415,13 +462,25 @@ impl WrappedTron {
.ok_or(TronError::InvalidRawTxCryptoBytes(
"empty transaction field for payload content".to_string(),
))?;
- let mut token_short_name: Option<String> = None;
+ // `override` is supplied by the sender and is not part of the signed
+ // transaction. Only the contract-address allowlist below may provide
+ // human-readable token metadata.
+ let token_short_name: Option<String> = None;
let mut divider = DIVIDER;
match tx {
TronTx(tx) => {
- if let Some(value) = tx.to_owned().r#override {
- token_short_name = Some(value.token_short_name);
- divider = 10u64.pow(value.decimals as u32) as f64;
+ let contract_address = tx.contract_address.to_string();
+ let mut token = tx.token.to_string();
+ if !contract_address.is_empty() {
+ if let Some((known_token, known_divider)) =
+ known_token_metadata(&contract_address)
+ {
+ token = known_token.to_string();
+ divider = known_divider;
+ } else {
+ token = "TRC20 Token (raw)".to_string();
+ divider = 1.0;
+ }
}
let mut tron_tx = if tx.contract_address.is_empty() {
Self::build_transfer_tx(tx)
@@ -447,11 +506,12 @@ impl WrappedTron {
extended_pubkey: context.extended_public_key.to_string(),
tron_tx,
xfp: payload.xfp,
- token: tx.token.to_string(),
- contract_address: tx.contract_address.to_string(),
+ token,
+ contract_address,
from: tx.from.to_string(),
to: tx.to.to_string(),
value: tx.value.to_string(),
+ method: String::new(),
divider,
token_short_name,
fee_limit,
@@ -504,6 +564,9 @@ impl WrappedTron {
}
pub fn format_method(&self) -> Result<String> {
+ if !self.method.is_empty() {
+ return Ok(self.method.clone());
+ }
if !self.contract_address.is_empty() {
Ok("TRC-20 Transfer".to_string())
} else if !self.token.is_empty() && self.token != "TRX" {
@@ -532,6 +595,7 @@ mod tests {
extern crate std;
use super::*;
use crate::test::{prepare_parse_context, prepare_payload};
+ use crate::transaction::parser::TxParser;
use alloc::string::ToString;
use bitcoin::bip32::Fingerprint;
use core::str::FromStr;
@@ -586,6 +650,7 @@ mod tests {
from: "".to_string(),
to: "".to_string(),
value: "".to_string(),
+ method: "".to_string(),
token_short_name: None,
divider: 1.0,
fee_limit: 0,
@@ -639,6 +704,7 @@ mod tests {
from: "".to_string(),
to: "".to_string(),
value: "".to_string(),
+ method: "".to_string(),
token_short_name: None,
divider: 1.0,
fee_limit: 0,
@@ -800,10 +866,122 @@ mod tests {
tron_tx.raw_data = Some(raw);
let result = WrappedTron::from_raw_transaction(tron_tx, "m/44'/195'/0'/0/0".to_string());
- assert!(result.is_ok());
- let wrapped = result.unwrap();
- assert_eq!(wrapped.from, "");
- assert_eq!(wrapped.to, "");
+ assert!(matches!(
+ result,
+ Err(TronError::InvalidRawTxCryptoBytes(message))
+ if message.contains("unsupported contract type")
+ ));
+ }
+
+ #[test]
+ fn test_from_raw_transaction_rejects_multiple_contracts() {
+ let mut tron_tx = Transaction::default();
+ let mut raw = transaction::Raw::default();
+ raw.contract = vec![
+ transaction::Contract::default(),
+ transaction::Contract::default(),
+ ];
+ tron_tx.raw_data = Some(raw);
+
+ let result = WrappedTron::from_raw_transaction(tron_tx, "m/44'/195'/0'/0/0".to_string());
+ assert!(matches!(
+ result,
+ Err(TronError::InvalidRawTxCryptoBytes(message))
+ if message.contains("multiple contracts")
+ ));
+ }
+
+ #[test]
+ fn test_from_raw_transaction_decodes_trc20_approve() {
+ let owner = [vec![0x41], vec![0x11; 20]].concat();
+ let spender = [vec![0x41], vec![0x22; 20]].concat();
+ let token = bitcoin::base58::decode_check("TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t").unwrap();
+ let mut data = vec![0x09, 0x5e, 0xa7, 0xb3];
+ data.extend_from_slice(&[0u8; 12]);
+ data.extend_from_slice(&spender[1..]);
+ data.extend_from_slice(&[0u8; 31]);
+ data.push(1);
+
+ let trigger = TriggerSmartContract {
+ owner_address: owner,
+ contract_address: token,
+ call_value: 0,
+ data,
+ call_token_value: 0,
+ token_id: 0,
+ };
+ let contract = transaction::Contract {
+ r#type: 31,
+ parameter: Some(Any {
+ type_url: "type.googleapis.com/protocol.TriggerSmartContract".to_string(),
+ value: trigger.encode_to_vec(),
+ }),
+ ..Default::default()
+ };
+ let tx = Transaction {
+ raw_data: Some(transaction::Raw {
+ contract: vec![contract],
+ ..Default::default()
+ }),
+ ..Default::default()
+ };
+
+ let parsed = WrappedTron::from_raw_transaction(tx, "m/44'/195'/0'/0/0".to_string())
+ .unwrap()
+ .parse()
+ .unwrap();
+ assert_eq!("TRC-20 Approve", parsed.detail.method);
+ assert_eq!("1 USDT", parsed.detail.value);
+ assert_eq!(bitcoin::base58::encode_check(&spender), parsed.detail.to);
+ }
+
+ #[test]
+ fn test_from_payload_ignores_untrusted_token_override() {
+ let context = prepare_parse_context(
+ "xpub6D1AabNHCupeiLM65ZR9UStMhJ1vCpyV4XbZdyhMZBiJXALQtmn9p42VTQckoHVn8WNqS7dqnJokZHAHcHGoaQgmv8D45oNUKx6DZMNZBCd",
+ );
+ let payload = Payload {
+ r#type: payload::Type::SignTx as i32,
+ xfp: "73c5da0a".to_string(),
+ content: Some(payload::Content::SignTx(SignTransaction {
+ coin_code: "TRON".to_string(),
+ sign_id: "override-regression".to_string(),
+ hd_path: "m/44'/195'/0'/0/0".to_string(),
+ timestamp: 0,
+ decimal: 6,
+ transaction: Some(
+ ur_registry::pb::protoc::sign_transaction::Transaction::TronTx(
+ ur_registry::pb::protoc::TronTx {
+ token: "FAKE".to_string(),
+ contract_address: "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t".to_string(),
+ from: "TTS7Y53sS4rzrDCtZaiuRzqxd16atCe2UR".to_string(),
+ to: "TS5zPoC4XEBmHvDNAnnW2gH3MQhcRN6iRm".to_string(),
+ memo: String::new(),
+ value: "1000000".to_string(),
+ latest_block: Some(LatestBlock {
+ hash: "0000000000000000000000000000000000000000000000000000000000000000".to_string(),
+ number: 1,
+ timestamp: 1_000_000,
+ }),
+ r#override: Some(ur_registry::pb::protoc::Override {
+ token_short_name: "FAKE".to_string(),
+ token_full_name: "Fake Token".to_string(),
+ decimals: 0,
+ }),
+ fee: 0,
+ },
+ ),
+ ),
+ })),
+ };
+
+ let parsed = WrappedTron::from_payload(payload, &context)
+ .unwrap()
+ .parse()
+ .unwrap();
+ assert_eq!("1 USDT", parsed.detail.value);
+ assert_eq!("USDT", parsed.detail.token);
+ assert!(!parsed.detail.value.contains("FAKE"));
}
#[test]
### rust/rust_c/src/ethereum/structs.rs
@@ -22,6 +22,7 @@ use ur_registry::pb::protoc::EthTx;
pub struct DisplayETH {
pub(crate) tx_type: PtrString,
pub(crate) chain_id: u64,
+ pub replay_protected: bool,
pub(crate) overview: PtrT<DisplayETHOverview>,
pub(crate) detail: PtrT<DisplayETHDetail>,
}
@@ -85,6 +86,7 @@ impl TryFrom<EthTx> for DisplayETH {
let display_eth = DisplayETH {
tx_type: convert_c_char("Legacy".to_string()),
chain_id: 1,
+ replay_protected: false,
overview: display_tx_overview.c_ptr(),
detail: display_tx_detail.c_ptr(),
};
@@ -122,6 +124,7 @@ impl TryFrom<EthTx> for DisplayETH {
let display_eth = DisplayETH {
tx_type: convert_c_char("Legacy".to_string()),
chain_id: 1,
+ replay_protected: false,
overview: display_tx_overview.c_ptr(),
detail: display_tx_detail.c_ptr(),
};
@@ -215,6 +218,7 @@ impl From<ParsedEthereumTransaction> for DisplayETH {
None => convert_c_char(String::from("Legacy")),
},
chain_id: value.chain_id,
+ replay_protected: value.replay_protected,
overview: DisplayETHOverview::from(value.clone()).c_ptr(),
detail: DisplayETHDetail::from(value.clone()).c_ptr(),
}
### rust/rust_c/src/solana/mod.rs
@@ -69,6 +69,19 @@ pub unsafe extern "C" fn solana_check(
return TransactionCheckResult::from(RustCError::InvalidMasterFingerprint).c_ptr();
}
let sol_sign_request = extract_ptr_with_type!(ptr, SolSignRequest);
+ // A transaction with bytes appended after the serialized message must be
+ // rejected during the scan/check phase. Otherwise it passes the generic
+ // fingerprint check and the UI opens the transaction detail page before
+ // discovering the malformed payload during parsing/signing.
+ if matches!(
+ app_solana::classify_payload(&sol_sign_request.get_sign_data()),
+ app_solana::SolanaPayloadType::MalformedTransaction
+ ) {
+ return TransactionCheckResult::from(SolanaError::InvalidData(
+ "transaction contains hidden trailing data".to_string(),
+ ))
+ .c_ptr();
+ }
let mfp = extract_array!(master_fingerprint, u8, 4);
if let Ok(mfp) = (mfp.try_into() as Result<[u8; 4], _>) {
let derivation_path: ur_registry::crypto_key_path::CryptoKeyPath =
@@ -91,6 +104,15 @@ pub unsafe extern "C" fn solana_parse_tx(
) -> PtrT<TransactionParseResult<DisplaySolanaTx>> {
let solan_sign_reqeust = extract_ptr_with_type!(ptr, SolSignRequest);
let tx_hex = solan_sign_reqeust.get_sign_data();
+ if matches!(
+ app_solana::classify_payload(&tx_hex),
+ app_solana::SolanaPayloadType::MalformedTransaction
+ ) {
+ return TransactionParseResult::from(SolanaError::InvalidData(
+ "transaction contains hidden trailing data".to_string(),
+ ))
+ .c_ptr();
+ }
match app_solana::parse(&tx_hex.to_vec()) {
Ok(v) => TransactionParseResult::success(DisplaySolanaTx::from(v).c_ptr()).c_ptr(),
Err(e) => TransactionParseResult::from(e).c_ptr(),
@@ -104,6 +126,15 @@ pub unsafe extern "C" fn solana_parse_tx_with_pubkey(
) -> PtrT<TransactionParseResult<DisplaySolanaTx>> {
let solan_sign_reqeust = extract_ptr_with_type!(ptr, SolSignRequest);
let tx_hex = solan_sign_reqeust.get_sign_data();
+ if matches!(
+ app_solana::classify_payload(&tx_hex),
+ app_solana::SolanaPayloadType::MalformedTransaction
+ ) {
+ return TransactionParseResult::from(SolanaError::InvalidData(
+ "transaction contains hidden trailing data".to_string(),
+ ))
+ .c_ptr();
+ }
let pubkey = recover_c_char(pubkey);
let signer: [u8; 32] = match hex::decode(pubkey)
.ok()
### rust/rust_c/src/solana/structs.rs
@@ -24,6 +24,7 @@ pub struct DisplaySolanaTx {
#[repr(C)]
pub struct DisplaySolanaTxOverviewGeneral {
+ pub instruction_index: usize,
pub program: PtrString,
pub method: PtrString,
pub value: PtrString,
@@ -61,6 +62,7 @@ impl Free for DisplaySolanaTxOverviewGeneral {
impl From<&ProgramOverviewGeneral> for DisplaySolanaTxOverviewGeneral {
fn from(value: &ProgramOverviewGeneral) -> Self {
Self {
+ instruction_index: value.instruction_index,
program: convert_c_char(value.program.to_string()),
method: convert_c_char(value.method.to_string()),
value: convert_c_char(value.value.to_string()),
@@ -397,6 +399,16 @@ impl Free for DisplaySolanaTxOverview {
impl From<ParsedSolanaTx> for DisplaySolanaTx {
fn from(value: ParsedSolanaTx) -> Self {
let mut overview = DisplaySolanaTxOverview::from(&value);
+ if !value.additional_overviews.is_empty() {
+ overview.general = VecFFI::from(
+ value
+ .additional_overviews
+ .iter()
+ .map(DisplaySolanaTxOverviewGeneral::from)
+ .collect_vec(),
+ )
+ .c_ptr();
+ }
if !value.unknown_programs.is_empty() {
overview.additional_unknown_programs = VecFFI::from(
value
### rust/rust_c/src/zcash/mod.rs
@@ -878,59 +878,156 @@ use aes::cipher::{BlockDecryptMut, BlockEncryptMut, KeyIvInit};
type Aes256CbcEnc = cbc::Encryptor<aes::Aes256>;
type Aes256CbcDec = cbc::Decryptor<aes::Aes256>;
+/// Derives an AES-256 key from the wallet seed at a dedicated BIP32 path.
+/// The UFVK itself is seed-derived, so keying the ciphertext with the seed loses no security
+/// property and removes the weak `sha256(login password)` key derivation (see security review).
+#[no_mangle]
+pub unsafe extern "C" fn rust_derive_key_from_seed(
+ seed: PtrBytes,
+ seed_len: u32,
+) -> *mut SimpleResponse<u8> {
+ let seed = extract_array!(seed, u8, seed_len as usize);
+ // Dedicated path, distinct from the legacy fixed-IV path m/44'/1557192335'/0'/2'/0'.
+ let key_path = "m/44'/1557192335'/0'/3'/0'".to_string();
+ let key = match get_private_key_by_seed(seed, &key_path) {
+ Ok(key) => key,
+ Err(e) => return SimpleResponse::from(e).simple_c_ptr(),
+ };
+ SimpleResponse::success(Box::into_raw(Box::new(key)) as *mut u8).simple_c_ptr()
+}
+
+/// Raw AES-256-CBC encrypt (pure crypto, no blob layout). Returns hex(ciphertext).
#[no_mangle]
pub unsafe extern "C" fn rust_aes256_cbc_encrypt(
data: PtrString,
- password: PtrString,
+ key: PtrBytes,
+ key_len: u32,
iv: PtrBytes,
iv_len: u32,
) -> *mut SimpleResponse<c_char> {
let data = unsafe { recover_c_char(data) };
let data = data.as_bytes();
- let password = unsafe { recover_c_char(password) };
+ let key = extract_array!(key, u8, key_len as usize);
let iv = extract_array!(iv, u8, iv_len as usize);
- let key = sha256(password.as_bytes());
- let iv = GenericArray::from_slice(iv);
+ let iv_generic = GenericArray::from_slice(iv);
let key = GenericArray::from_slice(&key);
- let ct = Aes256CbcEnc::new(key, iv).encrypt_padded_vec_mut::<Pkcs7>(data);
+ let ct = Aes256CbcEnc::new(key, iv_generic).encrypt_padded_vec_mut::<Pkcs7>(data);
SimpleResponse::success(convert_c_char(hex::encode(ct))).simple_c_ptr()
}
+/// Raw AES-256-CBC decrypt (pure crypto, no blob layout). Input is hex(ciphertext).
#[no_mangle]
pub unsafe extern "C" fn rust_aes256_cbc_decrypt(
hex_data: PtrString,
- password: PtrString,
+ key: PtrBytes,
+ key_len: u32,
iv: PtrBytes,
iv_len: u32,
) -> *mut SimpleResponse<c_char> {
let hex_data = unsafe { recover_c_char(hex_data) };
- let data = hex::decode(hex_data).unwrap();
- let password = unsafe { recover_c_char(password) };
+ let data = match hex::decode(hex_data) {
+ Ok(data) => data,
+ Err(_) => {
+ return SimpleResponse::from(RustCError::InvalidHex("invalid ciphertext".to_string()))
+ .simple_c_ptr()
+ }
+ };
+ let key = extract_array!(key, u8, key_len as usize);
let iv = extract_array!(iv, u8, iv_len as usize);
- let key = sha256(password.as_bytes());
let iv = GenericArray::from_slice(iv);
let key = GenericArray::from_slice(&key);
match Aes256CbcDec::new(key, iv).decrypt_padded_vec_mut::<Pkcs7>(&data) {
- Ok(pt) => {
- SimpleResponse::success(convert_c_char(String::from_utf8(pt).unwrap())).simple_c_ptr()
- }
+ 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(_e) => SimpleResponse::from(RustCError::InvalidHex("decrypt failed".to_string()))
.simple_c_ptr(),
}
}
+/// Storage layout for the UFVK blob: hex(IV_16) || hex(ciphertext). The fresh random IV
+/// travels with the blob so the same plaintext+key never produces comparable ciphertext
+/// (see security review). Encryption side: pack the caller-provided random IV ahead of ct.
#[no_mangle]
-pub unsafe extern "C" fn rust_derive_iv_from_seed(
- seed: PtrBytes,
- seed_len: u32,
-) -> *mut SimpleResponse<u8> {
- let seed = extract_array!(seed, u8, seed_len as usize);
- let iv_path = "m/44'/1557192335'/0'/2'/0'".to_string();
- let iv = get_private_key_by_seed(seed, &iv_path).unwrap();
- let mut iv_bytes = [0; 16];
- iv_bytes.copy_from_slice(&iv[..16]);
- SimpleResponse::success(Box::into_raw(Box::new(iv_bytes)) as *mut u8).simple_c_ptr()
+pub unsafe extern "C" fn rust_encrypt_ufvk_blob(
+ data: PtrString,
+ key: PtrBytes,
+ key_len: u32,
+ iv: PtrBytes,
+ iv_len: u32,
+) -> *mut SimpleResponse<c_char> {
+ let data = unsafe { recover_c_char(data) };
+ let data = data.as_bytes();
+ let key = extract_array!(key, u8, key_len as usize);
+ let iv = extract_array!(iv, u8, iv_len as usize);
+ let iv_generic = GenericArray::from_slice(iv);
+ let key = GenericArray::from_slice(&key);
+ let ct = Aes256CbcEnc::new(key, iv_generic).encrypt_padded_vec_mut::<Pkcs7>(data);
+ // Storage format: "z2" || hex(IV_16) || hex(ciphertext). The magic prefix distinguishes the
+ // new layout from legacy pure-hex ciphertext; the fresh random IV travels with the blob so
+ // the same plaintext+key never produces comparable ciphertext (see security review).
+ let value = format!("z2{}{}", hex::encode(iv), hex::encode(ct));
+ SimpleResponse::success(convert_c_char(value)).simple_c_ptr()
+}
+
+/// Storage layout for the UFVK blob: hex(IV_16) || hex(ciphertext). Decryption side:
+/// split off the leading IV, then decrypt the rest.
+#[no_mangle]
+pub unsafe extern "C" fn rust_decrypt_ufvk_blob(
+ blob: PtrString,
+ key: PtrBytes,
+ key_len: u32,
+) -> *mut SimpleResponse<c_char> {
+ let blob = unsafe { recover_c_char(blob) };
+ let blob = blob.as_bytes();
+ // Magic prefix distinguishes the new layout from legacy pure-hex ciphertext (whose
+ // leading bytes would otherwise be misread as the IV). Legacy blobs fail here and are
+ // regenerated by the caller instead of being probed heuristically.
+ const MAGIC: &[u8] = b"z2";
+ if !blob.starts_with(MAGIC) {
+ return SimpleResponse::from(RustCError::InvalidHex("invalid blob magic".to_string()))
+ .simple_c_ptr();
+ }
+ let payload = &blob[MAGIC.len()..];
+ if payload.len() < 2 * 16 {
+ return SimpleResponse::from(RustCError::InvalidHex("invalid blob".to_string()))
+ .simple_c_ptr();
+ }
+ let (iv_hex, ct_hex) = payload.split_at(2 * 16);
+ let iv = match hex::decode(iv_hex) {
+ Ok(iv) => iv,
+ Err(_) => {
+ return SimpleResponse::from(RustCError::InvalidHex("invalid iv".to_string()))
+ .simple_c_ptr()
+ }
+ };
+ let data = match hex::decode(ct_hex) {
+ Ok(data) => data,
+ Err(_) => {
+ return SimpleResponse::from(RustCError::InvalidHex("invalid ciphertext".to_string()))
+ .simple_c_ptr()
+ }
+ };
+ let key = extract_array!(key, u8, key_len as usize);
+ let iv = GenericArray::from_slice(&iv);
+ let key = GenericArray::from_slice(&key);
+
+ 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(_e) => SimpleResponse::from(RustCError::InvalidHex("decrypt failed".to_string()))
+ .simple_c_ptr(),
+ }
}
#[cfg(test)]
@@ -1148,41 +1245,91 @@ mod tests {
#[test]
fn test_aes256_cbc_encrypt() {
let mut data = convert_c_char("hello world".to_string());
- let mut password = convert_c_char("password".to_string());
let mut seed = hex::decode("5eb00bbddcf069084889a8ab9155568165f5c453ccb85e70811aaed6f6da5fc19a5ac40b389cd370d086206dec8aa6c43daea6690f20ad3d8d48b2d2ce9e38e4").unwrap();
- let iv = unsafe { rust_derive_iv_from_seed(seed.as_mut_ptr(), 64) };
- let mut iv = unsafe { slice::from_raw_parts_mut((*iv).data, 16) };
- let iv_len = 16;
- let ct = unsafe { rust_aes256_cbc_encrypt(data, password, iv.as_mut_ptr(), iv_len as u32) };
+ let key_resp = unsafe { rust_derive_key_from_seed(seed.as_mut_ptr(), 64) };
+ let mut key = unsafe { slice::from_raw_parts_mut((*key_resp).data, 32) };
+ let mut iv_bytes = [0u8; 16];
+ iv_bytes.copy_from_slice(&hex::decode("73e6ca87d5cd5622cdc747367905efe7").unwrap());
+ let ct = unsafe {
+ rust_aes256_cbc_encrypt(data, key.as_mut_ptr(), 32, iv_bytes.as_mut_ptr(), 16)
+ };
assert!(!ct.is_null());
let ct_vec = unsafe { (*ct).data };
let value = unsafe { recover_c_char(ct_vec) };
- assert_eq!(value, "4989eed8515d7d3fcc16b009d8cdff9e");
+ // Pure ciphertext only: "hello world" -> 16-byte block -> 32 hex chars, no IV prefix.
+ assert_eq!(value.len(), 32);
+ assert!(hex::decode(value).is_ok());
+ }
+
+ #[test]
+ fn test_aes256_ufvk_blob() {
+ let mut data = convert_c_char("hello world".to_string());
+ let mut seed = hex::decode("5eb00bbddcf069084889a8ab9155568165f5c453ccb85e70811aaed6f6da5fc19a5ac40b389cd370d086206dec8aa6c43daea6690f20ad3d8d48b2d2ce9e38e4").unwrap();
+ let key_resp = unsafe { rust_derive_key_from_seed(seed.as_mut_ptr(), 64) };
+ let mut key = unsafe { slice::from_raw_parts_mut((*key_resp).data, 32) };
+ let mut iv_bytes = [0u8; 16];
+ iv_bytes.copy_from_slice(&hex::decode("73e6ca87d5cd5622cdc747367905efe7").unwrap());
+ // Blob encrypt packs magic + hex(IV) || hex(ciphertext).
+ let blob_resp = unsafe {
+ rust_encrypt_ufvk_blob(data, key.as_mut_ptr(), 32, iv_bytes.as_mut_ptr(), 16)
+ };
+ assert!(!blob_resp.is_null());
+ let blob = unsafe { recover_c_char((*blob_resp).data) };
+ assert_eq!(blob.len(), 2 + 32 + 32); // magic + IV prefix + one-block ciphertext
+ assert!(blob.starts_with("z2"));
+ assert!(blob[2..].starts_with("73e6ca87d5cd5622cdc747367905efe7"));
+
+ // Blob decrypt splits magic+IV off and recovers the plaintext.
+ let blob_input = convert_c_char(blob);
+ let pt_resp = unsafe { rust_decrypt_ufvk_blob(blob_input, key.as_mut_ptr(), 32) };
+ assert!(!pt_resp.is_null());
+ let pt = unsafe { recover_c_char((*pt_resp).data) };
+ assert_eq!(pt, "hello world");
+ }
+
+ #[test]
+ fn test_aes256_ufvk_blob_rejects_legacy_or_garbage() {
+ let mut seed = hex::decode("5eb00bbddcf069084889a8ab9155568165f5c453ccb85e70811aaed6f6da5fc19a5ac40b389cd370d086206dec8aa6c43daea6690f20ad3d8d48b2d2ce9e38e4").unwrap();
+ let key_resp = unsafe { rust_derive_key_from_seed(seed.as_mut_ptr(), 64) };
+ let mut key = unsafe { slice::from_raw_parts_mut((*key_resp).data, 32) };
+
+ // Legacy blob: pure hex ciphertext without the magic prefix must be rejected
+ // (so the caller regenerates) instead of being misparsed or panicking.
+ let legacy = convert_c_char(
+ "73e6ca87d5cd5622cdc747367905efe700000000000000000000000000000000".to_string(),
+ );
+ let r1 = unsafe { rust_decrypt_ufvk_blob(legacy, key.as_mut_ptr(), 32) };
+ assert!(!r1.is_null());
+ assert!(unsafe { (*r1).data.is_null() });
+
+ // Truncated blob (magic but too short) must be rejected, not panic.
+ let truncated = convert_c_char("z273e6ca87d5cd56".to_string());
+ let r2 = unsafe { rust_decrypt_ufvk_blob(truncated, key.as_mut_ptr(), 32) };
+ assert!(!r2.is_null());
+ assert!(unsafe { (*r2).data.is_null() });
}
#[test]
fn test_aes256_cbc_decrypt() {
- //8dd387c3b2656d9f24ace7c3daf6fc26a1c161098460f8dddd37545fc951f9cd7da6c75c71ae52f32ceb8827eca2169ef4a643d2ccb9f01389d281a85850e2ddd100630ab1ca51310c3e6ccdd3029d0c48db18cdc971dba8f0daff9ad281b56221ffefc7d32333ea310a1f74f99dea444f8a089002cf1f0cd6a4ddf608a7b5388dc09f9417612657b9bf335a466f951547f9707dd129b3c24c900a26010f51c543eba10e9aabef7062845dc6969206b25577a352cb4d984db67c54c7615fe60769726bffa59fd8bd0b66fe29ee3c358af13cf0796c2c062bc79b73271eb0366f0536e425f8e42307ead4c695804fd3281aca5577d9a621e3a8047b14128c280c45343b5bbb783a065d94764e90ad6820fe81a200637401c256b1fb8f58a9d412d303b89c647411662907cdc55ed93adb
- //73e6ca87d5cd5622cdc747367905efe7
- //68487dc295052aa79c530e283ce698b8c6bb1b42ff0944252e1910dbecdc5425
let mut seed = hex::decode("5eb00bbddcf069084889a8ab9155568165f5c453ccb85e70811aaed6f6da5fc19a5ac40b389cd370d086206dec8aa6c43daea6690f20ad3d8d48b2d2ce9e38e4").unwrap();
- // First encrypt to get ciphertext
+ let mut iv_bytes = [0u8; 16];
+ iv_bytes.copy_from_slice(&hex::decode("73e6ca87d5cd5622cdc747367905efe7").unwrap());
+ // Encrypt to get pure ciphertext under the seed-derived key.
let enc_data = convert_c_char("hello world".to_string());
- let enc_password = convert_c_char("password".to_string());
- let iv_resp = unsafe { rust_derive_iv_from_seed(seed.as_mut_ptr(), 64) };
- let mut iv_enc = unsafe { slice::from_raw_parts_mut((*iv_resp).data, 16) };
- let ct =
- unsafe { rust_aes256_cbc_encrypt(enc_data, enc_password, iv_enc.as_mut_ptr(), 16) };
+ let key_resp = unsafe { rust_derive_key_from_seed(seed.as_mut_ptr(), 64) };
+ let mut key = unsafe { slice::from_raw_parts_mut((*key_resp).data, 32) };
+ let ct = unsafe {
+ rust_aes256_cbc_encrypt(enc_data, key.as_mut_ptr(), 32, iv_bytes.as_mut_ptr(), 16)
+ };
let ct_hex = unsafe { recover_c_char((*ct).data) };
- assert_eq!(ct_hex, "4989eed8515d7d3fcc16b009d8cdff9e");
- // Now decrypt
+ // Decrypt back with the same IV.
let data = convert_c_char(ct_hex);
- let password = convert_c_char("password".to_string());
- let iv = unsafe { rust_derive_iv_from_seed(seed.as_mut_ptr(), 64) };
- let iv = unsafe { slice::from_raw_parts_mut((*iv).data, 16) };
- let iv_len = 16;
- let pt = unsafe { rust_aes256_cbc_decrypt(data, password, iv.as_mut_ptr(), iv_len as u32) };
+ let key_resp = unsafe { rust_derive_key_from_seed(seed.as_mut_ptr(), 64) };
+ let mut key = unsafe { slice::from_raw_parts_mut((*key_resp).data, 32) };
+ let pt = unsafe {
+ rust_aes256_cbc_decrypt(data, key.as_mut_ptr(), 32, iv_bytes.as_mut_ptr(), 16)
+ };
assert!(!pt.is_null());
let ct_vec = unsafe { (*pt).data };
let value = unsafe { recover_c_char(ct_vec) };
@@ -1193,13 +1340,16 @@ mod tests {
fn test_dep_aes256() {
let mut data = b"hello world";
let seed = hex::decode("5eb00bbddcf069084889a8ab9155568165f5c453ccb85e70811aaed6f6da5fc19a5ac40b389cd370d086206dec8aa6c43daea6690f20ad3d8d48b2d2ce9e38e4").unwrap();
- let iv_path = "m/44'/1557192335'/0'/2'/0'".to_string();
- let iv = get_private_key_by_seed(&seed, &iv_path).unwrap();
- let mut iv_bytes = [0; 16];
- iv_bytes.copy_from_slice(&iv[..16]);
- let key = sha256(b"password");
+ // 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 mut iv_bytes = [0u8; 16];
+ iv_bytes.copy_from_slice(&hex::decode("73e6ca87d5cd5622cdc747367905efe7").unwrap());
let iv = GenericArray::from_slice(&iv_bytes);
- let key = GenericArray::from_slice(&key);
+ let key = GenericArray::from_slice(&key_bytes);
let encrypter = Aes256CbcEnc::new(key, iv);
let decrypter = Aes256CbcDec::new(key, iv);
### src/crypto/account_public_info.c
@@ -5,6 +5,7 @@
#include "sha256.h"
#include "flash_address.h"
#include "drv_gd25qxx.h"
+#include "drv_trng.h"
#include "keystore.h"
#include "user_memory.h"
#include "account_public_info.h"
@@ -63,6 +64,7 @@ static void PrintInfo(void);
static void SetIsTempAccount(bool isTemp);
static void SaveCurrentPublicInfoToFlash(uint8_t accountIndex, uint32_t addr);
#ifdef CYPHERPUNK_VERSION
+static bool IsValidZcashUfvkBlob(const char *value);
static SimpleResponse_c_char *DeriveEncryptedZcashUFVK(const uint8_t *seed, int seedLen, const char *password, char *ufvkOut, uint32_t ufvkOutLen);
#endif
@@ -975,7 +977,7 @@ int32_t AccountPublicInfoSwitch(uint8_t accountIndex, const char *password, bool
#ifdef CYPHERPUNK_VERSION
if (!regeneratePubKey && IsZcashSupportedForCurrentMnemonic()) {
char *zcashEncrypted = GetCurrentAccountPublicKey(ZCASH_UFVK_ENCRYPTED_0);
- if (!IsHexStringWithLen(zcashEncrypted, 0)) {
+ if (!IsValidZcashUfvkBlob(zcashEncrypted)) {
regeneratePubKey = true;
}
}
@@ -1029,26 +1031,54 @@ static void SaveCurrentPublicInfoToFlash(uint8_t accountIndex, uint32_t addr)
}
#ifdef CYPHERPUNK_VERSION
-/// @brief Derive the Zcash UFVK from seed and AES-256-CBC encrypt it with the login password,
-/// using the seed-derived IV. Single source of the UFVK encryption scheme.
+/// @brief Check a stored Zcash UFVK blob has an acceptable layout.
+/// New format is "z2" || hex(IV_16) || hex(ciphertext); legacy blobs are pure hex and
+/// are accepted too so they flow into the decrypt-fail -> regenerate migration path.
+/// @param[in] value Stored value, may be NULL.
+/// @return true if the blob layout is valid.
+static bool IsValidZcashUfvkBlob(const char *value)
+{
+ if (value == NULL) {
+ return false;
+ }
+ if (value[0] == 'z' && value[1] == '2') {
+ return IsHexStringWithLen(value + 2, 0);
+ }
+ // Legacy pure-hex blob (accepted; SetupZcashCache regenerates it on decrypt failure).
+ return IsHexStringWithLen(value, 0);
+}
+
+/// @brief Derive the Zcash UFVK from seed and AES-256-CBC encrypt it with a seed-derived key
+/// and a fresh random IV (prepended to the ciphertext). Single source of the UFVK scheme.
/// @param[in] seed Wallet seed.
/// @param[in] seedLen Seed length.
-/// @param[in] password Password to encrypt the UFVK with.
+/// @param[in] password Unused by the encryption (key is seed-derived); kept for interface stability.
/// @param[out] ufvkOut Optional, receives the plaintext UFVK. Can be NULL if not needed.
/// @param[in] ufvkOutLen Size of ufvkOut.
-/// @return Encrypted UFVK hex response, or a response carrying the derivation/encryption error. Caller frees.
+/// @return Encrypted UFVK hex response (hex(IV) || hex(ciphertext)), or a response carrying
+/// the derivation/encryption error. Caller frees.
static SimpleResponse_c_char *DeriveEncryptedZcashUFVK(const uint8_t *seed, int seedLen, const char *password, char *ufvkOut, uint32_t ufvkOutLen)
{
+ (void)password;
SimpleResponse_c_char *ufvkResponse = derive_zcash_ufvk((uint8_t *)seed, seedLen, g_chainTable[ZCASH_UFVK_ENCRYPTED_0].path);
if (ufvkResponse == NULL || ufvkResponse->error_code != 0) {
return ufvkResponse;
}
- SimpleResponse_u8 *ivResponse = rust_derive_iv_from_seed((uint8_t *)seed, seedLen);
- //iv_response won't fail
+ SimpleResponse_u8 *keyResponse = rust_derive_key_from_seed((uint8_t *)seed, seedLen);
+ if (keyResponse == NULL || keyResponse->error_code != 0) {
+ if (keyResponse != NULL) {
+ free_simple_response_u8(keyResponse);
+ }
+ free_simple_response_c_char(ufvkResponse);
+ return NULL;
+ }
+ uint8_t keyBytes[32];
+ memcpy_s(keyBytes, sizeof(keyBytes), keyResponse->data, sizeof(keyBytes));
+ free_simple_response_u8(keyResponse);
uint8_t ivBytes[16];
- memcpy_s(ivBytes, sizeof(ivBytes), ivResponse->data, sizeof(ivBytes));
- free_simple_response_u8(ivResponse);
- SimpleResponse_c_char *encryptResult = rust_aes256_cbc_encrypt(ufvkResponse->data, password, ivBytes, 16);
+ TrngGet(ivBytes, sizeof(ivBytes));
+ SimpleResponse_c_char *encryptResult = rust_encrypt_ufvk_blob(ufvkResponse->data, keyBytes, sizeof(keyBytes), ivBytes, sizeof(ivBytes));
+ CLEAR_ARRAY(keyBytes);
CLEAR_ARRAY(ivBytes);
if (ufvkOut != NULL && encryptResult != NULL && encryptResult->error_code == 0) {
strcpy_s(ufvkOut, ufvkOutLen, ufvkResponse->data);
@@ -1057,10 +1087,10 @@ static SimpleResponse_c_char *DeriveEncryptedZcashUFVK(const uint8_t *seed, int
return encryptResult;
}
-/// @brief Re-derive the Zcash UFVK from seed, encrypt it with password and replace the stored ciphertext.
-/// The UFVK ciphertext is keyed by the login password: ChangePassword calls this to keep it in
-/// sync, and SetupZcashCache calls it on decrypt failure to migrate wallets whose ciphertext
-/// went stale before the sync existed (or whose sync was interrupted).
+/// @brief Re-derive the Zcash UFVK from seed, encrypt it with a seed-derived key and replace the
+/// stored ciphertext. ChangePassword calls this to keep the blob in sync, and
+/// SetupZcashCache calls it on decrypt failure to migrate wallets whose blob went stale
+/// before the sync existed (or whose sync was interrupted).
/// @param[in] accountIndex Account index, 0~2.
/// @param[in] seed Wallet seed, already fetched with a verified password.
/// @param[in] seedLen Seed length.
### src/managers/account_manager.c
@@ -813,36 +813,42 @@ int32_t SetupZcashCache(uint8_t accountIndex, const char* password)
return ret;
}
- SimpleResponse_u8 *iv_response = rust_derive_iv_from_seed(seed, len);
- if (iv_response->error_code != 0) {
- ret = iv_response->error_code;
+ SimpleResponse_u8 *key_response = rust_derive_key_from_seed(seed, len);
+ if (key_response == NULL || key_response->error_code != 0) {
+ ret = key_response ? key_response->error_code : ERR_GENERAL_FAIL;
CLEAR_ARRAY(seed);
- printf("error: %s\r\n", iv_response->error_message);
- free_simple_response_u8(iv_response);
+ if (key_response != NULL) {
+ printf("error: %s\r\n", key_response->error_message);
+ free_simple_response_u8(key_response);
+ }
return ret;
}
-
- uint8_t iv_bytes[16];
- memcpy_s(iv_bytes, 16, iv_response->data, 16);
- free_simple_response_u8(iv_response);
+ uint8_t key_bytes[32];
+ memcpy_s(key_bytes, sizeof(key_bytes), key_response->data, sizeof(key_bytes));
+ free_simple_response_u8(key_response);
char *zcashEncrypted = GetCurrentAccountPublicKey(ZCASH_UFVK_ENCRYPTED_0);
if (zcashEncrypted == NULL) {
CLEAR_ARRAY(seed);
- CLEAR_ARRAY(iv_bytes);
+ CLEAR_ARRAY(key_bytes);
return ERR_GENERAL_FAIL;
}
- SimpleResponse_c_char *response = rust_aes256_cbc_decrypt(zcashEncrypted, password, iv_bytes, 16);
- CLEAR_ARRAY(iv_bytes);
+ // Storage format: hex(IV_16) || hex(ciphertext). Legacy blobs (no 32-char IV prefix) fail the
+ // decrypt in Rust and are regenerated from the seed instead of failing the login.
+ SimpleResponse_c_char *response = rust_decrypt_ufvk_blob(zcashEncrypted, key_bytes, sizeof(key_bytes));
+ CLEAR_ARRAY(key_bytes);
char ufvk[ZCASH_UFVK_BUFFER_SIZE] = {'\0'};
- if (response->error_code != 0) {
- // The stored ciphertext is keyed by an older password (e.g. the password was
- // changed without re-encrypting it). The entered password already passed SE
- // verification, so regenerate the UFVK from the seed instead of failing the
- // login and locking the user out.
- printf("zcash ufvk decrypt failed, regenerating from seed. error: %s\r\n", response->error_message);
- free_simple_response_c_char(response);
+ if (response == NULL || response->error_code != 0) {
+ // Stale or legacy ciphertext (old password keying / no IV prefix). The entered password
+ // already passed SE verification, so regenerate the UFVK from the seed instead of failing
+ // the login and locking the user out.
+ if (response != NULL) {
+ printf("zcash ufvk decrypt failed, regenerating from seed. error: %s\r\n", response->error_message);
+ free_simple_response_c_char(response);
+ } else {
+ printf("zcash ufvk decrypt failed, regenerating from seed.\r\n");
+ }
ret = RegenerateZcashUFVK(accountIndex, seed, len, password, ufvk, sizeof(ufvk));
if (ret != SUCCESS_CODE) {
CLEAR_ARRAY(ufvk);
### src/tasks/usb_task.c
@@ -16,7 +16,9 @@
#include "gui_setup_widgets.h"
#include "low_power.h"
#include "account_manager.h"
+#ifndef BTC_ONLY
#include "general/eapdu_services/service_resolve_ur.h"
+#endif
static void UsbTask(void *argument);
void ClearUSBRequestId(void);
@@ -90,6 +92,7 @@ static void UsbTask(void *argument)
SetUsbState(false);
}
break;
+#ifndef BTC_ONLY
case USB_MSG_HANDLE_UR_RESULT: {
if ((rcvMsg.buffer != NULL) && (rcvMsg.length >= sizeof(USBURResultMsg_t))) {
USBURResultMsg_t *msg = (USBURResultMsg_t *)rcvMsg.buffer;
@@ -108,6 +111,7 @@ static void UsbTask(void *argument)
}
}
break;
+#endif
default:
break;
}
### src/ui/gui_analyze/gui_analyze.c
@@ -432,6 +432,18 @@ void GuiWidgetBaseInit(lv_obj_t *obj, cJSON *json)
!strcmp(textFunc->valuestring, "GetSolMessageRaw"));
continue;
}
+#endif
+#ifdef FEATURE_ETHEREUM
+ cJSON *ethType = cJSON_GetObjectItem(child, "type");
+ cJSON *ethTextFunc = cJSON_GetObjectItem(child, "text_func");
+ if (ethType != NULL && ethTextFunc != NULL &&
+ !strcmp(ethType->valuestring, "label") &&
+ (!strcmp(ethTextFunc->valuestring, "GetMessageUtf8") ||
+ !strcmp(ethTextFunc->valuestring, "GetMessageRaw"))) {
+ GuiShowEthMessagePaged(obj, g_totalData,
+ !strcmp(ethTextFunc->valuestring, "GetMessageRaw"));
+ continue;
+ }
#endif
GuiWidgetFactoryCreate(obj, child);
}
### src/ui/gui_chain/gui_btc.c
@@ -1465,7 +1465,7 @@ void GuiBtcTxOverview(lv_obj_t *parent, void *totalData)
}
if (overviewData->is_large_fee) {
- lastView = CreateSighashWarningView(parent, lastView, _("btc_large_fee_warning"));
+ lastView = CreateSighashWarningView(parent, lastView, _("utxo_large_fee_warning"));
}
if (NeedShowCheckInputValueHint(txData)) {
### src/ui/gui_chain/gui_chain.h
@@ -93,6 +93,9 @@ typedef enum {
CHAIN_QCK,
CHAIN_TGD,
// cosmos end
+
+ // Transactions whose chain cannot be identified use a text-only title.
+ CHAIN_UNKNOWN,
#endif
#ifndef BTC_ONLY
### src/ui/gui_chain/gui_chain_components.c
@@ -1,5 +1,23 @@
#include "gui_chain_components.h"
+#include <stdint.h>
#include <string.h>
+#include "user_memory.h"
+
+#define GUI_PAGED_MESSAGE_BYTES 512
+#define GUI_PAGED_MESSAGE_BREAK_SEARCH_BYTES 128
+
+typedef struct {
+ GuiPagedMessageSource_t source;
+ size_t offset;
+ size_t page;
+ size_t page_count;
+ lv_obj_t *viewport;
+ lv_obj_t *warning;
+ lv_obj_t *label;
+ lv_obj_t *page_label;
+ lv_obj_t *prev;
+ lv_obj_t *next;
+} GuiPagedMessageState_t;
const lv_font_t *GetOverviewAmountFont(const char *value)
{
@@ -292,9 +310,14 @@ lv_obj_t *CreateContentContainer(lv_obj_t *parent, uint16_t w, uint16_t h)
}
lv_obj_t *CreateNoticeCard(lv_obj_t *parent, const char *notice)
+{
+ return CreateNoticeCardWithWidth(parent, notice, 408);
+}
+
+lv_obj_t *CreateNoticeCardWithWidth(lv_obj_t *parent, const char *notice, uint16_t width)
{
uint16_t height = 24 + 36 + 8 + 24;
- lv_obj_t* card = GuiCreateContainerWithParent(parent, 408, 24);
+ lv_obj_t* card = GuiCreateContainerWithParent(parent, width, 24);
lv_obj_set_style_radius(card, 24, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_bg_color(card, WHITE_COLOR, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_bg_opa(card, 30, LV_PART_MAIN | LV_STATE_DEFAULT);
@@ -307,7 +330,7 @@ lv_obj_t *CreateNoticeCard(lv_obj_t *parent, const char *notice)
lv_obj_align_to(title_label, noticeIcon, LV_ALIGN_OUT_RIGHT_MID, 8, 0);
lv_obj_t* content_label = GuiCreateIllustrateLabel(card, notice);
- lv_obj_set_width(content_label, 360);
+ lv_obj_set_width(content_label, width - 48);
lv_obj_update_layout(content_label);
height += lv_obj_get_self_height(content_label);
lv_obj_set_height(card, height);
@@ -342,3 +365,248 @@ lv_obj_t *CreateNoticeView(lv_obj_t *parent, uint16_t width, uint16_t height, co
return noticeContainer;
}
+
+static char GuiPagedMessageTextByteAt(void *context, size_t offset)
+{
+ return ((const char *)context)[offset];
+}
+
+static bool GuiPagedMessageIsUtf8Continuation(char value)
+{
+ return (((uint8_t)value) & 0xC0) == 0x80;
+}
+
+static size_t GuiPagedMessageFindBreak(const GuiPagedMessageState_t *state,
+ size_t offset, size_t length, bool newlineOnly)
+{
+ size_t minimum = length > GUI_PAGED_MESSAGE_BREAK_SEARCH_BYTES
+ ? length - GUI_PAGED_MESSAGE_BREAK_SEARCH_BYTES
+ : 0;
+ for (size_t i = length; i > minimum; i--) {
+ char value = state->source.byte_at(state->source.context, offset + i - 1);
+ if (value == '\n' || (!newlineOnly && (value == ' ' || value == '\t'))) {
+ return i;
+ }
+ }
+ return 0;
+}
+
+static size_t GuiPagedMessagePageEnd(const GuiPagedMessageState_t *state, size_t offset)
+{
+ if (offset >= state->source.length) {
+ return state->source.length;
+ }
+ size_t remaining = state->source.length - offset;
+ size_t length = remaining < GUI_PAGED_MESSAGE_BYTES
+ ? remaining
+ : GUI_PAGED_MESSAGE_BYTES;
+ size_t byteLimit = length;
+
+ if (offset + length < state->source.length && state->source.utf8) {
+ size_t semanticBreak = GuiPagedMessageFindBreak(state, offset, length, true);
+ if (semanticBreak == 0) {
+ semanticBreak = GuiPagedMessageFindBreak(state, offset, length, false);
+ }
+ if (semanticBreak > 0) {
+ length = semanticBreak;
+ }
+ while (length > 0 && GuiPagedMessageIsUtf8Continuation(
+ state->source.byte_at(state->source.context, offset + length))) {
+ length--;
+ }
+ if (length == 0) {
+ length = byteLimit;
+ }
+ }
+ return offset + length;
+}
+
+static size_t GuiPagedMessagePageOffset(const GuiPagedMessageState_t *state, size_t page)
+{
+ size_t offset = 0;
+ for (size_t i = 0; i < page && offset < state->source.length; i++) {
+ offset = GuiPagedMessagePageEnd(state, offset);
+ }
+ return offset;
+}
+
+static void GuiPagedMessageRefresh(GuiPagedMessageState_t *state)
+{
+ char pageText[GUI_PAGED_MESSAGE_BYTES + 1];
+ size_t end = GuiPagedMessagePageEnd(state, state->offset);
+ size_t length = end - state->offset;
+ for (size_t i = 0; i < length; i++) {
+ pageText[i] = state->source.byte_at(state->source.context, state->offset + i);
+ }
+ pageText[length] = '\0';
+
+ lv_coord_t labelY = 0;
+ if (state->warning != NULL) {
+ if (state->page == 0) {
+ lv_obj_clear_flag(state->warning, LV_OBJ_FLAG_HIDDEN);
+ lv_obj_update_layout(state->warning);
+ labelY = lv_obj_get_height(state->warning) + 16;
+ } else {
+ lv_obj_add_flag(state->warning, LV_OBJ_FLAG_HIDDEN);
+ }
+ }
+ lv_obj_set_y(state->label, labelY);
+ lv_label_set_text(state->label, pageText);
+ lv_obj_set_height(state->label, LV_SIZE_CONTENT);
+ lv_obj_update_layout(state->viewport);
+ lv_obj_scroll_to_y(state->viewport, 0, LV_ANIM_OFF);
+ lv_label_set_text_fmt(state->page_label, "%u / %u",
+ (unsigned)(state->page + 1),
+ (unsigned)state->page_count);
+
+ if (state->page == 0) {
+ lv_obj_add_state(state->prev, LV_STATE_DISABLED);
+ } else {
+ lv_obj_clear_state(state->prev, LV_STATE_DISABLED);
+ }
+ if (state->page + 1 >= state->page_count) {
+ lv_obj_add_state(state->next, LV_STATE_DISABLED);
+ } else {
+ lv_obj_clear_state(state->next, LV_STATE_DISABLED);
+ }
+}
+
+static void GuiPagedMessageEvent(lv_event_t *event)
+{
+ GuiPagedMessageState_t *state = lv_event_get_user_data(event);
+ lv_obj_t *target = lv_event_get_target(event);
+ if (target == state->prev && state->page > 0) {
+ state->page--;
+ } else if (target == state->next && state->page + 1 < state->page_count) {
+ state->page++;
+ } else {
+ return;
+ }
+ state->offset = GuiPagedMessagePageOffset(state, state->page);
+ GuiPagedMessageRefresh(state);
+}
+
+static void GuiPagedMessageDelete(lv_event_t *event)
+{
+ GuiPagedMessageState_t *state = lv_event_get_user_data(event);
+ SRAM_FREE(state);
+}
+
+static lv_obj_t *GuiPagedMessageButton(lv_obj_t *parent, const char *text)
+{
+ lv_obj_t *button = lv_btn_create(parent);
+ lv_obj_set_size(button, 72, 48);
+ lv_obj_set_style_radius(button, 12, LV_PART_MAIN);
+ lv_obj_set_style_bg_color(button, WHITE_COLOR, LV_PART_MAIN);
+ lv_obj_set_style_bg_opa(button, 30, LV_PART_MAIN);
+ lv_obj_t *label = lv_label_create(button);
+ lv_obj_set_style_text_font(label, g_defIllustrateFont, LV_PART_MAIN);
+ lv_obj_set_style_text_color(label, WHITE_COLOR, LV_PART_MAIN);
+ lv_label_set_text(label, text);
+ lv_obj_center(label);
+ return button;
+}
+
+static lv_obj_t *GuiPagedMessageWarning(lv_obj_t *parent, const char *titleText,
+ const char *contentText, lv_coord_t width)
+{
+ lv_obj_t *warning = lv_obj_create(parent);
+ lv_obj_set_width(warning, width);
+ lv_obj_set_height(warning, LV_SIZE_CONTENT);
+ lv_obj_set_style_pad_all(warning, 16, LV_PART_MAIN);
+ lv_obj_set_style_pad_row(warning, 8, LV_PART_MAIN);
+ lv_obj_set_style_border_width(warning, 0, LV_PART_MAIN);
+ lv_obj_set_style_radius(warning, 8, LV_PART_MAIN);
+ lv_obj_set_style_bg_color(warning, lv_color_hex(0xF55831), LV_PART_MAIN);
+ lv_obj_set_style_bg_opa(warning, 48, LV_PART_MAIN);
+ lv_obj_set_flex_flow(warning, LV_FLEX_FLOW_COLUMN);
+
+ lv_obj_t *title = GuiCreateTextLabel(warning, titleText);
+ lv_obj_set_width(title, width - 32);
+ lv_obj_set_style_text_color(title, lv_color_hex(0xF55831), LV_PART_MAIN);
+
+ lv_obj_t *content = GuiCreateIllustrateLabel(warning, contentText);
+ lv_obj_set_width(content, width - 32);
+ lv_label_set_long_mode(content, LV_LABEL_LONG_WRAP);
+ lv_obj_set_style_text_color(content, WHITE_COLOR, LV_PART_MAIN);
+ return warning;
+}
+
+void GuiShowPagedMessage(lv_obj_t *parent, const GuiPagedMessageSource_t *source)
+{
+ if (parent == NULL || source == NULL || source->byte_at == NULL) {
+ return;
+ }
+
+ GuiPagedMessageState_t *state = SRAM_MALLOC(sizeof(GuiPagedMessageState_t));
+ if (state == NULL) {
+ return;
+ }
+ memset(state, 0, sizeof(GuiPagedMessageState_t));
+ state->source = *source;
+
+ size_t offset = 0;
+ do {
+ state->page_count++;
+ offset = GuiPagedMessagePageEnd(state, offset);
+ } while (offset < state->source.length);
+
+ lv_obj_clear_flag(parent, LV_OBJ_FLAG_SCROLLABLE);
+ lv_obj_update_layout(parent);
+ lv_coord_t contentWidth = lv_obj_get_width(parent);
+ lv_coord_t parentHeight = lv_obj_get_height(parent);
+ lv_coord_t viewportHeight = parentHeight > 64 ? parentHeight - 64 : parentHeight;
+
+ state->viewport = lv_obj_create(parent);
+ lv_obj_set_pos(state->viewport, 0, 0);
+ lv_obj_set_size(state->viewport, contentWidth, viewportHeight);
+ lv_obj_set_style_pad_all(state->viewport, 0, LV_PART_MAIN);
+ lv_obj_set_style_border_width(state->viewport, 0, LV_PART_MAIN);
+ lv_obj_set_style_bg_opa(state->viewport, 0, LV_PART_MAIN);
+ lv_obj_set_scrollbar_mode(state->viewport, LV_SCROLLBAR_MODE_OFF);
+ lv_obj_set_scroll_dir(state->viewport, LV_DIR_VER);
+ lv_obj_add_flag(state->viewport, LV_OBJ_FLAG_SCROLLABLE);
+
+ if (source->warning_title != NULL && source->warning_content != NULL) {
+ state->warning = GuiPagedMessageWarning(
+ state->viewport, source->warning_title, source->warning_content, contentWidth);
+ lv_obj_set_pos(state->warning, 0, 0);
+ }
+
+ state->label = lv_label_create(state->viewport);
+ lv_obj_set_pos(state->label, 0, 0);
+ lv_obj_set_width(state->label, contentWidth);
+ lv_obj_set_height(state->label, LV_SIZE_CONTENT);
+ lv_obj_set_style_text_font(state->label, g_defIllustrateFont, LV_PART_MAIN);
+ lv_obj_set_style_text_color(state->label, WHITE_COLOR, LV_PART_MAIN);
+ lv_label_set_long_mode(state->label, LV_LABEL_LONG_WRAP);
+
+ state->prev = GuiPagedMessageButton(parent, "<");
+ lv_obj_align(state->prev, LV_ALIGN_BOTTOM_LEFT, 0, 0);
+ state->next = GuiPagedMessageButton(parent, ">");
+ lv_obj_align(state->next, LV_ALIGN_BOTTOM_RIGHT, 0, 0);
+ state->page_label = lv_label_create(parent);
+ lv_obj_set_style_text_font(state->page_label, g_defIllustrateFont, LV_PART_MAIN);
+ lv_obj_set_style_text_color(state->page_label, WHITE_COLOR, LV_PART_MAIN);
+ lv_obj_align(state->page_label, LV_ALIGN_BOTTOM_MID, 0, -9);
+
+ lv_obj_add_event_cb(state->prev, GuiPagedMessageEvent, LV_EVENT_CLICKED, state);
+ lv_obj_add_event_cb(state->next, GuiPagedMessageEvent, LV_EVENT_CLICKED, state);
+ lv_obj_add_event_cb(parent, GuiPagedMessageDelete, LV_EVENT_DELETE, state);
+ GuiPagedMessageRefresh(state);
+}
+
+void GuiShowPagedMessageText(lv_obj_t *parent, const char *text, bool utf8,
+ const char *warningTitle, const char *warningContent)
+{
+ const char *safeText = text == NULL ? "" : text;
+ GuiPagedMessageSource_t source = {
+ .context = (void *)safeText,
+ .length = strlen(safeText),
+ .byte_at = GuiPagedMessageTextByteAt,
+ .utf8 = utf8,
+ .warning_title = warningTitle,
+ .warning_content = warningContent,
+ };
+ GuiShowPagedMessage(parent, &source);
+}
### src/ui/gui_chain/gui_chain_components.h
@@ -1,8 +1,21 @@
#ifndef _GUI_CHAIN_COMPONENTS_H
#define _GUI_CHAIN_COMPONENTS_H
+#include <stdbool.h>
+#include <stddef.h>
#include "gui_obj.h"
+typedef char (*GuiPagedMessageByteAtFunc)(void *context, size_t offset);
+
+typedef struct {
+ void *context;
+ size_t length;
+ GuiPagedMessageByteAtFunc byte_at;
+ bool utf8;
+ const char *warning_title;
+ const char *warning_content;
+} GuiPagedMessageSource_t;
+
lv_obj_t *CreateTransactionContentContainer(lv_obj_t *parent, uint16_t w, uint16_t h);
lv_obj_t* CreateRelativeTransactionContentContainer(lv_obj_t *parent, uint16_t w, uint16_t h, lv_obj_t *last_view);
lv_obj_t *CreateTransactionItemView(lv_obj_t *parent, const char* title, const char* value, lv_obj_t *lastView);
@@ -17,10 +30,14 @@ lv_obj_t *CreateContentContainer(lv_obj_t *parent, uint16_t w, uint16_t h);
lv_obj_t *CreateValueDetailValue(lv_obj_t *parent, char* inputValue, char *outputValue, char *fee);
lv_obj_t *CreateDynamicInfoView(lv_obj_t *parent, char *key[], char *value[], int keyLen);
lv_obj_t *CreateNoticeCard(lv_obj_t *parent, const char* notice);
+lv_obj_t *CreateNoticeCardWithWidth(lv_obj_t *parent, const char *notice, uint16_t width);
lv_obj_t *CreateSingleInfoTwoLineView(lv_obj_t *parent, char* key, char *value);
lv_obj_t *CreateTransactionOvewviewCard(lv_obj_t *parent, const char* title1, const char* text1, const char* title2, const char* text2);
lv_obj_t *CreateTransactionOverviewCardWithWidth(lv_obj_t *parent, const char* title1, const char* text1, const char* title2, const char* text2, uint16_t width);
lv_obj_t *CreateNoticeView(lv_obj_t *parent, uint16_t width, uint16_t height, const char *notice);
void GuiCustomPathNotice(lv_obj_t *parent, void *totalData);
+void GuiShowPagedMessage(lv_obj_t *parent, const GuiPagedMessageSource_t *source);
+void GuiShowPagedMessageText(lv_obj_t *parent, const char *text, bool utf8,
+ const char *warning_title, const char *warning_content);
#endif
### src/ui/gui_chain/multi/web3/gui_cosmos.c
@@ -61,11 +61,20 @@ static void ClearCosmosDetailCache(void);
static cJSON *GetCosmosParsedDetailRoot(DisplayCosmosTx *tx);
static lv_obj_t *CreateCosmosMessageTitle(lv_obj_t *parent, size_t index, lv_obj_t *lastView);
static lv_obj_t *CreateCosmosJsonItem(lv_obj_t *parent, const char *key, const cJSON *value, lv_obj_t *lastView);
+static lv_obj_t *CreateCosmosHighlightedJsonItem(lv_obj_t *parent, const char *key,
+ 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 *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);
+static lv_obj_t *CreateCosmosOverviewValue(lv_obj_t *parent, const char *value, lv_obj_t *lastView);
+static lv_obj_t *CreateCosmosOverviewVote(lv_obj_t *parent, const cJSON *message, lv_obj_t *lastView);
+static lv_obj_t *CreateCosmosOverviewAddresses(lv_obj_t *parent, const cJSON *message, lv_obj_t *lastView);
+static lv_obj_t *CreateCosmosVoteDetails(lv_obj_t *parent, const cJSON *message, lv_obj_t *lastView);
+static lv_obj_t *CreateCosmosFeeDetails(lv_obj_t *parent, const cJSON *common, lv_obj_t *lastView);
+static lv_obj_t *CreateCosmosNetworkDetails(lv_obj_t *parent, const cJSON *common, lv_obj_t *lastView);
static bool IsCosmosBlindSignMessage(const cJSON *object);
static void InitCosmosTxContainer(lv_obj_t *parent);
@@ -237,6 +246,12 @@ void GuiCosmosTxOverview(lv_obj_t *parent, void *totalData)
if (cJSON_IsArray(kind)) {
int messageCount = cJSON_GetArraySize(kind);
+ cJSON *singleMessage = messageCount == 1 ? cJSON_GetArrayItem(kind, 0) : NULL;
+ if (cJSON_IsObject(singleMessage) && !IsCosmosBlindSignMessage(singleMessage)) {
+ CreateCosmosSingleOverview(parent, singleMessage, common);
+ lv_obj_update_layout(parent);
+ return;
+ }
for (int i = 0; i < messageCount; i++) {
cJSON *message = cJSON_GetArrayItem(kind, i);
if (!cJSON_IsObject(message)) {
@@ -252,6 +267,173 @@ void GuiCosmosTxOverview(lv_obj_t *parent, void *totalData)
lv_obj_update_layout(parent);
}
+static const char *GetCosmosJsonString(const cJSON *object, const char *key)
+{
+ cJSON *value = cJSON_IsObject(object) ? cJSON_GetObjectItem(object, key) : NULL;
+ if (!cJSON_IsString(value) || value->valuestring == NULL || value->valuestring[0] == '\0') {
+ return NULL;
+ }
+ return value->valuestring;
+}
+
+static lv_obj_t *CreateCosmosSingleOverview(lv_obj_t *parent, const cJSON *message, const cJSON *common)
+{
+ lv_obj_t *lastView = NULL;
+ const char *value = GetCosmosJsonString(message, "Value");
+ const char *method = GetCosmosJsonString(message, "Method");
+ const char *network = GetCosmosJsonString(common, "Network");
+
+ if (value != NULL) {
+ lastView = CreateCosmosOverviewValue(parent, value, lastView);
+ } else if (method != NULL && strcmp(method, "Vote") == 0) {
+ lastView = CreateCosmosOverviewVote(parent, message, lastView);
+ }
+ if (network != NULL) {
+ lastView = CreateTransactionItemView(parent, _("Network"), network, lastView);
+ }
+ if (method != NULL) {
+ lastView = CreateTransactionItemView(parent, _("Method"), method, lastView);
+ }
+ lastView = CreateCosmosOverviewAddresses(parent, message, lastView);
+
+ if (network != NULL && strcmp(network, "Unknown Network") == 0) {
+ lastView = CreateCosmosJsonItem(parent, "Chain ID",
+ cJSON_GetObjectItem(common, "Chain ID"), lastView);
+ }
+ cJSON *memo = cJSON_IsObject(common) ? cJSON_GetObjectItem(common, "Memo") : NULL;
+ return CreateCosmosMemoView(parent, memo, lastView);
+}
+
+static lv_obj_t *CreateCosmosOverviewValue(lv_obj_t *parent, const char *value, 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);
+ }
+
+ lv_obj_t *title = GuiCreateIllustrateLabel(container, _("Value"));
+ lv_obj_align(title, LV_ALIGN_TOP_LEFT, 24, 16);
+ lv_obj_set_style_text_opa(title, LV_OPA_64, LV_PART_MAIN);
+
+ lv_obj_t *amount = GuiCreateLabelWithFont(container, value, GetOverviewAmountFont(value));
+ lv_obj_set_width(amount, 360);
+ lv_label_set_long_mode(amount, LV_LABEL_LONG_WRAP);
+ lv_obj_set_style_text_color(amount, ORANGE_COLOR, LV_PART_MAIN);
+ lv_obj_align(amount, LV_ALIGN_TOP_LEFT, 24, 50);
+ lv_obj_update_layout(amount);
+ lv_obj_set_height(container, 50 + lv_obj_get_height(amount) + 26);
+ return container;
+}
+
+static lv_obj_t *CreateCosmosOverviewVote(lv_obj_t *parent, const cJSON *message, lv_obj_t *lastView)
+{
+ const char *proposal = GetCosmosJsonString(message, "Proposal");
+ const char *voted = GetCosmosJsonString(message, "Voted");
+ if (proposal == NULL && voted == NULL) {
+ return lastView;
+ }
+
+ lv_obj_t *container = CreateContentContainer(parent, 408, 106);
+ if (lastView != NULL) {
+ lv_obj_align_to(container, lastView, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 16);
+ }
+ if (proposal != NULL) {
+ lv_obj_t *title = GuiCreateIllustrateLabel(container, _("Proposal"));
+ lv_obj_align(title, LV_ALIGN_TOP_LEFT, 24, 16);
+ lv_obj_set_style_text_opa(title, LV_OPA_64, LV_PART_MAIN);
+ lv_obj_t *text = GuiCreateIllustrateLabel(container, proposal);
+ lv_obj_set_style_text_color(text, ORANGE_COLOR, LV_PART_MAIN);
+ lv_obj_align_to(text, title, LV_ALIGN_OUT_RIGHT_MID, 16, 0);
+ }
+ if (voted != NULL) {
+ lv_obj_t *title = GuiCreateIllustrateLabel(container, _("Voted"));
+ lv_obj_align(title, LV_ALIGN_TOP_LEFT, 24, 54);
+ lv_obj_set_style_text_opa(title, LV_OPA_64, LV_PART_MAIN);
+ lv_obj_t *text = GuiCreateIllustrateLabel(container, voted);
+ lv_obj_set_style_text_color(text, ORANGE_COLOR, LV_PART_MAIN);
+ lv_obj_align_to(text, title, LV_ALIGN_OUT_RIGHT_MID, 16, 0);
+ }
+ return container;
+}
+
+static lv_obj_t *CreateCosmosOverviewAddresses(lv_obj_t *parent, const cJSON *message, lv_obj_t *lastView)
+{
+ static const char *fallbackKeys[] = {
+ "Delegator", "From", "Voter", "To", "Validator", "New Validator"
+ };
+ const char *keys[2] = {NULL, NULL};
+ const char *values[2] = {NULL, NULL};
+ size_t count = 0;
+ const char *method = GetCosmosJsonString(message, "Method");
+
+ if (method != NULL) {
+ if (strcmp(method, "Send") == 0 || strcmp(method, "IBC Transfer") == 0) {
+ keys[0] = "From";
+ keys[1] = "To";
+ } else if (strcmp(method, "Delegate") == 0) {
+ keys[0] = "Delegator";
+ keys[1] = "Validator";
+ } else if (strcmp(method, "Undelegate") == 0) {
+ keys[0] = "Validator";
+ keys[1] = "To";
+ } else if (strcmp(method, "Re-delegate") == 0) {
+ keys[0] = "To";
+ keys[1] = "New Validator";
+ } else if (strcmp(method, "Withdraw Reward") == 0) {
+ keys[0] = "To";
+ keys[1] = "Validator";
+ } else if (strcmp(method, "Vote") == 0) {
+ keys[0] = "Voter";
+ }
+ }
+
+ for (size_t i = 0; i < NUMBER_OF_ARRAYS(keys); i++) {
+ if (keys[i] == NULL) {
+ continue;
+ }
+ const char *value = GetCosmosJsonString(message, keys[i]);
+ if (value != NULL) {
+ values[count] = value;
+ keys[count] = keys[i];
+ count++;
+ }
+ }
+ if (count == 0) {
+ for (size_t i = 0; i < NUMBER_OF_ARRAYS(fallbackKeys) && count < NUMBER_OF_ARRAYS(keys); i++) {
+ const char *value = GetCosmosJsonString(message, fallbackKeys[i]);
+ if (value != NULL) {
+ keys[count] = fallbackKeys[i];
+ values[count] = value;
+ count++;
+ }
+ }
+ }
+ if (count == 0) {
+ return 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;
+ for (size_t i = 0; i < count; i++) {
+ lv_obj_t *title = GuiCreateIllustrateLabel(container, _(keys[i]));
+ lv_obj_align(title, LV_ALIGN_TOP_LEFT, 24, y);
+ lv_obj_set_style_text_opa(title, LV_OPA_64, LV_PART_MAIN);
+
+ lv_obj_t *text = GuiCreateIllustrateLabel(container, values[i]);
+ lv_obj_set_width(text, 360);
+ lv_label_set_long_mode(text, LV_LABEL_LONG_WRAP);
+ lv_obj_align(text, LV_ALIGN_TOP_LEFT, 24, y + 38);
+ lv_obj_update_layout(text);
+ y += 38 + lv_obj_get_height(text) + 16;
+ }
+ lv_obj_set_height(container, y);
+ return container;
+}
+
void GuiCosmosTxDetails(lv_obj_t *parent, void *totalData)
{
DisplayCosmosTx *tx = (DisplayCosmosTx *)totalData;
@@ -264,6 +446,17 @@ void GuiCosmosTxDetails(lv_obj_t *parent, void *totalData)
if (cJSON_IsArray(kind)) {
int messageCount = cJSON_GetArraySize(kind);
+ cJSON *singleMessage = messageCount == 1 ? cJSON_GetArrayItem(kind, 0) : NULL;
+ const char *singleMethod = GetCosmosJsonString(singleMessage, "Method");
+ if (singleMethod != NULL && strcmp(singleMethod, "Vote") == 0) {
+ lastView = CreateCosmosVoteDetails(parent, singleMessage, lastView);
+ 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);
+ return;
+ }
for (int i = 0; i < messageCount; i++) {
cJSON *message = cJSON_GetArrayItem(kind, i);
if (!cJSON_IsObject(message)) {
@@ -279,6 +472,91 @@ void GuiCosmosTxDetails(lv_obj_t *parent, void *totalData)
lv_obj_update_layout(parent);
}
+static lv_obj_t *CreateCosmosDetailInlineValue(lv_obj_t *container, const char *titleText,
+ const char *valueText, uint16_t y, bool highlight)
+{
+ if (valueText == NULL) {
+ return NULL;
+ }
+ lv_obj_t *title = GuiCreateIllustrateLabel(container, _(titleText));
+ lv_obj_align(title, LV_ALIGN_TOP_LEFT, 24, y);
+ lv_obj_set_style_text_opa(title, LV_OPA_64, LV_PART_MAIN);
+
+ lv_obj_t *value = GuiCreateIllustrateLabel(container, valueText);
+ if (highlight) {
+ lv_obj_set_style_text_color(value, ORANGE_COLOR, LV_PART_MAIN);
+ }
+ lv_obj_align_to(value, title, LV_ALIGN_OUT_RIGHT_MID, 16, 0);
+ return value;
+}
+
+static lv_obj_t *CreateCosmosVoteDetails(lv_obj_t *parent, const cJSON *message, 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);
+ }
+
+ CreateCosmosDetailInlineValue(container, "Proposal", GetCosmosJsonString(message, "Proposal"), 16, true);
+ CreateCosmosDetailInlineValue(container, "Voted", GetCosmosJsonString(message, "Voted"), 54, true);
+ CreateCosmosDetailInlineValue(container, "Method", GetCosmosJsonString(message, "Method"), 92, false);
+
+ lv_obj_t *voterTitle = GuiCreateIllustrateLabel(container, _("Voter"));
+ lv_obj_align(voterTitle, LV_ALIGN_TOP_LEFT, 24, 130);
+ lv_obj_set_style_text_opa(voterTitle, LV_OPA_64, LV_PART_MAIN);
+
+ const char *voter = GetCosmosJsonString(message, "Voter");
+ lv_obj_t *voterValue = GuiCreateIllustrateLabel(container, voter == NULL ? "" : voter);
+ lv_obj_set_width(voterValue, 360);
+ lv_label_set_long_mode(voterValue, LV_LABEL_LONG_WRAP);
+ lv_obj_align(voterValue, LV_ALIGN_TOP_LEFT, 24, 168);
+ lv_obj_update_layout(voterValue);
+ lv_obj_set_height(container, 168 + lv_obj_get_height(voterValue) + 16);
+ return container;
+}
+
+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) {
+ return lastView;
+ }
+
+ lv_obj_t *container = CreateContentContainer(parent, 408, 170);
+ 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);
+ return container;
+}
+
+static lv_obj_t *CreateCosmosNetworkDetails(lv_obj_t *parent, const cJSON *common, lv_obj_t *lastView)
+{
+ const char *network = GetCosmosJsonString(common, "Network");
+ const char *chainId = GetCosmosJsonString(common, "Chain ID");
+ if (network == NULL && chainId == NULL) {
+ return lastView;
+ }
+
+ 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, "Network", network, 16, false);
+ CreateCosmosDetailInlineValue(container, "Chain ID", chainId, 54, false);
+ return container;
+}
+
static void InitCosmosTxContainer(lv_obj_t *parent)
{
lv_obj_t *tabChild = lv_obj_get_parent(parent);
@@ -314,12 +592,25 @@ static lv_obj_t *CreateCosmosJsonItem(lv_obj_t *parent, const char *key, const c
return CreateTransactionItemView(parent, _(key), value->valuestring, lastView);
}
+static lv_obj_t *CreateCosmosHighlightedJsonItem(lv_obj_t *parent, const char *key,
+ const cJSON *value, lv_obj_t *lastView)
+{
+ lv_obj_t *view = CreateCosmosJsonItem(parent, key, value, lastView);
+ if (view != lastView) {
+ lv_obj_t *valueLabel = lv_obj_get_child(view, 1);
+ if (valueLabel != NULL) {
+ lv_obj_set_style_text_color(valueLabel, ORANGE_COLOR, LV_PART_MAIN);
+ }
+ }
+ return view;
+}
+
static lv_obj_t *CreateCosmosJsonFields(lv_obj_t *parent, const cJSON *object, lv_obj_t *lastView,
bool overview, bool showMessageIndex)
{
static const char *overviewKeys[] = {
- "Value", "Method", "From", "To", "Validator", "Old Validator", "New Validator",
- "Proposal", "Voted", "Voter", "Source Channel"
+ "Value", "Method", "Delegator", "From", "To", "Validator", "New Validator",
+ "Proposal", "Voted", "Voter"
};
if (!cJSON_IsObject(object)) {
@@ -339,7 +630,11 @@ static lv_obj_t *CreateCosmosJsonFields(lv_obj_t *parent, const cJSON *object, l
if (overview) {
for (size_t i = 0; i < NUMBER_OF_ARRAYS(overviewKeys); i++) {
cJSON *value = cJSON_GetObjectItem(object, overviewKeys[i]);
- lastView = CreateCosmosJsonItem(parent, overviewKeys[i], value, lastView);
+ if (strcmp(overviewKeys[i], "Proposal") == 0 || strcmp(overviewKeys[i], "Voted") == 0) {
+ lastView = CreateCosmosHighlightedJsonItem(parent, overviewKeys[i], value, lastView);
+ } else {
+ lastView = CreateCosmosJsonItem(parent, overviewKeys[i], value, lastView);
+ }
}
return lastView;
}
@@ -508,7 +803,9 @@ void GetCosmosAddress1Value(void *indata, void *param, uint32_t maxLen)
void GetCosmosAddress1Label(void *indata, void *param, uint32_t maxLen)
{
DisplayCosmosTx *tx = (DisplayCosmosTx *)param;
- if (strcmp(tx->overview->display_type, GuiGetCosmosTxTypeName(COSMOS_TX_UNDELEGATE)) == 0) {
+ if (strcmp(tx->overview->display_type, GuiGetCosmosTxTypeName(COSMOS_TX_DELEGATE)) == 0) {
+ strcpy_s((char *)indata, maxLen, "Delegator");
+ } else if (strcmp(tx->overview->display_type, GuiGetCosmosTxTypeName(COSMOS_TX_UNDELEGATE)) == 0) {
strcpy_s((char *)indata, maxLen, "Validator");
} else if (strcmp(tx->overview->display_type, GuiGetCosmosTxTypeName(COSMOS_TX_VOTE)) == 0) {
strcpy_s((char *)indata, maxLen, "Voter");
@@ -546,7 +843,9 @@ void GetCosmosAddress2Value(void *indata, void *param, uint32_t maxLen)
void GetCosmosAddress2Label(void *indata, void *param, uint32_t maxLen)
{
DisplayCosmosTx *tx = (DisplayCosmosTx *)param;
- if (strcmp(tx->overview->display_type, GuiGetCosmosTxTypeName(COSMOS_TX_REDELEGATE)) == 0) {
+ if (strcmp(tx->overview->display_type, GuiGetCosmosTxTypeName(COSMOS_TX_DELEGATE)) == 0) {
+ strcpy_s((char *)indata, maxLen, "Validator");
+ } else if (strcmp(tx->overview->display_type, GuiGetCosmosTxTypeName(COSMOS_TX_REDELEGATE)) == 0) {
snprintf_s((char *)indata, maxLen, "New Validator");
} else if (
strcmp(tx->overview->display_type, GuiGetCosmosTxTypeName(COSMOS_TX_WITHDRAW_REWARD)) == 0 ||
@@ -860,7 +1159,7 @@ uint8_t GuiGetCosmosTxChain(void)
if (strcmp(chain_id, "evmos_9000-4") == 0) {
return CHAIN_EVMOS;
}
- return CHAIN_ATOM;
+ return CHAIN_UNKNOWN;
}
UREncodeResult *GuiGetCosmosSignQrCodeData(void)
### src/ui/gui_chain/multi/web3/gui_eth.c
@@ -18,11 +18,15 @@
#include "gui_views.h"
#include "gui_chain_components.h"
+#define ETH_COMPONENT_WIDTH 376
+#define ETH_COMPONENT_CONTENT_WIDTH (ETH_COMPONENT_WIDTH - 48)
+
static void decodeEthContractData(void *parseResult);
static bool GetEthErc20ContractData(void *parseResult);
static lv_obj_t *CreateEthOverviewValueView(lv_obj_t *parent, DisplayETH *eth, lv_obj_t *lastView);
static uint16_t CreateEthOverviewValueRow(lv_obj_t *container, const char *title, const char *value, uint16_t y);
static lv_obj_t *CreateEthOverviewNetworkView(lv_obj_t *parent, DisplayETH *eth, lv_obj_t *lastView);
+static lv_obj_t *CreateEthReplayProtectionWarning(lv_obj_t *parent, lv_obj_t *lastView);
static lv_obj_t *CreateEthAddressView(lv_obj_t *parent, DisplayETH *eth, lv_obj_t *lastView, bool details);
static lv_obj_t *CreateEthDetailsFeeView(lv_obj_t *parent, DisplayETH *eth, lv_obj_t *lastView);
static lv_obj_t *CreateEthDetailsContractViews(lv_obj_t *parent, DisplayETH *eth, lv_obj_t *lastView);
@@ -1135,6 +1139,18 @@ void GetMessageRaw(void *indata, void *param, uint32_t maxLen)
}
}
+void GuiShowEthMessagePaged(lv_obj_t *parent, void *param, bool raw)
+{
+ DisplayETHPersonalMessage *message = (DisplayETHPersonalMessage *)param;
+ const char *text = raw ? message->raw_message : message->utf8_message;
+ GuiShowPagedMessageText(
+ parent,
+ text,
+ !raw,
+ raw ? _("solana_blind_sign_title") : NULL,
+ raw ? _("solana_unparsed_message_warning") : NULL);
+}
+
static uint8_t GetEthPublickeyIndex(char* rootPath)
{
if (strcmp(rootPath, "44'/60'/0'") == 0) return XPUB_TYPE_ETH_BIP44_STANDARD;
@@ -1197,13 +1213,21 @@ PtrT_TransactionCheckResult GuiGetEthCheckResult(void)
void GuiEthTxOverview(lv_obj_t *parent, void *totalData)
{
DisplayETH *eth = (DisplayETH *)totalData;
- lv_obj_set_size(parent, 408, 444);
+ lv_obj_set_size(parent, ETH_COMPONENT_WIDTH, 444);
lv_obj_add_flag(parent, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_add_flag(parent, LV_OBJ_FLAG_CLICKABLE);
lv_obj_t *lastView = NULL;
+ if (!eth->replay_protected) {
+ lastView = CreateEthReplayProtectionWarning(parent, lastView);
+ }
if (eth->overview->from == NULL) {
- lastView = CreateNoticeCard(parent, _("custom_path_parse_notice"));
+ lv_obj_t *notice = CreateNoticeCardWithWidth(
+ parent, _("custom_path_parse_notice"), ETH_COMPONENT_WIDTH);
+ if (lastView != NULL) {
+ lv_obj_align_to(notice, lastView, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 16);
+ }
+ lastView = notice;
}
lastView = CreateEthOverviewValueView(parent, eth, lastView);
lastView = CreateEthOverviewNetworkView(parent, eth, lastView);
@@ -1214,24 +1238,34 @@ void GuiEthTxOverview(lv_obj_t *parent, void *totalData)
void GuiEthTxDetails(lv_obj_t *parent, void *totalData)
{
DisplayETH *eth = (DisplayETH *)totalData;
- lv_obj_set_size(parent, 408, 444);
+ lv_obj_set_size(parent, ETH_COMPONENT_WIDTH, 444);
lv_obj_add_flag(parent, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_add_flag(parent, LV_OBJ_FLAG_CLICKABLE);
lv_obj_t *lastView = NULL;
+ if (!eth->replay_protected) {
+ lastView = CreateEthReplayProtectionWarning(parent, lastView);
+ }
if (eth->overview->from == NULL) {
- lastView = CreateNoticeCard(parent, _("custom_path_parse_notice"));
+ lv_obj_t *notice = CreateNoticeCardWithWidth(
+ parent, _("custom_path_parse_notice"), ETH_COMPONENT_WIDTH);
+ if (lastView != NULL) {
+ lv_obj_align_to(notice, lastView, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 16);
+ }
+ lastView = notice;
}
lastView = CreateEthDetailsFeeView(parent, eth, lastView);
lastView = CreateEthOverviewNetworkView(parent, eth, lastView);
if (g_contractDataExist) {
char method[96] = {0};
GetEthMethodName(method, eth, sizeof(method));
- lastView = CreateTransactionItemView(parent, _("Method"), method, lastView);
+ lastView = CreateTransactionItemViewWithWidth(
+ parent, _("Method"), method, lastView, ETH_COMPONENT_WIDTH);
}
- lastView = CreateTransactionItemView(parent, _("nonce"), eth->detail->nonce, lastView);
+ lastView = CreateTransactionItemViewWithWidth(
+ parent, _("nonce"), eth->detail->nonce, lastView, ETH_COMPONENT_WIDTH);
lastView = CreateEthAddressView(parent, eth, lastView, true);
lastView = CreateEthDetailsContractViews(parent, eth, lastView);
lv_obj_update_layout(parent);
@@ -1250,14 +1284,15 @@ static lv_obj_t *CreateEthOverviewValueView(lv_obj_t *parent, DisplayETH *eth, l
GetEthValue(value, eth, sizeof(value));
}
- lv_obj_t *container = CreateRelativeTransactionContentContainer(parent, 408, 144, lastView);
+ lv_obj_t *container = CreateRelativeTransactionContentContainer(
+ parent, ETH_COMPONENT_WIDTH, 144, lastView);
lv_obj_t *label = GuiCreateIllustrateLabel(container, title);
lv_obj_align(label, LV_ALIGN_TOP_LEFT, 24, 16);
lv_obj_set_style_text_opa(label, LV_OPA_64, LV_PART_MAIN);
label = GuiCreateLittleTitleLabel(container, value);
lv_obj_align(label, LV_ALIGN_TOP_LEFT, 24, 50);
- lv_obj_set_width(label, 360);
+ lv_obj_set_width(label, ETH_COMPONENT_CONTENT_WIDTH);
lv_label_set_long_mode(label, LV_LABEL_LONG_WRAP);
lv_obj_set_style_text_color(label, ORANGE_COLOR, LV_PART_MAIN);
lv_obj_update_layout(label);
@@ -1287,13 +1322,13 @@ static uint16_t CreateEthOverviewValueRow(lv_obj_t *container, const char *title
uint16_t titleWidth = lv_obj_get_width(titleLabel);
uint16_t valueWidth = lv_obj_get_width(valueLabel);
- if (24 + titleWidth + 16 + valueWidth + 24 <= 408) {
+ if (24 + titleWidth + 16 + valueWidth + 24 <= ETH_COMPONENT_WIDTH) {
lv_obj_align_to(valueLabel, titleLabel, LV_ALIGN_OUT_RIGHT_MID, 16, 0);
return y + 38;
}
lv_obj_align_to(valueLabel, titleLabel, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 0);
- lv_obj_set_width(valueLabel, 360);
+ lv_obj_set_width(valueLabel, ETH_COMPONENT_CONTENT_WIDTH);
lv_label_set_long_mode(valueLabel, LV_LABEL_LONG_WRAP);
lv_obj_update_layout(valueLabel);
return y + lv_obj_get_height(titleLabel) + lv_obj_get_height(valueLabel) + 8;
@@ -1303,7 +1338,31 @@ static lv_obj_t *CreateEthOverviewNetworkView(lv_obj_t *parent, DisplayETH *eth,
{
char network[64] = {0};
GetEthNetWork(network, eth, sizeof(network));
- return CreateTransactionItemView(parent, _("Network"), network, lastView);
+ return CreateTransactionItemViewWithWidth(
+ parent, _("Network"), network, lastView, ETH_COMPONENT_WIDTH);
+}
+
+static lv_obj_t *CreateEthReplayProtectionWarning(lv_obj_t *parent, lv_obj_t *lastView)
+{
+ lv_obj_t *card = CreateRelativeTransactionContentContainer(
+ parent, ETH_COMPONENT_WIDTH, 128, lastView);
+ lv_obj_set_style_bg_color(card, lv_color_hex(0xF55831), LV_PART_MAIN);
+ lv_obj_set_style_bg_opa(card, LV_OPA_20, LV_PART_MAIN);
+
+ lv_obj_t *warningIcon = GuiCreateImg(card, &imgWarningRed);
+ lv_obj_align(warningIcon, LV_ALIGN_TOP_LEFT, 24, 20);
+
+ lv_obj_t *title = GuiCreateTextLabel(card, _("Warning"));
+ lv_obj_set_style_text_color(title, lv_color_hex(0xF55831), LV_PART_MAIN);
+ lv_obj_align_to(title, warningIcon, LV_ALIGN_OUT_RIGHT_MID, 8, 0);
+
+ lv_obj_t *content = GuiCreateIllustrateLabel(card, _("eth_replay_protection_warning"));
+ lv_obj_set_width(content, ETH_COMPONENT_CONTENT_WIDTH);
+ lv_label_set_long_mode(content, LV_LABEL_LONG_WRAP);
+ lv_obj_align(content, LV_ALIGN_TOP_LEFT, 24, 64);
+ lv_obj_update_layout(content);
+ lv_obj_set_height(card, 64 + lv_obj_get_height(content) + 24);
+ return card;
}
static lv_obj_t *CreateEthAddressView(lv_obj_t *parent, DisplayETH *eth, lv_obj_t *lastView, bool details)
@@ -1323,7 +1382,7 @@ static lv_obj_t *CreateEthAddressView(lv_obj_t *parent, DisplayETH *eth, lv_obj_
GetEthGetFromAddress(address, eth, sizeof(address));
label = GuiCreateIllustrateLabel(container, address);
- lv_obj_set_width(label, 360);
+ lv_obj_set_width(label, ETH_COMPONENT_CONTENT_WIDTH);
lv_obj_align(label, LV_ALIGN_TOP_LEFT, 24, y + 38);
y += 114;
@@ -1347,7 +1406,7 @@ static lv_obj_t *CreateEthAddressView(lv_obj_t *parent, DisplayETH *eth, lv_obj_
GetEthGetToAddress(address, eth, sizeof(address));
}
label = GuiCreateIllustrateLabel(container, address);
- lv_obj_set_width(label, 360);
+ lv_obj_set_width(label, ETH_COMPONENT_CONTENT_WIDTH);
lv_obj_align(label, LV_ALIGN_TOP_LEFT, 24, y + 38);
if (g_toEnsExist) {
@@ -1368,129 +1427,260 @@ static lv_obj_t *CreateEthAddressView(lv_obj_t *parent, DisplayETH *eth, lv_obj_
lv_obj_align(icon, LV_ALIGN_TOP_LEFT, 24, y - 1);
label = GuiCreateIllustrateLabel(container, contractName);
lv_obj_align(label, LV_ALIGN_TOP_LEFT, 62, y - 4);
- lv_obj_set_style_text_color(label, lv_color_hex(0xA4877F), LV_PART_MAIN);
+ lv_obj_set_style_text_color(label, lv_color_hex(0xA485FF), LV_PART_MAIN);
}
return container;
}
-static void CreateEthDetailsPair(lv_obj_t *container, const char *title, const char *value, uint16_t y)
+static uint16_t CreateEthDetailsPair(lv_obj_t *container, const char *title, const char *value,
+ uint16_t y, bool highlightValue)
{
lv_obj_t *titleLabel = GuiCreateIllustrateLabel(container, title);
lv_obj_align(titleLabel, LV_ALIGN_TOP_LEFT, 24, y);
lv_obj_set_style_text_opa(titleLabel, LV_OPA_64, LV_PART_MAIN);
+ lv_obj_update_layout(titleLabel);
lv_obj_t *valueLabel = GuiCreateIllustrateLabel(container, value);
- lv_obj_align_to(valueLabel, titleLabel, LV_ALIGN_OUT_RIGHT_MID, 16, 0);
+ if (highlightValue) {
+ lv_obj_set_style_text_color(valueLabel, ORANGE_COLOR, LV_PART_MAIN);
+ }
+ lv_obj_update_layout(valueLabel);
+
+ uint16_t titleWidth = lv_obj_get_width(titleLabel);
+ uint16_t valueWidth = lv_obj_get_width(valueLabel);
+ uint16_t titleHeight = lv_obj_get_height(titleLabel);
+ uint16_t valueHeight = lv_obj_get_height(valueLabel);
+ if (24 + titleWidth + 16 + valueWidth + 24 <= ETH_COMPONENT_WIDTH &&
+ valueHeight <= TEXT_LINE_HEIGHT) {
+ lv_obj_align_to(valueLabel, titleLabel, LV_ALIGN_OUT_RIGHT_MID, 16, 0);
+ return y + (titleHeight > valueHeight ? titleHeight : valueHeight) + 8;
+ }
+
+ lv_obj_set_width(valueLabel, ETH_COMPONENT_CONTENT_WIDTH);
+ lv_label_set_long_mode(valueLabel, LV_LABEL_LONG_WRAP);
+ lv_obj_align_to(valueLabel, titleLabel, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 4);
+ lv_obj_update_layout(valueLabel);
+ return y + titleHeight + 4 + lv_obj_get_height(valueLabel) + 8;
+}
+
+static uint16_t CreateEthDetailsDescription(lv_obj_t *container, const char *text,
+ uint16_t y, const lv_font_t *font)
+{
+ lv_obj_t *label = GuiCreateIllustrateLabel(container, text);
+ lv_obj_align(label, LV_ALIGN_TOP_LEFT, 24, y);
+ lv_obj_set_width(label, ETH_COMPONENT_CONTENT_WIDTH);
+ lv_label_set_long_mode(label, LV_LABEL_LONG_WRAP);
+ if (font != NULL) {
+ lv_obj_set_style_text_font(label, font, LV_PART_MAIN);
+ }
+ lv_obj_set_style_text_opa(label, LV_OPA_64, LV_PART_MAIN);
+ lv_obj_update_layout(label);
+ return y + lv_obj_get_height(label) + 8;
}
static lv_obj_t *CreateEthDetailsFeeView(lv_obj_t *parent, DisplayETH *eth, lv_obj_t *lastView)
{
bool feeMarket = strcmp(eth->tx_type, "FeeMarket") == 0;
bool erc20Transfer = isErc20Transfer(eth);
- uint16_t offset = erc20Transfer ? 38 : 0;
- lv_obj_t *container =
- CreateRelativeTransactionContentContainer(parent, 408, (feeMarket ? 316 : 208) + offset, lastView);
+ uint16_t y = 16;
+ lv_obj_t *container = CreateRelativeTransactionContentContainer(
+ parent, ETH_COMPONENT_WIDTH, 24, lastView);
char value[96] = {0};
if (erc20Transfer) {
GetErc20TransferValue(eth, value, sizeof(value));
- CreateEthDetailsPair(container, _("Value"), value, 16);
+ y = CreateEthDetailsPair(container, _("Value"), value, y, true);
GetEthValue(value, eth, sizeof(value));
- CreateEthDetailsPair(container, _("Native Transfer"), value, 54);
+ y = CreateEthDetailsPair(container, _("Native Transfer"), value, y, true);
} else {
GetEthValue(value, eth, sizeof(value));
- CreateEthDetailsPair(container,
- strlen(eth->detail->input) > 0 ? _("Native Transfer") : _("Value"),
- value, 16);
+ y = CreateEthDetailsPair(
+ container,
+ strlen(eth->detail->input) > 0 ? _("Native Transfer") : _("Value"),
+ value, y, true);
}
if (feeMarket) {
GetEthMaxFee(value, eth, sizeof(value));
- CreateEthDetailsPair(container, _("MaxFee"), value, 54 + offset);
-
- lv_obj_t *label = GuiCreateIllustrateLabel(container, _("·MaxFeePrice*GasLimit"));
- lv_obj_align(label, LV_ALIGN_TOP_LEFT, 24, 92 + offset);
- lv_obj_set_style_text_opa(label, LV_OPA_64, LV_PART_MAIN);
+ y = CreateEthDetailsPair(container, _("MaxFee"), value, y, false);
+ y = CreateEthDetailsDescription(container, _("·MaxFeePrice*GasLimit"), y, NULL);
GetEthMaxPriority(value, eth, sizeof(value));
- CreateEthDetailsPair(container, _("MaxPriority"), value, 124 + offset);
-
- label = GuiCreateIllustrateLabel(container, _("·MaxPriorityFeePrice*GasLimit"));
- lv_obj_align(label, LV_ALIGN_TOP_LEFT, 24, 162 + offset);
- lv_obj_set_style_text_opa(label, LV_OPA_64, LV_PART_MAIN);
+ y = CreateEthDetailsPair(container, _("MaxPriority"), value, y, false);
+ y = CreateEthDetailsDescription(
+ container, _("·MaxPriorityFeePrice*GasLimit"), y, NULL);
GetEthMaxFeePrice(value, eth, sizeof(value));
- CreateEthDetailsPair(container, _("MaxFeePrice"), value, 194 + offset);
+ y = CreateEthDetailsPair(container, _("MaxFeePrice"), value, y, false);
GetEthMaxPriorityFeePrice(value, eth, sizeof(value));
- CreateEthDetailsPair(container, _("MaxPriorityFeePrice"), value, 232 + offset);
- CreateEthDetailsPair(container, _("GasLimit"), eth->overview->gas_limit, 270 + offset);
+ y = CreateEthDetailsPair(container, _("MaxPriorityFeePrice"), value, y, false);
+ y = CreateEthDetailsPair(container, _("GasLimit"), eth->overview->gas_limit, y, false);
} else {
GetEthTxFee(value, eth, sizeof(value));
- CreateEthDetailsPair(container, _("MaxTxnFee"), value, 54 + offset);
-
- lv_obj_t *label = GuiCreateIllustrateLabel(container, " \xE2\x80\xA2 Max Txn Fee = Gas Price * Gas Limit");
- lv_obj_align(label, LV_ALIGN_TOP_LEFT, 24, 92 + offset);
- lv_obj_set_style_text_font(label, &openSansDesc, LV_PART_MAIN);
- lv_obj_set_style_text_opa(label, LV_OPA_64, LV_PART_MAIN);
-
- CreateEthDetailsPair(container, _("GasPrice"), eth->overview->gas_price, 124 + offset);
- CreateEthDetailsPair(container, _("GasLimit"), eth->overview->gas_limit, 162 + offset);
- }
+ y = CreateEthDetailsPair(container, _("MaxTxnFee"), value, y, false);
+ y = CreateEthDetailsDescription(
+ container, " \xE2\x80\xA2 Max Txn Fee = Gas Price * Gas Limit", y,
+ &openSansDesc);
+ y = CreateEthDetailsPair(
+ container, _("GasPrice"), eth->overview->gas_price, y, false);
+ y = CreateEthDetailsPair(
+ container, _("GasLimit"), eth->overview->gas_limit, y, false);
+ }
+ lv_obj_set_height(container, y + 8);
return container;
}
static lv_obj_t *CreateEthDetailsRawDataButton(lv_obj_t *parent, lv_obj_t *lastView)
{
lv_obj_t *button = GuiCreateBtnWithFont(parent, _("Check the Raw Data"), &openSansEnIllustrate);
- lv_obj_set_size(button, 408, 62);
+ lv_obj_t *label = lv_obj_get_child(button, 0);
+ lv_obj_set_size(button, ETH_COMPONENT_WIDTH, 62);
lv_obj_set_style_radius(button, 24, LV_PART_MAIN);
lv_obj_set_style_bg_color(button, WHITE_COLOR, LV_PART_MAIN);
lv_obj_set_style_bg_opa(button, LV_OPA_12, LV_PART_MAIN);
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);
+ }
if (lastView != NULL) {
lv_obj_align_to(button, lastView, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 16);
}
lv_obj_add_event_cb(button, EthContractCheckRawData, LV_EVENT_CLICKED, NULL);
return button;
}
+static lv_obj_t *CreateEthUnknownContractView(
+ lv_obj_t *parent, DisplayETH *eth, lv_obj_t *lastView)
+{
+ char input[64] = {0};
+ GetEthTransactionData(input, eth, sizeof(input));
+
+ lv_obj_t *container = CreateRelativeTransactionContentContainer(
+ parent, ETH_COMPONENT_WIDTH, 0, lastView);
+ uint16_t y = 16;
+
+ lv_obj_t *title = GuiCreateIllustrateLabel(container, _("InputData"));
+ 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);
+ y += lv_obj_get_height(title) + 8;
+
+ lv_obj_t *value = GuiCreateIllustrateLabel(container, input);
+ lv_obj_set_width(value, ETH_COMPONENT_CONTENT_WIDTH);
+ lv_label_set_long_mode(value, LV_LABEL_LONG_WRAP);
+ lv_obj_align(value, LV_ALIGN_TOP_LEFT, 24, y);
+ lv_obj_update_layout(value);
+ y += lv_obj_get_height(value) + 8;
+
+ lv_obj_t *unknown = GuiCreateIllustrateLabel(container, _("UnknownContract"));
+ lv_obj_set_width(unknown, ETH_COMPONENT_CONTENT_WIDTH);
+ lv_label_set_long_mode(unknown, LV_LABEL_LONG_WRAP);
+ lv_obj_set_style_text_color(unknown, ORANGE_COLOR, LV_PART_MAIN);
+ lv_obj_align(unknown, LV_ALIGN_TOP_LEFT, 24, y);
+ lv_obj_update_layout(unknown);
+ y += lv_obj_get_height(unknown) + 8;
+
+ // Keep the learn-more action inside the InputData card, matching the
+ // original layout while allowing the rows above it to grow dynamically.
+ lv_obj_t *learnMore = GuiCreateContainerWithParent(
+ container, ETH_COMPONENT_CONTENT_WIDTH, 30);
+ lv_obj_set_style_bg_opa(learnMore, LV_OPA_TRANSP, LV_PART_MAIN);
+ lv_obj_set_style_border_width(learnMore, 0, LV_PART_MAIN);
+ lv_obj_set_style_pad_all(learnMore, 0, LV_PART_MAIN);
+ lv_obj_clear_flag(learnMore, LV_OBJ_FLAG_SCROLLABLE);
+ lv_obj_add_flag(learnMore, LV_OBJ_FLAG_CLICKABLE);
+ lv_obj_align(learnMore, LV_ALIGN_TOP_LEFT, 24, y);
+ lv_obj_add_event_cb(learnMore, EthContractLearnMore, LV_EVENT_CLICKED, NULL);
+
+ lv_obj_t *learnMoreLabel = GuiCreateIllustrateLabel(learnMore, _("LearnMore"));
+ lv_obj_set_style_text_color(learnMoreLabel, lv_color_hex(0x1BE0C6), LV_PART_MAIN);
+ lv_obj_align(learnMoreLabel, LV_ALIGN_LEFT_MID, 0, 0);
+
+ lv_obj_t *qrIcon = GuiCreateImg(learnMore, &imgQrcodeTurquoise);
+ lv_obj_align_to(qrIcon, learnMoreLabel, LV_ALIGN_OUT_RIGHT_MID, 12, 0);
+
+ lv_obj_set_height(container, y + 30 + 16);
+ return container;
+}
+
+static uint16_t CreateEthContractField(
+ lv_obj_t *container, const char *titleText, const char *valueText, uint16_t y)
+{
+ lv_obj_t *title = GuiCreateIllustrateLabel(container, titleText);
+ 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);
+ y += lv_obj_get_height(title) + 8;
+
+ lv_obj_t *value = GuiCreateIllustrateLabel(container, valueText);
+ lv_obj_set_width(value, ETH_COMPONENT_CONTENT_WIDTH);
+ lv_label_set_long_mode(value, LV_LABEL_LONG_WRAP);
+ lv_obj_align(value, LV_ALIGN_TOP_LEFT, 24, y);
+ lv_obj_update_layout(value);
+ return y + lv_obj_get_height(value) + 16;
+}
+
+static lv_obj_t *CreateEthParsedContractView(
+ lv_obj_t *parent, DisplayETH *eth, lv_obj_t *lastView)
+{
+ lv_obj_t *sectionTitle = GuiCreateIllustrateLabel(parent, _("InputData"));
+ lv_obj_set_style_text_opa(sectionTitle, LV_OPA_64, LV_PART_MAIN);
+ if (lastView == NULL) {
+ lv_obj_align(sectionTitle, LV_ALIGN_TOP_LEFT, 0, 0);
+ } else {
+ lv_obj_align_to(sectionTitle, lastView, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 16);
+ }
+
+ lv_obj_t *container = CreateTransactionContentContainer(
+ parent, ETH_COMPONENT_WIDTH, 0);
+ lv_obj_align_to(container, sectionTitle, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 8);
+
+ char method[96] = {0};
+ GetEthMethodName(method, eth, sizeof(method));
+ uint16_t y = CreateEthContractField(container, _("Method"), method, 16);
+
+ Response_DisplayContractData *contractData = (Response_DisplayContractData *)g_contractData;
+ if (contractData != NULL && contractData->data != NULL &&
+ contractData->data->params != NULL) {
+ char truncatedValue[BUFFER_SIZE_512 + 1] = {0};
+ for (size_t i = 0; i < contractData->data->params->size; i++) {
+ DisplayContractParam *param = &contractData->data->params->data[i];
+ const char *displayValue = param->value;
+ if (strnlen_s(param->value, BUFFER_SIZE_512 + 1) > BUFFER_SIZE_512) {
+ memcpy(truncatedValue, param->value, BUFFER_SIZE_512 - 3);
+ memcpy(&truncatedValue[BUFFER_SIZE_512 - 3], "...", 4);
+ displayValue = truncatedValue;
+ }
+ y = CreateEthContractField(container, param->name, displayValue, y);
+ }
+ }
+
+ lv_obj_t *rawDataButton = GuiCreateBtnWithFont(
+ container, _("Check the Raw Data"), &openSansEnIllustrate);
+ lv_obj_t *rawDataLabel = lv_obj_get_child(rawDataButton, 0);
+ lv_obj_set_size(rawDataButton, ETH_COMPONENT_CONTENT_WIDTH, 46);
+ lv_obj_set_style_bg_opa(rawDataButton, LV_OPA_TRANSP, LV_PART_MAIN);
+ 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(rawDataButton, LV_ALIGN_TOP_LEFT, 24, y);
+ lv_obj_add_event_cb(rawDataButton, EthContractCheckRawData, LV_EVENT_CLICKED, NULL);
+ lv_obj_set_height(container, y + 46 + 16);
+ return container;
+}
+
static lv_obj_t *CreateEthDetailsContractViews(lv_obj_t *parent, DisplayETH *eth, lv_obj_t *lastView)
{
if (eth->detail->input == NULL || strlen(eth->detail->input) == 0) {
return lastView;
}
if (!g_contractDataExist || g_contractData == NULL) {
- char input[64] = {0};
- GetEthTransactionData(input, eth, sizeof(input));
- lastView = CreateTransactionItemViewWithHint(parent, _("InputData"), input, lastView, _("UnknownContract"));
-
- lv_obj_t *learnMore = GuiCreateBtnWithFont(parent, _("LearnMore"), &openSansEnIllustrate);
- lv_obj_set_size(learnMore, 144, 46);
- lv_obj_set_style_radius(learnMore, 23, LV_PART_MAIN);
- lv_obj_set_style_bg_color(learnMore, lv_color_hex(0x1D1D1D), LV_PART_MAIN);
- lv_obj_set_style_text_color(learnMore, lv_color_hex(0x1BE0C6), LV_PART_MAIN);
- lv_obj_align_to(learnMore, lastView, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 8);
- lv_obj_add_event_cb(learnMore, EthContractLearnMore, LV_EVENT_CLICKED, NULL);
- lastView = learnMore;
- } else {
- Response_DisplayContractData *contractData = (Response_DisplayContractData *)g_contractData;
- if (contractData->data != NULL && contractData->data->params != NULL) {
- // Keep contract parameter rendering bounded. Raw Data is available on demand below.
- char truncatedValue[BUFFER_SIZE_512 + 1] = {0};
- for (size_t i = 0; i < contractData->data->params->size; i++) {
- DisplayContractParam *param = &contractData->data->params->data[i];
- const char *displayValue = param->value;
- if (strnlen_s(param->value, BUFFER_SIZE_512 + 1) > BUFFER_SIZE_512) {
- memcpy(truncatedValue, param->value, BUFFER_SIZE_512 - 3);
- memcpy(&truncatedValue[BUFFER_SIZE_512 - 3], "...", 4);
- displayValue = truncatedValue;
- }
- lastView = CreateTransactionItemView(parent, param->name, displayValue, lastView);
- }
- }
+ lastView = CreateEthUnknownContractView(parent, eth, lastView);
+ return CreateEthDetailsRawDataButton(parent, lastView);
}
- return CreateEthDetailsRawDataButton(parent, lastView);
+ return CreateEthParsedContractView(parent, eth, lastView);
}
static void GetEthTxFee(void *indata, void *param, uint32_t maxLen)
@@ -1638,7 +1828,7 @@ bool GetEthMessageFromNotExist(void *indata, void *param)
static void GetEthToFromSize(uint16_t *width, uint16_t *height, void *param)
{
DisplayETH *eth = (DisplayETH *)param;
- *width = 408;
+ *width = ETH_COMPONENT_WIDTH;
*height = (244 - 114) + (eth->overview->from != NULL) * 114 +
(g_fromEnsExist + g_toEnsExist) * (GAP + TEXT_LINE_HEIGHT) +
g_contractDataExist * (GAP + TEXT_LINE_HEIGHT);
### src/ui/gui_chain/multi/web3/gui_eth.h
@@ -31,6 +31,7 @@ void GetEthPersonalMessageType(void *indata, void *param, uint32_t maxLen);
void GetMessageFrom(void *indata, void *param, uint32_t maxLen);
void GetMessageUtf8(void *indata, void *param, uint32_t maxLen);
void GetMessageRaw(void *indata, void *param, uint32_t maxLen);
+void GuiShowEthMessagePaged(lv_obj_t *parent, void *param, bool raw);
void EthContractCheckRawDataCallback(void);
void *GuiGetEthTypeData(void);
### src/ui/gui_chain/multi/web3/gui_sol.c
@@ -10,31 +10,11 @@
#include "account_manager.h"
#include "assert.h"
#include "cjson/cJSON.h"
-#include "user_memory.h"
#include "gui_qr_hintbox.h"
#define SQUADS_V4_CREATE_MULTISIG_CONTRACT_ADDRESS "5DH2e3cJmFpyi6mk65EGFediunm4ui6BiKNUNrhWtD1b"
#define SOL_COMPONENT_WIDTH 376
#define SOL_COMPONENT_CONTENT_WIDTH (SOL_COMPONENT_WIDTH - 48)
-#define SOL_MESSAGE_PAGE_BYTES 512
-
-typedef struct {
- const char *text;
- const char *suffix;
- size_t text_len;
- size_t suffix_len;
- size_t offset;
- size_t page;
- size_t page_count;
- bool utf8;
- lv_obj_t *viewport;
- lv_obj_t *warning;
- lv_obj_t *label;
- lv_obj_t *page_label;
- lv_obj_t *prev;
- lv_obj_t *next;
-} SolMessagePager_t;
-
typedef struct SolanaLearnMoreData {
PtrString title;
PtrString content;
@@ -213,216 +193,16 @@ void GetSolMessageRaw(void *indata, void *param, uint32_t maxLen)
snprintf_s((char *)indata, maxLen, "%.*s", maxLen - 1, message->raw_message);
}
-static size_t SolMessagePagerLength(const SolMessagePager_t *pager)
-{
- return pager->text_len + pager->suffix_len;
-}
-
-static char SolMessagePagerByteAt(const SolMessagePager_t *pager, size_t offset)
-{
- if (offset < pager->text_len) {
- return pager->text[offset];
- }
- return pager->suffix[offset - pager->text_len];
-}
-
-static bool SolMessageIsUtf8Continuation(char value)
-{
- return (((uint8_t)value) & 0xC0) == 0x80;
-}
-
-static size_t SolMessagePageEnd(const SolMessagePager_t *pager, size_t offset)
-{
- size_t total = SolMessagePagerLength(pager);
- if (offset >= total) {
- return total;
- }
- size_t remaining = total - offset;
- size_t length = remaining < SOL_MESSAGE_PAGE_BYTES
- ? remaining
- : SOL_MESSAGE_PAGE_BYTES;
-
- if (pager->utf8 && offset + length < total) {
- while (length > 0 &&
- SolMessageIsUtf8Continuation(SolMessagePagerByteAt(pager, offset + length))) {
- length--;
- }
-
- }
- return offset + length;
-}
-
-static size_t SolMessagePageOffset(const SolMessagePager_t *pager, size_t page)
-{
- size_t offset = 0;
- for (size_t i = 0; i < page && offset < SolMessagePagerLength(pager); i++) {
- offset = SolMessagePageEnd(pager, offset);
- }
- return offset;
-}
-
-static void SolMessagePagerRefresh(SolMessagePager_t *pager)
-{
- char page_text[SOL_MESSAGE_PAGE_BYTES + 1];
- size_t end = SolMessagePageEnd(pager, pager->offset);
- size_t length = end - pager->offset;
- for (size_t i = 0; i < length; i++) {
- page_text[i] = SolMessagePagerByteAt(pager, pager->offset + i);
- }
- page_text[length] = '\0';
-
- lv_coord_t label_y = 0;
- if (pager->warning != NULL) {
- if (pager->page == 0) {
- lv_obj_clear_flag(pager->warning, LV_OBJ_FLAG_HIDDEN);
- lv_obj_update_layout(pager->warning);
- label_y = lv_obj_get_height(pager->warning) + 16;
- } else {
- lv_obj_add_flag(pager->warning, LV_OBJ_FLAG_HIDDEN);
- }
- }
- lv_obj_set_y(pager->label, label_y);
- lv_label_set_text(pager->label, page_text);
- lv_obj_set_height(pager->label, LV_SIZE_CONTENT);
- lv_obj_update_layout(pager->viewport);
- lv_obj_scroll_to_y(pager->viewport, 0, LV_ANIM_OFF);
- lv_label_set_text_fmt(pager->page_label, "%u / %u",
- (unsigned)(pager->page + 1),
- (unsigned)pager->page_count);
- if (pager->page == 0) {
- lv_obj_add_state(pager->prev, LV_STATE_DISABLED);
- } else {
- lv_obj_clear_state(pager->prev, LV_STATE_DISABLED);
- }
- if (pager->page + 1 >= pager->page_count) {
- lv_obj_add_state(pager->next, LV_STATE_DISABLED);
- } else {
- lv_obj_clear_state(pager->next, LV_STATE_DISABLED);
- }
-}
-
-static void SolMessagePagerEvent(lv_event_t *event)
-{
- SolMessagePager_t *pager = lv_event_get_user_data(event);
- lv_obj_t *target = lv_event_get_target(event);
- if (target == pager->prev && pager->page > 0) {
- pager->page--;
- } else if (target == pager->next && pager->page + 1 < pager->page_count) {
- pager->page++;
- } else {
- return;
- }
- pager->offset = SolMessagePageOffset(pager, pager->page);
- SolMessagePagerRefresh(pager);
-}
-
-static void SolMessagePagerDelete(lv_event_t *event)
-{
- SolMessagePager_t *pager = lv_event_get_user_data(event);
- SRAM_FREE(pager);
-}
-
-static lv_obj_t *SolMessagePagerButton(lv_obj_t *parent, const char *text)
-{
- lv_obj_t *button = lv_btn_create(parent);
- lv_obj_set_size(button, 72, 48);
- lv_obj_set_style_radius(button, 12, LV_PART_MAIN);
- lv_obj_set_style_bg_color(button, WHITE_COLOR, LV_PART_MAIN);
- lv_obj_set_style_bg_opa(button, 30, LV_PART_MAIN);
- lv_obj_t *label = lv_label_create(button);
- lv_obj_set_style_text_font(label, g_defIllustrateFont, LV_PART_MAIN);
- lv_obj_set_style_text_color(label, WHITE_COLOR, LV_PART_MAIN);
- lv_label_set_text(label, text);
- lv_obj_center(label);
- return button;
-}
-
-static lv_obj_t *SolMessageRiskWarning(lv_obj_t *parent)
-{
- lv_obj_t *warning = lv_obj_create(parent);
- lv_obj_set_width(warning, 360);
- lv_obj_set_height(warning, LV_SIZE_CONTENT);
- lv_obj_set_style_pad_all(warning, 16, LV_PART_MAIN);
- lv_obj_set_style_pad_row(warning, 8, LV_PART_MAIN);
- lv_obj_set_style_border_width(warning, 0, LV_PART_MAIN);
- lv_obj_set_style_radius(warning, 8, LV_PART_MAIN);
- lv_obj_set_style_bg_color(warning, lv_color_hex(0xF55831), LV_PART_MAIN);
- lv_obj_set_style_bg_opa(warning, 48, LV_PART_MAIN);
- lv_obj_set_flex_flow(warning, LV_FLEX_FLOW_COLUMN);
-
- lv_obj_t *title = GuiCreateTextLabel(warning, _("solana_blind_sign_title"));
- lv_obj_set_width(title, 328);
- lv_obj_set_style_text_color(title, lv_color_hex(0xF55831), LV_PART_MAIN);
-
- lv_obj_t *content = GuiCreateIllustrateLabel(
- warning, _("solana_unparsed_message_warning"));
- lv_obj_set_width(content, 328);
- lv_label_set_long_mode(content, LV_LABEL_LONG_WRAP);
- lv_obj_set_style_text_color(content, WHITE_COLOR, LV_PART_MAIN);
- return warning;
-}
-
void GuiShowSolMessagePaged(lv_obj_t *parent, void *param, bool raw)
{
DisplaySolanaMessage *message = (DisplaySolanaMessage *)param;
const char *text = raw ? message->raw_message : message->utf8_message;
- if (text == NULL) {
- text = "";
- }
-
- SolMessagePager_t *pager = SRAM_MALLOC(sizeof(SolMessagePager_t));
- memset(pager, 0, sizeof(SolMessagePager_t));
- pager->text = text;
- pager->suffix = "";
- pager->text_len = strlen(pager->text);
- pager->suffix_len = strlen(pager->suffix);
- pager->utf8 = !raw;
-
- size_t offset = 0;
- do {
- pager->page_count++;
- offset = SolMessagePageEnd(pager, offset);
- } while (offset < SolMessagePagerLength(pager));
-
- lv_obj_clear_flag(parent, LV_OBJ_FLAG_SCROLLABLE);
- lv_obj_update_layout(parent);
- lv_coord_t viewport_height = lv_obj_get_height(parent) - 64;
- pager->viewport = lv_obj_create(parent);
- lv_obj_set_pos(pager->viewport, 0, 0);
- lv_obj_set_size(pager->viewport, 360, viewport_height);
- lv_obj_set_style_pad_all(pager->viewport, 0, LV_PART_MAIN);
- lv_obj_set_style_border_width(pager->viewport, 0, LV_PART_MAIN);
- lv_obj_set_style_bg_opa(pager->viewport, 0, LV_PART_MAIN);
- lv_obj_set_scrollbar_mode(pager->viewport, LV_SCROLLBAR_MODE_OFF);
- lv_obj_set_scroll_dir(pager->viewport, LV_DIR_VER);
- lv_obj_add_flag(pager->viewport, LV_OBJ_FLAG_SCROLLABLE);
-
- if (raw) {
- pager->warning = SolMessageRiskWarning(pager->viewport);
- lv_obj_set_pos(pager->warning, 0, 0);
- }
-
- pager->label = lv_label_create(pager->viewport);
- lv_obj_set_pos(pager->label, 0, 0);
- lv_obj_set_width(pager->label, 360);
- lv_obj_set_height(pager->label, LV_SIZE_CONTENT);
- lv_obj_set_style_text_font(pager->label, g_defIllustrateFont, LV_PART_MAIN);
- lv_obj_set_style_text_color(pager->label, WHITE_COLOR, LV_PART_MAIN);
- lv_label_set_long_mode(pager->label, LV_LABEL_LONG_WRAP);
-
- pager->prev = SolMessagePagerButton(parent, "<");
- lv_obj_align(pager->prev, LV_ALIGN_BOTTOM_LEFT, 0, 0);
- pager->next = SolMessagePagerButton(parent, ">");
- lv_obj_align(pager->next, LV_ALIGN_BOTTOM_RIGHT, 0, 0);
- pager->page_label = lv_label_create(parent);
- lv_obj_set_style_text_font(pager->page_label, g_defIllustrateFont, LV_PART_MAIN);
- lv_obj_set_style_text_color(pager->page_label, WHITE_COLOR, LV_PART_MAIN);
- lv_obj_align(pager->page_label, LV_ALIGN_BOTTOM_MID, 0, -9);
-
- lv_obj_add_event_cb(pager->prev, SolMessagePagerEvent, LV_EVENT_CLICKED, pager);
- lv_obj_add_event_cb(pager->next, SolMessagePagerEvent, LV_EVENT_CLICKED, pager);
- lv_obj_add_event_cb(parent, SolMessagePagerDelete, LV_EVENT_DELETE, pager);
- SolMessagePagerRefresh(pager);
+ GuiShowPagedMessageText(
+ parent,
+ text,
+ !raw,
+ raw ? _("solana_blind_sign_title") : NULL,
+ raw ? _("solana_unparsed_message_warning") : NULL);
}
static void SetContainerDefaultStyle(lv_obj_t *container)
@@ -771,7 +551,7 @@ static lv_obj_t * GuiShowSplTokenInfoOverviewCard(lv_obj_t *parent, PtrT_Display
}
return container;
}
-static void GuiShowJupiterV6SwapOverview(lv_obj_t *parent, PtrT_DisplaySolanaTxOverview overviewData)
+static lv_obj_t *GuiShowJupiterV6SwapOverview(lv_obj_t *parent, PtrT_DisplaySolanaTxOverview overviewData)
{
lv_obj_t *swapOverviewContainer = GuiCreateAutoHeightContainer(parent, SOL_COMPONENT_WIDTH, 16);
SetContainerDefaultStyle(swapOverviewContainer);
@@ -909,26 +689,35 @@ static void GuiShowJupiterV6SwapOverview(lv_obj_t *parent, PtrT_DisplaySolanaTxO
lv_obj_align_to(partnerReferralFeeValueLabel, partnerReferralFeeLabel, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 8);
// platform container align to swap container
lv_obj_align_to(platformOverviewContainer, swapOverviewContainer, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 16);
+ return platformOverviewContainer;
}
static void GuiShowSplTokenTransferOverview(lv_obj_t *parent, PtrT_DisplaySolanaTxOverview overviewData)
{
- lv_obj_t *tokenInfoCard = GuiShowSplTokenInfoOverviewCard(parent, overviewData);
PtrT_DisplaySolanaTxSplTokenTransferOverview splTokenTransfer = overviewData->spl_token_transfer;
- lv_obj_t *lastInfoCard = tokenInfoCard;
- if (strcmp(splTokenTransfer->token_name, "Unknown") == 0) {
- lv_obj_t *noticeCard = GuiCreateSolNoticeCard(parent);
- lv_obj_align_to(tokenInfoCard, noticeCard, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 16);
+ lv_obj_t *lastInfoCard = NULL;
+ // Plain SPL Token `Transfer` instructions do not contain mint metadata.
+ // In that case keep the original Amount/Account review card and its three
+ // account explanations, but do not render an empty token-info card.
+ if (strlen(splTokenTransfer->token_mint_account) > 0) {
+ lv_obj_t *tokenInfoCard = GuiShowSplTokenInfoOverviewCard(parent, overviewData);
lastInfoCard = tokenInfoCard;
- }
- if (splTokenTransfer->unusual_decimals) {
- lv_obj_t *warningCard =
- GuiCreateUnusualDecimalsWarningCard(parent, splTokenTransfer->decimals);
- lv_obj_align_to(warningCard, lastInfoCard, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 16);
- lastInfoCard = warningCard;
+ if (strcmp(splTokenTransfer->token_name, "Unknown") == 0) {
+ lv_obj_t *noticeCard = GuiCreateSolNoticeCard(parent);
+ lv_obj_align_to(tokenInfoCard, noticeCard, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 16);
+ lastInfoCard = tokenInfoCard;
+ }
+ if (splTokenTransfer->unusual_decimals) {
+ lv_obj_t *warningCard =
+ GuiCreateUnusualDecimalsWarningCard(parent, splTokenTransfer->decimals);
+ lv_obj_align_to(warningCard, lastInfoCard, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 16);
+ lastInfoCard = warningCard;
+ }
}
lv_obj_t *container = GuiCreateAutoHeightContainer(parent, SOL_COMPONENT_WIDTH, 16);
SetContainerDefaultStyle(container);
- lv_obj_align_to(container, lastInfoCard, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 16);
+ if (lastInfoCard != NULL) {
+ lv_obj_align_to(container, lastInfoCard, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 16);
+ }
lv_obj_t *label = lv_label_create(container);
@@ -1096,15 +885,21 @@ static void GuiShowSolTxVoteOverview(lv_obj_t *parent, PtrT_DisplaySolanaTxOverv
lv_obj_align(label, LV_ALIGN_TOP_LEFT, 24, 54);
}
-static void GuiShowSolTxGeneralOverview(lv_obj_t *parent, PtrT_DisplaySolanaTxOverview overviewData)
+static lv_obj_t *GuiShowSolTxGeneralOverview(
+ lv_obj_t *parent,
+ PtrT_DisplaySolanaTxOverview overviewData,
+ lv_obj_t *lastView)
{
PtrT_VecFFI_DisplaySolanaTxOverviewGeneral general = overviewData->general;
- lv_obj_t *lastView = NULL;
+
+ if (general == NULL) {
+ return lastView;
+ }
for (int i = 0; i < general->size; i++) {
char *program = general->data[i].program;
char order[BUFFER_SIZE_16] = {0};
- snprintf_s(order, BUFFER_SIZE_16, "#%d", i + 1);
+ snprintf_s(order, BUFFER_SIZE_16, "#%u", (unsigned int)general->data[i].instruction_index);
const char *method = strlen(general->data[i].method) > 0
? general->data[i].method
: "Unknown";
@@ -1161,6 +956,30 @@ static void GuiShowSolTxGeneralOverview(lv_obj_t *parent, PtrT_DisplaySolanaTxOv
parent, _("Destination"), general->data[i].destination, lastView, SOL_COMPONENT_WIDTH);
}
}
+ return lastView;
+}
+
+/*
+ * Specialized overviews own their layout, so they do not all expose the
+ * final card they created. Find the bottom-most direct child and use it as
+ * the anchor for generic sibling instructions. This keeps the specialized
+ * overview intact and appends addons after it instead of replacing it.
+ */
+static lv_obj_t *GuiGetSolTxBottomView(lv_obj_t *parent)
+{
+ lv_obj_update_layout(parent);
+ lv_obj_t *bottomView = NULL;
+ int32_t bottom = 0;
+ uint32_t childCount = lv_obj_get_child_cnt(parent);
+ for (uint32_t i = 0; i < childCount; i++) {
+ lv_obj_t *child = lv_obj_get_child(parent, i);
+ int32_t childBottom = lv_obj_get_y(child) + lv_obj_get_height(child);
+ if (bottomView == NULL || childBottom > bottom) {
+ bottomView = child;
+ bottom = childBottom;
+ }
+ }
+ return bottomView;
}
static void GuiShowSolTxAdditionalUnknownPrograms(
@@ -1172,16 +991,22 @@ static void GuiShowSolTxAdditionalUnknownPrograms(
return;
}
- int32_t contentChildCount = lv_obj_get_child_cnt(parent);
lv_obj_t *warningCard = GuiCreateWarningCard(parent);
lv_obj_align(warningCard, LV_ALIGN_TOP_LEFT, 0, 0);
lv_obj_update_layout(warningCard);
int32_t contentOffset = lv_obj_get_height(warningCard) + 16;
lv_obj_t *lastView = warningCard;
int32_t lastBottom = lv_obj_get_height(warningCard);
- for (int32_t i = 0; i < contentChildCount; i++) {
+ int32_t childCount = lv_obj_get_child_cnt(parent);
+ for (int32_t i = 0; i < childCount; i++) {
lv_obj_t *child = lv_obj_get_child(parent, i);
+ // LVGL's child order is an implementation detail. Identify the new
+ // warning by pointer so every pre-existing overview card is shifted
+ // exactly once, regardless of where the warning appears in the list.
+ if (child == warningCard) {
+ continue;
+ }
lv_obj_set_y(child, lv_obj_get_y(child) + contentOffset);
int32_t childBottom = lv_obj_get_y(child) + lv_obj_get_height(child);
if (childBottom > lastBottom) {
@@ -1626,34 +1451,37 @@ void GuiShowSolTxOverview(lv_obj_t *parent, void *totalData)
} else if (0 == strcmp(overviewData->display_type, "Vote")) {
GuiShowSolTxVoteOverview(parent, overviewData);
} else if (0 == strcmp(overviewData->display_type, "General")) {
- GuiShowSolTxGeneralOverview(parent, overviewData);
+ GuiShowSolTxGeneralOverview(parent, overviewData, NULL);
} else if (0 == strcmp(overviewData->display_type, "squads_multisig_create")) {
GuiShowSolTxMultiSigCreateOverview(parent, overviewData);
} else if (0 == strcmp(overviewData->display_type, "TokenTransfer")) {
GuiShowSplTokenTransferOverview(parent, overviewData);
} else if (0 == strcmp(overviewData->display_type, "squads_proposal")) {
GuiShowSolTxSquadsProposalOverview(parent, overviewData);
} else if (0 == strcmp(overviewData->display_type, "jupiterv6_swap")) {
- // todo add jupiterv6 swap overview
- GuiShowJupiterV6SwapOverview(parent, overviewData);
+ lv_obj_t *lastView = GuiShowJupiterV6SwapOverview(parent, overviewData);
+ GuiShowSolTxGeneralOverview(parent, overviewData, lastView);
} else {
GuiShowSolTxInstructionsOverview(parent, overviewData);
return;
}
+
+ /*
+ * Transfer, token-transfer, vote and Squads pages retain their original
+ * purpose-built cards. Any sibling instructions prepared by the parser
+ * are appended below those cards as generic addons.
+ */
+ if (0 != strcmp(overviewData->display_type, "General") &&
+ 0 != strcmp(overviewData->display_type, "jupiterv6_swap")) {
+ GuiShowSolTxGeneralOverview(parent, overviewData, GuiGetSolTxBottomView(parent));
+ }
GuiShowSolTxAdditionalUnknownPrograms(parent, overviewData);
+ lv_obj_update_layout(parent);
+ lv_obj_scroll_to_y(parent, 0, LV_ANIM_OFF);
}
-void GuiShowSolTxDetail(lv_obj_t *parent, void *totalData)
+static void GuiShowSolTxRawDetailCard(lv_obj_t *parent, PtrString txDetail, lv_obj_t *lastView)
{
- lv_obj_set_size(parent, 408, LV_SIZE_CONTENT);
- lv_obj_clear_flag(parent, LV_OBJ_FLAG_SCROLLABLE);
- lv_obj_clear_flag(parent, LV_OBJ_FLAG_CLICKABLE);
- DisplaySolanaTx *txData = (DisplaySolanaTx*)totalData;
- PtrT_DisplaySolanaTxOverview overviewData = txData->overview;
- if (0 == strcmp(overviewData->display_type, "squads_multisig_create")) {
- GuiShowSolTxMultiSigCreateDetail(parent, overviewData);
- return ;
- }
lv_obj_t *cont = lv_obj_create(parent);
lv_obj_set_size(cont, SOL_COMPONENT_WIDTH, 444);
lv_obj_set_style_border_width(cont, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
@@ -1671,16 +1499,43 @@ void GuiShowSolTxDetail(lv_obj_t *parent, void *totalData)
lv_obj_add_flag(cont, LV_OBJ_FLAG_CLICKABLE);
lv_obj_set_scrollbar_mode(cont, LV_SCROLLBAR_MODE_OFF);
- PtrString txDetail = txData->detail;
-
lv_obj_t *label = lv_label_create(cont);
- cJSON *root = cJSON_Parse((const char *)txDetail);
- char *retStr = cJSON_PrintBuffered(root, BUFFER_SIZE_1024, false);
- lv_label_set_text(label, retStr);
+ const char *rawDetail = txDetail == NULL ? "" : txDetail;
+ cJSON *root = 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);
cJSON_Delete(root);
lv_label_set_long_mode(label, LV_LABEL_LONG_WRAP);
lv_obj_set_width(label, SOL_COMPONENT_CONTENT_WIDTH);
SetTitleLabelStyle(label);
lv_obj_align(label, LV_ALIGN_TOP_LEFT, 0, 0);
+ if (lastView == NULL) {
+ lv_obj_align(cont, LV_ALIGN_TOP_LEFT, 0, 0);
+ } else {
+ lv_obj_align_to(cont, lastView, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 16);
+ }
+}
+
+void GuiShowSolTxDetail(lv_obj_t *parent, void *totalData)
+{
+ lv_obj_set_size(parent, 408, LV_SIZE_CONTENT);
+ lv_obj_clear_flag(parent, LV_OBJ_FLAG_SCROLLABLE);
+ lv_obj_clear_flag(parent, LV_OBJ_FLAG_CLICKABLE);
+ DisplaySolanaTx *txData = (DisplaySolanaTx*)totalData;
+ PtrT_DisplaySolanaTxOverview overviewData = txData->overview;
+ if (0 == strcmp(overviewData->display_type, "squads_multisig_create")) {
+ // The specialized page contains multiple cards and a raw-detail card;
+ // keep a fixed viewport so the complete page can be scrolled.
+ lv_obj_set_height(parent, 444);
+ lv_obj_add_flag(parent, LV_OBJ_FLAG_SCROLLABLE);
+ lv_obj_set_scrollbar_mode(parent, LV_SCROLLBAR_MODE_OFF);
+ GuiShowSolTxMultiSigCreateDetail(parent, overviewData);
+ lv_obj_update_layout(parent);
+ GuiShowSolTxRawDetailCard(parent, txData->detail, GuiGetSolTxBottomView(parent));
+ lv_obj_update_layout(parent);
+ lv_obj_scroll_to_y(parent, 0, LV_ANIM_OFF);
+ return;
+ }
+ GuiShowSolTxRawDetailCard(parent, txData->detail, NULL);
}
### src/ui/gui_components/gui_status_bar.c
@@ -753,9 +753,16 @@ void SetNavBarMidBtn(NavBarWidget_t *navBarWidget, NVS_MID_BUTTON_ENUM button,
void SetCoinWallet(NavBarWidget_t *navBarWidget, GuiChainCoinType index,
const char *name)
{
+#ifdef WEB3_VERSION
+ if (index == CHAIN_UNKNOWN) {
+ SetMidBtnLabel(navBarWidget, NVS_BAR_MID_LABEL,
+ (name != NULL) ? name : _("confirm_transaction"));
+ return;
+ }
+#endif
SetNavBarMidBtn(navBarWidget, NVS_BAR_MID_COIN, NULL, NULL);
CoinWalletInfo_t *coin = (CoinWalletInfo_t *)g_coinWalletBtn;
- for (size_t i = 0; i < CHAIN_BUTT; i++) {
+ for (size_t i = 0; i < NUMBER_OF_ARRAYS(g_coinWalletBtn); i++) {
if (g_coinWalletBtn[i].index == index) {
coin = &g_coinWalletBtn[i];
break;
### src/ui/gui_widgets/multi/web3/gui_connect_wallet_widgets.c
@@ -853,6 +853,9 @@ static void GuiCreateBtcWalletTutorialWidget(lv_obj_t *parent)
tutorial = GuiCreateWalletTutorialWidget(parent, BULL_WALLET_TITLE, BULL_WALLET_LINK);
lv_obj_align(tutorial, LV_ALIGN_TOP_MID, 0, 630);
+
+ tutorial = GuiCreateWalletTutorialWidget(parent, "Lace", "https://keyst.one/t/3rd/lace");
+ lv_obj_align(tutorial, LV_ALIGN_TOP_MID, 0, 756);
}
static void GuiCreateQrCodeWidget(lv_obj_t *parent)
### src/ui/lv_i18n/data.csv
@@ -928,7 +928,7 @@ Wallet Profile,24,wallet_profile_mid_btn,Wallet Profile,Профиль коше
,20,solana_squads_amount_desc,"This amount is the deploy fee, consisting of three parts. For details, please refer to the Squads Official Website.","Эта сумма является комиссией за развертывание и состоит из трех частей. Для получения подробной информации, пожалуйста, обратитесь к официальному сайту Squads.","이 금액은 배포 수수료로 세 부분으로 구성되어 있습니다. 자세한 내용은 Squads 공식 웹사이트를 참조하십시오.","该金额是部署费用,由三部分组成。详情请参考 Squads 官方网站。","Este monto es la tarifa de despliegue, que consta de tres partes. Para más detalles, por favor consulte el sitio web oficial de Squads.","Dieser Betrag ist die Bereitstellungsgebühr und besteht aus drei Teilen. Für weitere Details besuchen Sie bitte die offizielle Website von Squads.",この金額はデプロイ料金であり、3つの部分で構成されています。詳細については、Squads公式ウェブサイトを参照してください。
,20,solana_squads_amount_brief,"The amount consists of three parts, you can see it in """Details","Сумма состоит из трех частей, которые вы можете увидеть в разделе ""Детали"".","금액은 세 부분으로 구성되며, ""상세 정보""에서 확인하실 수 있습니다.",该金额由三部分组成,您可以在"详情"中查看。,"El monto consta de tres partes, puedes verlo en ""Detalles"".","Der Betrag besteht aus drei Teilen, die Sie unter ""Details"" einsehen können.",金額は3つの部分で構成されており、"詳細"でご確認いただけます。
,20,solana_squads_amount_lm,"The amount consists of three parts, you can see it in """Leran More","Сумма состоит из трех частей, вы можете увидеть это в разделе ""Подробнее"".","금액은 세 부분으로 구성되며, ""자세히 알아보기""에서 확인하실 수 있습니다.",该金额由三部分组成,您可以在"了解更多"中查看。,"El monto consta de tres partes, puedes verlo en ""Más información"".","Der Betrag besteht aus drei Teilen, die Sie unter ""Mehr erfahren"" einsehen können.",金額は3つの部分で構成されており、"詳しく見る"でご確認いただけます。
-,20,solana_warning,"This transaction may be risky, please check the details carefully before signing.","Эта транзакция может быть рискованной, пожалуйста, внимательно проверьте детали перед подписанием.","이 거래는 위험할 수 있으므로, 서명하기 전에 세부 사항을 주의 깊게 확인하시기 바랍니다.",此交易可能存在风险,请在签署前仔细检查详情。,"Esta transacción puede ser riesgosa, por favor revise cuidadosamente los detalles antes de firmar.","Diese Transaktion könnte riskant sein. Bitte überprüfen Sie die Details sorgfältig, bevor Sie unterschreiben.",この取引はリスクを伴う可能性があります。署名する前に詳細を注意深く確認してください。
+,20,solana_warning,"This transaction contains an unparsed program. Please review it carefully.","Эта транзакция содержит программу, которую не удалось разобрать. Внимательно проверьте её.","이 거래에는 분석할 수 없는 프로그램이 포함되어 있습니다. 내용을 주의 깊게 확인하세요.","这笔交易存在无法解析的 Program,请仔细甄别。","Esta transacción contiene un programa que no se puede analizar. Revíselo detenidamente.","Diese Transaktion enthält ein nicht analysierbares Programm. Bitte prüfen Sie es sorgfältig.","この取引には解析できないプログラムが含まれています。内容を慎重に確認してください。"
,20,receive_ton_hint,"Keystone default address format is V4R2. If they don’t match Tonkeeper, please check and switch in Tonkeeper. This address is exclusively for TON transactions only. Sending other types of digital assets to this address will result in their loss.","Формат адреса по умолчанию для Keystone - V4R2. Если они не совпадают с Tonkeeper, пожалуйста, проверьте и переключите в Tonkeeper. Этот адрес используется исключительно для транзакций TON. Отправка других типов цифровых активов на этот адрес приведет к их потере.","Keystone 기본 주소 형식은 V4R2입니다. Tonkeeper와 일치하지 않는 경우 Tonkeeper에서 확인하고 전환하십시오. 이 주소는 TON 거래에만 사용됩니다. 이 주소로 다른 종류의 디지털 자산을 보내면 손실됩니다.","Keystone 默认地址格式为 V4R2。如果它们与 Tonkeeper 不匹配,请在 Tonkeeper 中检查并切换。此地址仅用于 TON 交易。将其他类型的数字资产发送到此地址将导致其丢失。","El formato de dirección predeterminado de Keystone es V4R2. Si no coinciden con Tonkeeper, por favor verifique y cambie en Tonkeeper. Esta dirección es exclusivamente para transacciones de TON. Enviar otros tipos de activos digitales a esta dirección resultará en su pérdida.","Das Standardadressenformat von Keystone ist V4R2. Wenn sie nicht mit Tonkeeper übereinstimmen, überprüfen und wechseln Sie bitte in Tonkeeper. Diese Adresse ist ausschließlich für TON-Transaktionen. Das Senden anderer Arten von digitalen Assets an diese Adresse führt zu deren Verlust.","Keystoneのデフォルトアドレス形式はV4R2です。Tonkeeperと一致しない場合は、Tonkeeperで確認して切り替えてください。このアドレスはTON取引専用です。このアドレスに他の種類のデジタル資産を送ると、紛失することになります。"
,20,connect_thorwallet_title,THORWallet,THORWallet,THORWallet,THORWallet,THORWallet,THORWallet,THORWallet
,20,connect_thorwallet_link,https://keyst.one/t/3rd/thorwallet,https://keyst.one/t/3rd/thorwallet,https://keyst.one/t/3rd/thorwallet,https://keyst.one/t/3rd/thorwallet,https://keyst.one/t/3rd/thorwallet,https://keyst.one/t/3rd/thorwallet,https://keyst.one/t/3rd/thorwallet
@@ -1072,6 +1072,7 @@ Wallet Profile,24,wallet_profile_mid_btn,Wallet Profile,Профиль коше
,20,btc_check_input_value_hint,"Please confirm each input value is correct.","Пожалуйста, проверьте корректность суммы каждого входа.","각 입력 금액이 올바른지 확인해 주세요.","请确认每个输入金额都正确。","Confirma que el valor de cada entrada sea correcto.","Bitte bestätigen Sie, dass jeder Eingabewert korrekt ist.","各入力金額が正しいことを確認してください。"
,20,btc_sighash_single_warning,"Only the corresponding output is locked. Other outputs may be changed after signing.","Зафиксирован только соответствующий выход. Другие выходы могут быть изменены после подписания.","해당 출력만 고정됩니다. 서명 후 다른 출력은 변경될 수 있습니다.","只有对应的输出被锁定。签名后其他输出仍可能被更改。","Solo la salida correspondiente está bloqueada. Las otras salidas pueden cambiarse después de firmar.","Nur der entsprechende Output ist festgelegt. Andere Outputs können nach dem Signieren geändert werden.","対応する出力のみが固定されます。署名後に他の出力が変更される可能性があります。"
,20,btc_sighash_none_warning,"Outputs are not locked and this signature may be reused with different outputs.","Выходы не зафиксированы и эта подпись может быть повторно использована с другими выходами.","출력이 고정되지 않으며 이 서명은 다른 출력과 함께 재사용될 수 있습니다.","输出未被锁定,此签名可能被复用于不同的输出。","Las salidas no están bloqueadas y esta firma puede reutilizarse con salidas diferentes.","Outputs sind nicht festgelegt und diese Signatur kann mit anderen Outputs wiederverwendet werden.","出力は固定されておらず、この署名は別の出力で再利用される可能性があります。"
-,20,btc_large_fee_warning,"The transaction fee is unusually high. Verify it before signing.","Комиссия транзакции необычно высока. Проверьте её перед подписанием.","거래 수수료가 비정상적으로 높습니다. 서명하기 전에 확인하세요.","交易手续费异常高,请在签名前仔细核对。","La comisión de la transacción es inusualmente alta. Verifíquela antes de firmar.","Die Transaktionsgebühr ist ungewöhnlich hoch. Prüfen Sie sie vor dem Signieren.","取引手数料が異常に高額です。署名前に確認してください。"
+,20,utxo_large_fee_warning,"The transaction fee is unusually high. Verify it before signing.","Комиссия транзакции необычно высока. Проверьте её перед подписанием.","거래 수수료가 비정상적으로 높습니다. 서명하기 전에 확인하세요.","交易手续费异常高,请在签名前仔细核对。","La comisión de la transacción es inusualmente alta. Verifíquela antes de firmar.","Die Transaktionsgebühr ist ungewöhnlich hoch. Prüfen Sie sie vor dem Signieren.","取引手数料が異常に高額です。署名前に確認してください。"
,24,solana_blind_sign_title,"Blind Sign","Слепая подпись","블라인드 서명","盲签","Firma ciega","Blindes Signieren","ブラインド署名"
,20,solana_unparsed_message_warning,"This message could not be parsed. Its effect cannot be verified. Review all data before signing.","Это сообщение не удалось разобрать. Его действие невозможно проверить. Проверьте все данные перед подписанием.","이 메시지를 해석할 수 없어 영향을 확인할 수 없습니다. 서명하기 전에 모든 데이터를 검토하세요.","此消息无法解析,设备无法验证其影响。请在签名前核对全部数据。","Este mensaje no se pudo analizar y no se puede verificar su efecto. Revise todos los datos antes de firmar.","Diese Nachricht konnte nicht analysiert werden. Ihre Wirkung lässt sich nicht prüfen. Kontrollieren Sie vor dem Signieren alle Daten.","このメッセージは解析できず、その影響を確認できません。署名前にすべてのデータを確認してください。"
+,20,eth_replay_protection_warning,"This legacy transaction has no chain ID. Its signature can be replayed on other EVM networks. Verify the destination and amount carefully.","Эта устаревшая транзакция не содержит идентификатора сети. Её подпись может быть повторно использована в других сетях EVM. Внимательно проверьте адрес и сумму.","이 레거시 거래에는 체인 ID가 없습니다. 서명이 다른 EVM 네트워크에서 재사용될 수 있으므로 수신 주소와 금액을 주의 깊게 확인하세요.","此传统交易不包含链 ID,签名可能在其他 EVM 网络上被重放。请仔细核对收款地址和金额。","Esta transacción heredada no tiene ID de cadena. Su firma puede reutilizarse en otras redes EVM. Verifique cuidadosamente el destino y el importe.","Diese Legacy-Transaktion enthält keine Chain-ID. Ihre Signatur kann in anderen EVM-Netzwerken wiederverwendet werden. Prüfen Sie Zieladresse und Betrag sorgfältig.","このレガシートランザクションにはチェーンIDがありません。署名が他のEVMネットワークで再利用される可能性があります。送信先と金額を慎重に確認してください。"
### src/ui/lv_i18n/lv_i18n.c
@@ -164,6 +164,7 @@ const static lv_i18n_phrase_t en_singulars[] = {
{"boot_version_not_match_title", "Security Notification"},
{"btc_avalanche_notice", "This transaction uses an Avalanche BTC address. Verify all details carefully to avoid errors or asset loss."},
{"btc_check_input_value_hint", "Please confirm each input value is correct."},
+ {"utxo_large_fee_warning", "The transaction fee is unusually high. Verify it before signing."},
{"btc_sighash_none_warning", "Outputs are not locked and this signature may be reused with different outputs."},
{"btc_sighash_single_warning", "Only the corresponding output is locked. Other outputs may be changed after signing."},
{"calculat_modal_title", "Calculating"},
@@ -376,6 +377,7 @@ const static lv_i18n_phrase_t en_singulars[] = {
{"enable_passphrase", "Enable Passphrase"},
{"enter_passcode", "Enter Passcode"},
{"enter_system", "Enter System"},
+ {"eth_replay_protection_warning", "This legacy transaction has no chain ID. Its signature can be replayed on other EVM networks. Verify the destination and amount carefully."},
{"error_box_duplicated_seed_phrase", "Duplicate Seed Phrase"},
{"error_box_duplicated_seed_phrase_desc", "This phrase you typed is already used in a wallet account, please import another set of seed phrase."},
{"error_box_firmware_not_detected", "Firmware Not Detected"},
@@ -805,7 +807,7 @@ const static lv_i18n_phrase_t en_singulars[] = {
{"solana_squads_amount_lm", "The amount consists of three parts, you can see it in \"Leran More\""},
{"solana_squads_amount_title", "Amount Details"},
{"solana_unparsed_message_warning", "This message could not be parsed. Its effect cannot be verified. Review all data before signing."},
- {"solana_warning", "This transaction may be risky, please check the details carefully before signing."},
+ {"solana_warning", "This transaction contains an unparsed program. Please review it carefully."},
{"spl_notice", "Unknown SPL Token account. Please verify to prevent asset loss."},
{"support_link", "support@keyst.one"},
{"swap_token_approve_hint", "This transaction may invovle a swap and token approve operation. Please review details carefully."},
@@ -1126,6 +1128,7 @@ const static lv_i18n_phrase_t de_singulars[] = {
{"boot_version_not_match_title", "Sicherheitsbenachrichtigung"},
{"btc_avalanche_notice", "Diese Transaktion verwendet eine Avalanche BTC-Adresse. Überprüfen Sie alle Details sorgfältig, um Fehler oder Vermögensverluste zu vermeiden."},
{"btc_check_input_value_hint", "Bitte bestätigen Sie, dass jeder Eingabewert korrekt ist."},
+ {"utxo_large_fee_warning", "Die Transaktionsgebühr ist ungewöhnlich hoch. Prüfen Sie sie vor dem Signieren."},
{"btc_sighash_none_warning", "Outputs sind nicht festgelegt und diese Signatur kann mit anderen Outputs wiederverwendet werden."},
{"btc_sighash_single_warning", "Nur der entsprechende Output ist festgelegt. Andere Outputs können nach dem Signieren geändert werden."},
{"calculat_modal_title", "Berechnung"},
@@ -1338,6 +1341,7 @@ const static lv_i18n_phrase_t de_singulars[] = {
{"enable_passphrase", "Passphrase aktivieren"},
{"enter_passcode", "Zugangscode eingeben"},
{"enter_system", "Systeme eingeben"},
+ {"eth_replay_protection_warning", "Diese Legacy-Transaktion enthält keine Chain-ID. Ihre Signatur kann in anderen EVM-Netzwerken wiederverwendet werden. Prüfen Sie Zieladresse und Betrag sorgfältig."},
{"error_box_duplicated_seed_phrase", "Doppelte Wiederherstellungsphrase"},
{"error_box_duplicated_seed_phrase_desc", "Diese von Ihnen eingegebene Phrase wird bereits in einem Wallet-Konto verwendet. Bitte importieren Sie eine andere Satzfolge von Seed-Phrasen."},
{"error_box_firmware_not_detected", "Firmware nicht erkannt"},
@@ -1767,7 +1771,7 @@ const static lv_i18n_phrase_t de_singulars[] = {
{"solana_squads_amount_lm", "Der Betrag besteht aus drei Teilen, die Sie unter \"Mehr erfahren\" einsehen können."},
{"solana_squads_amount_title", "Betragsdetails"},
{"solana_unparsed_message_warning", "Diese Nachricht konnte nicht analysiert werden. Ihre Wirkung lässt sich nicht prüfen. Kontrollieren Sie vor dem Signieren alle Daten."},
- {"solana_warning", "Diese Transaktion könnte riskant sein. Bitte überprüfen Sie die Details sorgfältig, bevor Sie unterschreiben."},
+ {"solana_warning", "Diese Transaktion enthält ein nicht analysierbares Programm. Bitte prüfen Sie es sorgfältig."},
{"spl_notice", "Unbekanntes SPL-Token-Konto. Bitte überprüfen Sie es, um Vermögensverluste zu vermeiden."},
{"support_link", "support@keyst.one"},
{"swap_token_approve_hint", "Diese Transaktion kann einen Token-Swap und einen Token-Approval-Vorgang umfassen. Bitte überprüfen Sie alle Details sorgfältig."},
@@ -2088,6 +2092,7 @@ const static lv_i18n_phrase_t es_singulars[] = {
{"boot_version_not_match_title", "Aviso de seguridad"},
{"btc_avalanche_notice", "Esta transacción utiliza una dirección BTC de Avalanche. Verifique cuidadosamente todos los detalles para evitar errores o pérdida de activos."},
{"btc_check_input_value_hint", "Confirma que el valor de cada entrada sea correcto."},
+ {"utxo_large_fee_warning", "La comisión de la transacción es inusualmente alta. Verifíquela antes de firmar."},
{"btc_sighash_none_warning", "Las salidas no están bloqueadas y esta firma puede reutilizarse con salidas diferentes."},
{"btc_sighash_single_warning", "Solo la salida correspondiente está bloqueada. Las otras salidas pueden cambiarse después de firmar."},
{"calculat_modal_title", "Calculando"},
@@ -2300,6 +2305,7 @@ const static lv_i18n_phrase_t es_singulars[] = {
{"enable_passphrase", "Habilitar frase de contraseña"},
{"enter_passcode", "Ingresar código de acceso"},
{"enter_system", "Entrar al sistema"},
+ {"eth_replay_protection_warning", "Esta transacción heredada no tiene ID de cadena. Su firma puede reutilizarse en otras redes EVM. Verifique cuidadosamente el destino y el importe."},
{"error_box_duplicated_seed_phrase", "Frase de Semilla Duplicada"},
{"error_box_duplicated_seed_phrase_desc", "Esta frase que escribiste ya se utiliza en una cuenta de billetera, por favor importa otro conjunto de frase semilla."},
{"error_box_firmware_not_detected", "Firmware no detectado"},
@@ -2729,7 +2735,7 @@ const static lv_i18n_phrase_t es_singulars[] = {
{"solana_squads_amount_lm", "El monto consta de tres partes, puedes verlo en \"Más información\"."},
{"solana_squads_amount_title", "Detalles del monto"},
{"solana_unparsed_message_warning", "Este mensaje no se pudo analizar y no se puede verificar su efecto. Revise todos los datos antes de firmar."},
- {"solana_warning", "Esta transacción puede ser riesgosa, por favor revise cuidadosamente los detalles antes de firmar."},
+ {"solana_warning", "Esta transacción contiene un programa que no se puede analizar. Revíselo detenidamente."},
{"spl_notice", "Cuenta de token SPL desconocida. Por favor, verifica para evitar la pérdida de activos."},
{"support_link", "support@keyst.one"},
{"swap_token_approve_hint", "Esta transacción puede incluir una operación de intercambio y aprobación de tokens. Revise cuidadosamente todos los detalles."},
@@ -3047,6 +3053,7 @@ const static lv_i18n_phrase_t ja_singulars[] = {
{"boot_version_not_match_title", "セキュリティ通知"},
{"btc_avalanche_notice", "このトランザクションはAvalanche BTCアドレスを使用しています。エラーや資産の損失を避けるため、すべての詳細を慎重に確認してください。"},
{"btc_check_input_value_hint", "各入力金額が正しいことを確認してください。"},
+ {"utxo_large_fee_warning", "取引手数料が異常に高額です。署名前に確認してください。"},
{"btc_sighash_none_warning", "出力は固定されておらず、この署名は別の出力で再利用される可能性があります。"},
{"btc_sighash_single_warning", "対応する出力のみが固定されます。署名後に他の出力が変更される可能性があります。"},
{"calculat_modal_title", "計算しています"},
@@ -3259,6 +3266,7 @@ const static lv_i18n_phrase_t ja_singulars[] = {
{"enable_passphrase", "パスフレーズを有効にする"},
{"enter_passcode", "パスコード入力"},
{"enter_system", "システムに入る"},
+ {"eth_replay_protection_warning", "このレガシートランザクションにはチェーンIDがありません。署名が他のEVMネットワークで再利用される可能性があります。送信先と金額を慎重に確認してください。"},
{"error_box_duplicated_seed_phrase", "重複したシードフレーズ"},
{"error_box_duplicated_seed_phrase_desc", "このフレーズはすでにウォレットアカウントで使用されています.別のシードフレーズをインポートしてください."},
{"error_box_firmware_not_detected", "ファームウェアが検出されませんでした."},
@@ -3688,7 +3696,7 @@ const static lv_i18n_phrase_t ja_singulars[] = {
{"solana_squads_amount_lm", "金額は3つの部分で構成されており、\"詳しく見る\"でご確認いただけます。"},
{"solana_squads_amount_title", "金額の詳細"},
{"solana_unparsed_message_warning", "このメッセージは解析できず、その影響を確認できません。署名前にすべてのデータを確認してください。"},
- {"solana_warning", "この取引はリスクを伴う可能性があります。署名する前に詳細を注意深く確認してください。"},
+ {"solana_warning", "この取引には解析できないプログラムが含まれています。内容を慎重に確認してください。"},
{"spl_notice", "不明なSPLトークンアカウントです。資産の損失を防ぐために確認してください。"},
{"support_link", "support@keyst.one"},
{"swap_token_approve_hint", "このトランザクションはトークン交換とトークン承認操作を含む場合があります。すべての詳細を注意深く確認してください。"},
@@ -4004,6 +4012,7 @@ const static lv_i18n_phrase_t ko_singulars[] = {
{"boot_version_not_match_title", "보안 알림"},
{"btc_avalanche_notice", "이 트랜잭션은 Avalanche BTC 주소를 사용합니다. 오류나 자산 손실을 방지하기 위해 모든 세부 사항을 주의 깊게 확인하십시오."},
{"btc_check_input_value_hint", "각 입력 금액이 올바른지 확인해 주세요."},
+ {"utxo_large_fee_warning", "거래 수수료가 비정상적으로 높습니다. 서명하기 전에 확인하세요."},
{"btc_sighash_none_warning", "출력이 고정되지 않으며 이 서명은 다른 출력과 함께 재사용될 수 있습니다."},
{"btc_sighash_single_warning", "해당 출력만 고정됩니다. 서명 후 다른 출력은 변경될 수 있습니다."},
{"calculat_modal_title", "계산중 "},
@@ -4216,6 +4225,7 @@ const static lv_i18n_phrase_t ko_singulars[] = {
{"enable_passphrase", "패스프레이즈 활성화"},
{"enter_passcode", "비밀번호 입력"},
{"enter_system", "시스템 진입"},
+ {"eth_replay_protection_warning", "이 레거시 거래에는 체인 ID가 없습니다. 서명이 다른 EVM 네트워크에서 재사용될 수 있으므로 수신 주소와 금액을 주의 깊게 확인하세요."},
{"error_box_duplicated_seed_phrase", "중복된 시드 구문"},
{"error_box_duplicated_seed_phrase_desc", "입력하신 문구가 이미 지갑 계정에서 사용되고 있습니다. 다른 시드 문구 세트를 가져오십시오."},
{"error_box_firmware_not_detected", "펌웨어를 찾을 수 없습니다. "},
@@ -4645,7 +4655,7 @@ const static lv_i18n_phrase_t ko_singulars[] = {
{"solana_squads_amount_lm", "금액은 세 부분으로 구성되며, \"자세히 알아보기\"에서 확인하실 수 있습니다."},
{"solana_squads_amount_title", "금액 세부 정보"},
{"solana_unparsed_message_warning", "이 메시지를 해석할 수 없어 영향을 확인할 수 없습니다. 서명하기 전에 모든 데이터를 검토하세요."},
- {"solana_warning", "이 거래는 위험할 수 있으므로, 서명하기 전에 세부 사항을 주의 깊게 확인하시기 바랍니다."},
+ {"solana_warning", "이 거래에는 분석할 수 없는 프로그램이 포함되어 있습니다. 내용을 주의 깊게 확인하세요."},
{"spl_notice", "알 수 없는 SPL 토큰 계정입니다. 자산 손실을 방지하기 위해 확인해 주세요."},
{"support_link", "support@keyst.one"},
{"swap_token_approve_hint", "이 거래는 스왑 및 토큰 승인 작업을 포함할 수 있습니다. 모든 세부 사항을 주의 깊게 검토하세요."},
@@ -5169,6 +5179,7 @@ const static lv_i18n_phrase_t pl_singulars[] = {
{"enable_passphrase", "Włącz hasło"},
{"enter_passcode", "Wprowadź hasło"},
{"enter_system", "Wejdź do Systemu"},
+ {"eth_replay_protection_warning", "This legacy transaction has no chain ID. Its signature can be replayed on other EVM networks. Verify the destination and amount carefully."},
{"error_box_duplicated_seed_phrase", "Zduplikowana fraza źródłowa"},
{"error_box_duplicated_seed_phrase_desc", "Wpisana fraza jest już używana na koncie portfela. Zaimportuj inny zestaw fraz początkowych."},
{"error_box_firmware_not_detected", "Nie wykryto oprogramowania sprzętowego"},
@@ -5596,7 +5607,7 @@ const static lv_i18n_phrase_t pl_singulars[] = {
{"solana_squads_amount_lm", "Kwota składa się z trzech części, można ją zobaczyć w „Leran More”"},
{"solana_squads_amount_title", "Szczegóły kwoty"},
{"solana_unparsed_message_warning", "This message could not be parsed. Its effect cannot be verified. Review all data before signing."},
- {"solana_warning", "Ta transakcja może być ryzykowna, proszę dokładnie sprawdzić szczegóły przed podpisaniem."},
+ {"solana_warning", "Ta transakcja zawiera program, którego nie można przeanalizować. Sprawdź go uważnie."},
{"spl_notice", "Nieznane konto tokenu SPL. Zweryfikuj, aby zapobiec utracie zasobów."},
{"support_link", "support@keyst.one"},
{"swap_token_approve_hint", "Transakcja ta może obejmować operację wymiany i zatwierdzenia tokenu. Prosimy o dokładne zapoznanie się ze szczegółami."},
@@ -5925,6 +5936,7 @@ const static lv_i18n_phrase_t ru_singulars[] = {
{"boot_version_not_match_title", "Безопасность уведомление"},
{"btc_avalanche_notice", "Эта транзакция использует адрес Avalanche BTC. Внимательно проверьте все детали, чтобы избежать ошибок или потери активов."},
{"btc_check_input_value_hint", "Пожалуйста, проверьте корректность суммы каждого входа."},
+ {"utxo_large_fee_warning", "Комиссия транзакции необычно высока. Проверьте её перед подписанием."},
{"btc_sighash_none_warning", "Выходы не зафиксированы и эта подпись может быть повторно использована с другими выходами."},
{"btc_sighash_single_warning", "Зафиксирован только соответствующий выход. Другие выходы могут быть изменены после подписания."},
{"calculat_modal_title", "Расчет"},
@@ -6137,6 +6149,7 @@ const static lv_i18n_phrase_t ru_singulars[] = {
{"enable_passphrase", "Включить пароль"},
{"enter_passcode", "Введите код-пароль"},
{"enter_system", "Вход в систему"},
+ {"eth_replay_protection_warning", "Эта устаревшая транзакция не содержит идентификатора сети. Её подпись может быть повторно использована в других сетях EVM. Внимательно проверьте адрес и сумму."},
{"error_box_duplicated_seed_phrase", "Дубликат сид фразы"},
{"error_box_duplicated_seed_phrase_desc", "Введенная вами сид фраза уже используется в этом устройстве. Импортируйте другую фразу."},
{"error_box_firmware_not_detected", "Прошивка не найдена"},
@@ -6566,7 +6579,7 @@ const static lv_i18n_phrase_t ru_singulars[] = {
{"solana_squads_amount_lm", "Сумма состоит из трех частей, вы можете увидеть это в разделе \"Подробнее\"."},
{"solana_squads_amount_title", "Детали суммы"},
{"solana_unparsed_message_warning", "Это сообщение не удалось разобрать. Его действие невозможно проверить. Проверьте все данные перед подписанием."},
- {"solana_warning", "Эта транзакция может быть рискованной, пожалуйста, внимательно проверьте детали перед подписанием."},
+ {"solana_warning", "Эта транзакция содержит программу, которую не удалось разобрать. Внимательно проверьте её."},
{"spl_notice", "Неизвестный аккаунт SPL токена. Пожалуйста, проверьте, чтобы предотвратить потерю активов."},
{"support_link", "support@keyst.one"},
{"swap_token_approve_hint", "Эта транзакция может включать обмен и операцию утверждения токена. Пожалуйста, тщательно проверьте все детали."},
@@ -6890,6 +6903,7 @@ const static lv_i18n_phrase_t zh_cn_singulars[] = {
{"boot_version_not_match_title", "安全通知"},
{"btc_avalanche_notice", "此交易使用Avalanche BTC地址。请仔细验证所有详细信息,以避免错误或资产损失。"},
{"btc_check_input_value_hint", "请确认每个输入金额都正确。"},
+ {"utxo_large_fee_warning", "交易手续费异常高,请在签名前仔细核对。"},
{"btc_sighash_none_warning", "输出未被锁定,此签名可能被复用于不同的输出。"},
{"btc_sighash_single_warning", "只有对应的输出被锁定。签名后其他输出仍可能被更改。"},
{"calculat_modal_title", "计算中..."},
@@ -7102,6 +7116,7 @@ const static lv_i18n_phrase_t zh_cn_singulars[] = {
{"enable_passphrase", "启用密码短语"},
{"enter_passcode", "输入密码"},
{"enter_system", "进入系统"},
+ {"eth_replay_protection_warning", "此传统交易不包含链 ID,签名可能在其他 EVM 网络上被重放。请仔细核对收款地址和金额。"},
{"error_box_duplicated_seed_phrase", "重复的助记词"},
{"error_box_duplicated_seed_phrase_desc", "该助记词已存在于设备上,请导入另一组助记词."},
{"error_box_firmware_not_detected", "未检测到固件"},
@@ -7531,7 +7546,7 @@ const static lv_i18n_phrase_t zh_cn_singulars[] = {
{"solana_squads_amount_lm", "该金额由三部分组成,您可以在\"了解更多\"中查看。"},
{"solana_squads_amount_title", "金额详情"},
{"solana_unparsed_message_warning", "此消息无法解析,设备无法验证其影响。请在签名前核对全部数据。"},
- {"solana_warning", "此交易可能存在风险,请在签署前仔细检查详情。"},
+ {"solana_warning", "这笔交易存在无法解析的 Program,请仔细甄别。"},
{"spl_notice", "未知的 SPL 代币账户。请验证以防止资产损失。"},
{"support_link", "support@keyst.one"},
{"swap_token_approve_hint", "此交易可能涉及代币交换和代币批准操作。请仔细检查所有细节。"},
### src/utils/log/log_print.c
@@ -173,6 +173,7 @@ void LogRustPanic(char* panic_info)
#else
#include "draw_on_lcd.h"
+#include "mhscpu.h"
#include "presetting.h"
#include "version.h"
#include "hardware_version.h"
@@ -181,6 +182,7 @@ LV_FONT_DECLARE(openSans_20);
void LogRustPanic(char* panic_info)
{
+ NVIC_SystemReset();
PrintOnLcd(&openSans_20, 0xFFFF, "The error was caused by a failed data request.\nYour assets remain safe.\n");
PrintErrorInfoOnLcd();
uint32_t c = 0x666666;
### src/utils/user_utils.h
@@ -2,6 +2,7 @@
#define _USER_UTILS_H
#include <ctype.h>
+#include <stdio.h>
#include "stdint.h"
#include "stdbool.h"
#include "string.h"
### 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 76/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.