What changed, and why it matters
This commit refactors how TON (The Open Network) transactions are displayed on the Keystone 3 hardware wallet. Previously, the code assumed every transaction had exactly one message. Now it supports multiple messages, showing each one separately on screen. The change also fixes a small bug where a proof-result pointer was incorrectly cleared, and adds a safety check when parsing contract data JSON. There is no direct evidence in the commit of a security vulnerability being fixed, but supporting multi-message transactions is a meaningful hardening improvement because a malicious or unusual transaction with several messages could previously have been misrepresented or mishandled.
Review whether the previous single-message design could have caused a user to approve a transaction containing hidden or unexpected additional messages. Consider adding explicit tests for multi-message jetton/NFT transfers and for malformed contract_data JSON. Verify that the C UI correctly frees the new VecFFI message array and that the pointer-clearing macro fix is applied consistently elsewhere.
Security signals we found
Multi-message TON transaction support added, reducing risk of UI misrepresentation when more than one transfer is present
Single-message assumption removed from transaction parsing and display
Pointer-clearing bug fixed in TON proof cleanup macro (`g_proofParseResult` vs `g_parseResult`)
NULL check added after `cJSON_Parse` of contract_data before dereferencing
Default simulator feature changed from cypherpunk to multi-coins (build/config change, not a runtime security signal)
Evidence from the diff
The patch restructures the Rust TON transaction model: TonTransaction now contains a Vec<TonMessage> instead of embedding single-message fields directly. A new TonMessage struct carries to, amount, action, comment, data_view, and contract_data. The TryFrom<&SigningMessage> implementation now iterates over all messages and converts each to a TonMessage. The C FFI layer (rust_c/src/ton/structs.rs) is updated to expose DisplayTonMessage and a VecFFI<DisplayTonMessage> inside DisplayTonTransaction. The UI code (gui_ton.c) loops over messages and renders a separate overview/details block per message, adding a title when there is more than one. It also fixes a macro that was setting g_parseResult instead of g_proofParseResult, and adds a NULL check after cJSON_Parse for contract_data. A new unit test builds a two-message BOC fixture and verifies parsing. The default Cargo feature is switched from simulator-cypherpunk to simulator-multi-coins.
Changed components
rust/apps/ton/src/structs.rsrust/apps/ton/src/transaction.rsrust/rust_c/src/ton/structs.rssrc/ui/gui_chain/multi/web3/gui_ton.crust/rust_c/Cargo.tomlInspect captured patch +302 / −117
diff --git a/rust/apps/ton/src/structs.rs b/rust/apps/ton/src/structs.rs
index 9f34429..3195d77 100644
--- a/rust/apps/ton/src/structs.rs
+++ b/rust/apps/ton/src/structs.rs
@@ -3,27 +3,35 @@ use crate::jettons;
use crate::messages::jetton::JettonMessage;
use crate::messages::nft::NFTMessage;
use crate::messages::traits::ParseCell;
-use crate::messages::{Operation, SigningMessage};
+use crate::messages::{Operation, SigningMessage, TransferMessage};
use crate::utils::shorten_string;
use crate::vendor::address::TonAddress;
use crate::vendor::cell::BagOfCells;
-use alloc::string::{String, ToString};
+use alloc::{
+ string::{String, ToString},
+ vec::Vec,
+};
use hex;
use serde::Serialize;
use serde_json::{self, json, Value};
#[derive(Debug, Clone, Serialize, Default)]
-pub struct TonTransaction {
+pub struct TonMessage {
pub to: String,
pub amount: String,
pub action: String,
pub comment: Option<String>,
pub data_view: Option<String>,
- pub raw_data: String,
pub contract_data: Option<String>,
}
+#[derive(Debug, Clone, Serialize, Default)]
+pub struct TonTransaction {
+ pub raw_data: String,
+ pub messages: Vec<TonMessage>,
+}
+
impl TonTransaction {
pub fn parse(boc: BagOfCells) -> Result<Self> {
let root = boc.single_root()?;
@@ -45,19 +53,13 @@ impl TonTransaction {
}
}
-impl TryFrom<&SigningMessage> for TonTransaction {
+impl TryFrom<&TransferMessage> for TonMessage {
type Error = TonError;
- fn try_from(signing_message: &SigningMessage) -> Result<Self> {
- if signing_message.messages.is_empty() {
- return Err(TonError::InvalidTransaction(
- "transaction does not contain transfer info".to_string(),
- ));
- };
- let message = signing_message.messages[0].clone();
+ fn try_from(message: &TransferMessage) -> Result<Self> {
let to = message.dest_addr.clone();
let amount = message.value.clone();
- match message.data {
+ match &message.data {
None => Ok(Self {
to,
amount,
@@ -66,12 +68,12 @@ impl TryFrom<&SigningMessage> for TonTransaction {
}),
Some(data) => {
let action = data.action.clone().unwrap_or("Ton Transfer".to_string());
- match data.operation {
+ match &data.operation {
Operation::Comment(comment) => Ok(Self {
to,
amount,
action,
- comment: Some(comment),
+ comment: Some(comment.clone()),
..Default::default()
}),
Operation::JettonMessage(jetton_message) => match jetton_message {
@@ -100,9 +102,6 @@ impl TryFrom<&SigningMessage> for TonTransaction {
..Default::default()
})
}
- _ => Err(TonError::InvalidTransaction(
- "invalid jetton message".to_string(),
- )),
},
Operation::NFTMessage(nft_message) => match nft_message {
NFTMessage::NFTTransferMessage(nft_transfer_message) => {
@@ -125,9 +124,6 @@ impl TryFrom<&SigningMessage> for TonTransaction {
..Default::default()
})
}
- _ => Err(TonError::InvalidTransaction(
- "invalid nft message".to_string(),
- )),
},
Operation::OtherMessage(_other_message) => Ok(Self {
to,
@@ -141,6 +137,27 @@ impl TryFrom<&SigningMessage> for TonTransaction {
}
}
+impl TryFrom<&SigningMessage> for TonTransaction {
+ type Error = TonError;
+
+ fn try_from(signing_message: &SigningMessage) -> Result<Self> {
+ if signing_message.messages.is_empty() {
+ return Err(TonError::InvalidTransaction(
+ "transaction does not contain transfer info".to_string(),
+ ));
+ };
+ let messages = signing_message
+ .messages
+ .iter()
+ .map(TonMessage::try_from)
+ .collect::<Result<Vec<_>>>()?;
+ Ok(Self {
+ messages,
+ ..Default::default()
+ })
+ }
+}
+
#[derive(Debug, Clone, Serialize, Default)]
pub struct TonProof {
pub domain: String,
@@ -218,8 +235,74 @@ mod tests {
extern crate std;
use alloc::vec;
use base64::{engine::general_purpose::STANDARD, Engine};
+ use num_bigint::BigUint;
use std::println;
+ #[test]
+ fn test_build_multi_message_fixture() {
+ use crate::vendor::cell::CellBuilder;
+
+ fn build_transfer_message(
+ dest: &TonAddress,
+ value: u64,
+ data: Option<crate::vendor::cell::Cell>,
+ ) -> crate::vendor::cell::Cell {
+ let mut builder = CellBuilder::new();
+ builder.store_bit(false).unwrap();
+ builder.store_bit(true).unwrap();
+ builder.store_bit(true).unwrap();
+ builder.store_bit(false).unwrap();
+ builder.store_address(&TonAddress::NULL).unwrap();
+ builder.store_address(dest).unwrap();
+ builder.store_coins(&BigUint::from(value)).unwrap();
+ builder.store_bit(false).unwrap();
+ builder.store_coins(&BigUint::from(0u64)).unwrap();
+ builder.store_coins(&BigUint::from(0u64)).unwrap();
+ builder.store_u64(64, 0).unwrap();
+ builder.store_u32(32, 0).unwrap();
+ builder.store_bit(false).unwrap();
+ builder.store_bit(data.is_some()).unwrap();
+ if let Some(data) = data {
+ builder.store_child(data).unwrap();
+ }
+ builder.build().unwrap()
+ }
+
+ let address1 = TonAddress::new(0, &[0x11; 32]);
+ let address2 = TonAddress::new(0, &[0x22; 32]);
+
+ let message1 = build_transfer_message(&address1, 100_000_000, None);
+
+ let mut comment = CellBuilder::new();
+ comment.store_u32(32, 0).unwrap();
+ comment.store_string("multi message fixture").unwrap();
+ let comment = comment.build().unwrap();
+ let message2 = build_transfer_message(&address2, 250_000_000, Some(comment));
+
+ let mut root = CellBuilder::new();
+ root.store_u32(32, 0x29a9a317).unwrap();
+ root.store_u32(32, 0x66778899).unwrap();
+ root.store_u32(32, 1).unwrap();
+ root.store_u8(8, 3).unwrap();
+ root.store_u8(8, 0).unwrap();
+ root.store_child(message1).unwrap();
+ root.store_child(message2).unwrap();
+
+ let boc = BagOfCells::from_root(root.build().unwrap());
+ let serial = boc.serialize(true).unwrap();
+ let tx = TonTransaction::parse_hex(&serial).unwrap();
+
+ println!("multi message fixture body={}", STANDARD.encode(&serial));
+ println!("multi message fixture hex={}", hex::encode(&serial));
+ assert_eq!(tx.messages.len(), 2);
+ assert_eq!(tx.messages[0].amount, "0.1 Ton");
+ assert_eq!(tx.messages[1].amount, "0.25 Ton");
+ assert_eq!(
+ tx.messages[1].comment.as_deref(),
+ Some("multi message fixture")
+ );
+ }
+
#[test]
fn test_parse_simple_ton_transfer() {
// Simple TON transfer without comment
@@ -228,12 +311,14 @@ mod tests {
let tx = TonTransaction::parse_hex(&serial).unwrap();
- assert_eq!(tx.action, "Ton Transfer");
- assert!(tx.comment.is_none());
- assert!(tx.data_view.is_none());
- assert!(tx.contract_data.is_none());
- assert!(!tx.to.is_empty());
- assert!(!tx.amount.is_empty());
+ assert_eq!(tx.messages.len(), 1);
+ let message = &tx.messages[0];
+ assert_eq!(message.action, "Ton Transfer");
+ assert!(message.comment.is_none());
+ assert!(message.data_view.is_none());
+ assert!(message.contract_data.is_none());
+ assert!(!message.to.is_empty());
+ assert!(!message.amount.is_empty());
assert!(!tx.raw_data.is_empty());
}
@@ -245,12 +330,14 @@ mod tests {
let tx = TonTransaction::parse_hex(&serial).unwrap();
- assert_eq!(tx.action, "Ton Transfer");
- assert!(tx.comment.is_some());
- let comment = tx.comment.as_ref().unwrap();
+ assert_eq!(tx.messages.len(), 1);
+ let message = &tx.messages[0];
+ assert_eq!(message.action, "Ton Transfer");
+ assert!(message.comment.is_some());
+ let comment = message.comment.as_ref().unwrap();
assert!(comment.contains("Keystone"));
- assert!(!tx.to.is_empty());
- assert!(!tx.amount.is_empty());
+ assert!(!message.to.is_empty());
+ assert!(!message.amount.is_empty());
}
#[test]
@@ -261,13 +348,15 @@ mod tests {
let tx = TonTransaction::parse_hex(&serial).unwrap();
- assert_eq!(tx.action, "Jetton Transfer");
- assert!(tx.data_view.is_some());
- assert!(tx.contract_data.is_some());
- assert!(!tx.to.is_empty());
+ assert_eq!(tx.messages.len(), 1);
+ let message = &tx.messages[0];
+ assert_eq!(message.action, "Jetton Transfer");
+ assert!(message.data_view.is_some());
+ assert!(message.contract_data.is_some());
+ assert!(!message.to.is_empty());
// Contract data should contain Jetton Wallet Address
- let contract_data = tx.contract_data.as_ref().unwrap();
+ let contract_data = message.contract_data.as_ref().unwrap();
assert!(contract_data.contains("Jetton Wallet Address"));
}
@@ -279,9 +368,10 @@ mod tests {
let tx = TonTransaction::parse(boc).unwrap();
- assert_eq!(tx.action, "Ton Transfer");
- assert!(!tx.to.is_empty());
- assert!(!tx.amount.is_empty());
+ assert_eq!(tx.messages.len(), 1);
+ assert_eq!(tx.messages[0].action, "Ton Transfer");
+ assert!(!tx.messages[0].to.is_empty());
+ assert!(!tx.messages[0].amount.is_empty());
}
#[test]
@@ -293,9 +383,11 @@ mod tests {
let json = tx.to_json().unwrap();
assert!(json.is_object());
- assert!(json.get("to").is_some());
- assert!(json.get("amount").is_some());
- assert!(json.get("action").is_some());
+ assert!(json.get("raw_data").is_some());
+ assert!(json.get("messages").is_some());
+ let messages = json.get("messages").unwrap().as_array().unwrap();
+ assert_eq!(messages.len(), 1);
+ assert_eq!(messages[0].get("action").unwrap(), "Ton Transfer");
}
#[test]
@@ -383,13 +475,8 @@ mod tests {
fn test_ton_transaction_default() {
let tx = TonTransaction::default();
- assert_eq!(tx.to, "");
- assert_eq!(tx.amount, "");
- assert_eq!(tx.action, "");
- assert!(tx.comment.is_none());
- assert!(tx.data_view.is_none());
assert_eq!(tx.raw_data, "");
- assert!(tx.contract_data.is_none());
+ assert!(tx.messages.is_empty());
}
#[test]
@@ -410,9 +497,10 @@ mod tests {
let tx1 = TonTransaction::parse_hex(&serial).unwrap();
let tx2 = tx1.clone();
- assert_eq!(tx1.to, tx2.to);
- assert_eq!(tx1.amount, tx2.amount);
- assert_eq!(tx1.action, tx2.action);
+ assert_eq!(tx1.messages.len(), tx2.messages.len());
+ assert_eq!(tx1.messages[0].to, tx2.messages[0].to);
+ assert_eq!(tx1.messages[0].amount, tx2.messages[0].amount);
+ assert_eq!(tx1.messages[0].action, tx2.messages[0].action);
}
#[test]
diff --git a/rust/apps/ton/src/transaction.rs b/rust/apps/ton/src/transaction.rs
index 3f3fe1a..011253f 100644
--- a/rust/apps/ton/src/transaction.rs
+++ b/rust/apps/ton/src/transaction.rs
@@ -80,12 +80,17 @@ mod tests {
let serial = STANDARD.decode(body).unwrap();
let tx = parse_transaction(&serial).unwrap();
- assert_eq!(tx.to, "UQBWLOZeOAv7adGnTx-iG7vefsDP5iN8pyYncUiUoJqbRdx9");
- assert_eq!(tx.amount, "1 Ton");
- assert_eq!(tx.action, "Ton Transfer");
- assert!(tx.comment.is_none());
- assert!(tx.data_view.is_none());
- assert!(tx.contract_data.is_none());
+ assert_eq!(tx.messages.len(), 1);
+ let message = &tx.messages[0];
+ assert_eq!(
+ message.to,
+ "UQBWLOZeOAv7adGnTx-iG7vefsDP5iN8pyYncUiUoJqbRdx9"
+ );
+ assert_eq!(message.amount, "1 Ton");
+ assert_eq!(message.action, "Ton Transfer");
+ assert!(message.comment.is_none());
+ assert!(message.data_view.is_none());
+ assert!(message.contract_data.is_none());
let tx_json = tx.to_json().unwrap();
let tx_json_str = tx_json.to_string();
@@ -122,21 +127,26 @@ mod tests {
let tx = parse_transaction(&serial).unwrap();
// true destination UQBWLOZeOAv7adGnTx-iG7vefsDP5iN8pyYncUiUoJqbRdx9
- assert_eq!(tx.to, "UQBWLOZeOAv7adGnTx-iG7vefsDP5iN8pyYncUiUoJqbRdx9");
- assert_eq!(tx.amount, "10000000 Unit");
- assert_eq!(tx.action, "Jetton Transfer");
- assert!(tx.comment.is_none());
+ assert_eq!(tx.messages.len(), 1);
+ let message = &tx.messages[0];
+ assert_eq!(
+ message.to,
+ "UQBWLOZeOAv7adGnTx-iG7vefsDP5iN8pyYncUiUoJqbRdx9"
+ );
+ assert_eq!(message.amount, "10000000 Unit");
+ assert_eq!(message.action, "Jetton Transfer");
+ assert!(message.comment.is_none());
- assert!(tx.data_view.is_some());
- let data_view = tx.data_view.unwrap();
+ assert!(message.data_view.is_some());
+ let data_view = message.data_view.as_ref().unwrap();
assert!(data_view.contains("\"amount\":\"10000000\""));
assert!(data_view
.contains("\"destination\":\"UQBWLOZeOAv7adGnTx-iG7vefsDP5iN8pyYncUiUoJqbRdx9\""));
assert!(data_view.contains("\"forward_ton_amount\":\"1\""));
// transaction to: EQASODeyhIBbcGlrLvpUJiYjOHRwAZHCBGf1HV5tjKvZVsJb
- assert!(tx.contract_data.is_some());
- let contract_data = tx.contract_data.unwrap();
+ assert!(message.contract_data.is_some());
+ let contract_data = message.contract_data.as_ref().unwrap();
assert!(contract_data.contains("Jetton Wallet Address"));
assert!(contract_data.contains("EQASODeyhIBbcGlrLvpUJiYjOHRwAZHCBGf1HV5tjKvZVsJb"));
}
@@ -147,12 +157,17 @@ mod tests {
let serial = hex::decode(serial).unwrap();
let tx = parse_transaction(&serial).unwrap();
- assert_eq!(tx.to, "UQAmsPmeM4c2OUo4U-BKdZMaB3HLmfjJh1heQ4s4Qd0Wy7Nc");
- assert_eq!(tx.amount, "0.001 Ton");
- assert_eq!(tx.action, "Ton Transfer");
- assert!(tx.comment.is_some());
+ assert_eq!(tx.messages.len(), 1);
+ let message = &tx.messages[0];
+ assert_eq!(
+ message.to,
+ "UQAmsPmeM4c2OUo4U-BKdZMaB3HLmfjJh1heQ4s4Qd0Wy7Nc"
+ );
+ assert_eq!(message.amount, "0.001 Ton");
+ assert_eq!(message.action, "Ton Transfer");
+ assert!(message.comment.is_some());
- let comment = tx.comment.unwrap();
+ let comment = message.comment.as_ref().unwrap();
assert!(comment.contains("Keystone hardware wallet"));
assert!(comment.contains("3 PCI security chips"));
assert!(comment.contains("Bitcoin and other crypto assets offline"));
diff --git a/rust/rust_c/Cargo.toml b/rust/rust_c/Cargo.toml
index 4d56b97..a2008a3 100644
--- a/rust/rust_c/Cargo.toml
+++ b/rust/rust_c/Cargo.toml
@@ -134,7 +134,7 @@ simulator-multi-coins = ["simulator", "multi-coins"]
simulator-btc-only = ["simulator", "btc-only"]
simulator-cypherpunk = ["simulator", "cypherpunk"]
# make IDE happy
-default = ["simulator-cypherpunk"]
+default = ["simulator-multi-coins"]
[dev-dependencies]
keystore = { path = "../keystore" }
diff --git a/rust/rust_c/src/ton/structs.rs b/rust/rust_c/src/ton/structs.rs
index 0b66579..0f985fc 100644
--- a/rust/rust_c/src/ton/structs.rs
+++ b/rust/rust_c/src/ton/structs.rs
@@ -1,41 +1,52 @@
use core::ptr::null_mut;
use crate::common::{
+ ffi::VecFFI,
free::Free,
structs::TransactionParseResult,
types::{PtrString, PtrT},
utils::convert_c_char,
};
use crate::{check_and_free_ptr, free_str_ptr, impl_c_ptr, make_free_method};
-use app_ton::structs::{TonProof, TonTransaction};
+use alloc::vec::Vec;
+use app_ton::structs::{TonMessage, TonProof, TonTransaction};
#[repr(C)]
-pub struct DisplayTonTransaction {
+pub struct DisplayTonMessage {
amount: PtrString,
action: PtrString,
to: PtrString,
comment: PtrString,
data_view: PtrString,
- raw_data: PtrString,
contract_data: PtrString,
}
+#[repr(C)]
+pub struct DisplayTonTransaction {
+ raw_data: PtrString,
+ messages: PtrT<VecFFI<DisplayTonMessage>>,
+}
+
+impl_c_ptr!(DisplayTonMessage);
impl_c_ptr!(DisplayTonTransaction);
-impl From<&TonTransaction> for DisplayTonTransaction {
- fn from(tx: &TonTransaction) -> Self {
- DisplayTonTransaction {
- amount: convert_c_char(tx.amount.clone()),
- action: convert_c_char(tx.action.clone()),
- to: convert_c_char(tx.to.clone()),
- comment: tx.comment.clone().map(convert_c_char).unwrap_or(null_mut()),
- data_view: tx
+impl From<&TonMessage> for DisplayTonMessage {
+ fn from(message: &TonMessage) -> Self {
+ DisplayTonMessage {
+ amount: convert_c_char(message.amount.clone()),
+ action: convert_c_char(message.action.clone()),
+ to: convert_c_char(message.to.clone()),
+ comment: message
+ .comment
+ .clone()
+ .map(convert_c_char)
+ .unwrap_or(null_mut()),
+ data_view: message
.data_view
.clone()
.map(convert_c_char)
.unwrap_or(null_mut()),
- raw_data: convert_c_char(tx.raw_data.clone()),
- contract_data: tx
+ contract_data: message
.contract_data
.clone()
.map(convert_c_char)
@@ -44,18 +55,39 @@ impl From<&TonTransaction> for DisplayTonTransaction {
}
}
-impl Free for DisplayTonTransaction {
+impl From<&TonTransaction> for DisplayTonTransaction {
+ fn from(tx: &TonTransaction) -> Self {
+ DisplayTonTransaction {
+ raw_data: convert_c_char(tx.raw_data.clone()),
+ messages: VecFFI::from(
+ tx.messages
+ .iter()
+ .map(DisplayTonMessage::from)
+ .collect::<Vec<_>>(),
+ )
+ .c_ptr(),
+ }
+ }
+}
+
+impl Free for DisplayTonMessage {
unsafe fn free(&self) {
free_str_ptr!(self.amount);
free_str_ptr!(self.action);
free_str_ptr!(self.to);
free_str_ptr!(self.comment);
free_str_ptr!(self.data_view);
- free_str_ptr!(self.raw_data);
free_str_ptr!(self.contract_data);
}
}
+impl Free for DisplayTonTransaction {
+ unsafe fn free(&self) {
+ free_str_ptr!(self.raw_data);
+ check_and_free_ptr!(self.messages);
+ }
+}
+
make_free_method!(TransactionParseResult<DisplayTonTransaction>);
#[repr(C)]
diff --git a/src/ui/gui_chain/multi/web3/gui_ton.c b/src/ui/gui_chain/multi/web3/gui_ton.c
index b58445c..cef6523 100644
--- a/src/ui/gui_chain/multi/web3/gui_ton.c
+++ b/src/ui/gui_chain/multi/web3/gui_ton.c
@@ -16,7 +16,7 @@
if (result != NULL) \
{ \
free_TransactionParseResult_DisplayTonProof(g_proofParseResult); \
- g_parseResult = NULL; \
+ g_proofParseResult = NULL; \
}
static URParseResult *g_urResult = NULL;
@@ -26,12 +26,13 @@ static void *g_proofParseResult = NULL;
static bool g_isMulti = false;
static ViewType g_viewType = ViewTypeUnKnown;
-static lv_obj_t *CreateOverviewAmountView(lv_obj_t *parent, DisplayTonTransaction *data, lv_obj_t *lastView);
-static lv_obj_t *CreateOverviewActionView(lv_obj_t *parent, DisplayTonTransaction *data, lv_obj_t *lastView);
-static lv_obj_t *CreateOverviewDestinationView(lv_obj_t *parent, DisplayTonTransaction *data, lv_obj_t *lastView);
-static lv_obj_t *CreateOverviewCommentView(lv_obj_t *parent, DisplayTonTransaction *data, lv_obj_t *lastView);
-static lv_obj_t *CreateOverviewContractDataView(lv_obj_t *parent, DisplayTonTransaction *data, lv_obj_t *lastView);
-static lv_obj_t *CreateDetailsDataViewView(lv_obj_t *parent, DisplayTonTransaction *data, lv_obj_t *lastView);
+static lv_obj_t *CreateOverviewMessageTitleView(lv_obj_t *parent, size_t index, lv_obj_t *lastView);
+static lv_obj_t *CreateOverviewAmountView(lv_obj_t *parent, DisplayTonMessage *data, lv_obj_t *lastView);
+static lv_obj_t *CreateOverviewActionView(lv_obj_t *parent, DisplayTonMessage *data, lv_obj_t *lastView);
+static lv_obj_t *CreateOverviewDestinationView(lv_obj_t *parent, DisplayTonMessage *data, lv_obj_t *lastView);
+static lv_obj_t *CreateOverviewCommentView(lv_obj_t *parent, DisplayTonMessage *data, lv_obj_t *lastView);
+static lv_obj_t *CreateOverviewContractDataView(lv_obj_t *parent, DisplayTonMessage *data, lv_obj_t *lastView);
+static lv_obj_t *CreateDetailsDataViewView(lv_obj_t *parent, DisplayTonMessage *data, size_t index, bool showTitle, lv_obj_t *lastView);
static lv_obj_t *CreateDetailsRawDataView(lv_obj_t *parent, DisplayTonTransaction *data, lv_obj_t *lastView);
void GuiSetTonUrData(URParseResult *urResult, URParseMultiResult *urMultiResult, bool multi)
@@ -122,14 +123,25 @@ void GuiTonTxOverview(lv_obj_t *parent, void *totalData)
lv_obj_add_flag(parent, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_add_flag(parent, LV_OBJ_FLAG_CLICKABLE);
- lv_obj_t *lastView = CreateOverviewAmountView(parent, txData, NULL);
- lastView = CreateOverviewActionView(parent, txData, lastView);
- lastView = CreateOverviewDestinationView(parent, txData, lastView);
- if (txData->comment != NULL) {
- lastView = CreateOverviewCommentView(parent, txData, lastView);
- }
- if (txData->contract_data != NULL) {
- lastView = CreateOverviewContractDataView(parent, txData, lastView);
+ lv_obj_t *lastView = NULL;
+ VecFFI_DisplayTonMessage *messages = txData->messages;
+ if (messages != NULL && messages->size > 0) {
+ bool showTitle = messages->size > 1;
+ for (size_t i = 0; i < messages->size; i++) {
+ DisplayTonMessage *message = &messages->data[i];
+ if (showTitle) {
+ lastView = CreateOverviewMessageTitleView(parent, i, lastView);
+ }
+ lastView = CreateOverviewAmountView(parent, message, lastView);
+ lastView = CreateOverviewActionView(parent, message, lastView);
+ lastView = CreateOverviewDestinationView(parent, message, lastView);
+ if (message->comment != NULL) {
+ lastView = CreateOverviewCommentView(parent, message, lastView);
+ }
+ if (message->contract_data != NULL) {
+ lastView = CreateOverviewContractDataView(parent, message, lastView);
+ }
+ }
}
lv_obj_update_layout(parent);
}
@@ -141,16 +153,42 @@ void GuiTonTxRawData(lv_obj_t *parent, void *totalData)
lv_obj_add_flag(parent, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_add_flag(parent, LV_OBJ_FLAG_CLICKABLE);
lv_obj_t *lastView = NULL;
- if (txData->data_view != NULL) {
- lastView = CreateDetailsDataViewView(parent, txData, NULL);
+ VecFFI_DisplayTonMessage *messages = txData->messages;
+ if (messages != NULL && messages->size > 0) {
+ bool showTitle = messages->size > 1;
+ for (size_t i = 0; i < messages->size; i++) {
+ DisplayTonMessage *message = &messages->data[i];
+ if (message->data_view != NULL) {
+ lastView = CreateDetailsDataViewView(parent, message, i, showTitle, lastView);
+ }
+ }
}
lastView = CreateDetailsRawDataView(parent, txData, lastView);
}
+static lv_obj_t *CreateOverviewMessageTitleView(lv_obj_t *parent, size_t index, lv_obj_t *lastView)
+{
+ lv_obj_t *container = CreateContentContainer(parent, 408, 62);
+ if (lastView != NULL) {
+ lv_obj_align_to(container, lastView, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 16);
+ }
+
+ char title[32] = {0};
+ snprintf_s(title, sizeof(title), "Message %zu", index + 1);
+
+ lv_obj_t *label = GuiCreateTextLabel(container, title);
+ lv_obj_align(label, LV_ALIGN_TOP_LEFT, 24, 16);
+ lv_label_set_recolor(label, true);
+ lv_obj_set_style_text_color(label, ORANGE_COLOR, LV_PART_MAIN);
+ return container;
+}
-static lv_obj_t *CreateOverviewAmountView(lv_obj_t *parent, DisplayTonTransaction *data, lv_obj_t *lastView)
+static lv_obj_t *CreateOverviewAmountView(lv_obj_t *parent, DisplayTonMessage *data, lv_obj_t *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);
+ }
lv_obj_t *label = GuiCreateIllustrateLabel(container, _("Amount"));
lv_obj_align(label, LV_ALIGN_TOP_LEFT, 24, 16);
@@ -163,7 +201,7 @@ static lv_obj_t *CreateOverviewAmountView(lv_obj_t *parent, DisplayTonTransactio
return container;
}
-static lv_obj_t *CreateOverviewActionView(lv_obj_t *parent, DisplayTonTransaction *data, lv_obj_t *lastView)
+static lv_obj_t *CreateOverviewActionView(lv_obj_t *parent, DisplayTonMessage *data, lv_obj_t *lastView)
{
lv_obj_t *container = CreateContentContainer(parent, 408, 64);
lv_obj_align_to(container, lastView, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 16);
@@ -177,7 +215,7 @@ static lv_obj_t *CreateOverviewActionView(lv_obj_t *parent, DisplayTonTransactio
return container;
}
-static lv_obj_t *CreateOverviewDestinationView(lv_obj_t *parent, DisplayTonTransaction *data, lv_obj_t *lastView)
+static lv_obj_t *CreateOverviewDestinationView(lv_obj_t *parent, DisplayTonMessage *data, lv_obj_t *lastView)
{
lv_obj_t *container = CreateContentContainer(parent, 408, 244);
lv_obj_align_to(container, lastView, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 16);
@@ -212,7 +250,7 @@ static lv_obj_t *CreateOverviewDestinationView(lv_obj_t *parent, DisplayTonTrans
return container;
}
-static lv_obj_t *CreateOverviewCommentView(lv_obj_t *parent, DisplayTonTransaction *data, lv_obj_t *lastView)
+static lv_obj_t *CreateOverviewCommentView(lv_obj_t *parent, DisplayTonMessage *data, lv_obj_t *lastView)
{
lv_obj_t *container = CreateContentContainer(parent, 408, 62);
lv_obj_align_to(container, lastView, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 16);
@@ -245,22 +283,25 @@ static lv_obj_t *CreateOverviewCommentView(lv_obj_t *parent, DisplayTonTransacti
return container;
}
-static lv_obj_t *CreateOverviewContractDataView(lv_obj_t *parent, DisplayTonTransaction *data, lv_obj_t *lastView)
+static lv_obj_t *CreateOverviewContractDataView(lv_obj_t *parent, DisplayTonMessage *data, lv_obj_t *lastView)
{
cJSON *contractData = cJSON_Parse(data->contract_data);
+ if (contractData == NULL) {
+ return lastView;
+ }
int size = cJSON_GetArraySize(contractData);
printf("size: %d\n", size);
lv_obj_t *tempLastView = NULL;
for (size_t i = 0; i < size; i++) {
- cJSON *data = cJSON_GetArrayItem(contractData, i);
- char* title = cJSON_GetObjectItem(data, "title")->valuestring;
- char* value = cJSON_GetObjectItem(data, "value")->valuestring;
+ cJSON *item = cJSON_GetArrayItem(contractData, i);
+ char* title = cJSON_GetObjectItem(item, "title")->valuestring;
+ char* value = cJSON_GetObjectItem(item, "value")->valuestring;
//100 = 16(padding top) + 16(padding bottom) + 30(title) + 8(margin) + 30(value one line)
lv_obj_t *container = CreateContentContainer(parent, 408, 100);
- lv_obj_align_to(container, lastView, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 16);
+ lv_obj_align_to(container, tempLastView == NULL ? lastView : tempLastView, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 16);
lv_obj_t *label = GuiCreateIllustrateLabel(container, title);
lv_obj_align(label, LV_ALIGN_TOP_LEFT, 24, 16);
@@ -280,16 +321,25 @@ static lv_obj_t *CreateOverviewContractDataView(lv_obj_t *parent, DisplayTonTran
tempLastView = container;
}
- return tempLastView;
+ cJSON_Delete(contractData);
+ return tempLastView == NULL ? lastView : tempLastView;
}
-static lv_obj_t *CreateDetailsDataViewView(lv_obj_t *parent, DisplayTonTransaction *data, lv_obj_t *lastView)
+static lv_obj_t *CreateDetailsDataViewView(lv_obj_t *parent, DisplayTonMessage *data, size_t index, bool showTitle, lv_obj_t *lastView)
{
lv_obj_t *container = CreateContentContainer(parent, 408, 244);
lv_obj_add_flag(container, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_add_flag(container, LV_OBJ_FLAG_CLICKABLE);
+ if (lastView != NULL) {
+ lv_obj_align_to(container, lastView, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 16);
+ }
- lv_obj_t *label = GuiCreateTextLabel(container, _("Data View"));
+ char title[32] = {0};
+ if (showTitle) {
+ snprintf_s(title, sizeof(title), "Data View %zu", index + 1);
+ }
+
+ lv_obj_t *label = GuiCreateTextLabel(container, showTitle ? title : _("Data View"));
lv_obj_align(label, LV_ALIGN_TOP_LEFT, 24, 16);
lv_label_set_recolor(label, true);
lv_obj_set_style_text_color(label, ORANGE_COLOR, LV_PART_MAIN);
@@ -389,4 +439,4 @@ void GuiTonProofRawData(lv_obj_t *parent, void *totalData)
lv_obj_align(label, LV_ALIGN_TOP_LEFT, 24, 62);
lv_obj_set_width(label, 360);
lv_label_set_long_mode(label, LV_LABEL_LONG_WRAP);
-}
\ No newline at end of file
+}
Why this scored 28/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.