fix: regular AI code review and fix
What changed, and why it matters
This is a large, routine code-quality and hardening patch for the Keystone 3 hardware wallet firmware. It tightens how transactions are parsed and displayed for several cryptocurrencies (Arweave, Avalanche, Bitcoin, Cosmos, Ethereum, Solana). Key user-visible effects: duplicate or malformed transaction tags are now rejected, oversized Bitcoin fees trigger a warning, unknown Cosmos messages are shown as 'blind sign' with a digest instead of being silently dropped, and Ethereum nonces/values are handled with full precision. The commit message frames this as a regular AI-assisted code review and fix, not as a response to a disclosed security incident.
Treat as a legitimate hardening patch. Review the new validation rules for compatibility with edge-case transactions (e.g., Avalanche custom assets, Cosmos unknown messages, Bitcoin multisig P2SH/P2WSH variants, Arweave multi-block tags). Run the updated test suites and confirm UI warnings for large Bitcoin fees and blind-sign Cosmos messages are shown correctly. No immediate incident response is indicated, but the patch should be included in the next firmware release.
Security signals we found
Arweave: duplicate required tags now rejected, malformed/trailing Avro data now rejected
Arweave: DataItem tag count mismatch now rejected
Avalanche: trailing transaction bytes now rejected via parsed_size checks
Avalanche: non-native AVAX assets rejected in inputs/outputs/stake
Bitcoin: large-fee warning added (absolute and per-vbyte thresholds)
Bitcoin: multisig PSBT input script and derivation validation added
Cosmos: unknown messages no longer silently dropped; shown as blind-sign with SHA-256 digest
Cosmos: fee/amount formatting moved from f64 division to decimal string logic
Ethereum: nonce and gas price changed from u32/u64 to String/U256 to prevent truncation
Ethereum: EIP-712 typed-data conversion made fallible with explicit error propagation
Ethereum: SafeTx hash computation now validates fields and rejects invalid operation values
Evidence from the diff
The diff is a broad defensive patch across Rust transaction parsers and C UI code. Notable changes include: (1) Arweave DataItem/Tags Avro parsing rewritten to handle multi-block tags, reject trailing bytes, validate tag counts, and require unique required tags for AO transfers; (2) Avalanche parsers now enforce that all inputs/outputs use native AVAX assets for the given network and add parsed-size checks to reject trailing bytes; (3) Bitcoin adds a large-fee warning based on absolute threshold and estimated vbytes, plus multisig PSBT input validation; (4) Cosmos replaces floating-point amount/fee math with decimal string formatting, preserves unknown messages with a SHA-256 digest, and adds memo display; (5) Ethereum changes nonce and gas-price handling from u32/u64 to string/U256 to avoid truncation, and makes EIP-712/SafeTx hashing stricter with explicit error handling. CI coverage thresholds are also raised. No CVE, advisory, or researcher attribution is present in the commit or supplied references.
Changed components
rust/apps/arweaverust/apps/avalancherust/apps/bitcoinrust/apps/cosmosrust/apps/ethereumrust/apps/solanarust/rust_c bindingssrc/ui/gui_chain / gui_widgets / gui_analyzeCI coverage workflowsInspect captured patch +3998 / −1426
diff --git a/.github/workflows/rust-arweave-checks.yml b/.github/workflows/rust-arweave-checks.yml
index 3e8c7a7..78838cf 100644
--- a/.github/workflows/rust-arweave-checks.yml
+++ b/.github/workflows/rust-arweave-checks.yml
@@ -27,4 +27,4 @@ jobs:
uses: taiki-e/install-action@cargo-llvm-cov
- name: Run rust/apps/arweave
- run: cd rust/apps/arweave && cargo +$RUST_TOOLCHAIN llvm-cov --fail-under-regions 78 --fail-under-functions 81 --fail-under-lines 85 --ignore-filename-regex 'keystore/*|utils/*'
+ run: cd rust/apps/arweave && cargo +$RUST_TOOLCHAIN llvm-cov --fail-under-regions 87 --fail-under-functions 83 --fail-under-lines 87 --ignore-filename-regex 'keystore/*|utils/*'
diff --git a/.github/workflows/rust-bitcoin-checks.yml b/.github/workflows/rust-bitcoin-checks.yml
index b04a630..3d67305 100644
--- a/.github/workflows/rust-bitcoin-checks.yml
+++ b/.github/workflows/rust-bitcoin-checks.yml
@@ -27,4 +27,4 @@ jobs:
uses: taiki-e/install-action@cargo-llvm-cov
- name: Run rust/apps/bitcoin
- run: cd rust/apps/bitcoin && cargo +$RUST_TOOLCHAIN llvm-cov --fail-under-regions 74 --fail-under-functions 79 --fail-under-lines 86 --ignore-filename-regex 'keystore/*|utils/*'
+ run: cd rust/apps/bitcoin && cargo +$RUST_TOOLCHAIN llvm-cov --fail-under-regions 87 --fail-under-functions 80 --fail-under-lines 86 --ignore-filename-regex 'keystore/*|utils/*'
diff --git a/.github/workflows/rust-cosmos-checks.yml b/.github/workflows/rust-cosmos-checks.yml
index 90cd534..c74fe6e 100644
--- a/.github/workflows/rust-cosmos-checks.yml
+++ b/.github/workflows/rust-cosmos-checks.yml
@@ -27,4 +27,4 @@ jobs:
uses: taiki-e/install-action@cargo-llvm-cov
- name: Run rust/apps/cosmos
- run: cd rust/apps/cosmos && cargo +$RUST_TOOLCHAIN llvm-cov --fail-under-regions 77 --fail-under-functions 62 --fail-under-lines 74 --ignore-filename-regex 'keystore/*|utils/*'
+ run: cd rust/apps/cosmos && cargo +$RUST_TOOLCHAIN llvm-cov --fail-under-regions 79 --fail-under-functions 63 --fail-under-lines 76 --ignore-filename-regex 'keystore/*|utils/*'
diff --git a/.github/workflows/rust-ethereum-checks.yml b/.github/workflows/rust-ethereum-checks.yml
index a8d3674..637a682 100644
--- a/.github/workflows/rust-ethereum-checks.yml
+++ b/.github/workflows/rust-ethereum-checks.yml
@@ -27,4 +27,4 @@ jobs:
uses: taiki-e/install-action@cargo-llvm-cov
- name: Run rust/apps/ethereum
- run: cd rust/apps/ethereum && cargo +$RUST_TOOLCHAIN llvm-cov --fail-under-regions 72 --fail-under-functions 64 --fail-under-lines 76 --ignore-filename-regex 'keystore/*|utils/*'
+ run: cd rust/apps/ethereum && cargo +$RUST_TOOLCHAIN llvm-cov --fail-under-regions 73 --fail-under-functions 64 --fail-under-lines 76 --ignore-filename-regex 'keystore/*|utils/*'
diff --git a/.github/workflows/rust-solana-checks.yml b/.github/workflows/rust-solana-checks.yml
index 88686e5..867cde3 100644
--- a/.github/workflows/rust-solana-checks.yml
+++ b/.github/workflows/rust-solana-checks.yml
@@ -27,4 +27,4 @@ jobs:
uses: taiki-e/install-action@cargo-llvm-cov
- name: Run rust/apps/solana
- run: cd rust/apps/solana && cargo +$RUST_TOOLCHAIN llvm-cov --fail-under-regions 66 --fail-under-functions 73 --fail-under-lines 68 --ignore-filename-regex 'keystore/*|utils/*'
+ run: cd rust/apps/solana && cargo +$RUST_TOOLCHAIN llvm-cov --fail-under-regions 73 --fail-under-functions 73 --fail-under-lines 71 --ignore-filename-regex 'keystore/*|utils/*'
diff --git a/.gitignore b/.gitignore
index a2cba79..0533352 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,4 +14,5 @@ pyenv
!/rust/apps/Cargo.lock
*.rustfmt.toml
/tools/boot.sig
-/target
\ No newline at end of file
+/target
+/.agents/
diff --git a/rust/apps/arweave/src/ao_transaction.rs b/rust/apps/arweave/src/ao_transaction.rs
index ac7fe48..d802d7e 100644
--- a/rust/apps/arweave/src/ao_transaction.rs
+++ b/rust/apps/arweave/src/ao_transaction.rs
@@ -15,29 +15,36 @@ impl_public_struct!(AOTransferTransaction {
other_info: Vec<Tag>
});
+fn find_unique_tag<'a>(tags: &'a [Tag], name: &str) -> Result<&'a Tag, ArweaveError> {
+ let mut matches = tags.iter().filter(|tag| tag.name().eq(name));
+ let tag = matches.next().ok_or(ArweaveError::NotAOTransaction)?;
+ if matches.next().is_some() {
+ return Err(ArweaveError::NotAOTransaction);
+ }
+ Ok(tag)
+}
+
impl TryFrom<DataItem> for AOTransferTransaction {
type Error = ArweaveError;
fn try_from(value: DataItem) -> Result<Self, Self::Error> {
- let tags = value.get_tags().get_data();
- let protocol = tags
- .iter()
- .find(|i| i.get_name().eq("Data-Protocol") && i.get_value().eq("ao"));
- let action = tags
- .iter()
- .find(|i| i.get_name().eq("Action") && i.get_value().eq("Transfer"));
- let recipient = tags.iter().find(|i| i.get_name().eq("Recipient"));
- let quantity = tags.iter().find(|i| i.get_name().eq("Quantity"));
+ let tags = value.tags_ref().as_slice();
+ let protocol = find_unique_tag(&tags, "Data-Protocol")?;
+ let action = find_unique_tag(&tags, "Action")?;
+ let recipient = find_unique_tag(&tags, "Recipient")?;
+ let quantity = find_unique_tag(&tags, "Quantity")?;
+
+ if protocol.value().ne("ao") || action.value().ne("Transfer") {
+ return Err(ArweaveError::NotAOTransaction);
+ }
let token_id = value.get_target();
let rest_tags = tags.iter().filter(|v| {
- v.get_name().ne("DataProtocol")
- && v.get_name().ne("Action")
- && v.get_name().ne("Recipient")
- && v.get_name().ne("Quantity")
+ v.name().ne("Data-Protocol")
+ && v.name().ne("Action")
+ && v.name().ne("Recipient")
+ && v.name().ne("Quantity")
});
- if let (Some(_action), Some(_protocol), Some(token_id), Some(recipient), Some(quantity)) =
- (action, protocol, token_id, recipient, quantity)
- {
+ if let Some(token_id) = token_id {
let from = value.get_owner();
let to = recipient.get_value();
let quantity = quantity.get_value();
@@ -78,6 +85,7 @@ impl TryFrom<DataItem> for AOTransferTransaction {
#[cfg(test)]
mod tests {
+ use alloc::string::ToString;
use hex;
use crate::data_item::DataItem;
@@ -87,8 +95,8 @@ mod tests {
#[test]
fn test_transform_ao_transfer() {
let binary = hex::decode("01000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a999bac8b7906c0bc94f7d163ea9e7fe6ef34045b6a27035e5298aaaddeea05355c50efd30da262c97a68b5aa7219726754bf8501818429e60b9f8175ed66a23349757dc8b3f126abc199272c91174bdb96a9a13aad43b9b6195583188c222002d29b105169dc237dccb0e371895aa10b9263e0b6fbe2d03d3a0073fa7f278ecfa890e75a3fe812ca86eb44f134a7edaa664a5582e72fa43b7accdfeb03f0492c118235b9ff7784106ca1a2f6e7bc4bcc6e1ed98775b7c023a1ae1e332f42e3183ab17c43c58e6605353a47331452ebf659fb267d27492b961ecdafcde9657a0a623aec761f6b3130f89ff7136cae26ebc58aaaa0c6c2264d8e0aa7c78cb46b5210cd69be2ffca64fd3cb0990116034c582828dd22d0235edf9ad999ef0b25afbcab802330d03e9653eff2dbee7f9e0a695a63e04d2aaef73152c255a1d8e5f9cc525cbcfd796ffff337f21d846ae7091037e2bfd06efaf262375100323335e62c79ca63aa31226e3655acab5f2861913630be567210d3d0d5b0f0a6bdc7edfc986e9c14b28b9d32deab5041872a26f8b95341a8cdf6326207d0c2f728ef85554f18c9e285c9f3e01e1d1cb1adf2546eeb9ddfc81a51b0fdf94c9f9116adcd5878815d21038968cbef2b51cc4a27fb1911008c6d1d587830645aca9ca775cf1d67dd9901aadb830a1e8abe0548a47619b8d80083316a645c646820640067653101c54f73164ab75f6650ea8970355bebd6f5162237379174d6afbc4a403e9d875d000800000000000000b100000000000000100c416374696f6e105472616e7366657212526563697069656e745671667a34427465626f714d556f4e536c74457077394b546462663736665252446667783841693644474a77105175616e746974791631303030303030303030301a446174612d50726f746f636f6c04616f0e56617269616e740e616f2e544e2e3108547970650e4d6573736167650653444b12616f636f6e6e65637418436f6e74656e742d5479706514746578742f706c61696e0037373037").unwrap();
- let result = DataItem::deserialize(&binary).unwrap();
- let ao_transfer = AOTransferTransaction::try_from(result).unwrap();
+ let mut result = DataItem::deserialize(&binary).unwrap();
+ let ao_transfer = AOTransferTransaction::try_from(result.clone()).unwrap();
assert_eq!(
ao_transfer.from,
"nSkowCiV4VBZJVyI2UK2wT_6g9LVX5BLZvYSTjd0bVQ"
@@ -99,5 +107,16 @@ mod tests {
);
assert_eq!(ao_transfer.quantity, "0.01 AR");
assert_eq!(ao_transfer.token_id, "Wrapped AR");
+
+ let mut tags = result.get_tags();
+ let mut tag_data = tags.get_data();
+ tag_data.push(crate::data_item::Tag::new(
+ "Recipient".to_string(),
+ "different-recipient".to_string(),
+ ));
+ tags.set_len(tag_data.len() as i64);
+ tags.set_data(tag_data);
+ result.set_tags(tags);
+ assert!(AOTransferTransaction::try_from(result).is_err());
}
}
diff --git a/rust/apps/arweave/src/data_item.rs b/rust/apps/arweave/src/data_item.rs
index ab1c98b..5b0b061 100644
--- a/rust/apps/arweave/src/data_item.rs
+++ b/rust/apps/arweave/src/data_item.rs
@@ -18,16 +18,53 @@ impl_public_struct!(Tags {
impl Tags {
pub fn deserialize(serial: &[u8]) -> Result<Self> {
- let mut avro_bytes = serial.to_vec();
- let len = avro_decode_long(&mut avro_bytes)?;
+ let mut avro_bytes = serial;
let mut tags = vec![];
- for _i in 0..len {
- let name = avro_decode_string(&mut avro_bytes)?;
- let value = avro_decode_string(&mut avro_bytes)?;
- tags.push(Tag { name, value })
+
+ loop {
+ let block_count = avro_decode_long(&mut avro_bytes)?;
+ if block_count == 0 {
+ break;
+ }
+
+ let item_count = if block_count < 0 {
+ let item_count = block_count
+ .checked_abs()
+ .ok_or_else(|| ArweaveError::AvroError("Invalid block count".to_string()))?
+ as u64;
+ let block_size = avro_decode_long(&mut avro_bytes)?;
+ let block_size = usize::try_from(block_size).map_err(|_| {
+ ArweaveError::AvroError("Invalid negative block size".to_string())
+ })?;
+ let mut block = avro_take(&mut avro_bytes, block_size)?;
+ avro_decode_tags(&mut block, item_count, &mut tags)?;
+ if !block.is_empty() {
+ return Err(ArweaveError::AvroError(
+ "Avro tag block contains trailing bytes".to_string(),
+ ));
+ }
+ continue;
+ } else {
+ block_count as u64
+ };
+
+ avro_decode_tags(&mut avro_bytes, item_count, &mut tags)?;
+ }
+
+ if !avro_bytes.is_empty() {
+ return Err(ArweaveError::AvroError(
+ "Avro tags contain trailing bytes".to_string(),
+ ));
}
+
+ let len = i64::try_from(tags.len())
+ .map_err(|_| ArweaveError::AvroError("Too many tags".to_string()))?;
Ok(Tags { len, data: tags })
}
+
+ pub(crate) fn as_slice(&self) -> &[Tag] {
+ &self.data
+ }
}
impl_public_struct!(Tag {
@@ -35,20 +72,53 @@ impl_public_struct!(Tag {
value: String
});
-fn avro_decode_long(reader: &mut Vec<u8>) -> Result<i64> {
+impl Tag {
+ pub(crate) fn name(&self) -> &str {
+ &self.name
+ }
+
+ pub(crate) fn value(&self) -> &str {
+ &self.value
+ }
+}
+
+fn avro_take<'a>(reader: &mut &'a [u8], len: usize) -> Result<&'a [u8]> {
+ if len > reader.len() {
+ return Err(ArweaveError::AvroError(
+ "Unexpected end of Avro data".to_string(),
+ ));
+ }
+ let (value, rest) = reader.split_at(len);
+ *reader = rest;
+ Ok(value)
+}
+
+fn avro_decode_tags(reader: &mut &[u8], count: u64, tags: &mut Vec<Tag>) -> Result<()> {
+ for _ in 0..count {
+ let name = avro_decode_string(reader)?;
+ let value = avro_decode_string(reader)?;
+ tags.push(Tag { name, value });
+ }
+ Ok(())
+}
+
+fn avro_decode_long(reader: &mut &[u8]) -> Result<i64> {
let mut i = 0u64;
- let mut buf = [0u8; 1];
- let mut j = 0;
+ let mut j = 0u32;
loop {
- if j > 9 {
- // if j * 7 > 64
+ if j >= 10 {
return Err(ArweaveError::AvroError("Integer overflow".to_string()));
}
- let head = reader.remove(0);
- buf[0] = head;
- i |= (u64::from(buf[0] & 0x7F)) << (j * 7);
- if (buf[0] >> 7) == 0 {
+
+ let head = *avro_take(reader, 1)?
+ .first()
+ .ok_or_else(|| ArweaveError::AvroError("Unexpected end of Avro data".to_string()))?;
+ if j == 9 && head > 1 {
+ return Err(ArweaveError::AvroError("Integer overflow".to_string()));
+ }
+ i |= u64::from(head & 0x7f) << (j * 7);
+ if head & 0x80 == 0 {
break;
} else {
j += 1;
@@ -61,10 +131,12 @@ fn avro_decode_long(reader: &mut Vec<u8>) -> Result<i64> {
})
}
-fn avro_decode_string(reader: &mut Vec<u8>) -> Result<String> {
+fn avro_decode_string(reader: &mut &[u8]) -> Result<String> {
let len = avro_decode_long(reader)?;
- let buf = reader.drain(..len as usize).collect();
- String::from_utf8(buf).map_err(|e| ArweaveError::AvroError(format!("{e}")))
+ let len = usize::try_from(len)
+ .map_err(|_| ArweaveError::AvroError("Invalid negative string length".to_string()))?;
+ let buf = avro_take(reader, len)?;
+ String::from_utf8(buf.to_vec()).map_err(|e| ArweaveError::AvroError(format!("{e}")))
}
impl_public_struct!(DataItem {
@@ -96,6 +168,10 @@ enum SignatureType {
}
impl DataItem {
+ pub(crate) fn tags_ref(&self) -> &Tags {
+ &self.tags
+ }
+
pub fn deserialize(binary: &[u8]) -> Result<Self> {
let mut reader = binary.to_vec();
let signature_type =
@@ -138,8 +214,22 @@ impl DataItem {
|_| ArweaveError::ParseTxError("Invalid DataItem tags_number".to_string()),
)?);
- let raw_tags: Vec<u8> = reader.drain(..tags_bytes_number as usize).collect();
+ let tags_bytes_len = usize::try_from(tags_bytes_number).map_err(|_| {
+ ArweaveError::ParseTxError("DataItem tags byte length is too large".to_string())
+ })?;
+ if tags_bytes_len > reader.len() {
+ return Err(ArweaveError::ParseTxError(
+ "DataItem tags exceed remaining input".to_string(),
+ ));
+ }
+ let raw_tags: Vec<u8> = reader.drain(..tags_bytes_len).collect();
let tags = Tags::deserialize(&raw_tags)?;
+ if tags.as_slice().len() as u64 != tags_number {
+ return Err(ArweaveError::ParseTxError(format!(
+ "DataItem tags count mismatch: expected {tags_number}, decoded {}",
+ tags.as_slice().len()
+ )));
+ }
let raw_data = reader.clone();
let data = base64_url(raw_data.clone());
@@ -181,10 +271,44 @@ impl DataItem {
#[cfg(test)]
mod tests {
- use super::DataItem;
+ use super::{DataItem, Tags};
use hex;
+ #[test]
+ fn test_parse_tags_across_multiple_avro_blocks() {
+ let serial = [
+ 0x02, 0x12, b'R', b'e', b'c', b'i', b'p', b'i', b'e', b'n', b't', 0x02, b'A', 0x02,
+ 0x10, b'Q', b'u', b'a', b'n', b't', b'i', b't', b'y', 0x04, b'1', b'0', 0x00,
+ ];
+
+ let tags = Tags::deserialize(&serial).unwrap();
+
+ assert_eq!(tags.get_len(), 2);
+ assert_eq!(tags.get_data()[0].get_name(), "Recipient");
+ assert_eq!(tags.get_data()[0].get_value(), "A");
+ assert_eq!(tags.get_data()[1].get_name(), "Quantity");
+ assert_eq!(tags.get_data()[1].get_value(), "10");
+ }
+
+ #[test]
+ fn test_parse_negative_count_avro_block() {
+ let serial = [
+ 0x01, 0x18, 0x12, b'R', b'e', b'c', b'i', b'p', b'i', b'e', b'n', b't', 0x02, b'A',
+ 0x00,
+ ];
+
+ let tags = Tags::deserialize(&serial).unwrap();
+
+ assert_eq!(tags.get_len(), 1);
+ assert_eq!(tags.get_data()[0].get_name(), "Recipient");
+ }
+
+ #[test]
+ fn test_reject_trailing_avro_tag_bytes() {
+ assert!(Tags::deserialize(&[0x00, 0x02]).is_err());
+ }
+
#[test]
fn test_parse_data_item() {
//01000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a999bac8b7906c0bc94f7d163ea9e7fe6ef34045b6a27035e5298aaaddeea05355c50efd30da262c97a68b5aa7219726754bf8501818429e60b9f8175ed66a23349757dc8b3f126abc199272c91174bdb96a9a13aad43b9b6195583188c222002d29b105169dc237dccb0e371895aa10b9263e0b6fbe2d03d3a0073fa7f278ecfa890e75a3fe812ca86eb44f134a7edaa664a5582e72fa43b7accdfeb03f0492c118235b9ff7784106ca1a2f6e7bc4bcc6e1ed98775b7c023a1ae1e332f42e3183ab17c43c58e6605353a47331452ebf659fb267d27492b961ecdafcde9657a0a623aec761f6b3130f89ff7136cae26ebc58aaaa0c6c2264d8e0aa7c78cb46b5210cd69be2ffca64fd3cb0990116034c582828dd22d0235edf9ad999ef0b25afbcab802330d03e9653eff2dbee7f9e0a695a63e04d2aaef73152c255a1d8e5f9cc525cbcfd796ffff337f21d846ae7091037e2bfd06efaf262375100323335e62c79ca63aa31226e3655acab5f2861913630be567210d3d0d5b0f0a6bdc7edfc986e9c14b28b9d32deab5041872a26f8b95341a8cdf6326207d0c2f728ef85554f18c9e285c9f3e01e1d1cb1adf2546eeb9ddfc81a51b0fdf94c9f9116adcd5878815d21038968cbef2b51cc4a27fb1911008c6d1d587830645aca9ca775cf1d67dd9901aadb830a1e8abe0548a47619b8d80083316a645c646820640067653101c54f73164ab75f6650ea8970355bebd6f5162237379174d6afbc4a403e9d875d000800000000000000b100000000000000100c416374696f6e105472616e7366657212526563697069656e745671667a34427465626f714d556f4e536c74457077394b546462663736665252446667783841693644474a77105175616e746974791631303030303030303030301a446174612d50726f746f636f6c04616f0e56617269616e740e616f2e544e2e3108547970650e4d6573736167650653444b12616f636f6e6e65637418436f6e74656e742d5479706514746578742f706c61696e0037373037
diff --git a/rust/apps/arweave/src/lib.rs b/rust/apps/arweave/src/lib.rs
index 4639e9a..c230036 100644
--- a/rust/apps/arweave/src/lib.rs
+++ b/rust/apps/arweave/src/lib.rs
@@ -161,7 +161,6 @@ pub fn parse_data_item(serial: &[u8]) -> Result<DataItem> {
mod tests {
use super::*;
use alloc::borrow::ToOwned;
- use hex::ToHex;
use {hex, rsa::PublicKeyParts};
#[test]
diff --git a/rust/apps/arweave/src/tokens.rs b/rust/apps/arweave/src/tokens.rs
index e051892..ed7c59f 100644
--- a/rust/apps/arweave/src/tokens.rs
+++ b/rust/apps/arweave/src/tokens.rs
@@ -66,3 +66,16 @@ pub(crate) fn find_token(token_id: &str) -> Option<TokenInfo> {
.find(|v| v.get_token_id().eq(token_id))
.cloned()
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn token_lookup_and_quantity_conversion() {
+ let token = find_token("xU9zFkq3X2ZQ6olwNVvr1vUWIjc3kXTWr7xKQD6dh10").unwrap();
+ assert_eq!(token.convert_quantity("10000000000").unwrap(), "0.01 AR");
+ assert!(token.convert_quantity("invalid").is_err());
+ assert!(find_token("unknown").is_none());
+ }
+}
diff --git a/rust/apps/arweave/src/transaction.rs b/rust/apps/arweave/src/transaction.rs
index caab798..327fd09 100644
--- a/rust/apps/arweave/src/transaction.rs
+++ b/rust/apps/arweave/src/transaction.rs
@@ -278,4 +278,24 @@ mod tests {
let base64 = Base64("Hello world".as_bytes().to_vec());
assert_eq!(base64.to_string(), "SGVsbG8gd29ybGQ");
}
+
+ #[test]
+ fn test_tag_constructors() {
+ let raw = Tag::<Base64>::from_utf8_strs("name", "value").unwrap();
+ assert_eq!(
+ raw,
+ Tag {
+ name: Base64(b"name".to_vec()),
+ value: Base64(b"value".to_vec())
+ }
+ );
+ let text = Tag::<String>::from_utf8_strs("name", "value").unwrap();
+ assert_eq!(
+ text,
+ Tag {
+ name: "name".into(),
+ value: "value".into()
+ }
+ );
+ }
}
diff --git a/rust/apps/avalanche/src/constants.rs b/rust/apps/avalanche/src/constants.rs
index e07c55f..c48144c 100644
--- a/rust/apps/avalanche/src/constants.rs
+++ b/rust/apps/avalanche/src/constants.rs
@@ -8,6 +8,16 @@ pub const C_CHAIN_ADDRESS_LEN: usize = 20;
pub const ASSET_ID_LEN: usize = 32;
pub const ADDRESS_LEN: usize = 20;
pub const NAVAX_TO_AVAX_RATIO: f64 = 1_000_000_000.0;
+pub const MAINNET_NETWORK_ID: u32 = 1;
+pub const FUJI_NETWORK_ID: u32 = 5;
+pub const MAINNET_AVAX_ASSET_ID: [u8; ASSET_ID_LEN] = [
+ 33, 230, 115, 23, 203, 196, 190, 42, 235, 0, 103, 122, 214, 70, 39, 120, 168, 245, 34, 116,
+ 185, 214, 5, 223, 37, 145, 178, 48, 39, 168, 125, 255,
+];
+pub const FUJI_AVAX_ASSET_ID: [u8; ASSET_ID_LEN] = [
+ 61, 155, 218, 192, 237, 29, 118, 19, 48, 207, 104, 14, 253, 235, 26, 66, 21, 158, 179, 135,
+ 214, 210, 149, 12, 150, 247, 210, 143, 97, 187, 226, 170,
+];
pub const C_CHAIN_PREFIX: &str = "m/44'/60'/0'";
pub const X_P_CHAIN_PREFIX: &str = "m/44'/9000'/0'";
diff --git a/rust/apps/avalanche/src/lib.rs b/rust/apps/avalanche/src/lib.rs
index 405df13..381b730 100644
--- a/rust/apps/avalanche/src/lib.rs
+++ b/rust/apps/avalanche/src/lib.rs
@@ -7,7 +7,7 @@ use alloc::{string::ToString, vec::Vec};
pub use address::get_address;
use bytes::{Buf, Bytes};
-use transactions::tx_header::Header;
+use transactions::{structs::ParsedSizeAble, tx_header::Header};
use crate::errors::{AvaxError, Result};
use core::convert::TryFrom;
@@ -26,11 +26,15 @@ use transactions::type_id::TypeId;
pub fn parse_avax_tx<T>(data: Vec<u8>) -> Result<T>
where
- T: TryFrom<Bytes>,
+ T: TryFrom<Bytes> + ParsedSizeAble,
{
+ let input_len = data.len();
let bytes = Bytes::from(data);
match T::try_from(bytes) {
- Ok(data) => Ok(data),
+ Ok(data) if data.parsed_size() == input_len => Ok(data),
+ Ok(_) => Err(AvaxError::InvalidTransaction(
+ "unexpected trailing data".to_string(),
+ )),
Err(_) => Err(AvaxError::InvalidInput),
}
}
diff --git a/rust/apps/avalanche/src/transactions/asset_id.rs b/rust/apps/avalanche/src/transactions/asset_id.rs
index 7a03e29..ed97320 100644
--- a/rust/apps/avalanche/src/transactions/asset_id.rs
+++ b/rust/apps/avalanche/src/transactions/asset_id.rs
@@ -1,10 +1,23 @@
+use crate::constants::{
+ FUJI_AVAX_ASSET_ID, FUJI_NETWORK_ID, MAINNET_AVAX_ASSET_ID, MAINNET_NETWORK_ID,
+};
use crate::errors::AvaxError;
use bytes::Bytes;
use core::convert::TryFrom;
-#[derive(Clone, Debug)]
+#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AssetId(Bytes);
+impl AssetId {
+ pub fn is_native_avax(&self, network_id: u32) -> bool {
+ match network_id {
+ MAINNET_NETWORK_ID => self.0.as_ref() == MAINNET_AVAX_ASSET_ID,
+ FUJI_NETWORK_ID => self.0.as_ref() == FUJI_AVAX_ASSET_ID,
+ _ => false,
+ }
+ }
+}
+
impl TryFrom<Bytes> for AssetId {
type Error = AvaxError;
@@ -12,3 +25,20 @@ impl TryFrom<Bytes> for AssetId {
Ok(AssetId(bytes))
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn identifies_native_avax_asset_for_network() {
+ let mainnet = AssetId::try_from(Bytes::copy_from_slice(&MAINNET_AVAX_ASSET_ID)).unwrap();
+ let fuji = AssetId::try_from(Bytes::copy_from_slice(&FUJI_AVAX_ASSET_ID)).unwrap();
+
+ assert!(mainnet.is_native_avax(MAINNET_NETWORK_ID));
+ assert!(!mainnet.is_native_avax(FUJI_NETWORK_ID));
+ assert!(fuji.is_native_avax(FUJI_NETWORK_ID));
+ assert!(!fuji.is_native_avax(MAINNET_NETWORK_ID));
+ assert!(!fuji.is_native_avax(999));
+ }
+}
diff --git a/rust/apps/avalanche/src/transactions/base_tx.rs b/rust/apps/avalanche/src/transactions/base_tx.rs
index dbda5d7..9ff25e8 100644
--- a/rust/apps/avalanche/src/transactions/base_tx.rs
+++ b/rust/apps/avalanche/src/transactions/base_tx.rs
@@ -1,4 +1,6 @@
-use super::structs::{AvaxFromToInfo, AvaxMethodInfo, AvaxTxInfo, LengthPrefixedVec};
+use super::structs::{
+ AvaxFromToInfo, AvaxMethodInfo, AvaxTxInfo, LengthPrefixedVec, ParsedSizeAble,
+};
use super::transferable::{TransferableInput, TransferableOutput};
use super::tx_header::Header;
use super::type_id::TypeId;
@@ -40,6 +42,30 @@ impl BaseTx {
pub fn parsed_size(&self) -> usize {
self.tx_size
}
+
+ pub fn validate_native_avax_assets(&self) -> Result<()> {
+ let network_id = self.tx_header.get_network_id();
+ if !self
+ .outputs
+ .iter()
+ .all(|output| output.asset_id().is_native_avax(network_id))
+ || !self
+ .inputs
+ .iter()
+ .all(|input| input.asset_id().is_native_avax(network_id))
+ {
+ return Err(AvaxError::InvalidTransaction(
+ "unsupported non-AVAX asset".to_string(),
+ ));
+ }
+ Ok(())
+ }
+}
+
+impl ParsedSizeAble for BaseTx {
+ fn parsed_size(&self) -> usize {
+ self.tx_size
+ }
}
pub fn avax_base_sign(
@@ -133,7 +159,7 @@ impl TryFrom<Bytes> for BaseTx {
let memo = bytes.split_to(memo_len as usize).to_vec();
let tx_size = initial_len - bytes.len();
- Ok(BaseTx {
+ let transaction = BaseTx {
codec_id,
type_id,
tx_header,
@@ -142,7 +168,9 @@ impl TryFrom<Bytes> for BaseTx {
memo_len,
memo,
tx_size,
- })
+ };
+ transaction.validate_native_avax_assets()?;
+ Ok(transaction)
}
}
diff --git a/rust/apps/avalanche/src/transactions/c_chain/evm_export.rs b/rust/apps/avalanche/src/transactions/c_chain/evm_export.rs
index f09d735..411a0c0 100644
--- a/rust/apps/avalanche/src/transactions/c_chain/evm_export.rs
+++ b/rust/apps/avalanche/src/transactions/c_chain/evm_export.rs
@@ -63,6 +63,16 @@ pub struct ExportTx {
outputs: LengthPrefixedVec<TransferableOutput>,
}
+impl ParsedSizeAble for ExportTx {
+ fn parsed_size(&self) -> usize {
+ 2 + 4
+ + self.tx_header.parsed_size()
+ + BLOCKCHAIN_ID_LEN
+ + self.inputs.parsed_size()
+ + self.outputs.parsed_size()
+ }
+}
+
impl AvaxTxInfo for ExportTx {
fn get_total_input_amount(&self) -> u64 {
self.inputs
@@ -128,14 +138,29 @@ impl TryFrom<Bytes> for ExportTx {
let outputs = LengthPrefixedVec::<TransferableOutput>::try_from(bytes.clone())?;
bytes.advance(outputs.parsed_size());
- Ok(ExportTx {
+ let transaction = ExportTx {
codec_id,
type_id,
tx_header,
dest_chain,
inputs,
outputs,
- })
+ };
+ let network_id = transaction.tx_header.get_network_id();
+ if !transaction
+ .inputs
+ .iter()
+ .all(|input| input.asset_id.is_native_avax(network_id))
+ || !transaction
+ .outputs
+ .iter()
+ .all(|output| output.asset_id().is_native_avax(network_id))
+ {
+ return Err(AvaxError::InvalidTransaction(
+ "unsupported non-AVAX asset".to_string(),
+ ));
+ }
+ Ok(transaction)
}
}
diff --git a/rust/apps/avalanche/src/transactions/c_chain/evm_import.rs b/rust/apps/avalanche/src/transactions/c_chain/evm_import.rs
index e3513f9..2c0f76a 100644
--- a/rust/apps/avalanche/src/transactions/c_chain/evm_import.rs
+++ b/rust/apps/avalanche/src/transactions/c_chain/evm_import.rs
@@ -53,6 +53,16 @@ pub struct ImportTx {
outputs: LengthPrefixedVec<EvmOutput>,
}
+impl ParsedSizeAble for ImportTx {
+ fn parsed_size(&self) -> usize {
+ 2 + 4
+ + self.tx_header.parsed_size()
+ + BLOCKCHAIN_ID_LEN
+ + self.inputs.parsed_size()
+ + self.outputs.parsed_size()
+ }
+}
+
impl AvaxTxInfo for ImportTx {
fn get_total_input_amount(&self) -> u64 {
self.inputs
@@ -118,14 +128,29 @@ impl TryFrom<Bytes> for ImportTx {
let outputs = LengthPrefixedVec::<EvmOutput>::try_from(bytes.clone())?;
bytes.advance(outputs.parsed_size());
- Ok(ImportTx {
+ let transaction = ImportTx {
codec_id,
type_id,
tx_header,
source_chain,
inputs,
outputs,
- })
+ };
+ let network_id = transaction.tx_header.get_network_id();
+ if !transaction
+ .inputs
+ .iter()
+ .all(|input| input.asset_id().is_native_avax(network_id))
+ || !transaction
+ .outputs
+ .iter()
+ .all(|output| output.asset_id.is_native_avax(network_id))
+ {
+ return Err(AvaxError::InvalidTransaction(
+ "unsupported non-AVAX asset".to_string(),
+ ));
+ }
+ Ok(transaction)
}
}
diff --git a/rust/apps/avalanche/src/transactions/export.rs b/rust/apps/avalanche/src/transactions/export.rs
index 9924855..be12071 100644
--- a/rust/apps/avalanche/src/transactions/export.rs
+++ b/rust/apps/avalanche/src/transactions/export.rs
@@ -1,5 +1,7 @@
use super::base_tx::BaseTx;
-use super::structs::{AvaxFromToInfo, AvaxMethodInfo, AvaxTxInfo, LengthPrefixedVec};
+use super::structs::{
+ AvaxFromToInfo, AvaxMethodInfo, AvaxTxInfo, LengthPrefixedVec, ParsedSizeAble,
+};
use crate::constants::*;
use crate::errors::{AvaxError, Result};
use crate::transactions::{transferable::TransferableOutput, type_id::TypeId};
@@ -18,6 +20,12 @@ pub struct ExportTx {
transfer_out: LengthPrefixedVec<TransferableOutput>,
}
+impl ParsedSizeAble for ExportTx {
+ fn parsed_size(&self) -> usize {
+ self.base_tx.parsed_size() + BLOCKCHAIN_ID_LEN + self.transfer_out.parsed_size()
+ }
+}
+
impl AvaxTxInfo for ExportTx {
fn get_total_input_amount(&self) -> u64 {
self.base_tx.get_total_input_amount()
@@ -94,10 +102,20 @@ impl TryFrom<Bytes> for ExportTx {
let mut dest_chain = [0u8; BLOCKCHAIN_ID_LEN];
bytes.copy_to_slice(&mut dest_chain);
+ let transfer_out = LengthPrefixedVec::<TransferableOutput>::try_from(bytes.clone())?;
+ let network_id = base_tx.tx_header.get_network_id();
+ if !transfer_out
+ .iter()
+ .all(|output| output.asset_id().is_native_avax(network_id))
+ {
+ return Err(AvaxError::InvalidTransaction(
+ "unsupported non-AVAX asset".to_string(),
+ ));
+ }
Ok(ExportTx {
base_tx,
dest_chain,
- transfer_out: LengthPrefixedVec::<TransferableOutput>::try_from(bytes.clone())?,
+ transfer_out,
})
}
}
diff --git a/rust/apps/avalanche/src/transactions/import.rs b/rust/apps/avalanche/src/transactions/import.rs
index c120a5d..4170c94 100644
--- a/rust/apps/avalanche/src/transactions/import.rs
+++ b/rust/apps/avalanche/src/transactions/import.rs
@@ -1,5 +1,7 @@
use super::base_tx::BaseTx;
-use super::structs::{AvaxFromToInfo, AvaxMethodInfo, AvaxTxInfo, LengthPrefixedVec};
+use super::structs::{
+ AvaxFromToInfo, AvaxMethodInfo, AvaxTxInfo, LengthPrefixedVec, ParsedSizeAble,
+};
use super::transferable::TransferableInput;
use crate::constants::*;
use crate::errors::{AvaxError, Result};
@@ -17,6 +19,12 @@ pub struct ImportTx {
transfer_in: LengthPrefixedVec<TransferableInput>,
}
+impl ParsedSizeAble for ImportTx {
+ fn parsed_size(&self) -> usize {
+ self.base_tx.parsed_size() + BLOCKCHAIN_ID_LEN + self.transfer_in.parsed_size()
+ }
+}
+
impl AvaxTxInfo for ImportTx {
fn get_total_input_amount(&self) -> u64 {
self.transfer_in
@@ -69,10 +77,20 @@ impl TryFrom<Bytes> for ImportTx {
bytes.advance(base_tx.parsed_size());
let mut source_chain = [0u8; BLOCKCHAIN_ID_LEN];
bytes.copy_to_slice(&mut source_chain);
+ let transfer_in = LengthPrefixedVec::<TransferableInput>::try_from(bytes)?;
+ let network_id = base_tx.tx_header.get_network_id();
+ if !transfer_in
+ .iter()
+ .all(|input| input.asset_id().is_native_avax(network_id))
+ {
+ return Err(AvaxError::InvalidTransaction(
+ "unsupported non-AVAX asset".to_string(),
+ ));
+ }
Ok(ImportTx {
base_tx,
source_chain,
- transfer_in: LengthPrefixedVec::<TransferableInput>::try_from(bytes)?,
+ transfer_in,
})
}
}
diff --git a/rust/apps/avalanche/src/transactions/p_chain/add_permissionless_delegator.rs b/rust/apps/avalanche/src/transactions/p_chain/add_permissionless_delegator.rs
index 68b6278..d123fab 100644
--- a/rust/apps/avalanche/src/transactions/p_chain/add_permissionless_delegator.rs
+++ b/rust/apps/avalanche/src/transactions/p_chain/add_permissionless_delegator.rs
@@ -23,6 +23,16 @@ pub struct AddPermissionlessDelegatorTx {
delegator_owner: OutputOwner,
}
+impl ParsedSizeAble for AddPermissionlessDelegatorTx {
+ fn parsed_size(&self) -> usize {
+ self.base_tx.parsed_size()
+ + self.validator.parsed_size()
+ + SUBNET_ID_LEN
+ + self.stake_out.parsed_size()
+ + self.delegator_owner.parsed_size()
+ }
+}
+
impl AvaxTxInfo for AddPermissionlessDelegatorTx {
fn get_total_input_amount(&self) -> u64 {
self.base_tx.get_total_input_amount()
@@ -96,13 +106,24 @@ impl TryFrom<Bytes> for AddPermissionlessDelegatorTx {
let delegator_owner = OutputOwner::try_from(bytes.clone())?;
bytes.advance(delegator_owner.parsed_size());
- Ok(AddPermissionlessDelegatorTx {
+ let transaction = AddPermissionlessDelegatorTx {
base_tx,
validator,
subnet_id,
stake_out,
delegator_owner,
- })
+ };
+ let network_id = transaction.base_tx.tx_header.get_network_id();
+ if !transaction
+ .stake_out
+ .iter()
+ .all(|output| output.asset_id().is_native_avax(network_id))
+ {
+ return Err(AvaxError::InvalidTransaction(
+ "unsupported non-AVAX asset".to_string(),
+ ));
+ }
+ Ok(transaction)
}
}
diff --git a/rust/apps/avalanche/src/transactions/p_chain/add_permissionless_validator.rs b/rust/apps/avalanche/src/transactions/p_chain/add_permissionless_validator.rs
index 8fb2d62..3402fe6 100644
--- a/rust/apps/avalanche/src/transactions/p_chain/add_permissionless_validator.rs
+++ b/rust/apps/avalanche/src/transactions/p_chain/add_permissionless_validator.rs
@@ -28,6 +28,20 @@ pub struct AddPermissionlessValidatorTx {
delegator_share: u32,
}
+impl ParsedSizeAble for AddPermissionlessValidatorTx {
+ fn parsed_size(&self) -> usize {
+ self.base_tx.parsed_size()
+ + self.validator.parsed_size()
+ + SUBNET_ID_LEN
+ + PROOF_OF_POSESSION_PUBKEY_LEN
+ + PROOF_OF_POSESSION_SIGNATURE_LEN
+ + self.stake_out.parsed_size()
+ + self.validator_owner.parsed_size()
+ + self.delegator_owner.parsed_size()
+ + 4
+ }
+}
+
impl AvaxTxInfo for AddPermissionlessValidatorTx {
fn get_total_input_amount(&self) -> u64 {
self.base_tx.get_total_input_amount()
@@ -109,7 +123,7 @@ impl TryFrom<Bytes> for AddPermissionlessValidatorTx {
let delegator_share = bytes.get_u32();
- Ok(AddPermissionlessValidatorTx {
+ let transaction = AddPermissionlessValidatorTx {
base_tx,
validator,
subnet_id,
@@ -118,7 +132,18 @@ impl TryFrom<Bytes> for AddPermissionlessValidatorTx {
validator_owner,
delegator_owner,
delegator_share,
- })
+ };
+ let network_id = transaction.base_tx.tx_header.get_network_id();
+ if !transaction
+ .stake_out
+ .iter()
+ .all(|output| output.asset_id().is_native_avax(network_id))
+ {
+ return Err(AvaxError::InvalidTransaction(
+ "unsupported non-AVAX asset".to_string(),
+ ));
+ }
+ Ok(transaction)
}
}
diff --git a/rust/apps/avalanche/src/transactions/structs.rs b/rust/apps/avalanche/src/transactions/structs.rs
index c52301b..8bdd895 100644
--- a/rust/apps/avalanche/src/transactions/structs.rs
+++ b/rust/apps/avalanche/src/transactions/structs.rs
@@ -52,11 +52,25 @@ where
let len = bytes.get_u32() as usize;
+ // Every encoded item consumes at least one byte. Reject impossible
+ // counts before reserving attacker-controlled capacity.
+ if len > bytes.len() {
+ return Err(AvaxError::InvalidHex(
+ "LengthPrefixedVec count exceeds remaining data".to_string(),
+ ));
+ }
+
let mut items = Vec::with_capacity(len);
for _ in 0..len {
let item = T::try_from(bytes.clone())?;
- bytes.advance(item.parsed_size());
+ let parsed_size = item.parsed_size();
+ if parsed_size == 0 || parsed_size > bytes.len() {
+ return Err(AvaxError::InvalidHex(
+ "Invalid LengthPrefixedVec item size".to_string(),
+ ));
+ }
+ bytes.advance(parsed_size);
items.push(item);
}
@@ -73,23 +87,24 @@ pub trait AvaxTxInfo {
}
fn get_output_amount(&self, address: String, type_id: TypeId) -> u64 {
- let left_amount = self.get_total_input_amount() - self.get_import_tx_fee();
+ let left_amount = self
+ .get_total_input_amount()
+ .saturating_sub(self.get_import_tx_fee());
match type_id {
TypeId::PchainImportTx | TypeId::XchainImportTx => left_amount,
- _ => {
- left_amount
- - self
- .get_outputs_addresses()
- .iter()
- .find(|info| info.address[0] == address)
- .map(|info| info.amount)
- .unwrap_or(0)
- }
+ _ => left_amount.saturating_sub(
+ self.get_outputs_addresses()
+ .iter()
+ .find(|info| info.address.iter().any(|item| item == &address))
+ .map(|info| info.amount)
+ .unwrap_or(0),
+ ),
}
}
fn get_fee_amount(&self) -> u64 {
- self.get_total_input_amount() - self.get_total_output_amount()
+ self.get_total_input_amount()
+ .saturating_sub(self.get_total_output_amount())
}
fn get_outputs_addresses(&self) -> Vec<AvaxFromToInfo>;
fn get_network(&self) -> Option<String> {
diff --git a/rust/apps/avalanche/src/transactions/transferable.rs b/rust/apps/avalanche/src/transactions/transferable.rs
index 49ab7fd..c1fb875 100644
--- a/rust/apps/avalanche/src/transactions/transferable.rs
+++ b/rust/apps/avalanche/src/transactions/transferable.rs
@@ -125,6 +125,10 @@ impl TransferableInput {
pub fn get_amount(&self) -> u64 {
self.input.get_amount()
}
+
+ pub fn asset_id(&self) -> &AssetId {
+ &self.asset_id
+ }
}
impl TryFrom<Bytes> for TransferableInput {
diff --git a/rust/apps/bitcoin/src/lib.rs b/rust/apps/bitcoin/src/lib.rs
index 28355b6..9231a58 100644
--- a/rust/apps/bitcoin/src/lib.rs
+++ b/rust/apps/bitcoin/src/lib.rs
@@ -169,6 +169,7 @@ mod test {
.collect(),
network: $network.to_string(),
fee_larger_than_amount: $fee_larger_than_amount,
+ is_large_fee: false,
sign_status: Some("Unsigned".to_string()),
is_multisig: false,
need_sign: true,
diff --git a/rust/apps/bitcoin/src/transactions/legacy/parser.rs b/rust/apps/bitcoin/src/transactions/legacy/parser.rs
index 85b494e..220a414 100644
--- a/rust/apps/bitcoin/src/transactions/legacy/parser.rs
+++ b/rust/apps/bitcoin/src/transactions/legacy/parser.rs
@@ -24,7 +24,7 @@ impl TxParser for TxData {
.map(|each| self.parse_raw_tx_output(each))
.collect();
let network = Network::from_str(&self.network)?;
- self.normalize(mapped_inputs?, mapped_outputs?, &network, false)
+ self.normalize(mapped_inputs?, mapped_outputs?, &network, false, None)
}
fn determine_network(&self) -> Result<Network> {
Network::from_str(&self.network)
diff --git a/rust/apps/bitcoin/src/transactions/parsed_tx.rs b/rust/apps/bitcoin/src/transactions/parsed_tx.rs
index 55d07c9..857cc59 100644
--- a/rust/apps/bitcoin/src/transactions/parsed_tx.rs
+++ b/rust/apps/bitcoin/src/transactions/parsed_tx.rs
@@ -60,6 +60,7 @@ pub struct OverviewTx {
pub to: Vec<OverviewTo>,
pub network: String,
pub fee_larger_than_amount: bool,
+ pub is_large_fee: bool,
pub is_multisig: bool,
pub sign_status: Option<String>,
pub need_sign: bool,
@@ -214,6 +215,7 @@ pub trait TxParser {
outputs: Vec<ParsedOutput>,
network: &dyn NetworkT,
has_witness_only_inputs: bool,
+ estimated_signed_vbytes: Option<u64>,
) -> Result<ParsedTx> {
let total_input_value = inputs.iter().fold(0, |acc, cur| acc + cur.value);
let total_output_value = outputs.iter().fold(0, |acc, cur| acc + cur.value);
@@ -232,6 +234,15 @@ pub trait TxParser {
has_unlocked_outputs || (has_anyone_can_pay && total_input_value < total_output_value);
let fee_is_lower_bound = has_anyone_can_pay && !fee_is_unknown;
let fee = total_input_value.saturating_sub(total_output_value);
+ const LARGE_FEE_SATS: u64 = 5_000_000;
+ const LARGE_FEE_RATE_SAT_PER_VBYTE: u64 = 100;
+ let is_large_fee = !fee_is_unknown
+ && (fee > LARGE_FEE_SATS
+ || estimated_signed_vbytes
+ .filter(|vbytes| *vbytes > 0)
+ .is_some_and(|vbytes| {
+ fee > LARGE_FEE_RATE_SAT_PER_VBYTE.saturating_mul(vbytes)
+ }));
let fee_amount = Self::format_amount(fee, network);
let fee_sat = Self::format_sat(fee);
let overview_amount = outputs.iter().fold(0, |acc, cur| {
@@ -277,6 +288,7 @@ pub trait TxParser {
to: overview_to,
network: network.normalize(),
fee_larger_than_amount: fee > overview_amount,
+ is_large_fee,
is_multisig: inputs.iter().any(|v| v.is_multisig),
need_sign: Self::is_need_sign(&inputs),
};
@@ -438,6 +450,7 @@ mod tests {
vec![build_output(600)],
&Network::Bitcoin,
false,
+ None,
)
.unwrap();
@@ -459,6 +472,7 @@ mod tests {
vec![build_output(1_000)],
&Network::Bitcoin,
false,
+ None,
)
.unwrap();
@@ -481,6 +495,7 @@ mod tests {
vec![build_output(600)],
&Network::Bitcoin,
false,
+ None,
)
.unwrap();
@@ -494,4 +509,40 @@ mod tests {
assert_eq!("400 sats", parsed.detail.fee_sat);
}
}
+
+ #[test]
+ fn test_large_fee_thresholds() {
+ let absolute_large = DummyParser
+ .normalize(
+ vec![build_input_with_value(6_000_001, 0x01)],
+ vec![build_output(1_000_000)],
+ &Network::Bitcoin,
+ false,
+ None,
+ )
+ .unwrap();
+ assert!(absolute_large.overview.is_large_fee);
+
+ let rate_large = DummyParser
+ .normalize(
+ vec![build_input_with_value(20_001, 0x01)],
+ vec![build_output(10_000)],
+ &Network::Bitcoin,
+ false,
+ Some(100),
+ )
+ .unwrap();
+ assert!(rate_large.overview.is_large_fee);
+
+ let exact_limits = DummyParser
+ .normalize(
+ vec![build_input_with_value(5_010_000, 0x01)],
+ vec![build_output(10_000)],
+ &Network::Bitcoin,
+ false,
+ Some(50_000),
+ )
+ .unwrap();
+ assert!(!exact_limits.overview.is_large_fee);
+ }
}
diff --git a/rust/apps/bitcoin/src/transactions/psbt/parsed_psbt.rs b/rust/apps/bitcoin/src/transactions/psbt/parsed_psbt.rs
index f4ea652..bcc8f21 100644
--- a/rust/apps/bitcoin/src/transactions/psbt/parsed_psbt.rs
+++ b/rust/apps/bitcoin/src/transactions/psbt/parsed_psbt.rs
@@ -33,12 +33,23 @@ impl TxParser for WrappedPsbt {
.inputs
.iter()
.any(|input| input.witness_utxo.is_some() && input.non_witness_utxo.is_none());
+ let estimated_signed_vbytes = self.estimated_signed_vbytes();
match self.identify_fractal_bitcoin_tx() {
- Some(custom_net) => {
- self.normalize(inputs, outputs, &custom_net, has_witness_only_inputs)
- }
- None => self.normalize(inputs, outputs, &network, has_witness_only_inputs),
+ Some(custom_net) => self.normalize(
+ inputs,
+ outputs,
+ &custom_net,
+ has_witness_only_inputs,
+ estimated_signed_vbytes,
+ ),
+ None => self.normalize(
+ inputs,
+ outputs,
+ &network,
+ has_witness_only_inputs,
+ estimated_signed_vbytes,
+ ),
}
}
diff --git a/rust/apps/bitcoin/src/transactions/psbt/wrapped_psbt.rs b/rust/apps/bitcoin/src/transactions/psbt/wrapped_psbt.rs
index 52c0d51..31d00db 100644
--- a/rust/apps/bitcoin/src/transactions/psbt/wrapped_psbt.rs
+++ b/rust/apps/bitcoin/src/transactions/psbt/wrapped_psbt.rs
@@ -22,6 +22,7 @@ use bitcoin::psbt::{GetKey, KeyRequest, Psbt};
use bitcoin::psbt::{Input, Output};
use bitcoin::secp256k1::{Secp256k1, Signing, XOnlyPublicKey};
use bitcoin::taproot::TapLeafHash;
+use bitcoin::transaction::{predict_weight, InputWeightPrediction};
use bitcoin::{secp256k1, NetworkKind};
use bitcoin::{Network, PrivateKey};
use bitcoin::{PublicKey, ScriptBuf, TxOut};
@@ -74,6 +75,65 @@ impl GetKey for Keystore {
}
impl WrappedPsbt {
+ fn input_weight_prediction(
+ &self,
+ input: &Input,
+ index: usize,
+ ) -> Option<InputWeightPrediction> {
+ let prevout = self.get_input_prevout(input, index).ok()?;
+ let script = &prevout.script_pubkey;
+ if script.is_p2pkh() {
+ Some(InputWeightPrediction::P2PKH_COMPRESSED_MAX)
+ } else if script.is_p2wpkh() {
+ Some(InputWeightPrediction::P2WPKH_MAX)
+ } else if script.is_p2sh()
+ && input
+ .redeem_script
+ .as_ref()
+ .is_some_and(|redeem| redeem.is_p2wpkh())
+ && input.witness_script.is_none()
+ {
+ Some(InputWeightPrediction::new(23, [72usize, 33usize]))
+ } else if script.is_p2tr() && input.tap_scripts.is_empty() {
+ Some(if input.sighash_type.is_some() {
+ InputWeightPrediction::P2TR_KEY_NON_DEFAULT_SIGHASH
+ } else {
+ InputWeightPrediction::P2TR_KEY_DEFAULT_SIGHASH
+ })
+ } else {
+ // Multisig/script-path vsize cannot be estimated reliably here. The absolute-fee
+ // warning still applies without guessing their fee rate.
+ None
+ }
+ }
+
+ pub(crate) fn estimated_signed_vbytes(&self) -> Option<u64> {
+ if self
+ .psbt
+ .inputs
+ .iter()
+ .enumerate()
+ .any(|(index, input)| self.input_weight_prediction(input, index).is_none())
+ {
+ return None;
+ }
+ Some(
+ predict_weight(
+ self.psbt
+ .inputs
+ .iter()
+ .enumerate()
+ .filter_map(|(index, input)| self.input_weight_prediction(input, index)),
+ self.psbt
+ .unsigned_tx
+ .output
+ .iter()
+ .map(|output| output.script_pubkey.len()),
+ )
+ .to_vbytes_ceil(),
+ )
+ }
+
pub fn sign(&mut self, seed: &[u8], mfp: Fingerprint) -> Result<Psbt> {
let k = Keystore {
mfp,
@@ -172,8 +232,14 @@ impl WrappedPsbt {
// not my input
return Ok(false);
}
- self.check_my_input_derivation(input, index)?;
- self.check_my_input_script(input, index)?;
+ if context.multisig_wallet_config.is_some() {
+ self.check_my_multisig_input_key_and_script(input, index, context)?;
+ } else if context.verify_code.is_some() {
+ self.check_legacy_multisig_input_script(input, index)?;
+ } else {
+ self.check_my_input_derivation(input, index)?;
+ self.check_my_input_script(input, index)?;
+ }
self.check_my_input_signature(input, index, context)?;
self.check_my_input_value_tampered(input, index)?;
self.check_my_wallet_type(input, context)?;
@@ -189,6 +255,18 @@ impl WrappedPsbt {
}
fn check_my_wallet_type(&self, input: &Input, context: &ParseContext) -> Result<()> {
+ if let Some(config) = &context.multisig_wallet_config {
+ if context
+ .verify_code
+ .as_ref()
+ .is_some_and(|verify_code| verify_code != &config.verify_code)
+ {
+ return Err(BitcoinError::WalletTypeError(
+ "multisig wallet config does not match verify code".to_string(),
+ ));
+ }
+ return Ok(());
+ }
let input_verify_code = self.get_my_input_verify_code(input);
match &context.verify_code {
//single sig
@@ -229,6 +307,233 @@ impl WrappedPsbt {
Ok(())
}
+ fn check_my_multisig_input_key_and_script(
+ &self,
+ input: &Input,
+ index: usize,
+ context: &ParseContext,
+ ) -> Result<()> {
+ let config = context
+ .multisig_wallet_config
+ .as_ref()
+ .ok_or(BitcoinError::InvalidInput)?;
+ if self.is_taproot_input(input) {
+ return Err(BitcoinError::InvalidPsbt(
+ "multisig with taproot is not supported".to_string(),
+ ));
+ }
+ if config.xpub_items.len() as u32 != config.total {
+ return Err(BitcoinError::InvalidTransaction(format!(
+ "invalid input #{index}, current multisig wallet configuration is incomplete"
+ )));
+ }
+
+ let mut expected_suffix: Option<DerivationPath> = None;
+ for (cosigner_index, xpub_item) in config.xpub_items.iter().enumerate() {
+ let parent_path_text =
+ config
+ .get_derivation_by_index(cosigner_index)
+ .ok_or_else(|| {
+ BitcoinError::InvalidTransaction(format!(
+ "invalid input #{index}, missing cosigner derivation"
+ ))
+ })?;
+ let parent_path = DerivationPath::from_str(&parent_path_text).map_err(|_| {
+ BitcoinError::InvalidTransaction(format!(
+ "invalid input #{index}, cannot parse cosigner derivation"
+ ))
+ })?;
+ let xpub = Xpub::from_str(&xpub_item.xpub).map_err(|_| {
+ BitcoinError::InvalidTransaction(format!(
+ "invalid input #{index}, cannot parse cosigner xpub"
+ ))
+ })?;
+
+ for (claimed_key, (fingerprint, path)) in input.bip32_derivation.iter() {
+ if !xpub_item.xfp.eq_ignore_ascii_case(&fingerprint.to_string()) {
+ continue;
+ }
+ let Some(suffix) = Self::derivation_suffix(&parent_path, path) else {
+ continue;
+ };
+ let derived_key = derive_public_key_by_path(&xpub, &parent_path, path)?;
+ if &derived_key != claimed_key {
+ continue;
+ }
+ if let Some(expected) = &expected_suffix {
+ if expected != &suffix {
+ return Err(BitcoinError::InvalidTransaction(format!(
+ "invalid input #{index}, cosigner derivation suffixes do not match"
+ )));
+ }
+ }
+ expected_suffix = Some(suffix);
+ }
+ }
+
+ let suffix = expected_suffix.ok_or_else(|| {
+ BitcoinError::InvalidTransaction(format!(
+ "invalid input #{index}, no derivation matches current multisig wallet"
+ ))
+ })?;
+ let mut derived_keys = Vec::with_capacity(config.total as usize);
+ for (cosigner_index, xpub_item) in config.xpub_items.iter().enumerate() {
+ let parent_path_text =
+ config
+ .get_derivation_by_index(cosigner_index)
+ .ok_or_else(|| {
+ BitcoinError::InvalidTransaction(format!(
+ "invalid input #{index}, missing cosigner derivation"
+ ))
+ })?;
+ let parent_path = DerivationPath::from_str(&parent_path_text).map_err(|_| {
+ BitcoinError::InvalidTransaction(format!(
+ "invalid input #{index}, cannot parse cosigner derivation"
+ ))
+ })?;
+ let xpub = Xpub::from_str(&xpub_item.xpub).map_err(|_| {
+ BitcoinError::InvalidTransaction(format!(
+ "invalid input #{index}, cannot parse cosigner xpub"
+ ))
+ })?;
+ let child_path = parent_path.extend(suffix.as_ref());
+ let key = derive_public_key_by_path(&xpub, &parent_path, &child_path)?;
+ derived_keys.push(bitcoin::PublicKey::new(key));
+ }
+
+ derived_keys.sort();
+ let mut builder = bitcoin::script::Builder::new().push_int(config.threshold as i64);
+ for key in derived_keys.iter() {
+ builder = builder.push_key(key);
+ }
+ let multisig_script = builder
+ .push_int(config.total as i64)
+ .push_opcode(bitcoin::opcodes::all::OP_CHECKMULTISIG)
+ .into_script();
+
+ let format = MultiSigFormat::from(&config.format)?;
+ let expected_script_pubkey = match format {
+ MultiSigFormat::P2sh => {
+ if input.redeem_script.as_ref() != Some(&multisig_script)
+ || input.witness_script.is_some()
+ {
+ return Err(BitcoinError::InvalidTransaction(format!(
+ "invalid input #{index}, P2SH redeem script does not match current wallet"
+ )));
+ }
+ ScriptBuf::new_p2sh(&multisig_script.script_hash())
+ }
+ MultiSigFormat::P2wsh => {
+ if input.witness_script.as_ref() != Some(&multisig_script)
+ || input.redeem_script.is_some()
+ {
+ return Err(BitcoinError::InvalidTransaction(format!(
+ "invalid input #{index}, P2WSH witness script does not match current wallet"
+ )));
+ }
+ ScriptBuf::new_p2wsh(&multisig_script.wscript_hash())
+ }
+ MultiSigFormat::P2wshP2sh => {
+ let expected_redeem_script = ScriptBuf::new_p2wsh(&multisig_script.wscript_hash());
+ if input.witness_script.as_ref() != Some(&multisig_script)
+ || input.redeem_script.as_ref() != Some(&expected_redeem_script)
+ {
+ return Err(BitcoinError::InvalidTransaction(format!(
+ "invalid input #{index}, nested multisig scripts do not match current wallet"
+ )));
+ }
+ ScriptBuf::new_p2sh(&expected_redeem_script.script_hash())
+ }
+ };
+
+ if self.get_input_prevout(input, index)?.script_pubkey != expected_script_pubkey {
+ return Err(BitcoinError::InvalidTransaction(format!(
+ "invalid input #{index}, previous output does not belong to current multisig wallet"
+ )));
+ }
+ Ok(())
+ }
+
+ fn check_legacy_multisig_input_script(&self, input: &Input, index: usize) -> Result<()> {
+ let (multisig_script, format) = self.get_multi_sig_script_and_format(input)?;
+ if !multisig_script.is_multisig() {
+ return Err(BitcoinError::InvalidTransaction(format!(
+ "invalid input #{index}, legacy multisig script is not multisig"
+ )));
+ }
+ let expected_script_pubkey = match format {
+ MultiSigFormat::P2sh => ScriptBuf::new_p2sh(&multisig_script.script_hash()),
+ MultiSigFormat::P2wsh => ScriptBuf::new_p2wsh(&multisig_script.wscript_hash()),
+ MultiSigFormat::P2wshP2sh => {
+ let expected_redeem_script = ScriptBuf::new_p2wsh(&multisig_script.wscript_hash());
+ if input.redeem_script.as_ref() != Some(&expected_redeem_script) {
+ return Err(BitcoinError::InvalidTransaction(format!(
+ "invalid input #{index}, nested multisig redeem script is inconsistent"
+ )));
+ }
+ ScriptBuf::new_p2sh(&expected_redeem_script.script_hash())
+ }
+ };
+ if self.get_input_prevout(input, index)?.script_pubkey != expected_script_pubkey {
+ return Err(BitcoinError::InvalidTransaction(format!(
+ "invalid input #{index}, multisig script does not match previous output"
+ )));
+ }
+ Ok(())
+ }
+
+ fn derivation_suffix(
+ parent_path: &DerivationPath,
+ child_path: &DerivationPath,
+ ) -> Option<DerivationPath> {
+ if !child_path.as_ref().starts_with(parent_path.as_ref()) {
+ return None;
+ }
+ Some(DerivationPath::from(
+ &child_path.as_ref()[parent_path.len()..],
+ ))
+ }
+
+ fn get_input_prevout<'a>(&'a self, input: &'a Input, index: usize) -> Result<&'a TxOut> {
+ let tx_in = self
+ .psbt
+ .unsigned_tx
+ .input
+ .get(index)
+ .ok_or(BitcoinError::InvalidInput)?;
+ let non_witness_prevout = if let Some(prev_tx) = &input.non_witness_utxo {
+ if tx_in.previous_output.txid != prev_tx.compute_txid() {
+ return Err(BitcoinError::InvalidTransaction(format!(
+ "invalid input #{index}, non-witness UTXO txid does not match prevout"
+ )));
+ }
+ Some(
+ prev_tx
+ .output
+ .get(tx_in.previous_output.vout as usize)
+ .ok_or(BitcoinError::InvalidInput)?,
+ )
+ } else {
+ None
+ };
+
+ if let Some(witness_prevout) = &input.witness_utxo {
+ if let Some(prevout) = non_witness_prevout {
+ if prevout != witness_prevout {
+ return Err(BitcoinError::InputValueTampered(format!(
+ "input #{index}'s witness and non-witness UTXO do not match"
+ )));
+ }
+ }
+ return Ok(witness_prevout);
+ }
+ non_witness_prevout.ok_or_else(|| {
+ BitcoinError::InvalidTransaction(format!(
+ "invalid input #{index}, missing previous output"
+ ))
+ })
+ }
+
pub fn check_my_input_signature(
&self,
input: &Input,
@@ -1091,6 +1396,9 @@ mod tests {
use either::Left;
use hex::{self, FromHex, ToHex};
+ use crate::multi_sig::wallet::{MultiSigWalletConfig, MultiSigXPubItem};
+ use crate::multi_sig::Network as MultiSigNetwork;
+
use super::*;
fn empty_psbt() -> Psbt {
@@ -1271,6 +1579,117 @@ mod tests {
}
}
+ #[test]
+ fn test_multisig_wallet_config_validates_input_script() {
+ let psbt_hex = "70736274ff01005e0200000001d8d89245a905abe9e2ab7bb834ebbc50a75947c82f96eeec7b38e0b399a62c490000000000fdffffff0158070000000000002200202b9710701f5c944606bb6bab82d2ef969677d8b9d04174f59e2a631812ae739bf76c27004f01043587cf0473f7e9418000000147f2d1b4bef083e346eb1949bcd8e2b59f95d8391a9eb4e1ea9005df926585480365fd7b1eca553df2c4e17bc5b88384ceda3d0d98fa3145cff5e61e471671a0b214c45358fa300000800100008000000080010000804f01043587cf04bac1483980000001a73adbe2878487634dcbfc3f7ebde8b1fc994f1ec06860cf01c3fe2ea791ddb602e62a2a9973ee6b3a7af47c229a5bde70bca59bd04bbb297f5693d7aa256b976d1473c5da0a3000008001000080000000800100008000010120110800000000000017a914980ec372495334ee232575505208c0b2e142dbb5872202032ed737f53936afb128247fc71a0b0b5be4d9348e9a48bfda9ef31efe3e45fa2e47304402203109d97095c61395881d6f75093943b16a91e1a4fff73bf193fcfe6e7689a35c02203bd187fed5bba45ee2c322911b8abb07f1d091520f5598259047d0dee058a75e01010304010000000104220020ffac81e598dd9856d08bd6c55b712fd23ea8522bd075fcf48ed467ced2ee015601054752210267ea4562439356307e786faf40503730d8d95a203a0e345cb355a5dfa03fce0321032ed737f53936afb128247fc71a0b0b5be4d9348e9a48bfda9ef31efe3e45fa2e52ae2206032ed737f53936afb128247fc71a0b0b5be4d9348e9a48bfda9ef31efe3e45fa2e1cc45358fa30000080010000800000008001000080000000000000000022060267ea4562439356307e786faf40503730d8d95a203a0e345cb355a5dfa03fce031c73c5da0a3000008001000080000000800100008000000000000000000000";
+ let psbt = Psbt::deserialize(&Vec::from_hex(psbt_hex).unwrap()).unwrap();
+ let mut derivations = Vec::new();
+ let mut xpub_items = Vec::new();
+ for (xpub, (fingerprint, path)) in psbt.xpub.iter() {
+ derivations.push(path.to_string());
+ xpub_items.push(MultiSigXPubItem {
+ xfp: fingerprint.to_string(),
+ xpub: xpub.to_string(),
+ });
+ }
+ let config = MultiSigWalletConfig {
+ creator: "test".to_string(),
+ name: "test".to_string(),
+ threshold: 2,
+ total: 2,
+ derivations,
+ format: "P2WSH-P2SH".to_string(),
+ xpub_items,
+ verify_code: "test".to_string(),
+ verify_without_mfp: String::new(),
+ config_text: String::new(),
+ network: MultiSigNetwork::TestNet,
+ };
+ let mut context = ParseContext {
+ master_fingerprint: Fingerprint::from_str("73c5da0a").unwrap(),
+ extended_public_keys: BTreeMap::new(),
+ verify_code: Some("test".to_string()),
+ multisig_wallet_config: Some(config),
+ };
+ let wrapper = WrappedPsbt { psbt };
+ let input = wrapper.psbt.inputs[0].clone();
+ assert!(wrapper.check_my_wallet_type(&input, &context).is_ok());
+ context.verify_code = Some("wrong".to_string());
+ assert!(wrapper.check_my_wallet_type(&input, &context).is_err());
+ context.verify_code = Some("test".to_string());
+ assert!(wrapper
+ .check_my_multisig_input_key_and_script(&input, 0, &context)
+ .is_ok());
+
+ let multisig_script = input.witness_script.clone().unwrap();
+ context.multisig_wallet_config.as_mut().unwrap().format = "P2WSH".to_string();
+ let mut native_input = input.clone();
+ native_input.redeem_script = None;
+ native_input.witness_utxo.as_mut().unwrap().script_pubkey =
+ ScriptBuf::new_p2wsh(&multisig_script.wscript_hash());
+ assert!(wrapper
+ .check_my_multisig_input_key_and_script(&native_input, 0, &context)
+ .is_ok());
+
+ context.multisig_wallet_config.as_mut().unwrap().format = "P2SH".to_string();
+ let mut legacy_input = input.clone();
+ legacy_input.redeem_script = Some(multisig_script.clone());
+ legacy_input.witness_script = None;
+ legacy_input.witness_utxo.as_mut().unwrap().script_pubkey =
+ ScriptBuf::new_p2sh(&multisig_script.script_hash());
+ assert!(wrapper
+ .check_my_multisig_input_key_and_script(&legacy_input, 0, &context)
+ .is_ok());
+ assert!(wrapper
+ .check_legacy_multisig_input_script(&native_input, 0)
+ .is_ok());
+ assert!(wrapper
+ .check_legacy_multisig_input_script(&legacy_input, 0)
+ .is_ok());
+ assert!(wrapper
+ .check_legacy_multisig_input_script(&input, 0)
+ .is_ok());
+
+ let mut standard_input = input.clone();
+ for script in [
+ "76a914111111111111111111111111111111111111111188ac",
+ "00142222222222222222222222222222222222222222",
+ "51203333333333333333333333333333333333333333333333333333333333333333",
+ ] {
+ standard_input.witness_utxo.as_mut().unwrap().script_pubkey =
+ ScriptBuf::from_hex(script).unwrap();
+ assert!(wrapper
+ .input_weight_prediction(&standard_input, 0)
+ .is_some());
+ }
+
+ context.multisig_wallet_config.as_mut().unwrap().total = 3;
+ assert!(wrapper
+ .check_my_multisig_input_key_and_script(&legacy_input, 0, &context)
+ .is_err());
+ context.multisig_wallet_config.as_mut().unwrap().total = 2;
+
+ let saved_xpub = context.multisig_wallet_config.as_ref().unwrap().xpub_items[0]
+ .xpub
+ .clone();
+ context.multisig_wallet_config.as_mut().unwrap().xpub_items[0].xpub = "invalid".to_string();
+ assert!(wrapper
+ .check_my_multisig_input_key_and_script(&legacy_input, 0, &context)
+ .is_err());
+ context.multisig_wallet_config.as_mut().unwrap().xpub_items[0].xpub = saved_xpub;
+
+ let mut missing_prevout = legacy_input.clone();
+ missing_prevout.witness_utxo = None;
+ assert!(wrapper.get_input_prevout(&missing_prevout, 0).is_err());
+ assert!(wrapper.get_input_prevout(&legacy_input, 1).is_err());
+
+ let mut tampered_input = input;
+ tampered_input.witness_script = Some(ScriptBuf::new());
+ assert!(wrapper
+ .check_my_multisig_input_key_and_script(&tampered_input, 0, &context)
+ .is_err());
+ }
+
#[test]
fn test_get_multi_sig_input_threshold_and_total() {
let mut pk1 = vec![0x03];
diff --git a/rust/apps/cosmos/src/proto_wrapper/fee.rs b/rust/apps/cosmos/src/proto_wrapper/fee.rs
index 219e83d..a77e118 100644
--- a/rust/apps/cosmos/src/proto_wrapper/fee.rs
+++ b/rust/apps/cosmos/src/proto_wrapper/fee.rs
@@ -5,12 +5,11 @@ use crate::transaction::structs::FeeDetail;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
-use core::ops::Div;
use serde::Serialize;
-pub const ATOM_TO_UATOM_UNIT: f64 = 1000000f64;
-pub const DYM_TO_ADYM_UNIT: f64 = 1e18f64;
-pub const INJ_TO_INJ_UNIT: f64 = 1e18f64;
+pub const ATOM_DECIMALS: usize = 6;
+pub const DYM_DECIMALS: usize = 18;
+pub const INJ_DECIMALS: usize = 18;
#[derive(Debug, Serialize)]
pub struct Fee {
@@ -60,30 +59,58 @@ pub fn format_amount(amounts: Vec<Coin>) -> String {
pub fn format_coin(coin: Coin) -> Option<String> {
if coin.denom.to_lowercase().eq("uatom") {
- if let Ok(value) = coin.amount.as_str().parse::<f64>() {
- return Some(format!("{} {}", value.div(ATOM_TO_UATOM_UNIT), "ATOM"));
- }
+ return format_decimal_amount(&coin.amount, ATOM_DECIMALS)
+ .map(|value| format!("{value} ATOM"));
} else if coin.denom.to_lowercase().eq("adym") {
- if let Ok(value) = coin.amount.as_str().parse::<f64>() {
- return Some(format!("{} {}", value.div(DYM_TO_ADYM_UNIT), "DYM"));
- }
+ return format_decimal_amount(&coin.amount, DYM_DECIMALS)
+ .map(|value| format!("{value} DYM"));
} else if coin.denom.eq("inj") {
- if let Ok(value) = coin.amount.as_str().parse::<f64>() {
- return Some(format!("{} {}", value.div(INJ_TO_INJ_UNIT), "INJ"));
- }
+ return format_decimal_amount(&coin.amount, INJ_DECIMALS)
+ .map(|value| format!("{value} INJ"));
} else {
return Some(format!("{} {}", coin.amount, coin.denom));
}
- None
}
-pub fn parse_gas_limit(gas: &serde_json::Value) -> Result<f64> {
+fn format_decimal_amount(amount: &str, decimals: usize) -> Option<String> {
+ if amount.is_empty() || !amount.bytes().all(|byte| byte.is_ascii_digit()) {
+ return None;
+ }
+ let normalized = amount.trim_start_matches('0');
+ let digits = if normalized.is_empty() {
+ "0"
+ } else {
+ normalized
+ };
+ if decimals == 0 {
+ return Some(digits.to_string());
+ }
+
+ let (integer, fraction) = if digits.len() > decimals {
+ let split = digits.len() - decimals;
+ (digits[..split].to_string(), digits[split..].to_string())
+ } else {
+ (
+ "0".to_string(),
+ format!("{}{}", "0".repeat(decimals - digits.len()), digits),
+ )
+ };
+ let fraction = fraction.trim_end_matches('0');
+ if fraction.is_empty() {
+ Some(integer)
+ } else {
+ Some(format!("{integer}.{fraction}"))
+ }
+}
+
+pub fn parse_gas_limit(gas: &serde_json::Value) -> Result<String> {
if let Some(gas_limit) = gas.as_str() {
- let result = gas_limit.parse::<f64>()?;
- return Ok(result);
+ if gas_limit.bytes().all(|byte| byte.is_ascii_digit()) {
+ return Ok(gas_limit.to_string());
+ }
}
- if let Some(gas_limit) = gas.as_f64() {
- return Ok(gas_limit);
+ if let Some(gas_limit) = gas.as_u64() {
+ return Ok(gas_limit.to_string());
}
Err(CosmosError::InvalidData(format!(
"failed to parse gas {gas:?}"
@@ -92,45 +119,59 @@ pub fn parse_gas_limit(gas: &serde_json::Value) -> Result<f64> {
pub fn format_fee_from_value(data: serde_json::Value) -> Result<FeeDetail> {
let gas_limit = parse_gas_limit(&data["gas"])?;
- let mut max_fee: Vec<String> = Vec::new();
let mut fee: Vec<String> = Vec::new();
if let Some(amounts) = data["amount"].as_array() {
for each in amounts {
if let (Some(amount), Some(denom)) = (each["amount"].as_str(), each["denom"].as_str()) {
- if let Ok(value) = amount.parse::<f64>() {
- if denom.to_lowercase().eq("uatom") {
- fee.push(format!("{} {}", value.div(ATOM_TO_UATOM_UNIT), "ATOM"));
- max_fee.push(format!(
- "{} {}",
- value.div(ATOM_TO_UATOM_UNIT) * gas_limit,
- "ATOM"
- ))
- } else if denom.to_lowercase().eq("adym") {
- fee.push(format!("{} {}", value.div(DYM_TO_ADYM_UNIT), "DYM"));
- max_fee.push(format!(
- "{} {}",
- value.div(DYM_TO_ADYM_UNIT) * gas_limit,
- "DYM"
- ))
- } else if denom.eq("inj") {
- fee.push(format!("{} {}", value.div(INJ_TO_INJ_UNIT), "INJ"));
- max_fee.push(format!(
- "{} {}",
- value.div(INJ_TO_INJ_UNIT) * gas_limit,
- "INJ"
- ))
- } else {
- max_fee.push(format!("{} {}", value * gas_limit, denom));
- fee.push(format!("{value} {denom}"));
- };
+ if let Some(value) = format_coin(Coin {
+ amount: amount.to_string(),
+ denom: denom.to_string(),
+ }) {
+ fee.push(value);
}
}
}
+ let formatted_fee = fee.join(",");
return Ok(FeeDetail {
- max_fee: max_fee.join(","),
- fee: fee.join(","),
- gas_limit: gas_limit.to_string(),
+ max_fee: formatted_fee.clone(),
+ fee: formatted_fee,
+ gas_limit,
});
}
Err(CosmosError::InvalidData("can not parse fee".to_string()))
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use serde_json::json;
+
+ #[test]
+ fn fee_amount_is_total_fee_not_gas_price() {
+ let fee = format_fee_from_value(json!({
+ "amount": [{"amount": "2583", "denom": "uatom"}],
+ "gas": "103301"
+ }))
+ .unwrap();
+ assert_eq!("0.002583 ATOM", fee.fee);
+ assert_eq!("0.002583 ATOM", fee.max_fee);
+ assert_eq!("103301", fee.gas_limit);
+ }
+
+ #[test]
+ fn fee_formatting_preserves_large_integer_precision() {
+ let fee = format_fee_from_value(json!({
+ "amount": [{
+ "amount": "115792089237316195423570985008687907853269984665640564039457",
+ "denom": "uatom"
+ }],
+ "gas": "18446744073709551615"
+ }))
+ .unwrap();
+ assert_eq!(
+ "115792089237316195423570985008687907853269984665640564.039457 ATOM",
+ fee.fee
+ );
+ assert_eq!("18446744073709551615", fee.gas_limit);
+ }
+}
diff --git a/rust/apps/cosmos/src/proto_wrapper/msg/common.rs b/rust/apps/cosmos/src/proto_wrapper/msg/common.rs
index 038cc1a..cc36f84 100644
--- a/rust/apps/cosmos/src/proto_wrapper/msg/common.rs
+++ b/rust/apps/cosmos/src/proto_wrapper/msg/common.rs
@@ -13,6 +13,7 @@ use alloc::boxed::Box;
use alloc::string::ToString;
use alloc::vec::Vec;
+use crate::utils::sha256_digest;
use crate::CosmosError;
pub fn map_messages(messages: &[Any]) -> Result<Vec<Box<dyn Msg>>, CosmosError> {
@@ -151,6 +152,7 @@ pub fn map_messages(messages: &[Any]) -> Result<Vec<Box<dyn Msg>>, CosmosError>
message_vec.push(Box::new(NotSupportMessage {
type_url: other.to_string(),
err: "the type is not support!".to_string(),
+ data_digest: format!("0x{}", hex::encode(sha256_digest(&message.value))),
}));
}
}
diff --git a/rust/apps/cosmos/src/proto_wrapper/msg/msg.rs b/rust/apps/cosmos/src/proto_wrapper/msg/msg.rs
index a938e0a..2025f13 100644
--- a/rust/apps/cosmos/src/proto_wrapper/msg/msg.rs
+++ b/rust/apps/cosmos/src/proto_wrapper/msg/msg.rs
@@ -13,6 +13,7 @@ use serde_json::{json, Value};
pub struct NotSupportMessage {
pub type_url: String,
pub err: String,
+ pub data_digest: String,
}
impl SerializeJson for NotSupportMessage {
diff --git a/rust/apps/cosmos/src/transaction/detail.rs b/rust/apps/cosmos/src/transaction/detail.rs
index 52f15b9..28ac61a 100644
--- a/rust/apps/cosmos/src/transaction/detail.rs
+++ b/rust/apps/cosmos/src/transaction/detail.rs
@@ -7,6 +7,7 @@ pub use crate::transaction::overview::OverviewDelegate as DetailDelegate;
pub use crate::transaction::overview::OverviewMessage as DetailMessage;
pub use crate::transaction::overview::OverviewSend as DetailSend;
pub use crate::transaction::overview::OverviewUndelegate as DetailUndelegate;
+pub use crate::transaction::overview::OverviewUnknown as DetailUnknownMessage;
pub use crate::transaction::overview::OverviewVote as DetailVote;
pub use crate::transaction::overview::OverviewWithdrawReward as DetailWithdrawReward;
use crate::transaction::structs::FeeDetail;
@@ -90,6 +91,7 @@ pub enum MsgDetail {
Transfer(DetailTransfer),
Vote(DetailVote),
Message(DetailMessage),
+ Unknown(DetailUnknownMessage),
}
#[derive(Debug, Clone, Serialize)]
@@ -98,6 +100,8 @@ pub struct CommonDetail {
pub network: String,
#[serde(rename(serialize = "Chain ID"))]
pub chain_id: String,
+ #[serde(skip_serializing_if = "Option::is_none", rename(serialize = "Memo"))]
+ pub memo: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(flatten)]
pub fee: Option<FeeDetail>,
@@ -109,6 +113,8 @@ pub struct DetailUnknown {
pub network: String,
#[serde(rename(serialize = "Chain ID"))]
pub chain_id: String,
+ #[serde(skip_serializing_if = "Option::is_none", rename(serialize = "Memo"))]
+ pub memo: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(flatten)]
pub fee: Option<FeeDetail>,
@@ -121,6 +127,7 @@ impl CommonDetail {
DetailUnknown {
network: self.network.clone(),
chain_id: self.chain_id.clone(),
+ memo: self.memo.clone(),
fee: self.fee.clone(),
message: "Unknown Data".to_string(),
}
@@ -138,7 +145,7 @@ impl CosmosTxDetail {
let msg_arr = msgs
.as_array()
.ok_or(CosmosError::ParseTxError("empty msg".to_string()))?;
- for each in msg_arr {
+ for (index, each) in msg_arr.iter().enumerate() {
match crate::transaction::utils::detect_msg_type(each["type"].as_str()) {
"MsgSend" => {
let msg = from_value::<MsgSend>(each["value"].clone())?;
@@ -174,7 +181,9 @@ impl CosmosTxDetail {
let msg = from_value::<MsgSignData>(each["value"].clone())?;
kind.push(MsgDetail::Message(msg.try_into()?));
}
- _ => {}
+ _ => kind.push(MsgDetail::Unknown(DetailUnknownMessage::from_value(
+ each, index,
+ )?)),
};
}
Ok(kind)
diff --git a/rust/apps/cosmos/src/transaction/mod.rs b/rust/apps/cosmos/src/transaction/mod.rs
index 005b953..7f3b3e2 100644
--- a/rust/apps/cosmos/src/transaction/mod.rs
+++ b/rust/apps/cosmos/src/transaction/mod.rs
@@ -42,7 +42,7 @@ impl ParsedCosmosTx {
MsgOverview::Transfer(_) => CosmosTxDisplayType::Transfer,
MsgOverview::Vote(_) => CosmosTxDisplayType::Vote,
MsgOverview::Message(_) => CosmosTxDisplayType::Message,
- // _ => CosmosTxDisplayType::Unknown,
+ MsgOverview::Unknown(_) => CosmosTxDisplayType::Unknown,
}
}
fn build_overview_from_amino(data: &Value) -> Result<CosmosTxOverview> {
@@ -63,6 +63,10 @@ impl ParsedCosmosTx {
let common = CommonDetail {
network: get_network_by_chain_id(chain_id)?,
chain_id: chain_id.to_string(),
+ memo: data["memo"]
+ .as_str()
+ .filter(|memo| !memo.is_empty())
+ .map(ToString::to_string),
fee: format_fee_from_value(data["fee"].clone()).ok(),
};
let kind = CosmosTxDetail::from_value(&data["msgs"])?;
@@ -71,8 +75,10 @@ impl ParsedCosmosTx {
&common.to_unknown(),
)?);
}
- if let MsgDetail::Message(msg) = &kind[0] {
- return Ok(serde_json::to_string::<DetailMessage>(msg)?);
+ if kind.len() == 1 {
+ if let MsgDetail::Message(msg) = &kind[0] {
+ return Ok(serde_json::to_string::<DetailMessage>(msg)?);
+ }
}
let detail = serde_json::to_string::<CosmosTxDetail>(&CosmosTxDetail { common, kind })?;
Ok(detail)
@@ -131,7 +137,7 @@ mod tests {
"common": {
"Network": "Cosmos Hub",
"Chain ID": "cosmoshub-4",
- "Max Fee": "266.826483 ATOM",
+ "Max Fee": "0.002583 ATOM",
"Fee": "0.002583 ATOM",
"Gas Limit": "103301"
},
@@ -181,7 +187,7 @@ mod tests {
"Network": "Evmos Testnet",
"Chain ID": "evmos_9000-4",
"Fee": "2625000000000000 atevmos",
- "Max Fee": "275625000000000000000 atevmos",
+ "Max Fee": "2625000000000000 atevmos",
"Gas Limit": "105000",
},
"kind": [
@@ -228,7 +234,7 @@ mod tests {
"Chain ID": "evmos_9000-4",
"Fee": "8750000000000000 atevmos",
"Gas Limit": "350000",
- "Max Fee": "3062500000000000000000 atevmos",
+ "Max Fee": "8750000000000000 atevmos",
},
"kind": [
{
@@ -249,7 +255,7 @@ mod tests {
let result =
ParsedCosmosTx::build(&hex::decode(raw_tx).unwrap().to_vec(), DataType::Amino).unwrap();
let overview = result.overview;
- assert_eq!("Cosmos Hub", overview.common.network);
+ assert_eq!("Unknown Network", overview.common.network);
assert_eq!(CosmosTxDisplayType::Delegate, overview.display_type);
match overview.kind[0].clone() {
MsgOverview::Delegate(overview) => {
@@ -266,11 +272,11 @@ mod tests {
}
let expected_detail = json!({
"common": {
- "Network": "Cosmos Hub",
+ "Network": "Unknown Network",
"Chain ID": "osmo-test-5",
"Fee": "4625 uosmo",
"Gas Limit": "184991",
- "Max Fee": "855583375 uosmo",
+ "Max Fee": "4625 uosmo",
},
"kind": [
{
@@ -312,7 +318,7 @@ mod tests {
"Network": "Evmos Testnet",
"Chain ID": "evmos_9000-4",
"Fee": "7500000000000000 atevmos",
- "Max Fee": "2250000000000000000000 atevmos",
+ "Max Fee": "7500000000000000 atevmos",
"Gas Limit": "300000"
},
"kind": [
@@ -334,7 +340,7 @@ mod tests {
let result =
ParsedCosmosTx::build(&hex::decode(raw_tx).unwrap().to_vec(), DataType::Amino).unwrap();
let overview = result.overview;
- assert_eq!("Cosmos Hub", overview.common.network);
+ assert_eq!("Unknown Network", overview.common.network);
assert_eq!(CosmosTxDisplayType::Undelegate, overview.display_type);
match overview.kind[0].clone() {
MsgOverview::Undelegate(overview) => {
@@ -351,10 +357,10 @@ mod tests {
}
let expected_detail = json!({
"common": {
- "Network": "Cosmos Hub",
+ "Network": "Unknown Network",
"Chain ID": "osmo-test-5",
"Fee": "9512 uosmo",
- "Max Fee": "2261839456 uosmo",
+ "Max Fee": "9512 uosmo",
"Gas Limit": "237788"
},
"kind": [
@@ -397,7 +403,7 @@ mod tests {
"Network": "Evmos Testnet",
"Chain ID": "evmos_9000-4",
"Fee": "37500000000000000 atevmos",
- "Max Fee": "56250000000000000000000 atevmos",
+ "Max Fee": "37500000000000000 atevmos",
"Gas Limit": "1500000"
},
"kind": [
@@ -420,7 +426,7 @@ mod tests {
let result =
ParsedCosmosTx::build(&hex::decode(raw_tx).unwrap().to_vec(), DataType::Amino).unwrap();
let overview = result.overview;
- assert_eq!("Cosmos Hub", overview.common.network);
+ assert_eq!("Unknown Network", overview.common.network);
assert_eq!(CosmosTxDisplayType::Redelegate, overview.display_type);
match overview.kind[0].clone() {
MsgOverview::Redelegate(overview) => {
@@ -437,11 +443,11 @@ mod tests {
}
let expected_detail = json!({
"common": {
- "Network": "Cosmos Hub",
+ "Network": "Unknown Network",
"Chain ID": "osmo-test-5",
"Fee": "8164 uosmo",
"Gas Limit": "326559",
- "Max Fee": "2666027676 uosmo"
+ "Max Fee": "8164 uosmo"
},
"kind": [
{
@@ -483,7 +489,7 @@ mod tests {
"Network": "Evmos Testnet",
"Chain ID": "evmos_9000-4",
"Fee": "8750000000000000 atevmos",
- "Max Fee": "3062500000000000000000 atevmos",
+ "Max Fee": "8750000000000000 atevmos",
"Gas Limit": "350000"
},
"kind": [
@@ -523,8 +529,9 @@ mod tests {
"Network": "Evmos Testnet",
"Chain ID": "evmos_9000-4",
"Fee": "100 ucosm",
- "Max Fee": "25000 ucosm",
- "Gas Limit": "250"
+ "Max Fee": "100 ucosm",
+ "Gas Limit": "250",
+ "Memo": "Some memo"
},
"kind": [
{
@@ -565,8 +572,9 @@ mod tests {
"Network": "Evmos Testnet",
"Chain ID": "evmos_9000-4",
"Fee": "100 ucosm",
- "Max Fee": "25000 ucosm",
- "Gas Limit": "250"
+ "Max Fee": "100 ucosm",
+ "Gas Limit": "250",
+ "Memo": "Some memo"
},
"kind": [
{
@@ -608,7 +616,7 @@ mod tests {
"Network": "Osmosis",
"Chain ID": "osmosis-1",
"Fee": "5333 uosmo",
- "Max Fee": "1137555565 uosmo",
+ "Max Fee": "5333 uosmo",
"Gas Limit": "213305"
},
"kind": [
@@ -659,7 +667,7 @@ mod tests {
"Network": "Osmosis",
"Chain ID": "osmosis-1",
"Fee": "2000 uosmo",
- "Max Fee": "24690000 uosmo",
+ "Max Fee": "2000 uosmo",
"Gas Limit": "12345"
},
"kind": [
@@ -702,7 +710,7 @@ mod tests {
"Network": "Osmosis",
"Chain ID": "osmosis-1",
"Fee": "1946 uosmo",
- "Max Fee": "151426044 uosmo",
+ "Max Fee": "1946 uosmo",
"Gas Limit": "77814"
},
"kind": [
@@ -742,7 +750,7 @@ mod tests {
"Network": "Osmosis",
"Chain ID": "osmosis-1",
"Fee": "2000 uosmo",
- "Max Fee": "24690000 uosmo",
+ "Max Fee": "2000 uosmo",
"Gas Limit": "12345"
},
"kind": [
@@ -766,17 +774,27 @@ mod tests {
let overview = result.overview;
assert_eq!("Evmos Testnet", overview.common.network);
assert_eq!(CosmosTxDisplayType::Unknown, overview.display_type);
- assert_eq!(0, overview.kind.len());
- let expected_detail = json!({
- "Network": "Evmos Testnet",
- "Chain ID": "evmos_9000-4",
- "Fee": "100 ucosm",
- "Gas Limit": "250",
- "Max Fee": "25000 ucosm",
- "Message": "Unknown Data"
- });
+ assert_eq!(1, overview.kind.len());
+ match &overview.kind[0] {
+ MsgOverview::Unknown(unknown) => {
+ assert_eq!("/ibc.core.channel.v1.MsgAcknowledgement", unknown.type_url);
+ assert_eq!("1", unknown.message_index);
+ assert!(unknown.data_digest.starts_with("0x"));
+ assert_eq!(66, unknown.data_digest.len());
+ }
+ _ => panic!("unknown message was not preserved"),
+ }
let parsed_detail: Value = from_str(result.detail.as_str()).unwrap();
- assert_eq!(expected_detail, parsed_detail);
+ assert_eq!("Evmos Testnet", parsed_detail["common"]["Network"]);
+ assert_eq!("evmos_9000-4", parsed_detail["common"]["Chain ID"]);
+ assert_eq!("Some memo", parsed_detail["common"]["Memo"]);
+ assert_eq!("100 ucosm", parsed_detail["common"]["Fee"]);
+ assert_eq!("250", parsed_detail["common"]["Gas Limit"]);
+ assert_eq!("Blind Sign", parsed_detail["kind"][0]["Method"]);
+ assert_eq!(
+ "/ibc.core.channel.v1.MsgAcknowledgement",
+ parsed_detail["kind"][0]["Type URL"]
+ );
}
#[test]
@@ -785,15 +803,86 @@ mod tests {
let result =
ParsedCosmosTx::build(&hex::decode(raw_tx).unwrap().to_vec(), DataType::Amino).unwrap();
let overview = result.overview;
- assert_eq!("Cosmos Hub", overview.common.network);
+ assert_eq!("Unknown Network", overview.common.network);
assert_eq!(CosmosTxDisplayType::Unknown, overview.display_type);
assert_eq!(0, overview.kind.len());
let expected_detail = json!({
- "Network": "Cosmos Hub",
+ "Network": "Unknown Network",
"Chain ID": "pulsar-2",
+ "Memo": "Create Keplr Secret encryption key. Only approve requests by Keplr.",
"Message": "Unknown Data"
});
let parsed_detail: Value = from_str(result.detail.as_str()).unwrap();
assert_eq!(expected_detail, parsed_detail);
}
+
+ #[test]
+ fn test_parse_cosmos_supported_plus_unknown_amino() {
+ let tx = json!({
+ "account_number": "1",
+ "chain_id": "cosmoshub-4",
+ "fee": {
+ "amount": [{"amount": "2583", "denom": "uatom"}],
+ "gas": "103301"
+ },
+ "memo": "",
+ "msgs": [
+ {
+ "type": "cosmos-sdk/MsgSend",
+ "value": {
+ "amount": [{"amount": "12000", "denom": "uatom"}],
+ "from_address": "cosmos19rl4cm2hmr8afy4kldpxz3fka4jguq0auqdal4",
+ "to_address": "cosmos1kwml7yt4em4en7guy6het2q3308u73dff983s3"
+ }
+ },
+ {
+ "type": "/cosmos.authz.v1beta1.MsgExec",
+ "value": {"grantee": "cosmos19rl4cm2hmr8afy4kldpxz3fka4jguq0auqdal4"}
+ }
+ ],
+ "sequence": "2"
+ });
+
+ let result = ParsedCosmosTx::build_from_value(&tx).unwrap();
+ assert_eq!(CosmosTxDisplayType::Multiple, result.overview.display_type);
+ assert_eq!(2, result.overview.kind.len());
+ assert!(matches!(result.overview.kind[0], MsgOverview::Send(_)));
+ assert!(matches!(result.overview.kind[1], MsgOverview::Unknown(_)));
+
+ let detail: Value = from_str(result.detail.as_str()).unwrap();
+ assert_eq!("Send", detail["kind"][0]["Method"]);
+ assert_eq!("Blind Sign", detail["kind"][1]["Method"]);
+ assert_eq!("2", detail["kind"][1]["Message Index"]);
+ assert_eq!(
+ "/cosmos.authz.v1beta1.MsgExec",
+ detail["kind"][1]["Type URL"]
+ );
+ }
+
+ #[test]
+ fn test_parse_cosmos_preserves_long_memo() {
+ let memo = "SWAP:ETH.ETH:cosmos1destination:1/1/0:AFFILIATE-".repeat(35);
+ let tx = json!({
+ "account_number": "1",
+ "chain_id": "cosmoshub-4",
+ "fee": {
+ "amount": [{"amount": "2583", "denom": "uatom"}],
+ "gas": "103301"
+ },
+ "memo": memo.clone(),
+ "msgs": [{
+ "type": "cosmos-sdk/MsgSend",
+ "value": {
+ "amount": [{"amount": "1000000", "denom": "uatom"}],
+ "from_address": "cosmos19rl4cm2hmr8afy4kldpxz3fka4jguq0auqdal4",
+ "to_address": "cosmos1kwml7yt4em4en7guy6het2q3308u73dff983s3"
+ }
+ }],
+ "sequence": "2"
+ });
+
+ let result = ParsedCosmosTx::build_from_value(&tx).unwrap();
+ let detail: Value = from_str(result.detail.as_str()).unwrap();
+ assert_eq!(memo, detail["common"]["Memo"]);
+ }
}
diff --git a/rust/apps/cosmos/src/transaction/overview.rs b/rust/apps/cosmos/src/transaction/overview.rs
index e46dab8..4f6a6bd 100644
--- a/rust/apps/cosmos/src/transaction/overview.rs
+++ b/rust/apps/cosmos/src/transaction/overview.rs
@@ -11,6 +11,7 @@ use serde::{Deserialize, Serialize};
use serde_json::{from_value, Value};
use super::utils::{get_chain_id_by_address, get_network_by_chain_id};
+use crate::utils::sha256_digest;
#[derive(Debug, Clone, Serialize)]
pub struct OverviewSend {
@@ -256,6 +257,51 @@ impl TryFrom<MsgSignData> for OverviewMessage {
}
}
+#[derive(Debug, Clone, Serialize)]
+pub struct OverviewUnknown {
+ #[serde(rename(serialize = "Method"))]
+ pub method: String,
+ #[serde(rename(serialize = "Warning"))]
+ pub warning: String,
+ #[serde(rename(serialize = "Message Index"))]
+ pub message_index: String,
+ #[serde(rename(serialize = "Type URL"))]
+ pub type_url: String,
+ #[serde(rename(serialize = "Data Digest"))]
+ pub data_digest: String,
+}
+
+impl OverviewUnknown {
+ pub(crate) fn from_value(message: &Value, index: usize) -> Result<Self> {
+ let wrapper_type = message["type"].as_str().unwrap_or("Unknown");
+ let is_unsupported_direct = wrapper_type == "/NotSupportMessage";
+ let type_url = if is_unsupported_direct {
+ message["value"]["type_url"]
+ .as_str()
+ .unwrap_or(wrapper_type)
+ } else {
+ wrapper_type
+ }
+ .to_string();
+ let data_digest = if is_unsupported_direct {
+ message["value"]["data_digest"]
+ .as_str()
+ .map(ToString::to_string)
+ .unwrap_or_default()
+ } else {
+ let raw_value = serde_json::to_vec(&message["value"])?;
+ format!("0x{}", hex::encode(sha256_digest(&raw_value)))
+ };
+ Ok(Self {
+ method: "Blind Sign".to_string(),
+ warning: "This message cannot be parsed. Verify it before signing.".to_string(),
+ message_index: (index + 1).to_string(),
+ type_url,
+ data_digest,
+ })
+ }
+}
+
#[derive(Debug, Clone, Serialize)]
#[serde(untagged)]
pub enum MsgOverview {
@@ -267,6 +313,7 @@ pub enum MsgOverview {
Transfer(OverviewTransfer),
Vote(OverviewVote),
Message(OverviewMessage),
+ Unknown(OverviewUnknown),
}
#[derive(Debug, Clone)]
@@ -287,7 +334,7 @@ impl CosmosTxOverview {
let msg_arr = msgs
.as_array()
.ok_or(CosmosError::ParseTxError("empty msg".to_string()))?;
- for each in msg_arr {
+ for (index, each) in msg_arr.iter().enumerate() {
match crate::transaction::utils::detect_msg_type(each["type"].as_str()) {
"MsgSend" => {
let msg = from_value::<MsgSend>(each["value"].clone())?;
@@ -325,7 +372,9 @@ impl CosmosTxOverview {
let msg = from_value::<MsgSignData>(each["value"].clone())?;
kind.push(MsgOverview::Message(OverviewMessage::try_from(msg)?));
}
- _ => {}
+ _ => kind.push(MsgOverview::Unknown(OverviewUnknown::from_value(
+ each, index,
+ )?)),
};
}
Ok(kind)
diff --git a/rust/apps/cosmos/src/transaction/utils.rs b/rust/apps/cosmos/src/transaction/utils.rs
index d1dcf46..ea3adc8 100644
--- a/rust/apps/cosmos/src/transaction/utils.rs
+++ b/rust/apps/cosmos/src/transaction/utils.rs
@@ -63,7 +63,7 @@ pub fn get_network_by_chain_id(chain_id: &str) -> Result<String> {
Ok(map
.get(chain_id_prefix.as_str())
.map(|v| v.to_string())
- .unwrap_or("Cosmos Hub".to_string()))
+ .unwrap_or("Unknown Network".to_string()))
}
pub fn get_chain_id_by_address(address: &str) -> String {
@@ -131,6 +131,10 @@ mod tests {
get_network_by_chain_id("dymension_1100-1").unwrap(),
"Dymension"
);
+ assert_eq!(
+ get_network_by_chain_id("evilchain-999").unwrap(),
+ "Unknown Network"
+ );
}
#[test]
diff --git a/rust/apps/ethereum/src/batch_tx_rules.rs b/rust/apps/ethereum/src/batch_tx_rules.rs
index 1a6ff0e..8b29db7 100644
--- a/rust/apps/ethereum/src/batch_tx_rules.rs
+++ b/rust/apps/ethereum/src/batch_tx_rules.rs
@@ -51,7 +51,7 @@ mod tests {
fn create_test_transaction(input: String) -> ParsedEthereumTransaction {
ParsedEthereumTransaction {
- nonce: 0,
+ nonce: "0".to_string(),
chain_id: 1,
from: None,
to: "0x0000000000000000000000000000000000000000".to_string(),
diff --git a/rust/apps/ethereum/src/eip1559_transaction.rs b/rust/apps/ethereum/src/eip1559_transaction.rs
index 44d8dd8..1ef3399 100644
--- a/rust/apps/ethereum/src/eip1559_transaction.rs
+++ b/rust/apps/ethereum/src/eip1559_transaction.rs
@@ -48,7 +48,7 @@ impl Decodable for EIP1559Transaction {
pub struct ParsedEIP1559Transaction {
pub(crate) chain_id: u64,
- pub(crate) nonce: u32,
+ pub(crate) nonce: String,
pub(crate) max_priority_fee_per_gas: String,
pub(crate) max_fee_per_gas: String,
pub(crate) gas_limit: String,
@@ -64,9 +64,9 @@ impl From<EIP1559Transaction> for ParsedEIP1559Transaction {
fn from(value: EIP1559Transaction) -> Self {
Self {
chain_id: value.chain_id,
- nonce: value.nonce.as_u32(),
- max_priority_fee_per_gas: normalize_price(value.max_priority_fee_per_gas.as_u64()),
- max_fee_per_gas: normalize_price(value.max_fee_per_gas.as_u64()),
+ nonce: value.nonce.to_string(),
+ max_priority_fee_per_gas: normalize_price(value.max_priority_fee_per_gas),
+ max_fee_per_gas: normalize_price(value.max_fee_per_gas),
gas_limit: value.gas_limit.to_string(),
to: format!("0x{}", hex::encode(value.get_to())),
value: normalize_value(value.value),
@@ -87,6 +87,23 @@ mod tests {
extern crate std;
+ #[test]
+ fn test_parsed_eip1559_transaction_preserves_large_nonce() {
+ let tx = EIP1559Transaction {
+ chain_id: 1,
+ nonce: U256::from(4_294_967_297_u64),
+ max_priority_fee_per_gas: U256::from(1),
+ max_fee_per_gas: U256::from(1),
+ gas_limit: U256::from(21_000),
+ action: TransactionAction::Call(H160::zero()),
+ value: U256::zero(),
+ input: vec![],
+ };
+
+ let parsed = ParsedEIP1559Transaction::from(tx);
+ assert_eq!(parsed.nonce, "4294967297");
+ }
+
#[test]
fn test_parsed_eip1559_transaction() {
let tx = EIP1559Transaction {
@@ -104,7 +121,7 @@ mod tests {
let parsed = ParsedEIP1559Transaction::from(tx);
assert_eq!(parsed.chain_id, 1);
- assert_eq!(parsed.nonce, 42);
+ assert_eq!(parsed.nonce, "42");
assert_eq!(parsed.max_priority_fee_per_gas, "2 Gwei");
assert_eq!(parsed.max_fee_per_gas, "100 Gwei");
assert_eq!(parsed.gas_limit, "21000");
@@ -150,7 +167,7 @@ mod tests {
let parsed = ParsedEIP1559Transaction::from(tx);
assert_eq!(parsed.chain_id, 1);
- assert_eq!(parsed.nonce, 5);
+ assert_eq!(parsed.nonce, "5");
assert_eq!(parsed.max_priority_fee_per_gas, "3 Gwei");
assert_eq!(parsed.max_fee_per_gas, "150 Gwei");
assert_eq!(parsed.gas_limit, "500000");
@@ -178,7 +195,7 @@ mod tests {
let parsed = ParsedEIP1559Transaction::from(tx);
assert_eq!(parsed.chain_id, 137);
- assert_eq!(parsed.nonce, 10);
+ assert_eq!(parsed.nonce, "10");
assert_eq!(parsed.value, "2");
assert_eq!(parsed.input, "a9059cbb");
}
@@ -274,6 +291,6 @@ mod tests {
};
let parsed = ParsedEIP1559Transaction::from(tx);
- assert_eq!(parsed.nonce, 999999);
+ assert_eq!(parsed.nonce, "999999");
}
}
diff --git a/rust/apps/ethereum/src/eip712/eip712.rs b/rust/apps/ethereum/src/eip712/eip712.rs
index 68f74ee..e7a247f 100644
--- a/rust/apps/ethereum/src/eip712/eip712.rs
+++ b/rust/apps/ethereum/src/eip712/eip712.rs
@@ -328,18 +328,23 @@ pub struct TypedData {
pub message: BTreeMap<String, serde_json::Value>,
}
-impl Into<StructTypedDta> for TypedData {
- fn into(self) -> StructTypedDta {
- let domain_separator = self.domain.separator(Some(&self.types));
- let message_hash = self.struct_hash().unwrap();
- StructTypedDta {
- name: self.domain.name.unwrap_or_default(),
- version: self.domain.version.unwrap_or_default(),
- chain_id: self
+impl TryFrom<TypedData> for StructTypedDta {
+ type Error = Eip712Error;
+
+ fn try_from(value: TypedData) -> Result<Self, Self::Error> {
+ let message_hash = value.struct_hash()?;
+ let message = serde_json::to_string_pretty(&value.message)
+ .map_err(|e| Eip712Error::Message(e.to_string()))?;
+ let domain_separator = value.domain.separator(Some(&value.types));
+
+ Ok(StructTypedDta {
+ name: value.domain.name.unwrap_or_default(),
+ version: value.domain.version.unwrap_or_default(),
+ chain_id: value
.domain
.chain_id
.map_or("".to_string(), |v| v.to_string()),
- verifying_contract: self.domain.verifying_contract.map_or("".to_string(), |v| {
+ verifying_contract: value.domain.verifying_contract.map_or("".to_string(), |v| {
match Address::from_str(&v) {
Ok(address) => {
let mut s = String::from("0x");
@@ -349,17 +354,18 @@ impl Into<StructTypedDta> for TypedData {
Err(_) => v,
}
}),
- salt: self.domain.salt.map_or("".to_string(), |v| {
+ salt: value.domain.salt.map_or("".to_string(), |v| {
let mut s = String::from("0x");
s.push_str(&hex::encode(v));
s
}),
- primary_type: self.primary_type,
- message: serde_json::to_string_pretty(&self.message).unwrap_or("".to_string()),
+ primary_type: value.primary_type,
+ message,
from: None,
message_hash: hex::encode(&message_hash),
domain_separator: hex::encode(&domain_separator),
- }
+ safe_tx_hash: String::new(),
+ })
}
}
diff --git a/rust/apps/ethereum/src/legacy_transaction.rs b/rust/apps/ethereum/src/legacy_transaction.rs
index 7530134..27c419d 100644
--- a/rust/apps/ethereum/src/legacy_transaction.rs
+++ b/rust/apps/ethereum/src/legacy_transaction.rs
@@ -269,7 +269,7 @@ impl Encodable for LegacyTransaction {
}
pub struct ParsedLegacyTransaction {
- pub(crate) nonce: u32,
+ pub(crate) nonce: String,
pub(crate) gas_price: String,
pub(crate) gas_limit: String,
pub(crate) to: String,
@@ -282,8 +282,8 @@ pub struct ParsedLegacyTransaction {
impl From<LegacyTransaction> for ParsedLegacyTransaction {
fn from(value: LegacyTransaction) -> Self {
Self {
- nonce: value.nonce.as_u32(),
- gas_price: normalize_price(value.gas_price.as_u64()),
+ nonce: value.nonce.to_string(),
+ gas_price: normalize_price(value.gas_price),
gas_limit: value.gas_limit.to_string(),
to: format!("0x{}", hex::encode(value.get_to())),
value: normalize_value(value.get_value()),
@@ -337,6 +337,23 @@ mod tests {
use super::*;
extern crate std;
+
+ #[test]
+ fn test_parsed_legacy_transaction_preserves_large_nonce() {
+ let tx = LegacyTransaction {
+ nonce: U256::from(4_294_967_297_u64),
+ gas_price: U256::from(1),
+ gas_limit: U256::from(21_000),
+ action: TransactionAction::Call(H160::zero()),
+ value: U256::zero(),
+ input: vec![],
+ signature: None,
+ };
+
+ let parsed = ParsedLegacyTransaction::from(tx);
+ assert_eq!(parsed.nonce, "4294967297");
+ }
+
#[test]
fn test_transfer_erc20_legacy_transaction() {
let tx = LegacyTransaction::new(
diff --git a/rust/apps/ethereum/src/lib.rs b/rust/apps/ethereum/src/lib.rs
index 78831ad..455f62e 100644
--- a/rust/apps/ethereum/src/lib.rs
+++ b/rust/apps/ethereum/src/lib.rs
@@ -230,7 +230,7 @@ mod tests {
let result = parse_legacy_tx(&sign_data, Some(pubkey)).unwrap();
- assert_eq!(33, result.nonce);
+ assert_eq!("33", result.nonce);
assert_eq!(1, result.chain_id);
assert_eq!(
"0x9858EfFD232B4033E47d90003D41EC34EcaEda94",
@@ -253,7 +253,7 @@ mod tests {
let seed = hex::decode("5eb00bbddcf069084889a8ab9155568165f5c453ccb85e70811aaed6f6da5fc19a5ac40b389cd370d086206dec8aa6c43daea6690f20ad3d8d48b2d2ce9e38e4").unwrap();
let pubkey = get_public_key_by_seed(&seed, &path).unwrap();
let result = parse_fee_market_tx(&sign_data, Some(pubkey)).unwrap();
- assert_eq!(31, result.nonce);
+ assert_eq!("31", result.nonce);
assert_eq!(1, result.chain_id);
assert_eq!(
"0x9858EfFD232B4033E47d90003D41EC34EcaEda94",
@@ -289,6 +289,13 @@ mod tests {
);
}
+ #[test]
+ fn test_parse_typed_data_rejects_unhashable_message() {
+ let sign_data =
+ br#"{"types":{"EIP712Domain":[]},"primaryType":"Missing","domain":{},"message":{}}"#;
+ assert!(parse_typed_data_message(sign_data, None).is_err());
+ }
+
#[test]
fn test_sign_typed_data() {
diff --git a/rust/apps/ethereum/src/normalizer.rs b/rust/apps/ethereum/src/normalizer.rs
index c417ef1..7bf1f9e 100644
--- a/rust/apps/ethereum/src/normalizer.rs
+++ b/rust/apps/ethereum/src/normalizer.rs
@@ -1,40 +1,35 @@
use alloc::format;
use alloc::string::{String, ToString};
-use core::ops::Div;
use ethereum_types::U256;
-const F_DIVIDER: f64 = 1_000_000_000f64;
-
-pub fn normalize_price(gas: u64) -> String {
- format!("{} Gwei", (gas as f64).div(F_DIVIDER))
-}
-
-pub fn normalize_value(value: U256) -> String {
+fn normalize_units(value: U256, decimals: usize) -> String {
let value_str = value.to_string();
if value_str == "0" {
return "0".to_string();
}
- let padded_value = format!("{value_str:0>18}");
+ let padded_value = format!("{value_str:0>decimals$}");
let len = padded_value.len();
+ if len <= decimals {
+ let decimal = padded_value.trim_end_matches('0');
+ return format!("0.{decimal}");
+ }
- let res = if len <= 18 {
- let val = padded_value.trim_end_matches('0');
- if val.is_empty() {
- "0".to_string()
- } else {
- format!("0.{val}")
- }
+ let (int_part, decimal_part) = padded_value.split_at(len - decimals);
+ let decimal = decimal_part.trim_end_matches('0');
+ if decimal.is_empty() {
+ int_part.to_string()
} else {
- let (int_part, decimal_part) = padded_value.split_at(len - 18);
- let decimal = decimal_part.trim_end_matches('0');
- if decimal.is_empty() {
- int_part.to_string()
- } else {
- format!("{int_part}.{decimal}")
- }
- };
- res
+ format!("{int_part}.{decimal}")
+ }
+}
+
+pub fn normalize_price(gas: U256) -> String {
+ format!("{} Gwei", normalize_units(gas, 9))
+}
+
+pub fn normalize_value(value: U256) -> String {
+ normalize_units(value, 18)
}
#[cfg(test)]
@@ -97,16 +92,25 @@ mod tests {
#[test]
fn test_normalize_price() {
- let gas = 1000000000u64; // 1 Gwei
+ let gas = U256::from(1000000000u64); // 1 Gwei
let result = normalize_price(gas);
assert_eq!("1 Gwei", result);
- let gas = 20000000000u64; // 20 Gwei
+ let gas = U256::from(20000000000u64); // 20 Gwei
let result = normalize_price(gas);
assert_eq!("20 Gwei", result);
- let gas = 500000000u64; // 0.5 Gwei
+ let gas = U256::from(500000000u64); // 0.5 Gwei
let result = normalize_price(gas);
assert_eq!("0.5 Gwei", result);
}
+
+ #[test]
+ fn test_normalize_price_max_u256() {
+ let result = normalize_price(U256::MAX);
+ assert_eq!(
+ "115792089237316195423570985008687907853269984665640564039457584007913.129639935 Gwei",
+ result
+ );
+ }
}
diff --git a/rust/apps/ethereum/src/structs.rs b/rust/apps/ethereum/src/structs.rs
index 67fd326..a01ec0c 100644
--- a/rust/apps/ethereum/src/structs.rs
+++ b/rust/apps/ethereum/src/structs.rs
@@ -50,7 +50,7 @@ impl Encodable for TransactionAction {
#[derive(Clone, Debug)]
pub struct ParsedEthereumTransaction {
- pub nonce: u32,
+ pub nonce: String,
pub chain_id: u64,
pub from: Option<String>,
pub to: String,
@@ -169,6 +169,7 @@ pub struct TypedData {
pub from: Option<String>,
pub domain_separator: String,
pub message_hash: String,
+ pub safe_tx_hash: String,
}
impl TypedData {
@@ -180,84 +181,89 @@ impl TypedData {
}
pub fn from_raw(data: Eip712TypedData, from: Option<PublicKey>) -> Result<Self> {
- Self::from(Into::into(data), from)
+ let mut data = Self::try_from(data)
+ .map_err(|e| crate::errors::EthereumError::HashTypedDataError(e.to_string()))?;
+ data.safe_tx_hash = data.get_safe_tx_hash()?;
+ Self::from(data, from)
}
- pub fn get_safe_tx_hash(&self) -> String {
+ pub fn get_safe_tx_hash(&self) -> Result<String> {
// bytes32 private constant SAFE_TX_TYPEHASH = 0xbb8310d486368db6bd6f849402fdd73ad53d316b5a4b2644ad6efe0f941286d8;
// bytes32 safeTxHash = keccak256(
// abi.encode(SAFE_TX_TYPEHASH, to, value, keccak256(data), operation, safeTxGas, baseGas, gasPrice, gasToken, refundReceiver, _nonce)
// );
if self.primary_type != "SafeTx" {
- return "".to_string();
+ return Ok("".to_string());
}
- let safe_tx_typehash =
- hex::decode("bb8310d486368db6bd6f849402fdd73ad53d316b5a4b2644ad6efe0f941286d8")
- .unwrap();
- let message = serde_json::from_str::<Value>(&self.message).unwrap_or_default();
-
- let value_str = message["value"].as_str().unwrap_or_default();
- let value = U256::from_dec_str(value_str).unwrap_or_default();
-
- let data = hex::decode(
- message["data"]
- .as_str()
- .unwrap_or_default()
- .trim_start_matches("0x"),
- )
- .unwrap_or_default();
- let data_hash = keccak256(&data);
-
- let to_hex = message["to"]
- .as_str()
- .unwrap_or_default()
- .trim_start_matches("0x");
- let to_addr = hex::decode(to_hex).unwrap_or_default();
- let to_token = Token::Address(Address::from_slice(&to_addr));
-
- let operation = if let Some(op_str) = message["operation"].as_str() {
- U256::from_dec_str(op_str).unwrap_or_default()
- } else {
- U256::from(message["operation"].as_u64().unwrap_or_default())
- };
- let safe_tx_gas = if let Some(gas_str) = message["safeTxGas"].as_str() {
- U256::from_dec_str(gas_str).unwrap_or_default()
- } else {
- U256::from(message["safeTxGas"].as_u64().unwrap_or_default())
- };
+ fn error(field: &str) -> crate::errors::EthereumError {
+ crate::errors::EthereumError::HashTypedDataError(format!(
+ "invalid SafeTx field: {field}"
+ ))
+ }
- let base_gas = if let Some(gas_str) = message["baseGas"].as_str() {
- U256::from_dec_str(gas_str).unwrap_or_default()
- } else {
- U256::from(message["baseGas"].as_u64().unwrap_or_default())
- };
+ fn parse_u256(message: &Value, field: &str) -> Result<U256> {
+ let value = message.get(field).ok_or_else(|| error(field))?;
+ if let Some(value) = value.as_str() {
+ return U256::from_dec_str(value).map_err(|_| error(field));
+ }
+ value.as_u64().map(U256::from).ok_or_else(|| error(field))
+ }
- let gas_price = if let Some(price_str) = message["gasPrice"].as_str() {
- U256::from_dec_str(price_str).unwrap_or_default()
- } else {
- U256::from(message["gasPrice"].as_u64().unwrap_or_default())
- };
+ fn parse_address(message: &Value, field: &str) -> Result<Address> {
+ let value = message
+ .get(field)
+ .and_then(Value::as_str)
+ .ok_or_else(|| error(field))?;
+ let bytes = hex::decode(
+ value
+ .strip_prefix("0x")
+ .or_else(|| value.strip_prefix("0X"))
+ .unwrap_or(value),
+ )
+ .map_err(|_| error(field))?;
+ if bytes.len() != 20 {
+ return Err(error(field));
+ }
+ Ok(Address::from_slice(&bytes))
+ }
- let nonce = if let Some(nonce_str) = message["nonce"].as_str() {
- U256::from_dec_str(nonce_str).unwrap_or_default()
- } else {
- U256::from(message["nonce"].as_u64().unwrap_or_default())
- };
+ fn parse_bytes(message: &Value, field: &str) -> Result<Vec<u8>> {
+ let value = message
+ .get(field)
+ .and_then(Value::as_str)
+ .ok_or_else(|| error(field))?;
+ hex::decode(
+ value
+ .strip_prefix("0x")
+ .or_else(|| value.strip_prefix("0X"))
+ .unwrap_or(value),
+ )
+ .map_err(|_| error(field))
+ }
- let gas_token_addr = message["gasToken"]
- .as_str()
- .unwrap_or_default()
- .trim_start_matches("0x");
- let gas_token_addr = hex::decode(gas_token_addr).unwrap_or_default();
- let gas_token_token = Token::Address(Address::from_slice(&gas_token_addr));
+ let message = serde_json::from_str::<Value>(&self.message).map_err(|_| error("message"))?;
+ if !message.is_object() {
+ return Err(error("message"));
+ }
- let refund_receiver_addr = message["refundReceiver"]
- .as_str()
- .unwrap_or_default()
- .trim_start_matches("0x");
- let refund_receiver_addr = hex::decode(refund_receiver_addr).unwrap_or_default();
- let refund_receiver_token = Token::Address(Address::from_slice(&refund_receiver_addr));
+ let safe_tx_typehash =
+ hex::decode("bb8310d486368db6bd6f849402fdd73ad53d316b5a4b2644ad6efe0f941286d8")
+ .map_err(|_| error("typehash"))?;
+ let value = parse_u256(&message, "value")?;
+ let data = parse_bytes(&message, "data")?;
+ let data_hash = keccak256(&data);
+ let to_token = Token::Address(parse_address(&message, "to")?);
+ let operation = parse_u256(&message, "operation")?;
+ if operation > U256::from(1u8) {
+ return Err(error("operation"));
+ }
+ let safe_tx_gas = parse_u256(&message, "safeTxGas")?;
+ let base_gas = parse_u256(&message, "baseGas")?;
+ let gas_price = parse_u256(&message, "gasPrice")?;
+ let nonce = parse_u256(&message, "nonce")?;
+ let gas_token_token = Token::Address(parse_address(&message, "gasToken")?);
+ let refund_receiver_token = Token::Address(parse_address(&message, "refundReceiver")?);
let tokens = vec![
Token::FixedBytes(safe_tx_typehash),
@@ -279,15 +285,23 @@ impl TypedData {
// Convert to hex string with 0x prefix
// return abi.encodePacked(byte(0x19), byte(0x01), domainSeparator, safeTxHash);
- let domain_separator =
- hex::decode(self.domain_separator.trim_start_matches("0x")).unwrap_or_default();
+ let domain_separator = hex::decode(
+ self.domain_separator
+ .strip_prefix("0x")
+ .or_else(|| self.domain_separator.strip_prefix("0X"))
+ .unwrap_or(&self.domain_separator),
+ )
+ .map_err(|_| error("domainSeparator"))?;
+ if domain_separator.len() != 32 {
+ return Err(error("domainSeparator"));
+ }
let mut transaction_data = Vec::new();
transaction_data.push(0x19);
transaction_data.push(0x01);
transaction_data.extend_from_slice(&domain_separator);
transaction_data.extend_from_slice(&safe_tx_hash);
let transaction_hash = keccak256(&transaction_data);
- format!("0x{}", hex::encode(transaction_hash))
+ Ok(format!("0x{}", hex::encode(transaction_hash)))
}
}
@@ -298,6 +312,7 @@ pub mod tests {
extern crate std;
use crate::structs::TypedData;
+ use serde_json::Value;
use std::string::ToString;
#[test]
fn test_signature() {
@@ -315,6 +330,17 @@ pub mod tests {
}
}
+ #[test]
+ fn test_decoder_error_conversion() {
+ let error: crate::errors::EthereumError =
+ rlp::DecoderError::Custom("invalid test rlp").into();
+ assert!(matches!(
+ error,
+ crate::errors::EthereumError::RlpDecodingError(message)
+ if message.contains("invalid test rlp")
+ ));
+ }
+
#[test]
fn test_generate_safe_tx_hash() {
let message = serde_json::json!({
@@ -344,10 +370,44 @@ pub mod tests {
.to_string(),
message_hash: "0x2760e0669e7dbd5a2a9f695bac8db1432400df52a9895d8eae50d94dcb82976b"
.to_string(),
+ safe_tx_hash: "".to_string(),
};
assert_eq!(
- typed_data.get_safe_tx_hash(),
+ typed_data.get_safe_tx_hash().unwrap(),
"0x2f31649e0f48e410b00643088c8f209b1ab33871aae7150c9d7a008dda243c0a"
);
+
+ let mut invalid_address = typed_data.clone();
+ invalid_address.message = message
+ .as_object()
+ .map(|message| {
+ let mut message = message.clone();
+ message.insert("to".to_string(), serde_json::json!("0x1234"));
+ Value::Object(message).to_string()
+ })
+ .unwrap();
+ assert!(invalid_address.get_safe_tx_hash().is_err());
+
+ let mut invalid_operation = typed_data.clone();
+ invalid_operation.message = message
+ .as_object()
+ .map(|message| {
+ let mut message = message.clone();
+ message.insert("operation".to_string(), serde_json::json!("2"));
+ Value::Object(message).to_string()
+ })
+ .unwrap();
+ assert!(invalid_operation.get_safe_tx_hash().is_err());
+
+ let mut missing_nonce = typed_data;
+ missing_nonce.message = message
+ .as_object()
+ .map(|message| {
+ let mut message = message.clone();
+ message.remove("nonce");
+ Value::Object(message).to_string()
+ })
+ .unwrap();
+ assert!(missing_nonce.get_safe_tx_hash().is_err());
}
}
diff --git a/rust/apps/solana/src/compact.rs b/rust/apps/solana/src/compact.rs
index f97ae54..0b11290 100644
--- a/rust/apps/solana/src/compact.rs
+++ b/rust/apps/solana/src/compact.rs
@@ -12,6 +12,11 @@ pub struct Compact<T> {
impl<T: Read<T>> Compact<T> {
fn new(raw: &mut Vec<u8>) -> Result<Compact<T>> {
let length: u32 = Compact::<T>::read_length(raw)?;
+ if length as usize > raw.len() {
+ return Err(SolanaError::InvalidData(
+ "compact length exceeds remaining data".to_string(),
+ ));
+ }
let mut compact = Compact {
compact_length: length,
data: vec![],
@@ -24,19 +29,30 @@ impl<T: Read<T>> Compact<T> {
fn read_length(raw: &mut Vec<u8>) -> Result<u32> {
let mut len: u32 = 0;
- let mut size: u32 = 0;
- loop {
+ for byte_index in 0..3u32 {
if raw.is_empty() {
return Err(SolanaError::InvalidData("compact length".to_string()));
}
let element: u32 = raw.remove(0) as u32;
- len |= (element & 0x7f) << (size * 7);
- size += 1;
+ if byte_index == 2 && (element & 0x7c) != 0 {
+ return Err(SolanaError::InvalidData(
+ "compact length overflow".to_string(),
+ ));
+ }
+ let value = element & 0x7f;
+ if byte_index > 0 && value == 0 && (element & 0x80) == 0 {
+ return Err(SolanaError::InvalidData(
+ "non-canonical compact length".to_string(),
+ ));
+ }
+ len |= value << (byte_index * 7);
if (element & 0x80) == 0 {
- break;
+ return Ok(len);
}
}
- Ok(len)
+ Err(SolanaError::InvalidData(
+ "compact length is too long".to_string(),
+ ))
}
}
@@ -45,3 +61,29 @@ impl<T: Read<T>> Read<Compact<T>> for Compact<T> {
Compact::new(raw)
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::Compact;
+
+ #[test]
+ fn rejects_overlong_overflowing_and_non_canonical_lengths() {
+ assert!(Compact::<u8>::read_length(&mut vec![0x83, 0x80, 0x80, 0x80, 0x00]).is_err());
+ assert!(Compact::<u8>::read_length(&mut vec![0x80, 0x00]).is_err());
+ assert!(Compact::<u8>::read_length(&mut vec![0xff, 0xff, 0x04]).is_err());
+ }
+
+ #[test]
+ fn accepts_canonical_shortvec_lengths() {
+ assert_eq!(Compact::<u8>::read_length(&mut vec![0x00]).unwrap(), 0);
+ assert_eq!(Compact::<u8>::read_length(&mut vec![0x7f]).unwrap(), 127);
+ assert_eq!(
+ Compact::<u8>::read_length(&mut vec![0x80, 0x01]).unwrap(),
+ 128
+ );
+ assert_eq!(
+ Compact::<u8>::read_length(&mut vec![0xff, 0xff, 0x03]).unwrap(),
+ u16::MAX as u32
+ );
+ }
+}
diff --git a/rust/apps/solana/src/lib.rs b/rust/apps/solana/src/lib.rs
index ca039a9..fe825ab 100644
--- a/rust/apps/solana/src/lib.rs
+++ b/rust/apps/solana/src/lib.rs
@@ -34,11 +34,15 @@ mod solana_lib;
pub mod structs;
pub mod utils;
pub fn parse_message(tx_hex: Vec<u8>, from_key: &String) -> errors::Result<SolanaMessage> {
- let raw_message = hex::encode(tx_hex.clone());
- let mut utf8_message = String::from_utf8(tx_hex).unwrap_or_else(|_| "".to_string());
- if app_utils::is_cjk(&utf8_message) {
- utf8_message = "".to_string();
- }
+ // Keep only the representation that is displayed. Retaining both the
+ // UTF-8 string and its hex encoding doubles peak memory for long messages.
+ let (raw_message, utf8_message) = match String::from_utf8(tx_hex) {
+ Ok(message) if !message.as_bytes().contains(&0) && !app_utils::is_cjk(&message) => {
+ (String::new(), message)
+ }
+ Ok(message) => (hex::encode(message.as_bytes()), String::new()),
+ Err(error) => (hex::encode(error.as_bytes()), String::new()),
+ };
SolanaMessage::from(raw_message, utf8_message, from_key)
}
@@ -46,10 +50,34 @@ pub fn validate_tx(message: &mut Vec<u8>) -> bool {
message::Message::validate(message)
}
+pub fn has_tx_prefix(message: &mut Vec<u8>) -> bool {
+ message::Message::has_valid_prefix(message)
+}
+
+pub fn validate_tx_signer(message: &mut Vec<u8>, signer: &[u8; 32]) -> errors::Result<()> {
+ let transaction = message::Message::read_exact(message)?;
+ transaction.validate_signer(signer)
+}
+
+pub fn get_public_key(seed: &[u8], hd_path: &String) -> errors::Result<[u8; 32]> {
+ keystore::algorithms::ed25519::slip10_ed25519::get_public_key_by_seed(seed, hd_path).map_err(
+ |e| {
+ errors::SolanaError::KeystoreError(format!(
+ "derive public key failed {:?}",
+ e.to_string()
+ ))
+ },
+ )
+}
+
pub fn parse(data: &Vec<u8>) -> errors::Result<ParsedSolanaTx> {
ParsedSolanaTx::build(data)
}
+pub fn parse_for_signer(data: &Vec<u8>, signer: &[u8; 32]) -> errors::Result<ParsedSolanaTx> {
+ ParsedSolanaTx::build_for_signer(data, signer)
+}
+
pub fn sign(message: Vec<u8>, hd_path: &String, seed: &[u8]) -> errors::Result<[u8; 64]> {
keystore::algorithms::ed25519::slip10_ed25519::sign_message_by_seed(seed, hd_path, &message)
.map_err(|e| errors::SolanaError::KeystoreError(format!("sign failed {:?}", e.to_string())))
diff --git a/rust/apps/solana/src/message.rs b/rust/apps/solana/src/message.rs
index dbf0839..62cddd2 100644
--- a/rust/apps/solana/src/message.rs
+++ b/rust/apps/solana/src/message.rs
@@ -71,9 +71,14 @@ pub struct Message {
impl Read<Message> for Message {
fn read(raw: &mut Vec<u8>) -> Result<Message> {
- let first_byte = raw.first();
+ let first_byte = raw.first().copied();
let is_versioned = match first_byte {
Some(0x80) => true,
+ Some(value) if value & 0x80 != 0 => {
+ return Err(SolanaError::InvalidData(
+ "unsupported message version".to_string(),
+ ))
+ }
Some(_) => false,
None => return Err(SolanaError::InvalidData("empty message".to_string())),
};
@@ -88,37 +93,127 @@ impl Read<Message> for Message {
true => Some(Compact::read(raw)?.data),
false => None,
};
- Ok(Message {
+ let message = Message {
is_versioned,
header,
accounts,
block_hash,
instructions,
address_table_lookups,
- })
+ };
+ message.validate_structure()?;
+ Ok(message)
}
}
impl Message {
+ pub fn read_exact(raw: &mut Vec<u8>) -> Result<Message> {
+ let message = Self::read(raw)?;
+ if !raw.is_empty() {
+ return Err(SolanaError::InvalidData(
+ "trailing bytes after message".to_string(),
+ ));
+ }
+ Ok(message)
+ }
+
+ pub fn validate_signer(&self, signer: &[u8; 32]) -> Result<()> {
+ let required_signatures = self.header.num_required_signatures as usize;
+ if required_signatures == 0 {
+ return Err(SolanaError::InvalidData(
+ "transaction does not require a signer".to_string(),
+ ));
+ }
+ if self.accounts[..required_signatures]
+ .iter()
+ .any(|account| account.value.as_slice() == signer)
+ {
+ return Ok(());
+ }
+ Err(SolanaError::InvalidData(
+ "derived key is not a required transaction signer".to_string(),
+ ))
+ }
+
+ fn validate_structure(&self) -> Result<()> {
+ let static_account_count = self.accounts.len();
+ let required_signatures = self.header.num_required_signatures as usize;
+ let readonly_signed = self.header.num_readonly_signed_accounts as usize;
+ let readonly_unsigned = self.header.num_readonly_unsigned_accounts as usize;
+
+ if required_signatures > static_account_count {
+ return Err(SolanaError::InvalidData(
+ "required signatures exceed static accounts".to_string(),
+ ));
+ }
+ if readonly_signed > required_signatures {
+ return Err(SolanaError::InvalidData(
+ "readonly signed accounts exceed required signatures".to_string(),
+ ));
+ }
+ if readonly_unsigned > static_account_count.saturating_sub(required_signatures) {
+ return Err(SolanaError::InvalidData(
+ "readonly unsigned accounts exceed unsigned accounts".to_string(),
+ ));
+ }
+
+ let loaded_account_count = self
+ .address_table_lookups
+ .as_ref()
+ .map(|lookups| {
+ lookups.iter().fold(0usize, |count, lookup| {
+ count
+ .saturating_add(lookup.writable_indexes.len())
+ .saturating_add(lookup.readonly_indexes.len())
+ })
+ })
+ .unwrap_or(0);
+ let account_count = static_account_count.saturating_add(loaded_account_count);
+
+ for instruction in &self.instructions {
+ if instruction.program_index as usize >= account_count {
+ return Err(SolanaError::InvalidData(
+ "program index exceeds account list".to_string(),
+ ));
+ }
+ if instruction
+ .account_indexes
+ .iter()
+ .any(|index| *index as usize >= account_count)
+ {
+ return Err(SolanaError::InvalidData(
+ "instruction account index exceeds account list".to_string(),
+ ));
+ }
+ }
+ Ok(())
+ }
+
pub fn to_program_details(&self) -> Result<Vec<SolanaDetail>> {
- let accounts = self.prepare_accounts();
+ let resolved_accounts = self.prepare_accounts();
self.instructions
.iter()
.map(|instruction| {
- let accounts = instruction
+ let instruction_accounts = instruction
.account_indexes
.iter()
.map(|account_index| {
- accounts
+ resolved_accounts
.get(*account_index as usize)
.map(|v| v.to_string())
.unwrap_or("Unknown Account".to_string())
})
.collect::<Vec<String>>();
- let program_account =
- base58::encode(&self.accounts[usize::from(instruction.program_index)].value);
+ let program_account = resolved_accounts
+ .get(usize::from(instruction.program_index))
+ .ok_or_else(|| {
+ SolanaError::InvalidData(
+ "program index exceeds resolved account list".to_string(),
+ )
+ })?
+ .to_string();
// parse instruction data
- match instruction.parse(&program_account, accounts.clone()) {
+ match instruction.parse(&program_account, instruction_accounts.clone()) {
Ok(value) => Ok(value),
Err(_) => Ok(SolanaDetail {
common: CommonDetail {
@@ -127,7 +222,7 @@ impl Message {
},
kind: ProgramDetail::Instruction(ProgramDetailInstruction {
data: base58::encode(&instruction.data),
- accounts,
+ accounts: instruction_accounts,
program_account,
}),
}),
@@ -137,6 +232,10 @@ impl Message {
}
pub fn validate(raw: &mut Vec<u8>) -> bool {
+ Self::read_exact(raw).is_ok()
+ }
+
+ pub fn has_valid_prefix(raw: &mut Vec<u8>) -> bool {
Self::read(raw).is_ok()
}
@@ -225,3 +324,67 @@ impl Read<MessageAddressTableLookup> for MessageAddressTableLookup {
})
}
}
+
+#[cfg(test)]
+mod tests {
+ use alloc::vec;
+ use alloc::vec::Vec;
+
+ use super::Message;
+
+ fn minimal_legacy_message() -> Vec<u8> {
+ let mut message = vec![1, 0, 0, 1];
+ message.extend_from_slice(&[0u8; 32]);
+ message.extend_from_slice(&[0u8; 32]);
+ message.push(0);
+ message
+ }
+
+ fn system_transfer_message() -> Vec<u8> {
+ let mut message = vec![1, 0, 1, 3];
+ message.extend_from_slice(&[0x11u8; 32]);
+ message.extend_from_slice(&[0x22u8; 32]);
+ message.extend_from_slice(&[0u8; 32]);
+ message.extend_from_slice(&[0x77u8; 32]);
+ message.extend_from_slice(&[
+ 1, // instruction count
+ 2, // System Program index in the complete account list
+ 2, 0, 1, // account indexes
+ 12, 2, 0, 0, 0, // SystemInstruction::Transfer
+ 0, 202, 154, 59, 0, 0, 0, 0, // 1 SOL
+ ]);
+ message
+ }
+
+ #[test]
+ fn exact_parser_rejects_trailing_bytes() {
+ let mut message = minimal_legacy_message();
+ assert!(Message::read_exact(&mut message).is_ok());
+
+ let mut message_with_suffix = minimal_legacy_message();
+ message_with_suffix.push(0xaa);
+ assert!(Message::read_exact(&mut message_with_suffix).is_err());
+
+ let mut message_with_suffix = minimal_legacy_message();
+ message_with_suffix.push(0xaa);
+ assert!(Message::has_valid_prefix(&mut message_with_suffix));
+ }
+
+ #[test]
+ fn rejects_unsupported_versions_and_invalid_headers() {
+ assert!(Message::read_exact(&mut vec![0x81]).is_err());
+
+ let mut invalid_header = minimal_legacy_message();
+ invalid_header[0] = 2;
+ assert!(Message::read_exact(&mut invalid_header).is_err());
+ }
+
+ #[test]
+ fn resolves_program_index_from_complete_account_list() {
+ let message = Message::read_exact(&mut system_transfer_message()).unwrap();
+ let details = message.to_program_details().unwrap();
+ assert_eq!(details.len(), 1);
+ assert_eq!(details[0].common.program, "System");
+ assert_eq!(details[0].common.method, "Transfer");
+ }
+}
diff --git a/rust/apps/solana/src/parser/mod.rs b/rust/apps/solana/src/parser/mod.rs
index 9f01995..66d953c 100644
--- a/rust/apps/solana/src/parser/mod.rs
+++ b/rust/apps/solana/src/parser/mod.rs
@@ -26,8 +26,35 @@ pub mod overview;
pub mod structs;
impl ParsedSolanaTx {
+ fn is_lookup_table_reference(account: &str) -> bool {
+ account
+ .split_once('#')
+ .map(|(table, index)| !table.is_empty() && index.parse::<u8>().is_ok())
+ .unwrap_or(false)
+ }
+
+ fn format_account_for_display(account: &str) -> String {
+ if let Some((table, index)) = account.split_once('#') {
+ if !table.is_empty() && index.parse::<u8>().is_ok() {
+ return format!("Table: {}\nIndex: {}", table, index);
+ }
+ }
+ account.to_string()
+ }
+
pub fn build(data: &Vec<u8>) -> Result<Self> {
- let message = Message::read(data.clone().to_vec().as_mut())?;
+ Self::build_with_signer(data, None)
+ }
+
+ pub fn build_for_signer(data: &Vec<u8>, signer: &[u8; 32]) -> Result<Self> {
+ Self::build_with_signer(data, Some(signer))
+ }
+
+ fn build_with_signer(data: &Vec<u8>, signer: Option<&[u8; 32]>) -> Result<Self> {
+ let message = Message::read_exact(data.clone().to_vec().as_mut())?;
+ if let Some(signer) = signer {
+ message.validate_signer(signer)?;
+ }
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)?;
@@ -41,11 +68,27 @@ impl ParsedSolanaTx {
}
// detect the display type by the details vec contains number of details
fn detect_display_type(details: &[SolanaDetail]) -> SolanaTxDisplayType {
+ let unknown_count = details
+ .iter()
+ .filter(|d| Self::is_unknown_detail(&d.common))
+ .count();
+ if unknown_count == details.len() {
+ return SolanaTxDisplayType::Unknown;
+ }
+ if unknown_count > 0 {
+ return SolanaTxDisplayType::General;
+ }
+
let squads = details
.iter()
.filter(|d| Self::is_sqauds_v4_detail(&d.common))
.collect::<Vec<&SolanaDetail>>();
- if !squads.is_empty() {
+ if !squads.is_empty()
+ && details.iter().all(|detail| {
+ Self::is_sqauds_v4_detail(&detail.common)
+ || Self::is_system_transfer_detail(&detail.common)
+ })
+ {
return SolanaTxDisplayType::SquadsV4;
}
@@ -53,7 +96,7 @@ impl ParsedSolanaTx {
.iter()
.filter(|d| Self::is_jupiter_v6_detail(&d.common))
.collect::<Vec<&SolanaDetail>>();
- if !jupiter.is_empty() {
+ if jupiter.len() == 1 && details.len() == 1 {
return SolanaTxDisplayType::JupiterV6;
}
@@ -61,7 +104,7 @@ impl ParsedSolanaTx {
.iter()
.filter(|d| Self::is_system_transfer_detail(&d.common))
.collect::<Vec<&SolanaDetail>>();
- if transfer.len() == 1 {
+ if transfer.len() == 1 && details.len() == 1 {
return SolanaTxDisplayType::Transfer;
}
// if contains token transfer check
@@ -69,7 +112,7 @@ impl ParsedSolanaTx {
.iter()
.filter(|d| Self::is_token_transfer_checked_detail(&d.common))
.collect::<Vec<&SolanaDetail>>();
- if token_transfer.len() == 1 {
+ if token_transfer.len() == 1 && details.len() == 1 {
return SolanaTxDisplayType::TokenTransfer;
}
@@ -77,17 +120,9 @@ impl ParsedSolanaTx {
.iter()
.filter(|d| Self::is_vote_detail(&d.common))
.collect::<Vec<&SolanaDetail>>();
- if vote.len() == 1 {
+ if vote.len() == 1 && details.len() == 1 {
return SolanaTxDisplayType::Vote;
}
-
- let instructions: Vec<&SolanaDetail> = details
- .iter()
- .filter(|d| Self::is_unknown_detail(&d.common))
- .collect::<Vec<&SolanaDetail>>();
- if instructions.len() == details.len() {
- return SolanaTxDisplayType::Unknown;
- }
SolanaTxDisplayType::General
}
@@ -178,7 +213,13 @@ impl ParsedSolanaTx {
value: v.value.to_string(),
main_action: "SOL Transfer".to_string(),
from: v.from.to_string(),
- to: v.to.to_string(),
+ to: Self::format_account_for_display(&v.to),
+ to_in_lookup_table: Self::is_lookup_table_reference(&v.to),
+ to_lookup_table_reference: if Self::is_lookup_table_reference(&v.to) {
+ v.to.clone()
+ } else {
+ String::new()
+ },
}))
} else {
None
@@ -323,32 +364,68 @@ impl ParsedSolanaTx {
_ => ("SPLToken".to_string(), "Unknown".to_string(), 0),
}
}
+
+ fn validate_token_decimals(token_mint_address: &str, decimals: u8) -> Result<()> {
+ let token_info = Self::find_token_info(token_mint_address);
+ let is_known_token = token_info.1 != "Unknown";
+ if is_known_token && decimals != token_info.2 {
+ return Err(SolanaError::InvalidData(
+ "token decimals do not match known mint metadata".to_string(),
+ ));
+ }
+ Ok(())
+ }
+
+ fn has_unusual_token_decimals(token_mint_address: &str, decimals: u8) -> bool {
+ const MAX_NORMAL_TOKEN_DECIMALS: u8 = 18;
+ Self::find_token_info(token_mint_address).1 == "Unknown"
+ && decimals > MAX_NORMAL_TOKEN_DECIMALS
+ }
+
+ fn format_token_amount_for_display(
+ token_mint_address: &str,
+ amount: &str,
+ decimals: u8,
+ ) -> Result<String> {
+ if Self::has_unusual_token_decimals(token_mint_address, decimals) {
+ if amount.is_empty() || !amount.bytes().all(|value| value.is_ascii_digit()) {
+ return Err(SolanaError::InvalidData("invalid token amount".to_string()));
+ }
+ return Ok(format!("{} raw units (decimals: {})", amount, decimals));
+ }
+ utils::format_token_amount(amount, decimals)
+ }
+
fn build_token_transfer_checked_overview(details: &[SolanaDetail]) -> Result<SolanaOverview> {
- let overview: Option<SolanaOverview> = details
+ let detail = details
.iter()
.find(|d| Self::is_token_transfer_checked_detail(&d.common))
- .and_then(|detail| {
- if let ProgramDetail::TokenTransferChecked(v) = &detail.kind {
- let amount_f64 = v.amount.parse::<f64>().unwrap();
- let amount = amount_f64 / 10u64.pow(v.decimals as u32) as f64;
- Some(SolanaOverview::SplTokenTransfer(
- ProgramOverviewSplTokenTransfer {
- source: v.account.to_string(),
- destination: v.recipient.to_string(),
- authority: v.owner.to_string(),
- decimals: v.decimals,
- amount: format!("{} {}", amount, Self::find_token_info(&v.mint).0),
- token_mint_account: v.mint.clone(),
- token_symbol: Self::find_token_info(&v.mint).0,
- token_name: Self::find_token_info(&v.mint).1,
- },
- ))
- } else {
- None
- }
- });
- overview.ok_or(SolanaError::ParseTxError(
- "parse spl token transfer failed, empty transfer program".to_string(),
+ .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,
+ },
))
}
fn build_vote_overview(details: &[SolanaDetail]) -> Result<SolanaOverview> {
@@ -373,14 +450,56 @@ impl ParsedSolanaTx {
fn build_general_overview(details: &[SolanaDetail]) -> Result<SolanaOverview> {
let mut overview = Vec::new();
- details.iter().for_each(|d| {
- if d.common.program != SolanaTxDisplayType::Unknown.to_string() {
- overview.push(ProgramOverviewGeneral {
- program: d.common.program.to_string(),
- method: d.common.method.to_string(),
- })
+ for d in details {
+ let mut item = ProgramOverviewGeneral {
+ program: d.common.program.to_string(),
+ method: d.common.method.to_string(),
+ value: String::new(),
+ from: String::new(),
+ to: String::new(),
+ amount: String::new(),
+ source: String::new(),
+ destination: String::new(),
+ authority: String::new(),
+ token: String::new(),
+ mint: String::new(),
+ unusual_decimals: false,
+ decimals: 0,
+ to_in_lookup_table: false,
+ to_lookup_table_reference: String::new(),
+ };
+ match &d.kind {
+ ProgramDetail::SystemTransfer(value) => {
+ item.value = value.value.clone();
+ item.from = value.from.clone();
+ item.to = Self::format_account_for_display(&value.to);
+ item.to_in_lookup_table = Self::is_lookup_table_reference(&value.to);
+ if item.to_in_lookup_table {
+ item.to_lookup_table_reference = value.to.clone();
+ }
+ }
+ ProgramDetail::TokenTransferChecked(value) => {
+ Self::validate_token_decimals(&value.mint, value.decimals)?;
+ let token_info = Self::find_token_info(&value.mint);
+ let amount = Self::format_token_amount_for_display(
+ &value.mint,
+ &value.amount,
+ value.decimals,
+ )?;
+ item.amount = format!("{} {}", amount, token_info.0);
+ item.source = value.account.clone();
+ item.destination = value.recipient.clone();
+ item.authority = value.owner.clone();
+ item.token = format!("{} ({})", token_info.1, token_info.0);
+ item.mint = value.mint.clone();
+ item.unusual_decimals =
+ Self::has_unusual_token_decimals(&value.mint, value.decimals);
+ item.decimals = value.decimals;
+ }
+ _ => {}
}
- });
+ overview.push(item)
+ }
Ok(SolanaOverview::General(overview))
}
fn build_squads_v4_proposal_overview(details: &[SolanaDetail]) -> Result<SolanaOverview> {
@@ -461,32 +580,48 @@ impl ParsedSolanaTx {
fn build_squads_v4_multisig_overview(details: &[SolanaDetail]) -> Result<SolanaOverview> {
let mut transfer_overview_vec: Vec<ProgramOverviewTransfer> = Vec::new();
let mut total_value = 0f64;
- details.iter().for_each(|d| {
+ for d in details {
if let ProgramDetail::SystemTransfer(v) = &d.kind {
- total_value += v
+ let value = v
.value
.to_uppercase()
.replace("SOL", "")
.trim()
.parse::<f64>()
- .unwrap();
+ .map_err(|_| {
+ SolanaError::ParseTxError("invalid Squads transfer amount".to_string())
+ })?;
+ total_value += value;
transfer_overview_vec.push(ProgramOverviewTransfer {
value: v.value.to_string(),
main_action: "SOL Transfer".to_string(),
from: v.from.to_string(),
to: v.to.to_string(),
+ to_in_lookup_table: Self::is_lookup_table_reference(&v.to),
+ to_lookup_table_reference: if Self::is_lookup_table_reference(&v.to) {
+ v.to.clone()
+ } else {
+ String::new()
+ },
});
}
- });
+ }
for d in details {
if let ProgramDetail::SquadsV4MultisigCreate(v) = &d.kind {
- let memo = v.memo.clone().unwrap();
- let memo = serde_json::from_str::<serde_json::Value>(&memo).unwrap();
- let wallet_name = memo["n"]
- .as_str()
+ let memo = v
+ .memo
+ .as_deref()
+ .and_then(|value| serde_json::from_str::<serde_json::Value>(value).ok());
+ let wallet_name = memo
+ .as_ref()
+ .and_then(|value| value["n"].as_str())
.unwrap_or("SquadsV4 Multisig Wallet")
.to_string();
- let wallet_desc = memo["d"].as_str().unwrap_or("").to_string();
+ let wallet_desc = memo
+ .as_ref()
+ .and_then(|value| value["d"].as_str())
+ .unwrap_or("")
+ .to_string();
let threshold = v.threshold;
let member_count = v.members.len();
let members = v
@@ -508,13 +643,20 @@ impl ParsedSolanaTx {
));
}
if let ProgramDetail::SquadsV4MultisigCreateV2(v) = &d.kind {
- let memo = v.memo.clone().unwrap();
- let memo = serde_json::from_str::<serde_json::Value>(&memo).unwrap();
- let wallet_name = memo["n"]
- .as_str()
+ let memo = v
+ .memo
+ .as_deref()
+ .and_then(|value| serde_json::from_str::<serde_json::Value>(value).ok());
+ let wallet_name = memo
+ .as_ref()
+ .and_then(|value| value["n"].as_str())
.unwrap_or("SquadsV4 Multisig Wallet")
.to_string();
- let wallet_desc = memo["d"].as_str().unwrap_or("").to_string();
+ let wallet_desc = memo
+ .as_ref()
+ .and_then(|value| value["d"].as_str())
+ .unwrap_or("")
+ .to_string();
let threshold = v.threshold;
let member_count = v.members.len();
let members = v
@@ -646,9 +788,25 @@ impl ParsedSolanaTx {
// https://solscan.io/tx/4DHUYxDxy3rykXfTq9EmN6GEYgWQ8W1hCu4cT4B3gpgwrNpFdK1vwGccCNWGnKa8avZArH1tRFferf5ezJyDrivP
// https://solscan.io/tx/2mCi5xkVaPxgphtu7z6R1qUt7dmXkFUCHBrepCjSGbT8LjqYnoq9hB7NFtjLvSZ5WGu7cMtsYNbFVpRxEnTb3dRN
// index 7 : source token mint
- let token_a_mint = v.accounts[7].clone();
+ let token_a_mint = v
+ .accounts
+ .get(7)
+ .ok_or_else(|| {
+ SolanaError::ParseTxError(
+ "Jupiter source mint account is missing".to_string(),
+ )
+ })?
+ .clone();
// index 8 : destination token mint
- let token_b_mint = v.accounts[8].clone();
+ let token_b_mint = v
+ .accounts
+ .get(8)
+ .ok_or_else(|| {
+ SolanaError::ParseTxError(
+ "Jupiter destination mint account is missing".to_string(),
+ )
+ })?
+ .clone();
return Ok(Self::genreate_jupiter_swap_overview(
"JupiterV6SharedAccountsRoute",
&token_a_mint,
@@ -663,9 +821,25 @@ impl ParsedSolanaTx {
// https://solscan.io/tx/4DHUYxDxy3rykXfTq9EmN6GEYgWQ8W1hCu4cT4B3gpgwrNpFdK1vwGccCNWGnKa8avZArH1tRFferf5ezJyDrivP
// https://solscan.io/tx/2mCi5xkVaPxgphtu7z6R1qUt7dmXkFUCHBrepCjSGbT8LjqYnoq9hB7NFtjLvSZ5WGu7cMtsYNbFVpRxEnTb3dRN
// index 7 : source token mint
- let token_a_mint = v.accounts[7].clone();
+ let token_a_mint = v
+ .accounts
+ .get(7)
+ .ok_or_else(|| {
+ SolanaError::ParseTxError(
+ "Jupiter source mint account is missing".to_string(),
+ )
+ })?
+ .clone();
// index 8 : destination token mint
- let token_b_mint = v.accounts[8].clone();
+ let token_b_mint = v
+ .accounts
+ .get(8)
+ .ok_or_else(|| {
+ SolanaError::ParseTxError(
+ "Jupiter destination mint account is missing".to_string(),
+ )
+ })?
+ .clone();
return Ok(Self::genreate_jupiter_swap_overview(
"JupiterV6SharedAccountsExactOutRoute",
&token_a_mint,
@@ -679,9 +853,25 @@ impl ParsedSolanaTx {
ProgramDetail::JupiterV6ExactOutRoute(v) => {
// https://solscan.io/tx/XnRGNPgKgtD6Qk8p6Pxg9Z9PRyf5cfUJbjU9grhiDkicbSYUo3h1geaVj87JvZaoeY1VRe2Gcr9aYXH83Vrgki7
// index 5 : source token mint
- let token_a_mint = v.accounts[5].clone();
+ let token_a_mint = v
+ .accounts
+ .get(5)
+ .ok_or_else(|| {
+ SolanaError::ParseTxError(
+ "Jupiter source mint account is missing".to_string(),
+ )
+ })?
+ .clone();
// index 6 : destination token mint
- let token_b_mint = v.accounts[6].clone();
+ let token_b_mint = v
+ .accounts
+ .get(6)
+ .ok_or_else(|| {
+ SolanaError::ParseTxError(
+ "Jupiter destination mint account is missing".to_string(),
+ )
+ })?
+ .clone();
return Ok(Self::genreate_jupiter_swap_overview(
"JupiterV6ExactOutRoute",
&token_a_mint,
@@ -697,7 +887,15 @@ impl ParsedSolanaTx {
// index 4 : source token mint is Unknown
let token_a_mint = "Unknown";
// index 5 destination token mint
- let token_b_mint = v.accounts[5].clone();
+ let token_b_mint = v
+ .accounts
+ .get(5)
+ .ok_or_else(|| {
+ SolanaError::ParseTxError(
+ "Jupiter destination mint account is missing".to_string(),
+ )
+ })?
+ .clone();
return Ok(Self::genreate_jupiter_swap_overview(
"JupiterV6Route",
token_a_mint,
@@ -768,6 +966,145 @@ mod tests {
use super::*;
+ fn detail(program: &str, method: &str) -> SolanaDetail {
+ SolanaDetail {
+ common: CommonDetail {
+ program: program.into(),
+ method: method.into(),
+ },
+ kind: ProgramDetail::GeneralUnknown(ProgramDetailGeneralUnknown::default()),
+ }
+ }
+
+ #[test]
+ fn display_classification_and_account_helpers_cover_security_boundaries() {
+ assert!(ParsedSolanaTx::is_lookup_table_reference("table#255"));
+ assert!(!ParsedSolanaTx::is_lookup_table_reference("#1"));
+ assert!(!ParsedSolanaTx::is_lookup_table_reference("table#256"));
+ assert_eq!(
+ ParsedSolanaTx::format_account_for_display("table#7"),
+ "Table: table\nIndex: 7"
+ );
+ assert_eq!(
+ ParsedSolanaTx::format_account_for_display("ordinary"),
+ "ordinary"
+ );
+
+ assert!(matches!(
+ ParsedSolanaTx::detect_display_type(&[detail("Unknown", "")]),
+ SolanaTxDisplayType::Unknown
+ ));
+ assert!(matches!(
+ ParsedSolanaTx::detect_display_type(&[
+ detail("System", "Transfer"),
+ detail("Unknown", "")
+ ]),
+ SolanaTxDisplayType::General
+ ));
+ for (program, method, expected) in [
+ ("System", "Transfer", SolanaTxDisplayType::Transfer),
+ (
+ "Token",
+ "TransferChecked",
+ SolanaTxDisplayType::TokenTransfer,
+ ),
+ ("Vote", "Vote", SolanaTxDisplayType::Vote),
+ ("JupiterV6", "Route", SolanaTxDisplayType::JupiterV6),
+ ("Other", "Method", SolanaTxDisplayType::General),
+ ] {
+ let actual = ParsedSolanaTx::detect_display_type(&[detail(program, method)]);
+ assert_eq!(
+ core::mem::discriminant(&actual),
+ core::mem::discriminant(&expected)
+ );
+ }
+ assert!(matches!(
+ ParsedSolanaTx::detect_display_type(&[
+ detail("SquadsV4", "ProposalCreate"),
+ detail("System", "Transfer")
+ ]),
+ SolanaTxDisplayType::SquadsV4
+ ));
+ assert!(ParsedSolanaTx::is_instructions_detail(
+ &detail("Instructions", "").common
+ ));
+ }
+
+ #[test]
+ fn token_metadata_and_decimal_safety_helpers() {
+ let known_mints = [
+ "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN",
+ "MNDEFzGvMt87ueuHvVU9VcTqsAP5b3fTGPsHuuPA5ey",
+ "HZ1JovNiVvGrGNiiYvEozEVgZ58xaU3RKwX8eACQBCt3",
+ "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
+ "jtojtomepa8beP8AuQc6eXt5FriJwfFMwQx2v2f9mCL",
+ "85VBFQZC9TZkfaptBWjvUw7YbZjy52A6mjtPGjstQAmQ",
+ "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB",
+ "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263",
+ "J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn",
+ "EKpQGSJtjMFqKZ9KQanSqYXRcF8fBopzLHYxdM65zcjm",
+ "BZLbGTNCSFfoth2GYDtwr7e4imWzpR5jqcUuGEwr646K",
+ "rndrizKT3MK1iimdxRdWabcF7Zg7AR5T4nud4EkHBof",
+ "4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R",
+ "7atgF8KQo4wJrD5ATGX7t1V2zVvykPJbFfNeVf1icFv1",
+ "mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So",
+ "hntyVP6YFm1Hg25TN9WGLqM12b8TQmcknKrdu1oxWux",
+ "27G8MtK7VtTcCHkpASjSDdkWWYfoqT6ggEuKidVJidD4",
+ "8BMzMi2XxZn9afRaMx5Z6fauk9foHXqV5cLTCYWRcVje",
+ "ukHH6c7mMyiWCf1b9pnWe25TSpkDDt3H5pQZgZ74J82",
+ "7GCihgDB8fe6KNjn2MYtkzZcRjQy3t9GHdC8uHYmW2hr",
+ "4vMsoUT2BWatFweudnQM1xedRLfJgJ7hswhcpz4xgBTy",
+ "NeonTjSjsuo3rexg9o6vHuMXw62f9V7zvmu8M8Zut44",
+ "MEW1gQWJ3nEXg2qgERiKu7FAFj79PHvQVREQUzScPP5",
+ "TNSRxcUxoT9xBG3de7PiJyTDYu7kskLqcpddxnEJAS6",
+ "jupSoLaHXQiZZTSfEWMTRRgpnyFm8f6sZdosWBjx93v",
+ "bSo13r4TkiE4KumL71LsHTPpL2euBYLFx6h9HP3piy1",
+ "SRMuApVNdxXokk5GT7XD5cUUgXMBCoAz2LHeuAoKWRt",
+ "5z3EqYQo9HiCEs3R84RCDMu2n7anpDMxRhdK8PSWmrRC",
+ "2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo",
+ "z3dn17yLaGMKffVogeFHQ9zWVcXgqgf3PQnDsNs2g6M",
+ "METAewgxyPbgwsseH8T16a39CQ5VyVxZi9zXiDPY18m",
+ "KMNo3nJsBXfcpJTVhZcXLW7RmTwTt4GVFE7suUBo9sS",
+ "DriFtupJYLTosbwoN8koMbEYSx54aFAVLddWsbksjwg7",
+ "FoXyMu5xwXre7zEoSvzViRk3nGawHUp9kUh97y2NDhcq",
+ "7i5KKsX2weiTkry7jA4ZwSuXGhs5eJBEjY8vVxR4pfRx",
+ "5oVNBeEEQvYi1cX3ir8Dx5n1P7pdxydbGF2X4TxVusJm",
+ "EchesyfXePKdLtoiZSL8pBe8Myagyy8ZRqsACNCFGnvp",
+ "orcaEKTdK7LKz57vaAYr9QeNsVEPfiu6QeMU1kektZE",
+ "EzgfrTjgFyHGaU5BEBRETyfawt66bAYqJvcryWuJtQ5w",
+ "ZEUS1aR7aX8DFFJf5QjWj2ftDDdNTroMNGo8YoQm3Gq",
+ "mb1eu7TzEc71KxDpsmsKoucSSuuoGLv1drys1oP2jh6",
+ "iotEVVZLEywoTn1QdwNPddxPWszn3zFhEot3MfL9fns",
+ "So11111111111111111111111111111111111111112",
+ ];
+ for mint in known_mints {
+ let (_, name, decimals) = ParsedSolanaTx::find_token_info(mint);
+ assert_ne!(name, "Unknown");
+ assert!(ParsedSolanaTx::validate_token_decimals(mint, decimals).is_ok());
+ }
+ assert!(ParsedSolanaTx::validate_token_decimals(known_mints[3], 9).is_err());
+ assert!(ParsedSolanaTx::has_unusual_token_decimals("unknown", 19));
+ assert!(!ParsedSolanaTx::has_unusual_token_decimals("unknown", 18));
+ assert_eq!(
+ ParsedSolanaTx::format_token_amount_for_display("unknown", "1", 19).unwrap(),
+ "1 raw units (decimals: 19)"
+ );
+ assert!(ParsedSolanaTx::format_token_amount_for_display("unknown", "bad", 19).is_err());
+ }
+
+ #[test]
+ 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_vote_overview(empty).is_err());
+ let _ = ParsedSolanaTx::build_squads_v4_proposal_overview(empty);
+ let _ = ParsedSolanaTx::build_squads_v4_multisig_overview(empty);
+ let _ = ParsedSolanaTx::build_squads_overview(empty);
+ let _ = ParsedSolanaTx::build_jupiter_v6_overview(empty);
+ assert!(ParsedSolanaTx::build_instructions_overview(empty).is_ok());
+ }
+
#[test]
fn test_parse_transaction_1() {
// System.Transfer
@@ -934,15 +1271,19 @@ mod tests {
let parsed = ParsedSolanaTx::build(&transaction).unwrap();
match parsed.overview {
SolanaOverview::General(overview) => {
- let overview_1 = overview.get(0).unwrap();
- let overview_2 = overview.get(1).unwrap();
- let overview_3 = overview.get(2).unwrap();
- assert_eq!("System", overview_1.program);
- assert_eq!("CreateAccount", overview_1.method);
- assert_eq!("Token", overview_2.program);
- assert_eq!("InitializeMint", overview_2.method);
- assert_eq!("Token", overview_3.program);
- assert_eq!("MintTo", overview_3.method);
+ let expected = [
+ ("System", "CreateAccount"),
+ ("Token", "InitializeMint"),
+ ("Unknown", ""),
+ ("Unknown", ""),
+ ("Token", "MintTo"),
+ ("Unknown", ""),
+ ];
+ assert_eq!(expected.len(), overview.len());
+ for (item, (program, method)) in overview.iter().zip(expected) {
+ assert_eq!(program, item.program);
+ assert_eq!(method, item.method);
+ }
}
_ => println!("program overview parse error!"),
};
@@ -1146,21 +1487,19 @@ mod tests {
let parsed = ParsedSolanaTx::build(&transaction).unwrap();
match parsed.overview {
SolanaOverview::General(overview) => {
- let overview_1 = overview.get(0).unwrap();
- assert_eq!("System", overview_1.program);
- assert_eq!("CreateAccount", overview_1.method);
- let overview_2 = overview.get(1).unwrap();
- assert_eq!("Token", overview_2.program);
- assert_eq!("InitializeAccount", overview_2.method);
- let overview_3 = overview.get(2).unwrap();
- assert_eq!("Token", overview_3.program);
- assert_eq!("Approve", overview_3.method);
- let overview_4 = overview.get(3).unwrap();
- assert_eq!("Token", overview_4.program);
- assert_eq!("Revoke", overview_4.method);
- let overview_5 = overview.get(4).unwrap();
- assert_eq!("Token", overview_5.program);
- assert_eq!("CloseAccount", overview_5.method);
+ let expected = [
+ ("System", "CreateAccount"),
+ ("Token", "InitializeAccount"),
+ ("Token", "Approve"),
+ ("Unknown", ""),
+ ("Token", "Revoke"),
+ ("Token", "CloseAccount"),
+ ];
+ assert_eq!(expected.len(), overview.len());
+ for (item, (program, method)) in overview.iter().zip(expected) {
+ assert_eq!(program, item.program);
+ assert_eq!(method, item.method);
+ }
}
_ => println!("program overview parse error!"),
};
@@ -1561,97 +1900,12 @@ mod tests {
// https://solscan.io/tx/3KCJ2aWgKc6cyEagFdk74WfM9eDw7VumB7rPw96fQkD1CmjG3w29gTazDEvNsc2bNbkQaZAvL2att11Siy8qF89k
let data = "0301080fae0e9965d80b3bb521ed714366a4d461fd58d7b7c97caa15564ba34c3ec5c04d940d487f489c470872533e2d8b55a5ec1ae1fd130cefae0f1bd1527a9b6955c1ab9daad5867d8a4dba28bb9b9bc4146bc81a83e877c01d693d9860e2863df6f5aaf29edc6d0d3544fccda1232277d6032783264c5cfc335600c85f30754adaa9f604b96c15a6018a88598d0c5a310fe2b6333aa48ba916e502be02578ca50384cc51e45da7f68a2906979e692c1e8bc87e51deca9ddfe7e673895a01ef80facf5f4019373457f129bf4cae6a4255518b885bf718157dd4357233dc79268c4cbf069b8857feab8184fb687f634618c035dac439dc1aeb3b5598a0f0000000000106a7d517192c5c51218cc94c3d4af17f58daee089ba1fd44e3dbd98a0000000006a7d51718c774c928566398691d5eb68b5eb8a39b4b6d5c73555b210000000023166cdfc331b06925f390147d4270172c25a5b218580326b09081a9f3bbe90c051e8a28c6a067b32fbb33323ed92334b6adbdc4639b871c8a2e44f47058ef8506ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a900000000000000000000000000000000000000000000000000000000000000000508c2ceb1b5d05c874980ac52cf659740e7e9b9356aaf2a0362673263526c15e83b5d0c7735cf4f76914b1488bc665d32dce3140950851428922cc65fbb565b070c03030200090424eb0700000000000d0200013400000000f01d1f0000000000a50000000000000006ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a90c040107000801010e02090401080e0a03010405060a0b02090c090424eb0700000000000c02030001050c03010000010901";
let transaction = Vec::from_hex(data).unwrap();
- let parsed = ParsedSolanaTx::build(&transaction).unwrap();
- match parsed.overview {
- SolanaOverview::General(overview) => {
- let overview_1 = overview.get(0).unwrap();
- assert_eq!("Token", overview_1.program);
- assert_eq!("Approve", overview_1.method);
- let overview_2 = overview.get(1).unwrap();
- assert_eq!("System", overview_2.program);
- assert_eq!("CreateAccount", overview_2.method);
- let overview_3 = overview.get(2).unwrap();
- assert_eq!("Token", overview_3.program);
- assert_eq!("InitializeAccount", overview_3.method);
- let overview_4 = overview.get(3).unwrap();
- assert_eq!("TokenLending", overview_4.program);
- assert_eq!("DepositReserveLiquidity", overview_4.method);
- let overview_5 = overview.get(4).unwrap();
- assert_eq!("Token", overview_5.program);
- assert_eq!("Revoke", overview_5.method);
- let overview_6 = overview.get(5).unwrap();
- assert_eq!("Token", overview_6.program);
- assert_eq!("CloseAccount", overview_6.method);
- }
- _ => println!("program overview parse error!"),
- };
- let parsed_detail: Value = serde_json::from_str(parsed.detail.as_str()).unwrap();
-
- let expected_detail = json!([
- {
- "amount": "518948",
- "delegate_account": "CYvAAqCR6LjctqdWvPe1CfBW9p5uSc85Da45gENrVSr8",
- "method": "Approve",
- "owner": "CiSrMrPbsnr2pXFHEKXSvHqw1r29qbpRnK1qV9n7zYCC",
- "program": "Token",
- "source_account": "CWJtEyYYHy3ydjHn5Beh48mHiW9BBHSYjcGDJkB8awNx"
- },
- {
- "amount": "2039280",
- "funding_account": "CiSrMrPbsnr2pXFHEKXSvHqw1r29qbpRnK1qV9n7zYCC",
- "method": "CreateAccount",
- "new_account": "Axw63e2KwrSmqWsZcNUQNXHH4cSfv2xEJBZG7Ua5Rrit",
- "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
- "program": "System",
- "space": "165"
- },
- {
- "account": "Axw63e2KwrSmqWsZcNUQNXHH4cSfv2xEJBZG7Ua5Rrit",
- "method": "InitializeAccount",
- "mint": "So11111111111111111111111111111111111111112",
- "owner": "CiSrMrPbsnr2pXFHEKXSvHqw1r29qbpRnK1qV9n7zYCC",
- "program": "Token",
- "sysver_rent": "SysvarRent111111111111111111111111111111111"
- },
- {
- "accounts": [
- "SysvarC1ock11111111111111111111111111111111",
- "HZMUNJQDwT8rdEiY2r15UR6h8yYg7QkxiekjyJGFFwnB"
- ],
- "data": "9",
- "program": "Unknown",
- "program_account": "LendZqTs7gn5CTSJU1jWKhKuVpjJGom45nnwPb2AMTi"
- },
- {
- "destination_collateral_account": "Axw63e2KwrSmqWsZcNUQNXHH4cSfv2xEJBZG7Ua5Rrit",
- "lending_market_account": "3My6wgR1fHmDFqBvv1hys7PigtH1megLncRCh2PkBMTR",
- "lending_market_authority_pubkey": "Lz3nGpTr7SfSf7eJqcoQEkXK2fSK3dfCoSdQSKxbXxQ",
- "liquidity_amount": "518948",
- "method": "DepositReserveLiquidity",
- "program": "TokenLending",
- "reserve_account": "HZMUNJQDwT8rdEiY2r15UR6h8yYg7QkxiekjyJGFFwnB",
- "reserve_collateral_mint": "7QpRNyLenfoUA8SrpDTaaurtx4JxAJ2j4zdkNUMsTa6A",
- "reserve_liquidity_supply_account": "EkabaFX962r7gbdjQ6i2kfbrjFA6XppgKZ4APeUhA7gS",
- "source_liquidity_account": "CWJtEyYYHy3ydjHn5Beh48mHiW9BBHSYjcGDJkB8awNx",
- "sysvar_clock": "SysvarC1ock11111111111111111111111111111111",
- "token_program_id": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
- "user_transfer_authority_pubkey": "CYvAAqCR6LjctqdWvPe1CfBW9p5uSc85Da45gENrVSr8"
- },
- {
- "method": "Revoke",
- "owner": "CiSrMrPbsnr2pXFHEKXSvHqw1r29qbpRnK1qV9n7zYCC",
- "program": "Token",
- "source_account": "CWJtEyYYHy3ydjHn5Beh48mHiW9BBHSYjcGDJkB8awNx"
- },
- {
- "account": "Axw63e2KwrSmqWsZcNUQNXHH4cSfv2xEJBZG7Ua5Rrit",
- "method": "CloseAccount",
- "owner": "CiSrMrPbsnr2pXFHEKXSvHqw1r29qbpRnK1qV9n7zYCC",
- "program": "Token",
- "recipient": "CiSrMrPbsnr2pXFHEKXSvHqw1r29qbpRnK1qV9n7zYCC"
- }
- ]);
- assert_eq!(expected_detail, parsed_detail);
+ let result = ParsedSolanaTx::build(&transaction);
+ assert!(matches!(
+ result,
+ Err(SolanaError::InvalidData(message))
+ if message == "trailing bytes after message"
+ ));
}
#[test]
diff --git a/rust/apps/solana/src/parser/overview.rs b/rust/apps/solana/src/parser/overview.rs
index 374bee7..c6e7059 100644
--- a/rust/apps/solana/src/parser/overview.rs
+++ b/rust/apps/solana/src/parser/overview.rs
@@ -7,6 +7,8 @@ pub struct ProgramOverviewTransfer {
pub main_action: String,
pub from: String,
pub to: String,
+ pub to_in_lookup_table: bool,
+ pub to_lookup_table_reference: String,
}
#[derive(Debug, Clone)]
@@ -19,6 +21,7 @@ pub struct ProgramOverviewSplTokenTransfer {
pub token_mint_account: String,
pub token_symbol: String,
pub token_name: String,
+ pub unusual_decimals: bool,
}
#[derive(Debug, Clone)]
@@ -32,6 +35,19 @@ pub struct ProgramOverviewVote {
pub struct ProgramOverviewGeneral {
pub program: String,
pub method: String,
+ pub value: String,
+ pub from: String,
+ pub to: String,
+ pub amount: String,
+ pub source: String,
+ pub destination: String,
+ pub authority: String,
+ pub token: String,
+ pub mint: String,
+ pub unusual_decimals: bool,
+ pub decimals: u8,
+ pub to_in_lookup_table: bool,
+ pub to_lookup_table_reference: String,
}
#[derive(Debug, Clone)]
diff --git a/rust/apps/solana/src/solana_lib/squads_v4/instructions.rs b/rust/apps/solana/src/solana_lib/squads_v4/instructions.rs
index ac1e2d6..8102527 100644
--- a/rust/apps/solana/src/solana_lib/squads_v4/instructions.rs
+++ b/rust/apps/solana/src/solana_lib/squads_v4/instructions.rs
@@ -96,25 +96,31 @@ pub enum SquadsInstructions {
impl Dispatch for SquadsInstructions {
fn dispatch(instrucion_data: &[u8]) -> Result<Self, ProgramError> {
let data = instrucion_data;
+ if data.len() < 8 {
+ return Err(SquadsV4Error::InvalidInstruction.into());
+ }
let ix_type = &data[..8];
let ix_data = &data[8..];
match hex::encode(ix_type).as_str() {
"7a4d509f54585ac5" => {
// sighash(SIGHASH_GLOBAL_NAMESPACE, "multisig_create")
Ok(SquadsInstructions::MultisigCreate(
- from_slice::<MultisigCreateArgs>(ix_data).unwrap(),
+ from_slice::<MultisigCreateArgs>(ix_data)
+ .map_err(|_| SquadsV4Error::InvalidInstruction)?,
))
}
"32ddc75d28f58be9" => {
// sighash(SIGHASH_GLOBAL_NAMESPACE, "multisig_create_v2")
Ok(SquadsInstructions::MultisigCreateV2(
- from_slice::<MultisigCreateArgsV2>(ix_data).unwrap(),
+ from_slice::<MultisigCreateArgsV2>(ix_data)
+ .map_err(|_| SquadsV4Error::InvalidInstruction)?,
))
}
"dc3c49e01e6c4f9f" => {
// sighash(SIGHASH_GLOBAL_NAMESPACE, "proposal_create")
Ok(SquadsInstructions::ProposalCreate(
- from_slice::<ProposalCreateArgs>(ix_data).unwrap(),
+ from_slice::<ProposalCreateArgs>(ix_data)
+ .map_err(|_| SquadsV4Error::InvalidInstruction)?,
))
}
"0b225cf89a1b336a" => {
@@ -124,26 +130,30 @@ impl Dispatch for SquadsInstructions {
"9025a488bcd82af8" => {
// sighash(SIGHASH_GLOBAL_NAMESPACE, "proposal_approve")
Ok(SquadsInstructions::ProposalApprove(
- from_slice::<ProposalVoteArgs>(ix_data).unwrap(),
+ from_slice::<ProposalVoteArgs>(ix_data)
+ .map_err(|_| SquadsV4Error::InvalidInstruction)?,
))
}
"1b2a7fed26a354cb" => {
// sighash(SIGHASH_GLOBAL_NAMESPACE, "proposal_cancel")
Ok(SquadsInstructions::ProposalCancel(
- from_slice::<ProposalVoteArgs>(ix_data).unwrap(),
+ from_slice::<ProposalVoteArgs>(ix_data)
+ .map_err(|_| SquadsV4Error::InvalidInstruction)?,
))
}
"f33e869ce66af687" => {
// sighash(SIGHASH_GLOBAL_NAMESPACE, "proposal_reject")
Ok(SquadsInstructions::ProposalReject(
- from_slice::<ProposalVoteArgs>(ix_data).unwrap(),
+ from_slice::<ProposalVoteArgs>(ix_data)
+ .map_err(|_| SquadsV4Error::InvalidInstruction)?,
))
}
"30fa4ea8d0e2dad3" => {
// sighash(SIGHASH_GLOBAL_NAMESPACE,vault_transaction_create
Ok(SquadsInstructions::VaultTransactionCreate(
- from_slice::<VaultTransactionCreateArgs>(ix_data).unwrap(),
+ from_slice::<VaultTransactionCreateArgs>(ix_data)
+ .map_err(|_| SquadsV4Error::InvalidInstruction)?,
))
}
"c208a15799a419ab" => {
diff --git a/rust/apps/solana/src/utils.rs b/rust/apps/solana/src/utils.rs
index 4441702..8f63f82 100644
--- a/rust/apps/solana/src/utils.rs
+++ b/rust/apps/solana/src/utils.rs
@@ -1,3 +1,46 @@
+use alloc::string::{String, ToString};
+
+use crate::errors::{Result, SolanaError};
+
+pub fn format_token_amount(amount: &str, decimals: u8) -> Result<String> {
+ if amount.is_empty() || !amount.bytes().all(|value| value.is_ascii_digit()) {
+ return Err(SolanaError::InvalidData("invalid token amount".to_string()));
+ }
+
+ let normalized = amount.trim_start_matches('0');
+ let digits = if normalized.is_empty() {
+ "0"
+ } else {
+ normalized
+ };
+ let decimals = decimals as usize;
+ if decimals == 0 {
+ return Ok(digits.to_string());
+ }
+
+ let mut result = String::new();
+ if digits.len() <= decimals {
+ result.push_str("0.");
+ for _ in 0..decimals - digits.len() {
+ result.push('0');
+ }
+ result.push_str(digits);
+ } else {
+ let split = digits.len() - decimals;
+ result.push_str(&digits[..split]);
+ result.push('.');
+ result.push_str(&digits[split..]);
+ }
+
+ while result.ends_with('0') {
+ result.pop();
+ }
+ if result.ends_with('.') {
+ result.pop();
+ }
+ Ok(result)
+}
+
// tokenAmount to human readable
pub fn token_amount_to_human_readable(token_amount: u64, decimals: u32) -> f64 {
token_amount as f64 / 10u64.pow(decimals) as f64
@@ -21,4 +64,15 @@ mod tests {
fn test_token_amount_to_human_readable_3() {
assert_eq!(token_amount_to_human_readable(10000, 0), 10000.0);
}
+
+ #[test]
+ fn formats_large_token_amounts_without_floating_point() {
+ assert_eq!(
+ format_token_amount("18446744073709551615", 6).unwrap(),
+ "18446744073709.551615"
+ );
+ assert_eq!(format_token_amount("1000000", 6).unwrap(), "1");
+ assert_eq!(format_token_amount("1", 10).unwrap(), "0.0000000001");
+ assert_eq!(format_token_amount("0", u8::MAX).unwrap(), "0");
+ }
}
diff --git a/rust/rust_c/src/avalanche/mod.rs b/rust/rust_c/src/avalanche/mod.rs
index 29b330b..8702fcb 100644
--- a/rust/rust_c/src/avalanche/mod.rs
+++ b/rust/rust_c/src/avalanche/mod.rs
@@ -285,7 +285,18 @@ pub unsafe extern "C" fn avax_check_transaction(
}
};
- match avax_tx.get_derivation_path()[0].get_source_fingerprint() {
+ let derivation_paths = avax_tx.get_derivation_path();
+ let first_path = match derivation_paths.first() {
+ Some(path) => path,
+ None => {
+ return TransactionCheckResult::from(RustCError::InvalidData(
+ "missing derivation path".to_string(),
+ ))
+ .c_ptr();
+ }
+ };
+
+ match first_path.get_source_fingerprint() {
Some(fingerprint) if fingerprint == mfp => TransactionCheckResult::new().c_ptr(),
_ => TransactionCheckResult::from(RustCError::MasterFingerprintMismatch).c_ptr(),
}
diff --git a/rust/rust_c/src/avalanche/structs.rs b/rust/rust_c/src/avalanche/structs.rs
index 39c2622..e2c953d 100644
--- a/rust/rust_c/src/avalanche/structs.rs
+++ b/rust/rust_c/src/avalanche/structs.rs
@@ -2,7 +2,6 @@ use alloc::boxed::Box;
use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
-use app_avalanche::constants::NAVAX_TO_AVAX_RATIO;
use core::ptr::null_mut;
use ur_registry::pb::protoc::payload::Type;
@@ -75,7 +74,7 @@ impl DisplayAvaxFromToInfo {
from_infos: &[(String, String)],
type_id: TypeId,
) -> Self {
- let address = value.address.first().unwrap().clone();
+ let address = value.address.first().cloned().unwrap_or_default();
let matched_path = from_infos
.iter()
.find(|(_, from_address)| *from_address == address)
@@ -94,10 +93,7 @@ impl DisplayAvaxFromToInfo {
};
DisplayAvaxFromToInfo {
address: convert_c_char(address.clone()),
- amount: convert_c_char(format!(
- "{} AVAX",
- value.amount as f64 / NAVAX_TO_AVAX_RATIO
- )),
+ amount: convert_c_char(format_avax(value.amount)),
is_change,
path,
}
@@ -152,10 +148,7 @@ impl DisplayTxAvaxData {
from_address: String,
type_id: TypeId,
) -> Self {
- let total_input_amount = format!(
- "{} AVAX",
- value.get_total_input_amount() as f64 / NAVAX_TO_AVAX_RATIO
- );
+ let total_input_amount = format_avax(value.get_total_input_amount());
let from = VecFFI::from(
from_infos
@@ -176,23 +169,13 @@ impl DisplayTxAvaxData {
DisplayTxAvaxData {
from,
- amount: convert_c_char(format!(
- "{} AVAX",
- value.get_output_amount(from_address.clone(), type_id) as f64 / NAVAX_TO_AVAX_RATIO
+ amount: convert_c_char(format_avax(
+ value.get_output_amount(from_address.clone(), type_id),
)),
- total_input_amount: convert_c_char(format!(
- "{} AVAX",
- value.get_total_input_amount() as f64 / NAVAX_TO_AVAX_RATIO
- )),
- total_output_amount: convert_c_char(format!(
- "{} AVAX",
- value.get_total_output_amount() as f64 / NAVAX_TO_AVAX_RATIO
- )),
- fee_amount: convert_c_char(format!(
- "{} AVAX",
- value.get_fee_amount() as f64 / NAVAX_TO_AVAX_RATIO
- )),
+ total_input_amount: convert_c_char(format_avax(value.get_total_input_amount())),
+ total_output_amount: convert_c_char(format_avax(value.get_total_output_amount())),
+ fee_amount: convert_c_char(format_avax(value.get_fee_amount())),
to: VecFFI::from(
value
.get_outputs_addresses()
@@ -240,8 +223,25 @@ impl Free for DisplayTxAvaxData {
free_str_ptr!(self.total_input_amount);
free_str_ptr!(self.fee_amount);
free_str_ptr!(self.reward_address);
- Box::from_raw(self.method);
+ if !self.method.is_null() {
+ Box::from_raw(self.method).free();
+ }
+ }
+}
+
+fn format_avax(value: u64) -> String {
+ const NAVAX_PER_AVAX: u64 = 1_000_000_000;
+ let whole = value / NAVAX_PER_AVAX;
+ let fraction = value % NAVAX_PER_AVAX;
+ if fraction == 0 {
+ return format!("{} AVAX", whole);
+ }
+
+ let mut fraction_string = format!("{:09}", fraction);
+ while fraction_string.ends_with('0') {
+ fraction_string.pop();
}
+ format!("{}.{} AVAX", whole, fraction_string)
}
impl Free for DisplayAvaxTx {
diff --git a/rust/rust_c/src/bitcoin/structs.rs b/rust/rust_c/src/bitcoin/structs.rs
index 18224e7..31d171a 100644
--- a/rust/rust_c/src/bitcoin/structs.rs
+++ b/rust/rust_c/src/bitcoin/structs.rs
@@ -60,6 +60,7 @@ pub struct DisplayTxOverview {
network: PtrString,
is_multisig: bool,
fee_larger_than_amount: bool,
+ is_large_fee: bool,
sign_status: PtrString,
need_sign: bool,
has_witness_only_inputs: bool,
@@ -138,6 +139,7 @@ impl From<OverviewTx> for DisplayTxOverview {
total_output_amount: convert_c_char(value.total_output_amount),
fee_amount: convert_c_char(value.fee_amount),
fee_larger_than_amount: value.fee_larger_than_amount,
+ is_large_fee: value.is_large_fee,
total_output_sat: convert_c_char(value.total_output_sat),
fee_sat: convert_c_char(value.fee_sat),
from: VecFFI::from(
diff --git a/rust/rust_c/src/common/ur_ext.rs b/rust/rust_c/src/common/ur_ext.rs
index ef9d2fa..c394f1b 100644
--- a/rust/rust_c/src/common/ur_ext.rs
+++ b/rust/rust_c/src/common/ur_ext.rs
@@ -54,7 +54,7 @@ use ur_registry::pb::protobuf_parser::{parse_protobuf, unzip};
use ur_registry::pb::protoc;
use ur_registry::pb::protoc::Base;
#[cfg(feature = "solana")]
-use ur_registry::solana::sol_sign_request::SolSignRequest;
+use ur_registry::solana::sol_sign_request::{SignType as SolanaSignType, SolSignRequest};
#[cfg(feature = "stellar")]
use ur_registry::stellar::stellar_sign_request::{SignType as StellarSignType, StellarSignRequest};
#[cfg(feature = "sui")]
@@ -378,10 +378,18 @@ impl InferViewType for BtcSignRequest {
#[cfg(feature = "solana")]
impl InferViewType for SolSignRequest {
fn infer(&self) -> Result<ViewType, URError> {
- if app_solana::validate_tx(&mut self.get_sign_data()) {
- return Ok(ViewType::SolanaTx);
+ match self.get_sign_type() {
+ SolanaSignType::Transaction => Ok(ViewType::SolanaTx),
+ SolanaSignType::Message => {
+ let sign_data = self.get_sign_data();
+ if app_solana::validate_tx(&mut sign_data.clone())
+ || app_solana::has_tx_prefix(&mut sign_data.clone())
+ {
+ return Ok(ViewType::SolanaTx);
+ }
+ Ok(ViewType::SolanaMessage)
+ }
}
- Ok(ViewType::SolanaMessage)
}
}
diff --git a/rust/rust_c/src/ethereum/structs.rs b/rust/rust_c/src/ethereum/structs.rs
index aa63bf4..0509179 100644
--- a/rust/rust_c/src/ethereum/structs.rs
+++ b/rust/rust_c/src/ethereum/structs.rs
@@ -253,7 +253,7 @@ impl From<ParsedEthereumTransaction> for DisplayETHDetail {
gas_limit: convert_c_char(tx.gas_limit),
from: tx.from.map(convert_c_char).unwrap_or(null_mut()),
to: convert_c_char(tx.to),
- nonce: convert_c_char(tx.nonce.to_string()),
+ nonce: convert_c_char(tx.nonce),
input: convert_c_char(tx.input),
}
}
@@ -315,8 +315,6 @@ impl From<TypedData> for DisplayETHTypedData {
}
}
- let safe_tx_hash = message.get_safe_tx_hash();
-
Self {
name: to_ptr_string(message.name),
version: to_ptr_string(message.version),
@@ -328,7 +326,7 @@ impl From<TypedData> for DisplayETHTypedData {
from: message.from.map(to_ptr_string).unwrap_or(null_mut()),
domain_hash: to_ptr_string(message.domain_separator),
message_hash: to_ptr_string(message.message_hash),
- safe_tx_hash: to_ptr_string(safe_tx_hash),
+ safe_tx_hash: to_ptr_string(message.safe_tx_hash),
}
}
}
diff --git a/rust/rust_c/src/solana/mod.rs b/rust/rust_c/src/solana/mod.rs
index bb64d83..c37ee34 100644
--- a/rust/rust_c/src/solana/mod.rs
+++ b/rust/rust_c/src/solana/mod.rs
@@ -10,7 +10,7 @@ use app_solana::errors::SolanaError;
use app_solana::parse_message;
use cty::c_char;
use structs::{DisplaySolanaMessage, DisplaySolanaTx};
-use ur_registry::solana::sol_sign_request::SolSignRequest;
+use ur_registry::solana::sol_sign_request::{SignType, SolSignRequest};
use ur_registry::solana::sol_signature::SolSignature;
use ur_registry::traits::RegistryItem;
@@ -18,6 +18,22 @@ pub mod structs;
unsafe fn build_sign_result(ptr: PtrUR, seed: &[u8]) -> Result<SolSignature, SolanaError> {
let sign_request = extract_ptr_with_type!(ptr, SolSignRequest);
+ let sign_data = sign_request.get_sign_data();
+ let sign_type = sign_request.get_sign_type();
+ let is_complete_transaction = match sign_type {
+ // Exact parsing is performed together with the required-signer check below,
+ // avoiding a second full transaction parse on the normal signing path.
+ SignType::Transaction => true,
+ SignType::Message => {
+ let is_complete_transaction = app_solana::validate_tx(&mut sign_data.clone());
+ if !is_complete_transaction && app_solana::has_tx_prefix(&mut sign_data.clone()) {
+ return Err(SolanaError::InvalidData(
+ "message contains a transaction prefix with hidden trailing data".to_string(),
+ ));
+ }
+ is_complete_transaction
+ }
+ };
let mut path =
sign_request
.get_derivation_path()
@@ -28,7 +44,11 @@ unsafe fn build_sign_result(ptr: PtrUR, seed: &[u8]) -> Result<SolSignature, Sol
if !path.starts_with("m/") {
path = format!("m/{path}");
}
- let signature = app_solana::sign(sign_request.get_sign_data().to_vec(), &path, seed)?;
+ if is_complete_transaction {
+ let signer = app_solana::get_public_key(seed, &path)?;
+ app_solana::validate_tx_signer(&mut sign_data.clone(), &signer)?;
+ }
+ let signature = app_solana::sign(sign_data, &path, seed)?;
Ok(SolSignature::new(
sign_request.get_request_id(),
signature.to_vec(),
@@ -83,6 +103,32 @@ pub unsafe extern "C" fn solana_parse_tx(
}
}
+#[no_mangle]
+pub unsafe extern "C" fn solana_parse_tx_with_pubkey(
+ ptr: PtrUR,
+ pubkey: PtrString,
+) -> PtrT<TransactionParseResult<DisplaySolanaTx>> {
+ let solan_sign_reqeust = extract_ptr_with_type!(ptr, SolSignRequest);
+ let tx_hex = solan_sign_reqeust.get_sign_data();
+ let pubkey = recover_c_char(pubkey);
+ let signer: [u8; 32] = match hex::decode(pubkey)
+ .ok()
+ .and_then(|value| value.try_into().ok())
+ {
+ Some(value) => value,
+ None => {
+ return TransactionParseResult::from(SolanaError::InvalidData(
+ "invalid Solana signer public key".to_string(),
+ ))
+ .c_ptr()
+ }
+ };
+ match app_solana::parse_for_signer(&tx_hex.to_vec(), &signer) {
+ Ok(v) => TransactionParseResult::success(DisplaySolanaTx::from(v).c_ptr()).c_ptr(),
+ Err(e) => TransactionParseResult::from(e).c_ptr(),
+ }
+}
+
#[no_mangle]
// this function is used to sign the tx and message
pub unsafe extern "C" fn solana_sign_tx(
@@ -119,7 +165,7 @@ pub unsafe extern "C" fn solana_parse_message(
let sol_sign_request = extract_ptr_with_type!(ptr, SolSignRequest);
let pubkey = recover_c_char(pubkey);
// verify whether the UR is message to prevent using the tx as message
- if app_solana::validate_tx(&mut sol_sign_request.get_sign_data()) {
+ if app_solana::has_tx_prefix(&mut sol_sign_request.get_sign_data()) {
return TransactionParseResult::from(RustCError::UnsupportedTransaction(
"Transaction".to_string(),
))
diff --git a/rust/rust_c/src/solana/structs.rs b/rust/rust_c/src/solana/structs.rs
index 14f3358..de54191 100644
--- a/rust/rust_c/src/solana/structs.rs
+++ b/rust/rust_c/src/solana/structs.rs
@@ -26,12 +26,35 @@ pub struct DisplaySolanaTx {
pub struct DisplaySolanaTxOverviewGeneral {
pub program: PtrString,
pub method: PtrString,
+ pub value: PtrString,
+ pub from: PtrString,
+ pub to: PtrString,
+ pub amount: PtrString,
+ pub source: PtrString,
+ pub destination: PtrString,
+ pub authority: PtrString,
+ pub token: PtrString,
+ pub mint: PtrString,
+ pub unusual_decimals: bool,
+ pub decimals: u8,
+ pub to_in_lookup_table: bool,
+ pub to_lookup_table_reference: PtrString,
}
impl Free for DisplaySolanaTxOverviewGeneral {
unsafe fn free(&self) {
free_str_ptr!(self.program);
free_str_ptr!(self.method);
+ free_str_ptr!(self.value);
+ free_str_ptr!(self.from);
+ free_str_ptr!(self.to);
+ free_str_ptr!(self.amount);
+ free_str_ptr!(self.source);
+ free_str_ptr!(self.destination);
+ free_str_ptr!(self.authority);
+ free_str_ptr!(self.token);
+ free_str_ptr!(self.mint);
+ free_str_ptr!(self.to_lookup_table_reference);
}
}
@@ -40,6 +63,19 @@ impl From<&ProgramOverviewGeneral> for DisplaySolanaTxOverviewGeneral {
Self {
program: convert_c_char(value.program.to_string()),
method: convert_c_char(value.method.to_string()),
+ value: convert_c_char(value.value.to_string()),
+ from: convert_c_char(value.from.to_string()),
+ to: convert_c_char(value.to.to_string()),
+ amount: convert_c_char(value.amount.to_string()),
+ source: convert_c_char(value.source.to_string()),
+ destination: convert_c_char(value.destination.to_string()),
+ authority: convert_c_char(value.authority.to_string()),
+ token: convert_c_char(value.token.to_string()),
+ mint: convert_c_char(value.mint.to_string()),
+ unusual_decimals: value.unusual_decimals,
+ decimals: value.decimals,
+ to_in_lookup_table: value.to_in_lookup_table,
+ to_lookup_table_reference: convert_c_char(value.to_lookup_table_reference.to_string()),
}
}
}
@@ -109,6 +145,7 @@ pub struct DisplaySolanaTxSplTokenTransferOverview {
pub token_mint_account: PtrString,
pub token_symbol: PtrString,
pub token_name: PtrString,
+ pub unusual_decimals: bool,
}
impl_c_ptrs!(DisplaySolanaTxSplTokenTransferOverview);
impl Free for DisplaySolanaTxSplTokenTransferOverview {
@@ -183,6 +220,8 @@ pub struct DisplaySolanaTxOverview {
pub transfer_value: PtrString,
pub transfer_from: PtrString,
pub transfer_to: PtrString,
+ pub transfer_to_in_lookup_table: bool,
+ pub transfer_to_lookup_table_reference: PtrString,
// vote
pub votes_on: PtrT<VecFFI<DisplaySolanaTxOverviewVotesOn>>,
pub vote_account: PtrString,
@@ -278,6 +317,8 @@ impl Default for DisplaySolanaTxOverview {
main_action: null_mut(),
transfer_from: null_mut(),
transfer_to: null_mut(),
+ transfer_to_in_lookup_table: false,
+ transfer_to_lookup_table_reference: null_mut(),
votes_on: null_mut(),
vote_account: null_mut(),
general: null_mut(),
@@ -305,6 +346,7 @@ impl Free for DisplaySolanaTxOverview {
free_str_ptr!(self.transfer_value);
free_str_ptr!(self.transfer_from);
free_str_ptr!(self.transfer_to);
+ free_str_ptr!(self.transfer_to_lookup_table_reference);
free_str_ptr!(self.vote_account);
if !self.general.is_null() {
let x = Box::from_raw(self.general);
@@ -367,6 +409,10 @@ impl From<&ParsedSolanaTx> for DisplaySolanaTxOverview {
main_action: convert_c_char(overview.main_action.to_string()),
transfer_from: convert_c_char(overview.from.to_string()),
transfer_to: convert_c_char(overview.to.to_string()),
+ transfer_to_in_lookup_table: overview.to_in_lookup_table,
+ transfer_to_lookup_table_reference: convert_c_char(
+ overview.to_lookup_table_reference.to_string(),
+ ),
..DisplaySolanaTxOverview::default()
};
}
@@ -386,6 +432,7 @@ impl From<&ParsedSolanaTx> for DisplaySolanaTxOverview {
),
token_symbol: convert_c_char(overview.token_symbol.to_string()),
token_name: convert_c_char(overview.token_name.to_string()),
+ unusual_decimals: overview.unusual_decimals,
}
.c_ptr(),
..DisplaySolanaTxOverview::default()
diff --git a/src/ui/gui_analyze/gui_analyze.c b/src/ui/gui_analyze/gui_analyze.c
index aada31a..5e56dd6 100644
--- a/src/ui/gui_analyze/gui_analyze.c
+++ b/src/ui/gui_analyze/gui_analyze.c
@@ -421,6 +421,18 @@ void GuiWidgetBaseInit(lv_obj_t *obj, cJSON *json)
cJSON *childrenArray = cJSON_GetObjectItem(json, "children");
if (childrenArray != NULL) {
for (cJSON *child = childrenArray->child; child != NULL; child = child->next) {
+#ifdef FEATURE_SOLANA
+ cJSON *type = cJSON_GetObjectItem(child, "type");
+ cJSON *textFunc = cJSON_GetObjectItem(child, "text_func");
+ if (type != NULL && textFunc != NULL &&
+ !strcmp(type->valuestring, "label") &&
+ (!strcmp(textFunc->valuestring, "GetSolMessageUtf8") ||
+ !strcmp(textFunc->valuestring, "GetSolMessageRaw"))) {
+ GuiShowSolMessagePaged(obj, g_totalData,
+ !strcmp(textFunc->valuestring, "GetSolMessageRaw"));
+ continue;
+ }
+#endif
GuiWidgetFactoryCreate(obj, child);
}
}
diff --git a/src/ui/gui_analyze/multi/web3/gui_general_analyze.c b/src/ui/gui_analyze/multi/web3/gui_general_analyze.c
index 48bc3b3..3a1b544 100644
--- a/src/ui/gui_analyze/multi/web3/gui_general_analyze.c
+++ b/src/ui/gui_analyze/multi/web3/gui_general_analyze.c
@@ -7,8 +7,6 @@ static GetLabelDataLenFunc GuiAdaTextLenFuncGet(char *type);
static GetLabelDataLenFunc GuiEthTextLenFuncGet(char *type);
static GetLabelDataLenFunc GuiXrpTextLenFuncGet(char *type);
static GetLabelDataLenFunc GuiStellarTextLenFuncGet(char *type);
-static GetLabelDataLenFunc GuiArTextLenFuncGet(char *type);
-static GetTableDataFunc GuiEthTableFuncGet(char *type);
static GetTableDataFunc GuiAdaTabelFuncGet(char *type);
static GetLabelDataFunc GuiTrxTextFuncGet(char *type);
static GetLabelDataFunc GuiTrxPersonalMessageTextFuncGet(char *type);
@@ -18,12 +16,10 @@ static GetLabelDataLenFunc GuiSuiTextLenFuncGet(char *type);
static GetLabelDataFunc GuiAptosTextFuncGet(char *type);
static GetLabelDataLenFunc GuiAptosTextLenFuncGet(char *type);
static GetLabelDataFunc GuiXrpTextFuncGet(char *type);
-static GetLabelDataFunc GuiArTextFuncGet(char *type);
static GetLabelDataFunc GuiStellarTextFuncGet(char *type);
static GetLabelDataFunc GuiSolMessageTextFuncGet(char *type);
static GetLabelDataFunc GuiEthTypedDataTextFuncGet(char *type);
static GetLabelDataFunc GuiEthPersonalMessageTextFuncGet(char *type);
-static GetLabelDataFunc GuiEthTextFuncGet(char *type);
static GetContSizeFunc GetEthObjPos(char *type);
static GetContSizeFunc GetTrxPersonalMessageObjPos(char *type);
static GetContSizeFunc GetCosmosObjPos(char *type);
@@ -77,8 +73,12 @@ GetCustomContainerFunc GetOtherChainCustomFunc(char *funcName)
return GuiShowSolTxOverview;
} else if (!strcmp(funcName, "GuiShowSolTxDetail")) {
return GuiShowSolTxDetail;
- } else if (!strcmp(funcName, "GuiShowArweaveTxDetail")) {
- return GuiShowArweaveTxDetail;
+ } else if (!strcmp(funcName, "GuiArTxOverview")) {
+ return GuiArTxOverview;
+ } else if (!strcmp(funcName, "GuiArTxDetails")) {
+ return GuiArTxDetails;
+ } else if (!strcmp(funcName, "GuiArMessageOverview")) {
+ return GuiArMessageOverview;
} else if (!strcmp(funcName, "GetCatalystRewardsNotice")) {
return GetCatalystRewardsNotice;
} else if (!strcmp(funcName, "GuiStellarTxNotice")) {
@@ -93,6 +93,14 @@ GetCustomContainerFunc GetOtherChainCustomFunc(char *funcName)
return GuiTonProofOverview;
} else if (!strcmp(funcName, "GuiTonProofRawData")) {
return GuiTonProofRawData;
+ } else if (!strcmp(funcName, "GuiEthTxOverview")) {
+ return GuiEthTxOverview;
+ } else if (!strcmp(funcName, "GuiEthTxDetails")) {
+ return GuiEthTxDetails;
+ } else if (!strcmp(funcName, "GuiCosmosTxOverview")) {
+ return GuiCosmosTxOverview;
+ } else if (!strcmp(funcName, "GuiCosmosTxDetails")) {
+ return GuiCosmosTxDetails;
} else if (!strcmp(funcName, "GuiArDataItemOverview")) {
return GuiArDataItemOverview;
} else if (!strcmp(funcName, "GuiArDataItemDetail")) {
@@ -124,11 +132,7 @@ GetCustomContainerFunc GetOtherChainCustomFunc(char *funcName)
lv_event_cb_t GuiOtherChainEventCbGet(char *type)
{
- if (!strcmp(type, "EthContractLearnMore")) {
- return EthContractLearnMore;
- } else if (!strcmp(type, "EthContractCheckRawData")) {
- return EthContractCheckRawData;
- } else if (!strcmp(type, "TrxCheckVault")) {
+ if (!strcmp(type, "TrxCheckVault")) {
return TrxCheckVault;
}
@@ -137,28 +141,12 @@ lv_event_cb_t GuiOtherChainEventCbGet(char *type)
GetObjStateFunc GuiOtherChainStateFuncGet(char *type)
{
- if (!strcmp(type, "GetEthEnsExist")) {
- return GetEthEnsExist;
- } else if (!strcmp(type, "GetEthTypeDataHashExist")) {
+ if (!strcmp(type, "GetEthTypeDataHashExist")) {
return GetEthTypeDataHashExist;
- } else if (!strcmp(type, "GetToEthEnsExist")) {
- return GetToEthEnsExist;
} else if (!strcmp(type, "GetEthTypeDataChainExist")) {
return GetEthTypeDataChainExist;
} else if (!strcmp(type, "GetEthTypeDataVersionExist")) {
return GetEthTypeDataVersionExist;
- } else if (!strcmp(type, "GetEthContractDataExist")) {
- return GetEthContractDataExist;
- } else if (!strcmp(type, "GetEthContractDataNotExist")) {
- return GetEthContractDataNotExist;
- } else if (!strcmp(type, "GetEthInputDataExist")) {
- return GetEthInputDataExist;
- } else if (!strcmp(type, "EthInputExistContractNot")) {
- return EthInputExistContractNot;
- } else if (!strcmp(type, "GetEthFromAddressExist")) {
- return GetEthFromAddressExist;
- } else if (!strcmp(type, "GetEthFromAddressNotExist")) {
- return GetEthFromAddressNotExist;
} else if (!strcmp(type, "GetEthMessageFromExist")) {
return GetEthMessageFromExist;
} else if (!strcmp(type, "GetEthMessageFromNotExist")) {
@@ -220,8 +208,6 @@ GetObjStateFunc GuiOtherChainStateFuncGet(char *type)
GetTableDataFunc GuiOtherChainTableFuncGet(char *type, GuiRemapViewType remapIndex)
{
switch (remapIndex) {
- case REMAPVIEW_ETH:
- return GuiEthTableFuncGet(type);
case REMAPVIEW_ADA:
case REMAPVIEW_ADA_SIGN_DATA:
case REMAPVIEW_ADA_CATALYST:
@@ -234,8 +220,6 @@ GetTableDataFunc GuiOtherChainTableFuncGet(char *type, GuiRemapViewType remapInd
GetLabelDataFunc GuiOtherChainTextFuncGet(char *type, GuiRemapViewType remapIndex)
{
switch (remapIndex) {
- case REMAPVIEW_ETH:
- return GuiEthTextFuncGet(type);
case REMAPVIEW_ETH_PERSONAL_MESSAGE:
return GuiEthPersonalMessageTextFuncGet(type);
case REMAPVIEW_ETH_TYPEDDATA:
@@ -259,9 +243,6 @@ GetLabelDataFunc GuiOtherChainTextFuncGet(char *type, GuiRemapViewType remapInde
return GuiAdaTextFuncGet(type);
case REMAPVIEW_XRP:
return GuiXrpTextFuncGet(type);
- case REMAPVIEW_AR:
- case REMAPVIEW_AR_MESSAGE:
- return GuiArTextFuncGet(type);
case REMAPVIEW_STELLAR:
case REMAPVIEW_STELLAR_HASH:
return GuiStellarTextFuncGet(type);
@@ -288,9 +269,6 @@ GetLabelDataLenFunc GuiOtherChainTextLenFuncGet(char *type, GuiRemapViewType rem
case REMAPVIEW_ETH_TYPEDDATA:
case REMAPVIEW_ETH:
return GuiEthTextLenFuncGet(type);
- case REMAPVIEW_AR:
- case REMAPVIEW_AR_MESSAGE:
- return GuiArTextLenFuncGet(type);
case REMAPVIEW_STELLAR:
return GuiStellarTextLenFuncGet(type);
default:
@@ -381,8 +359,6 @@ static GetLabelDataLenFunc GuiEthTextLenFuncGet(char *type)
{
if (!strcmp(type, "GetEthTypedDataMessageLen")) {
return GetEthTypedDataMessageLen;
- } else if (!strcmp(type, "GetEthInputDataLen")) {
- return GetEthInputDataLen;
}
return NULL;
}
@@ -403,24 +379,6 @@ static GetLabelDataLenFunc GuiStellarTextLenFuncGet(char *type)
return NULL;
}
-static GetLabelDataLenFunc GuiArTextLenFuncGet(char *type)
-{
- if (!strcmp(type, "GetArweaveRawMessageLength")) {
- return GetArweaveRawMessageLength;
- } else if (!strcmp(type, "GetArweaveMessageLength")) {
- return GetArweaveMessageLength;
- }
- return NULL;
-}
-
-static GetTableDataFunc GuiEthTableFuncGet(char *type)
-{
- if (!strcmp(type, "GetEthContractData")) {
- return GetEthContractData;
- }
- return NULL;
-}
-
static GetTableDataFunc GuiAdaTabelFuncGet(char *type)
{
if (!strcmp(type, "GetAdaInputDetail")) {
@@ -565,28 +523,6 @@ static GetLabelDataFunc GuiXrpTextFuncGet(char *type)
return NULL;
}
-static GetLabelDataFunc GuiArTextFuncGet(char *type)
-{
- if (!strcmp(type, "GetArweaveValue")) {
- return GetArweaveValue;
- } else if (!strcmp(type, "GetArweaveFee")) {
- return GetArweaveFee;
- } else if (!strcmp(type, "GetArweaveFromAddress")) {
- return GetArweaveFromAddress;
- } else if (!strcmp(type, "GetArweaveToAddress")) {
- return GetArweaveToAddress;
- } else if (!strcmp(type, "GetArweaveValue")) {
- return GetArweaveValue;
- } else if (!strcmp(type, "GetArweaveMessageText")) {
- return GetArweaveMessageText;
- } else if (!strcmp(type, "GetArweaveRawMessage")) {
- return GetArweaveRawMessage;
- } else if (!strcmp(type, "GetArweaveMessageAddress")) {
- return GetArweaveMessageAddress;
- }
- return NULL;
-}
-
static GetLabelDataFunc GuiStellarTextFuncGet(char *type)
{
if (!strcmp(type, "GetStellarRawMessage")) {
@@ -650,58 +586,9 @@ static GetLabelDataFunc GuiEthTypedDataTextFuncGet(char *type)
return NULL;
}
-static GetLabelDataFunc GuiEthTextFuncGet(char *type)
-{
- if (!strcmp(type, "GetEthValue")) {
- return GetEthValue;
- } else if (!strcmp(type, "GetEthTxFee")) {
- return GetEthTxFee;
- } else if (!strcmp(type, "GetEthGasPrice")) {
- return GetEthGasPrice;
- } else if (!strcmp(type, "GetEthGasLimit")) {
- return GetEthGasLimit;
- } else if (!strcmp(type, "GetEthNetWork")) {
- return GetEthNetWork;
- } else if (!strcmp(type, "GetEthMaxFee")) {
- return GetEthMaxFee;
- } else if (!strcmp(type, "GetEthMaxPriority")) {
- return GetEthMaxPriority;
- } else if (!strcmp(type, "GetEthMaxFeePrice")) {
- return GetEthMaxFeePrice;
- } else if (!strcmp(type, "GetEthMaxPriorityFeePrice")) {
- return GetEthMaxPriorityFeePrice;
- } else if (!strcmp(type, "GetEthGetFromAddress")) {
- return GetEthGetFromAddress;
- } else if (!strcmp(type, "GetEthGetToAddress")) {
- return GetEthGetToAddress;
- } else if (!strcmp(type, "GetEthGetDetailPageToAddress")) {
- return GetEthGetDetailPageToAddress;
- } else if (!strcmp(type, "GetTxnFeeDesc")) {
- return GetTxnFeeDesc;
- } else if (!strcmp(type, "GetEthEnsName")) {
- return GetEthEnsName;
- } else if (!strcmp(type, "GetToEthEnsName")) {
- return GetToEthEnsName;
- } else if (!strcmp(type, "GetEthMethodName")) {
- return GetEthMethodName;
- } else if (!strcmp(type, "GetEthTransactionData")) {
- return GetEthTransactionData;
- } else if (!strcmp(type, "GetEthContractName")) {
- return GetEthContractName;
- } else if (!strcmp(type, "GetEthInputData")) {
- return GetEthInputData;
- } else if (!strcmp(type, "GetEthNonce")) {
- return GetEthNonce;
- }
-
- return NULL;
-}
-
static GetContSizeFunc GetEthObjPos(char *type)
{
- if (!strcmp(type, "GetEthToLabelPos")) {
- return GetEthToLabelPos;
- } else if (!strcmp(type, "GetEthTypeDomainPos")) {
+ if (!strcmp(type, "GetEthTypeDomainPos")) {
return GetEthTypeDomainPos;
} else if (!strcmp(type, "GetEthMessagePos")) {
return GetEthMessagePos;
@@ -761,11 +648,7 @@ static GetListLenFunc GetCosmosListLen(char *type)
static GetContSizeFunc GetEthContainerSize(char *type)
{
- if (!strcmp(type, "GetEthToFromSize")) {
- return GetEthToFromSize;
- } else if (!strcmp(type, "GetEthContractDataSize")) {
- return GetEthContractDataSize;
- } else if (!strcmp(type, "GetEthTypeDomainSize")) {
+ if (!strcmp(type, "GetEthTypeDomainSize")) {
return GetEthTypeDomainSize;
}
return NULL;
@@ -802,4 +685,4 @@ static GetContSizeFunc GetAdaContainerSize(char *type)
return GetCatalystVoteKeysSize;
}
return NULL;
-}
\ No newline at end of file
+}
diff --git a/src/ui/gui_analyze/multi/web3/gui_general_analyze.h b/src/ui/gui_analyze/multi/web3/gui_general_analyze.h
index a997421..ead1308 100644
--- a/src/ui/gui_analyze/multi/web3/gui_general_analyze.h
+++ b/src/ui/gui_analyze/multi/web3/gui_general_analyze.h
@@ -6,9 +6,9 @@
#define GUI_ANALYZE_OBJ_SURPLUS \
{\
REMAPVIEW_ETH,\
- "{\"type\":\"tabview\",\"pos\":[36,0],\"size\":[408,900],\"bg_color\":0,\"children\":[{\"type\":\"tabview_child\",\"index\":1,\"tab_name\":\"Overview\",\"text_color\":16777215,\"font\":\"openSansEnIllustrate\",\"children\":[{\"type\":\"custom_container\",\"bg_color\":0,\"bg_opa\":0,\"exist_func\":\"GetEthFromAddressNotExist\",\"pos\":[0,0],\"custom_show_func\":\"GuiCustomPathNotice\"},{\"type\":\"container\",\"pos\":[0,12],\"size\":[408,144],\"align_to\":-2,\"align\":13,\"bg_opa\":31,\"radius\":24,\"children\":[{\"type\":\"label\",\"text\":\"Value\",\"pos\":[24,16],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text\":\"MaxTxnFee\",\"pos\":[24,98],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetEthValue\",\"pos\":[24,50],\"text_color\":16090890,\"font\":\"openSansEnLittleTitle\"},{\"type\":\"label\",\"text_func\":\"GetEthTxFee\",\"pos\":[156,98],\"font\":\"openSansEnIllustrate\"}]},{\"type\":\"container\",\"pos\":[0,16],\"size\":[408,62],\"align_to\":-2,\"align\":13,\"bg_opa\":31,\"radius\":24,\"children\":[{\"type\":\"label\",\"text\":\"Network\",\"pos\":[24,16],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetEthNetWork\",\"pos\":[120,16],\"font\":\"openSansEnIllustrate\"}]},{\"type\":\"container\",\"pos\":[0,16],\"size_func\":\"GetEthToFromSize\",\"align_to\":-2,\"align\":13,\"bg_opa\":31,\"radius\":24,\"children\":[{\"type\":\"label\",\"text\":\"From\",\"pos\":[24,16],\"font\":\"openSansEnIllustrate\",\"exist_func\":\"GetEthFromAddressExist\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetEthGetFromAddress\",\"exist_func\":\"GetEthFromAddressExist\",\"text_width\":360,\"pos\":[24,54],\"font\":\"openSansEnIllustrate\"},{\"type\":\"img\",\"pos\":[24,129],\"exist_func\":\"GetEthEnsExist\",\"img_src\":\"imgEns\"},{\"type\":\"label\",\"text_func\":\"GetEthEnsName\",\"exist_func\":\"GetEthEnsExist\",\"pos\":[56,126],\"font\":\"openSansEnIllustrate\",\"text_color\":1827014},{\"type\":\"label\",\"text\":\"To\",\"pos_func\":\"GetEthToLabelPos\",\"text_opa\":144,\"font\":\"openSansEnIllustrate\"},{\"type\":\"label\",\"text_func\":\"GetEthGetToAddress\",\"text_width\":360,\"pos\":[0,8],\"align_to\":-2,\"align\":13,\"font\":\"openSansEnIllustrate\"},{\"type\":\"img\",\"pos\":[0,11],\"align_to\":-2,\"align\":13,\"exist_func\":\"GetToEthEnsExist\",\"img_src\":\"imgEns\"},{\"type\":\"label\",\"text_func\":\"GetToEthEnsName\",\"exist_func\":\"GetToEthEnsExist\",\"pos\":[8,0],\"align_to\":-2,\"align\":20,\"font\":\"openSansEnIllustrate\",\"text_color\":1827014}]}]},{\"type\":\"tabview_child\",\"index\":2,\"tab_name\":\"Details\",\"font\":\"openSansEnIllustrate\",\"children\":[{\"type\":\"custom_container\",\"bg_color\":0,\"bg_opa\":0,\"exist_func\":\"GetEthFromAddressNotExist\",\"pos\":[0,0],\"custom_show_func\":\"GuiCustomPathNotice\"},{\"table\":{\"FeeMarket\":{\"type\":\"container\",\"pos\":[0,12],\"align_to\":-2,\"align\":13,\"size\":[408,316],\"bg_opa\":31,\"radius\":24,\"children\":[{\"type\":\"label\",\"text\":\"Value\",\"pos\":[24,16],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetEthValue\",\"pos\":[92,16],\"text_color\":16090890,\"font\":\"openSansEnIllustrate\"},{\"type\":\"label\",\"text\":\"MaxFee\",\"pos\":[24,54],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetEthMaxFee\",\"pos\":[118,54],\"font\":\"openSansEnIllustrate\"},{\"type\":\"label\",\"text\":\"·MaxFeePrice*GasLimit\",\"pos\":[24,92],\"text_opa\":144,\"font\":\"openSansEnIllustrate\"},{\"type\":\"label\",\"text\":\"MaxPriority\",\"pos\":[24,124],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetEthMaxPriority\",\"pos\":[153,124],\"font\":\"openSansEnIllustrate\"},{\"type\":\"label\",\"text\":\"·MaxPriorityFeePrice*GasLimit\",\"pos\":[24,162],\"text_opa\":144,\"font\":\"openSansEnIllustrate\"},{\"type\":\"label\",\"text\":\"MaxFeePrice\",\"pos\":[24,194],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetEthMaxFeePrice\",\"pos\":[169,194],\"font\":\"openSansEnIllustrate\"},{\"type\":\"label\",\"text\":\"MaxPriorityFeePrice\",\"pos\":[24,232],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetEthMaxPriorityFeePrice\",\"pos\":[242,232],\"font\":\"openSansEnIllustrate\"},{\"type\":\"label\",\"text\":\"GasLimit\",\"pos\":[24,270],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetEthGasLimit\",\"pos\":[127,270],\"font\":\"openSansEnIllustrate\"}]},\"legacy\":{\"type\":\"container\",\"pos\":[0,12],\"size\":[408,208],\"align\":2,\"bg_opa\":31,\"radius\":24,\"children\":[{\"type\":\"label\",\"text\":\"Value\",\"pos\":[24,16],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetEthValue\",\"pos\":[92,16],\"text_color\":16090890,\"font\":\"openSansEnIllustrate\"},{\"type\":\"label\",\"text\":\"MaxTxnFee\",\"pos\":[24,54],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetEthTxFee\",\"pos\":[156,54],\"font\":\"openSansEnIllustrate\"},{\"type\":\"label\",\"text_func\":\"GetTxnFeeDesc\",\"pos\":[24,92],\"text_opa\":144,\"font\":\"openSansDesc\"},{\"type\":\"label\",\"text\":\"GasPrice\",\"pos\":[24,124],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetEthGasPrice\",\"pos\":[127,124],\"font\":\"openSansEnIllustrate\"},{\"type\":\"label\",\"text\":\"GasLimit\",\"pos\":[24,162],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetEthGasLimit\",\"pos\":[127,162],\"font\":\"openSansEnIllustrate\"}]}}},{\"type\":\"container\",\"pos\":[16,16],\"size\":[408,62],\"align_to\":-2,\"align\":13,\"bg_opa\":31,\"radius\":24,\"children\":[{\"type\":\"label\",\"text\":\"Network\",\"pos\":[24,16],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetEthNetWork\",\"pos\":[120,16],\"font\":\"openSansEnIllustrate\"}]},{\"type\":\"container\",\"exist_func\":\"GetEthContractDataExist\",\"pos\":[0,16],\"size\":[408,62],\"align_to\":-2,\"align\":13,\"bg_opa\":31,\"radius\":24,\"children\":[{\"type\":\"label\",\"text\":\"Method\",\"pos\":[24,16],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetEthMethodName\",\"pos\":[113,16],\"font\":\"openSansEnIllustrate\"}]},{\"type\":\"container\",\"pos\":[0,16],\"size\":[408,62],\"align_to\":-2,\"align\":13,\"bg_opa\":31,\"radius\":24,\"children\":[{\"type\":\"label\",\"text\":\"nonce\",\"pos\":[24,16],\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetEthNonce\",\"pos\":[101,16]}]},{\"type\":\"container\",\"pos\":[0,16],\"size_func\":\"GetEthToFromSize\",\"align_to\":-2,\"align\":13,\"bg_opa\":31,\"radius\":24,\"children\":[{\"type\":\"label\",\"text\":\"From\",\"pos\":[24,16],\"font\":\"openSansEnIllustrate\",\"exist_func\":\"GetEthFromAddressExist\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetEthGetFromAddress\",\"text_width\":360,\"pos\":[24,54],\"exist_func\":\"GetEthFromAddressExist\",\"font\":\"openSansEnIllustrate\"},{\"type\":\"img\",\"pos\":[24,129],\"exist_func\":\"GetEthEnsExist\",\"img_src\":\"imgEns\"},{\"type\":\"label\",\"text_func\":\"GetEthEnsName\",\"exist_func\":\"GetEthEnsExist\",\"pos\":[56,126],\"font\":\"openSansEnIllustrate\",\"text_color\":1827014},{\"type\":\"label\",\"text\":\"To\",\"pos_func\":\"GetEthToLabelPos\",\"text_opa\":144,\"font\":\"openSansEnIllustrate\"},{\"type\":\"label\",\"text_func\":\"GetEthGetDetailPageToAddress\",\"text_width\":360,\"pos\":[0,8],\"align_to\":-2,\"align\":13,\"font\":\"openSansEnIllustrate\"},{\"type\":\"img\",\"pos\":[0,11],\"align_to\":-2,\"align\":13,\"exist_func\":\"GetToEthEnsExist\",\"img_src\":\"imgEns\"},{\"type\":\"label\",\"text_func\":\"GetToEthEnsName\",\"exist_func\":\"GetToEthEnsExist\",\"pos\":[8,0],\"align_to\":-2,\"align\":20,\"font\":\"openSansEnIllustrate\",\"text_color\":1827014},{\"type\":\"img\",\"pos\":[0,8],\"align_to\":-2,\"align\":13,\"exist_func\":\"GetEthContractDataExist\",\"img_src\":\"imgContract\"},{\"type\":\"label\",\"text_func\":\"GetEthContractName\",\"exist_func\":\"GetEthContractDataExist\",\"pos\":[38,8],\"align_to\":-3,\"align\":13,\"font\":\"openSansEnIllustrate\",\"text_color\":10782207}]},{\"type\":\"label\",\"text\":\"InputData\",\"align_to\":-2,\"align\":13,\"exist_func\":\"GetEthInputDataExist\",\"pos\":[0,16],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"container\",\"pos\":[0,16],\"size_func\":\"GetEthContractDataSize\",\"exist_func\":\"GetEthContractDataExist\",\"align_to\":-2,\"align\":13,\"bg_opa\":31,\"radius\":24,\"children\":[{\"type\":\"label\",\"exist_func\":\"GetEthContractDataNotExist\",\"text_func\":\"GetEthTransactionData\",\"text_width\":360,\"pos\":[24,16],\"font\":\"openSansEnIllustrate\"},{\"type\":\"label\",\"exist_func\":\"GetEthContractDataNotExist\",\"text\":\"UnknownContract\",\"text_width\":360,\"pos\":[0,8],\"align_to\":-2,\"align\":13,\"text_color\":16105777,\"font\":\"openSansEnIllustrate\"},{\"type\":\"container\",\"exist_func\":\"GetEthContractDataNotExist\",\"aflag\":2,\"cb\":\"EthContractLearnMore\",\"pos\":[0,8],\"size\":[144,30],\"align_to\":-2,\"align\":13,\"bg_color\":1907997,\"children\":[{\"type\":\"label\",\"text\":\"LearnMore\",\"text_width\":360,\"pos\":[0,0],\"text_color\":1827014,\"font\":\"openSansEnIllustrate\"},{\"type\":\"img\",\"img_src\":\"imgQrcodeTurquoise\",\"pos\":[120,3],\"text_color\":3056500,\"font\":\"openSansEnIllustrate\"}]},{\"type\":\"label\",\"exist_func\":\"GetEthContractDataExist\",\"text\":\"Method\",\"pos\":[24,16],\"text_color\":16777215,\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"exist_func\":\"GetEthContractDataExist\",\"text_func\":\"GetEthMethodName\",\"pos\":[24,54],\"font\":\"openSansEnIllustrate\"},{\"name\":\"contract_data\",\"type\":\"table\",\"width\":360,\"align\":2,\"pos\":[0,100],\"bg_color\":1907997,\"key_width\":30,\"table_func\":\"GetEthContractData\",\"font\":\"openSansEnIllustrate\",\"exist_func\":\"GetEthContractDataExist\"},{\"type\":\"btn\",\"text\":\"Check the Raw Data\",\"exist_func\":\"GetEthInputDataExist\",\"pos\":[-10,16],\"size\":[408,62],\"align_to\":-2,\"align\":13,\"radius\":24,\"bg_opa\":0,\"text_color\":1827014,\"cb\":\"EthContractCheckRawData\"}]},{\"type\":\"btn\",\"text\":\"Check the Raw Data\",\"exist_func\":\"EthInputExistContractNot\",\"pos\":[0,16],\"size\":[408,62],\"align_to\":-2,\"align\":13,\"radius\":24,\"bg_opa\":31,\"text_color\":1827014,\"cb\":\"EthContractCheckRawData\"}]}]}", \
+ "{\"type\":\"tabview\",\"pos\":[36,0],\"size\":[408,900],\"bg_color\":0,\"children\":[{\"type\":\"tabview_child\",\"index\":1,\"tab_name\":\"Overview\",\"text_color\":16777215,\"font\":\"openSansEnIllustrate\",\"children\":[{\"type\":\"custom_container\",\"bg_color\":0,\"bg_opa\":0,\"pos\":[0,12],\"custom_show_func\":\"GuiEthTxOverview\"}]},{\"type\":\"tabview_child\",\"index\":2,\"tab_name\":\"Details\",\"font\":\"openSansEnIllustrate\",\"children\":[{\"type\":\"custom_container\",\"bg_color\":0,\"bg_opa\":0,\"pos\":[0,12],\"custom_show_func\":\"GuiEthTxDetails\"}]}]}", \
GuiGetEthData,\
- GetEthTransType,\
+ NULL,\
FreeEthMemory,\
},\
{\
@@ -48,7 +48,7 @@
},\
{\
REMAPVIEW_COSMOS,\
- "{\"table\":{\"tx\":{\"name\":\"cosmos_tx_page\",\"type\":\"tabview\",\"pos\":[36,0],\"size\":[408,900],\"bg_color\":0,\"children\":[{\"type\":\"tabview_child\",\"index\":1,\"tab_name\":\"Overview\",\"font\":\"openSansEnIllustrate\",\"children\":[{\"type\":\"list\",\"exist_func\":\"GetCosmosMsgListExist\",\"len_func\":\"GetCosmosMsgLen\",\"item_key_func\":\"GetCosmosMsgKey\",\"item_map\":{\"default\":{\"type\":\"container\",\"pos\":[0,16],\"size\":[408,402],\"bg_opa\":31,\"radius\":24,\"children\":[{\"type\":\"label\",\"text_func\":\"GetCosmosIndex\",\"pos\":[24,16],\"font\":\"openSansEnIllustrate\",\"text_color\":16090890},{\"type\":\"label\",\"text\":\"Value\",\"pos\":[24,62],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosTextOfKind\",\"text_key\":\"Value\",\"pos\":[24,96],\"text_color\":16090890,\"text_width\":2000,\"font\":\"openSansEnLittleTitle\"},{\"type\":\"label\",\"text\":\"Method\",\"pos\":[24,144],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosTextOfKind\",\"text_key\":\"Method\",\"pos\":[113,144],\"font\":\"openSansEnIllustrate\"},{\"type\":\"label\",\"text\":\"From\",\"pos\":[24,182],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosTextOfKind\",\"text_key\":\"From\",\"text_width\":360,\"pos\":[24,220],\"font\":\"openSansEnIllustrate\"},{\"type\":\"label\",\"text\":\"To\",\"pos\":[24,288],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosTextOfKind\",\"text_key\":\"To\",\"text_width\":360,\"pos\":[24,326],\"font\":\"openSansEnIllustrate\"}]},\"Undelegate\":{\"type\":\"container\",\"pos\":[0,16],\"size\":[408,402],\"bg_opa\":31,\"radius\":24,\"children\":[{\"type\":\"label\",\"text_func\":\"GetCosmosIndex\",\"pos\":[24,16],\"font\":\"openSansEnIllustrate\",\"text_color\":16090890},{\"type\":\"label\",\"text\":\"Value\",\"pos\":[24,62],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosTextOfKind\",\"text_key\":\"Value\",\"pos\":[24,96],\"text_color\":16090890,\"font\":\"openSansEnLittleTitle\"},{\"type\":\"label\",\"text\":\"Method\",\"pos\":[24,144],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosTextOfKind\",\"text_key\":\"Method\",\"pos\":[113,144],\"font\":\"openSansEnIllustrate\"},{\"type\":\"label\",\"text\":\"Validator\",\"pos\":[24,182],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosTextOfKind\",\"text_key\":\"Validator\",\"text_width\":360,\"pos\":[24,220],\"font\":\"openSansEnIllustrate\"},{\"type\":\"label\",\"text\":\"To\",\"pos\":[24,288],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosTextOfKind\",\"text_key\":\"To\",\"text_width\":360,\"pos\":[24,326],\"font\":\"openSansEnIllustrate\"}]},\"Re-delegate\":{\"type\":\"container\",\"pos\":[0,16],\"size\":[408,402],\"bg_opa\":31,\"radius\":24,\"children\":[{\"type\":\"label\",\"text_func\":\"GetCosmosIndex\",\"pos\":[24,16],\"font\":\"openSansEnIllustrate\",\"text_color\":16090890},{\"type\":\"label\",\"text\":\"Value\",\"pos\":[24,62],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosTextOfKind\",\"text_key\":\"Value\",\"pos\":[24,96],\"text_color\":16090890,\"font\":\"openSansEnLittleTitle\"},{\"type\":\"label\",\"text\":\"Method\",\"pos\":[24,144],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosTextOfKind\",\"text_key\":\"Method\",\"pos\":[113,144],\"font\":\"openSansEnIllustrate\"},{\"type\":\"label\",\"text\":\"To\",\"pos\":[24,182],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosTextOfKind\",\"text_key\":\"To\",\"text_width\":360,\"pos\":[24,220],\"font\":\"openSansEnIllustrate\"},{\"type\":\"label\",\"text\":\"New Validator\",\"pos\":[24,288],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosTextOfKind\",\"text_key\":\"New Validator\",\"text_width\":360,\"pos\":[24,326],\"font\":\"openSansEnIllustrate\"}]},\"Withdraw Reward\":{\"type\":\"container\",\"pos\":[0,16],\"size\":[408,320],\"bg_opa\":31,\"radius\":24,\"children\":[{\"type\":\"label\",\"text_func\":\"GetCosmosIndex\",\"pos\":[24,16],\"font\":\"openSansEnIllustrate\",\"text_color\":16090890},{\"type\":\"label\",\"text\":\"Method\",\"pos\":[24,62],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosTextOfKind\",\"text_key\":\"Method\",\"pos\":[113,62],\"font\":\"openSansEnIllustrate\"},{\"type\":\"label\",\"text\":\"To\",\"pos\":[24,100],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosTextOfKind\",\"text_key\":\"To\",\"text_width\":360,\"pos\":[24,138],\"font\":\"openSansEnIllustrate\"},{\"type\":\"label\",\"text\":\"Validator\",\"pos\":[24,206],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosTextOfKind\",\"text_key\":\"Validator\",\"text_width\":360,\"pos\":[24,244],\"font\":\"openSansEnIllustrate\"}]},\"Vote\":{\"type\":\"container\",\"pos\":[0,16],\"size\":[408,290],\"bg_opa\":31,\"radius\":24,\"children\":[{\"type\":\"label\",\"text_func\":\"GetCosmosIndex\",\"pos\":[24,16],\"font\":\"openSansEnIllustrate\",\"text_color\":16090890},{\"type\":\"label\",\"text\":\"Proposal\",\"pos\":[24,62],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosTextOfKind\",\"text_key\":\"Proposal\",\"pos\":[123,62],\"font\":\"openSansEnIllustrate\",\"text_color\":16090890},{\"type\":\"label\",\"text\":\"Voted\",\"pos\":[24,100],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosTextOfKind\",\"text_key\":\"Voted\",\"pos\":[95,100],\"font\":\"openSansEnIllustrate\",\"text_color\":16090890},{\"type\":\"label\",\"text\":\"Method\",\"pos\":[24,138],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosTextOfKind\",\"text_key\":\"Method\",\"pos\":[113,138],\"font\":\"openSansEnIllustrate\"},{\"type\":\"label\",\"text\":\"Voter\",\"pos\":[24,176],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosTextOfKind\",\"text_key\":\"Voter\",\"pos\":[24,214],\"text_width\":360,\"font\":\"openSansEnIllustrate\"}]}}},{\"type\":\"container\",\"pos\":[0,16],\"size\":[408,106],\"bg_opa\":31,\"radius\":24,\"exist_func\":\"GetCosmosValueExist\",\"children\":[{\"type\":\"label\",\"text\":\"Value\",\"pos\":[24,16],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosValue\",\"pos\":[24,50],\"text_color\":16090890,\"text_width\":2000,\"font\":\"openSansEnLittleTitle\"}]},{\"type\":\"container\",\"pos\":[0,16],\"size\":[408,106],\"bg_opa\":31,\"radius\":24,\"exist_func\":\"GetCosmosVoteExist\",\"children\":[{\"type\":\"label\",\"text\":\"Proposal\",\"pos\":[24,16],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosProposal\",\"pos\":[123,16],\"text_color\":16090890,\"font\":\"openSansEnIllustrate\"},{\"type\":\"label\",\"text\":\"Voted\",\"pos\":[24,54],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosVoted\",\"pos\":[95,54],\"text_color\":16090890,\"font\":\"openSansEnIllustrate\"}]},{\"type\":\"container\",\"pos\":[0,16],\"size\":[408,62],\"align_to\":-2,\"align\":13,\"bg_opa\":31,\"radius\":24,\"children\":[{\"type\":\"label\",\"text\":\"Network\",\"pos\":[24,16],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosNetwork\",\"pos\":[120,16],\"font\":\"openSansEnIllustrate\"}]},{\"type\":\"container\",\"pos\":[0,16],\"size\":[408,62],\"align_to\":-2,\"align\":13,\"bg_opa\":31,\"radius\":24,\"exist_func\":\"GetCosmosMethodExist\",\"children\":[{\"type\":\"label\",\"text\":\"Method\",\"pos\":[24,16],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosMethod\",\"pos\":[120,16],\"font\":\"openSansEnIllustrate\"}]},{\"type\":\"container\",\"pos\":[0,16],\"size_func\":\"GetCosmosOverviewAddrSize\",\"align_to\":-2,\"align\":13,\"bg_opa\":31,\"radius\":24,\"exist_func\":\"GetCosmosAddrExist\",\"children\":[{\"type\":\"label\",\"text_func\":\"GetCosmosAddress1Label\",\"pos\":[24,16],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosAddress1Value\",\"text_width\":360,\"pos\":[24,54],\"font\":\"openSansEnIllustrate\"},{\"type\":\"label\",\"text_func\":\"GetCosmosAddress2Label\",\"pos\":[24,130],\"text_opa\":144,\"font\":\"openSansEnIllustrate\",\"exist_func\":\"GetCosmosAddress2Exist\"},{\"type\":\"label\",\"text_func\":\"GetCosmosAddress2Value\",\"text_width\":360,\"pos\":[0,8],\"align_to\":-2,\"align\":13,\"font\":\"openSansEnIllustrate\",\"exist_func\":\"GetCosmosAddress2Exist\"}]}]},{\"type\":\"tabview_child\",\"index\":2,\"tab_name\":\"Details\",\"font\":\"openSansEnIllustrate\",\"children\":[{\"type\":\"list\",\"exist_func\":\"GetCosmosMsgListExist\",\"len_func\":\"GetCosmosMsgLen\",\"item_key_func\":\"GetCosmosMsgKey\",\"item_map\":{\"default\":{\"type\":\"container\",\"pos\":[0,16],\"size\":[408,358],\"bg_opa\":31,\"radius\":24,\"children\":[{\"type\":\"label\",\"text_func\":\"GetCosmosIndex\",\"pos\":[24,16],\"font\":\"openSansEnIllustrate\",\"text_color\":16090890},{\"type\":\"label\",\"text\":\"Value\",\"pos\":[24,62],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosTextOfKind\",\"text_key\":\"Value\",\"pos\":[92,62],\"text_color\":16090890,\"text_width\":2000,\"font\":\"openSansEnIllustrate\"},{\"type\":\"label\",\"text\":\"Method\",\"pos\":[24,100],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosTextOfKind\",\"text_key\":\"Method\",\"pos\":[113,100],\"font\":\"openSansEnIllustrate\"},{\"type\":\"label\",\"text\":\"From\",\"pos\":[24,138],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosTextOfKind\",\"text_key\":\"From\",\"text_width\":360,\"pos\":[24,176],\"font\":\"openSansEnIllustrate\"},{\"type\":\"label\",\"text\":\"To\",\"pos\":[24,244],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosTextOfKind\",\"text_key\":\"To\",\"text_width\":360,\"pos\":[24,282],\"font\":\"openSansEnIllustrate\"}]},\"IBC Transfer\":{\"type\":\"container\",\"pos\":[0,16],\"size\":[408,396],\"bg_opa\":31,\"radius\":24,\"children\":[{\"type\":\"label\",\"text_func\":\"GetCosmosIndex\",\"pos\":[24,16],\"font\":\"openSansEnIllustrate\",\"text_color\":16090890},{\"type\":\"label\",\"text\":\"Value\",\"pos\":[24,62],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosTextOfKind\",\"text_key\":\"Value\",\"pos\":[92,62],\"text_color\":16090890,\"text_width\":2000,\"font\":\"openSansEnIllustrate\"},{\"type\":\"label\",\"text\":\"Method\",\"pos\":[24,100],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosTextOfKind\",\"text_key\":\"Method\",\"pos\":[113,100],\"font\":\"openSansEnIllustrate\"},{\"type\":\"label\",\"text\":\"From\",\"pos\":[24,138],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosTextOfKind\",\"text_key\":\"From\",\"text_width\":360,\"pos\":[24,176],\"font\":\"openSansEnIllustrate\"},{\"type\":\"label\",\"text\":\"To\",\"pos\":[24,244],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosTextOfKind\",\"text_key\":\"To\",\"text_width\":360,\"pos\":[24,282],\"font\":\"openSansEnIllustrate\"},{\"type\":\"label\",\"text\":\"Source Channel\",\"pos\":[24,350],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosTextOfKind\",\"text_key\":\"Source Channel\",\"text_width\":360,\"pos\":[187,350],\"font\":\"openSansEnIllustrate\"}]},\"Undelegate\":{\"type\":\"container\",\"pos\":[0,16],\"size\":[408,358],\"bg_opa\":31,\"radius\":24,\"children\":[{\"type\":\"label\",\"text_func\":\"GetCosmosIndex\",\"pos\":[24,16],\"font\":\"openSansEnIllustrate\",\"text_color\":16090890},{\"type\":\"label\",\"text\":\"Value\",\"pos\":[24,62],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosTextOfKind\",\"text_key\":\"Value\",\"pos\":[92,62],\"text_color\":16090890,\"font\":\"openSansEnIllustrate\"},{\"type\":\"label\",\"text\":\"Method\",\"pos\":[24,100],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosTextOfKind\",\"text_key\":\"Method\",\"pos\":[113,100],\"font\":\"openSansEnIllustrate\"},{\"type\":\"label\",\"text\":\"Validator\",\"pos\":[24,138],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosTextOfKind\",\"text_key\":\"Validator\",\"text_width\":360,\"pos\":[24,176],\"font\":\"openSansEnIllustrate\"},{\"type\":\"label\",\"text\":\"To\",\"pos\":[24,244],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosTextOfKind\",\"text_key\":\"To\",\"text_width\":360,\"pos\":[24,282],\"font\":\"openSansEnIllustrate\"}]},\"Re-delegate\":{\"type\":\"container\",\"pos\":[0,16],\"size\":[408,464],\"bg_opa\":31,\"radius\":24,\"children\":[{\"type\":\"label\",\"text_func\":\"GetCosmosIndex\",\"pos\":[24,16],\"font\":\"openSansEnIllustrate\",\"text_color\":16090890},{\"type\":\"label\",\"text\":\"Value\",\"pos\":[24,62],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosTextOfKind\",\"text_key\":\"Value\",\"pos\":[92,62],\"text_color\":16090890,\"font\":\"openSansEnIllustrate\"},{\"type\":\"label\",\"text\":\"Method\",\"pos\":[24,100],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosTextOfKind\",\"text_key\":\"Method\",\"pos\":[113,100],\"font\":\"openSansEnIllustrate\"},{\"type\":\"label\",\"text\":\"To\",\"pos\":[24,138],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosTextOfKind\",\"text_key\":\"To\",\"text_width\":360,\"pos\":[24,176],\"font\":\"openSansEnIllustrate\"},{\"type\":\"label\",\"text\":\"Old Validator\",\"pos\":[24,244],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosTextOfKind\",\"text_key\":\"Old Validator\",\"text_width\":360,\"pos\":[24,282],\"font\":\"openSansEnIllustrate\"},{\"type\":\"label\",\"text\":\"New Validator\",\"pos\":[24,350],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosTextOfKind\",\"text_key\":\"New Validator\",\"text_width\":360,\"pos\":[24,388],\"font\":\"openSansEnIllustrate\"}]},\"Withdraw Reward\":{\"type\":\"container\",\"pos\":[0,16],\"size\":[408,322],\"bg_opa\":31,\"radius\":24,\"children\":[{\"type\":\"label\",\"text_func\":\"GetCosmosIndex\",\"pos\":[24,16],\"font\":\"openSansEnIllustrate\",\"text_color\":16090890},{\"type\":\"label\",\"text\":\"Method\",\"pos\":[24,62],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosTextOfKind\",\"text_key\":\"Method\",\"pos\":[113,62],\"font\":\"openSansEnIllustrate\"},{\"type\":\"label\",\"text\":\"To\",\"pos\":[24,100],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosTextOfKind\",\"text_key\":\"To\",\"text_width\":360,\"pos\":[24,138],\"font\":\"openSansEnIllustrate\"},{\"type\":\"label\",\"text\":\"Validator\",\"pos\":[24,206],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosTextOfKind\",\"text_key\":\"Validator\",\"text_width\":360,\"pos\":[24,244],\"font\":\"openSansEnIllustrate\"}]},\"Vote\":{\"type\":\"container\",\"pos\":[0,16],\"size\":[408,290],\"bg_opa\":31,\"radius\":24,\"children\":[{\"type\":\"label\",\"text_func\":\"GetCosmosIndex\",\"pos\":[24,16],\"font\":\"openSansEnIllustrate\",\"text_color\":16090890},{\"type\":\"label\",\"text\":\"Proposal\",\"pos\":[24,62],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosTextOfKind\",\"text_key\":\"Proposal\",\"pos\":[123,62],\"font\":\"openSansEnIllustrate\"},{\"type\":\"label\",\"text\":\"Voted\",\"pos\":[24,100],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosTextOfKind\",\"text_key\":\"Voted\",\"pos\":[95,100],\"font\":\"openSansEnIllustrate\"},{\"type\":\"label\",\"text\":\"Method\",\"pos\":[24,138],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosTextOfKind\",\"text_key\":\"Method\",\"pos\":[113,138],\"font\":\"openSansEnIllustrate\"},{\"type\":\"label\",\"text\":\"Voter\",\"pos\":[24,176],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosTextOfKind\",\"text_key\":\"Voter\",\"pos\":[24,214],\"text_width\":360,\"font\":\"openSansEnIllustrate\"}]}}},{\"type\":\"container\",\"pos\":[0,16],\"size_func\":\"GetCosmosDetailMsgSize\",\"exist_func\":\"GetCosmosMethodExist\",\"bg_opa\":31,\"radius\":24,\"children\":[{\"type\":\"label\",\"text\":\"Value\",\"pos\":[24,16],\"font\":\"openSansEnIllustrate\",\"text_opa\":144,\"exist_func\":\"GetCosmosValueExist\"},{\"type\":\"label\",\"text_func\":\"GetCosmosValue\",\"pos\":[92,16],\"text_color\":16090890,\"text_width\":2000,\"font\":\"openSansEnIllustrate\",\"exist_func\":\"GetCosmosValueExist\"},{\"type\":\"label\",\"text\":\"Proposal\",\"pos\":[24,16],\"font\":\"openSansEnIllustrate\",\"text_opa\":144,\"exist_func\":\"GetCosmosVoteExist\"},{\"type\":\"label\",\"text_func\":\"GetCosmosProposal\",\"pos\":[123,16],\"font\":\"openSansEnIllustrate\",\"exist_func\":\"GetCosmosVoteExist\"},{\"type\":\"label\",\"text\":\"Voted\",\"pos\":[24,62],\"font\":\"openSansEnIllustrate\",\"text_opa\":144,\"exist_func\":\"GetCosmosVoteExist\"},{\"type\":\"label\",\"text_func\":\"GetCosmosVoted\",\"pos\":[95,62],\"font\":\"openSansEnIllustrate\",\"exist_func\":\"GetCosmosVoteExist\"},{\"type\":\"label\",\"text\":\"Method\",\"pos_func\":\"GetCosmosDetailMethodLabelPos\",\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosMethod\",\"pos_func\":\"GetCosmosDetailMethodValuePos\",\"font\":\"openSansEnIllustrate\"},{\"type\":\"label\",\"text_func\":\"GetCosmosAddress1Label\",\"pos_func\":\"GetCosmosDetailAddress1LabelPos\",\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosAddress1Value\",\"text_width\":360,\"pos_func\":\"GetCosmosDetailAddress1ValuePos\",\"font\":\"openSansEnIllustrate\"},{\"type\":\"label\",\"text\":\"Old Validator\",\"pos\":[24,222],\"text_opa\":144,\"font\":\"openSansEnIllustrate\",\"exist_func\":\"GetCosmosOldValidatorExist\"},{\"type\":\"label\",\"text_func\":\"GetCosmosOldValidator\",\"text_width\":360,\"pos\":[24,260],\"font\":\"openSansEnIllustrate\",\"exist_func\":\"GetCosmosOldValidatorExist\"},{\"type\":\"label\",\"text_func\":\"GetCosmosAddress2Label\",\"pos_func\":\"GetCosmosDetailAddress2LabelPos\",\"text_opa\":144,\"font\":\"openSansEnIllustrate\",\"exist_func\":\"GetCosmosAddress2Exist\"},{\"type\":\"label\",\"text_func\":\"GetCosmosAddress2Value\",\"text_width\":360,\"pos_func\":\"GetCosmosDetailAddress2ValuePos\",\"font\":\"openSansEnIllustrate\",\"exist_func\":\"GetCosmosAddress2Exist\"},{\"type\":\"label\",\"text\":\"Source Channel\",\"pos\":[24,336],\"text_opa\":144,\"font\":\"openSansEnIllustrate\",\"exist_func\":\"GetCosmosChannelExist\"},{\"type\":\"label\",\"text_func\":\"GetCosmosChannel\",\"pos\":[187,336],\"font\":\"openSansEnIllustrate\",\"exist_func\":\"GetCosmosChannelExist\"}]},{\"type\":\"container\",\"pos\":[0,16],\"size\":[408,170],\"bg_opa\":31,\"radius\":24,\"align\":13,\"align_to\":-2,\"children\":[{\"type\":\"label\",\"text\":\"Max Fee\",\"pos\":[24,16],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosMaxFee\",\"pos\":[118,16],\"text_width\":2000,\"font\":\"openSansEnIllustrate\"},{\"type\":\"label\",\"text\":\" · Max Fee Price * Gas Limit\",\"pos\":[24,54],\"font\":\"openSansDesc\",\"text_opa\":144},{\"type\":\"label\",\"text\":\"Fee\",\"pos\":[24,86],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosFee\",\"pos\":[73,86],\"text_width\":2000,\"font\":\"openSansEnIllustrate\"},{\"type\":\"label\",\"text\":\"Gas Limit\",\"pos\":[24,124],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosGasLimit\",\"pos\":[127,124],\"font\":\"openSansEnIllustrate\"}]},{\"type\":\"container\",\"pos\":[0,12],\"size\":[408,100],\"bg_opa\":31,\"radius\":24,\"align\":13,\"align_to\":-2,\"children\":[{\"type\":\"label\",\"text\":\"Network\",\"pos\":[24,16],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosNetwork\",\"pos\":[120,16],\"font\":\"openSansEnIllustrate\"},{\"type\":\"label\",\"text\":\"Chain ID\",\"pos\":[24,54],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosChainId\",\"pos\":[120,54],\"font\":\"openSansEnIllustrate\"}]}]}]},\"unknown\":{\"name\":\"cosmos_unknown_page\",\"type\":\"container\",\"pos\":[36,0],\"size\":[408,600],\"bg_color\":0,\"children\":[{\"type\":\"container\",\"pos\":[0,80],\"size\":[408,170],\"bg_opa\":31,\"radius\":24,\"children\":[{\"type\":\"label\",\"text\":\"Max Fee\",\"pos\":[24,16],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosDetailItemValue\",\"text_key\":\"Max Fee\",\"pos\":[118,16],\"text_width\":2000,\"font\":\"openSansEnIllustrate\"},{\"type\":\"label\",\"text\":\" · Max Fee Price * Gas Limit\",\"pos\":[24,54],\"font\":\"openSansDesc\",\"text_opa\":144},{\"type\":\"label\",\"text\":\"Fee\",\"pos\":[24,86],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosDetailItemValue\",\"text_key\":\"Fee\",\"pos\":[73,86],\"text_width\":2000,\"font\":\"openSansEnIllustrate\"},{\"type\":\"label\",\"text\":\"Gas Limit\",\"pos\":[24,124],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosDetailItemValue\",\"text_key\":\"Gas Limit\",\"pos\":[127,124],\"font\":\"openSansEnIllustrate\"}]},{\"type\":\"container\",\"pos\":[0,16],\"size\":[408,62],\"bg_opa\":31,\"radius\":24,\"align\":13,\"align_to\":-2,\"children\":[{\"type\":\"label\",\"text\":\"Network\",\"pos\":[24,16],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosDetailItemValue\",\"text_key\":\"Network\",\"pos\":[120,16],\"font\":\"openSansEnIllustrate\"}]},{\"type\":\"container\",\"pos\":[0,16],\"size\":[408,62],\"bg_opa\":31,\"radius\":24,\"align\":13,\"align_to\":-2,\"children\":[{\"type\":\"label\",\"text\":\"Message\",\"pos\":[24,16],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosDetailItemValue\",\"text_key\":\"Message\",\"pos\":[120,16],\"font\":\"openSansEnIllustrate\",\"text_color\":16105777}]}]},\"msg\":{\"name\":\"cosmos_msg_page\",\"type\":\"container\",\"pos\":[36,0],\"size\":[408,600],\"bg_color\":0,\"children\":[{\"type\":\"container\",\"pos\":[0,16],\"size\":[408,62],\"bg_opa\":31,\"radius\":24,\"children\":[{\"type\":\"label\",\"text\":\"Network\",\"pos\":[24,16],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosDetailItemValue\",\"text_key\":\"Network\",\"pos\":[120,16],\"font\":\"openSansEnIllustrate\"}]},{\"type\":\"container\",\"pos\":[0,16],\"size\":[408,130],\"bg_opa\":31,\"radius\":24,\"align\":13,\"align_to\":-2,\"children\":[{\"type\":\"label\",\"text\":\"Signer\",\"pos\":[24,16],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosDetailItemValue\",\"text_key\":\"Signer\",\"text_width\":360,\"pos\":[24,54],\"font\":\"openSansEnIllustrate\"}]},{\"type\":\"container\",\"pos\":[0,16],\"size\":[408,250],\"bg_opa\":31,\"radius\":24,\"align\":13,\"align_to\":-2,\"children\":[{\"type\":\"label\",\"text\":\"Message\",\"pos\":[24,16],\"font\":\"openSansEnIllustrate\",\"text_opa\":144},{\"type\":\"label\",\"text_func\":\"GetCosmosDetailItemValue\",\"text_key\":\"Message\",\"pos\":[24,54],\"font\":\"openSansEnIllustrate\"}]}]}}}", \
+ "{\"table\":{\"tx\":{\"name\":\"cosmos_tx_page\",\"type\":\"tabview\",\"pos\":[36,0],\"size\":[408,530],\"bg_color\":0,\"children\":[{\"type\":\"tabview_child\",\"index\":1,\"tab_name\":\"Overview\",\"font\":\"openSansEnIllustrate\",\"children\":[{\"type\":\"custom_container\",\"bg_color\":0,\"bg_opa\":0,\"pos\":[0,12],\"custom_show_func\":\"GuiCosmosTxOverview\"}]},{\"type\":\"tabview_child\",\"index\":2,\"tab_name\":\"Details\",\"font\":\"openSansEnIllustrate\",\"children\":[{\"type\":\"custom_container\",\"bg_color\":0,\"bg_opa\":0,\"pos\":[0,12],\"custom_show_func\":\"GuiCosmosTxDetails\"}]}]},\"unknown\":{\"nameWhy this scored 63/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.