What changed, and why it matters
This commit is a code review and cleanup of Ethereum-related code in the Keystone 3 hardware wallet firmware. It adds many unit tests, fixes a typo in a function name (sign_fee_markey_tx → sign_fee_market_tx), replaces some static variables with constants, and fixes a few minor logic issues. The most user-visible change is adding a check for an SD card before trying to load external contract data, and fixing memory leaks where global pointers could be overwritten without freeing the old data. There is no clear evidence of an active security vulnerability being patched, but the memory management and contract-data-loading changes are defensive improvements.
Treat as a routine hardening/review commit. Reviewers should verify that the new SD-card guard does not break legitimate external ABI lookups, confirm the freed global pointers are not used elsewhere after free, and ensure the renamed FFI function is consistently updated on the C side. No urgent security response is indicated by the diff alone.
Security signals we found
Memory leak fix: global pointers g_erc20ContractData and g_contractData are now freed before being overwritten
External data loading now gated by SD card presence (SdCardInsert) before calling GetEthContractFromExternal
ERC-20 calldata parsing now validates function selector and exact calldata length
Legacy transaction signature decoding now uses match on item_count instead of repeated if/else calls
Address checksum implementation now has explicit EIP-55 reference and unit tests
Function name typo fix (sign_fee_markey_tx -> sign_fee_market_tx) removes potential FFI linkage confusion
Evidence from the diff
The commit is titled ‘review eth’ and touches 16 files, mostly in rust/apps/ethereum and rust/rust_c/src/ethereum, plus a few C UI files. Changes include: (1) extensive new unit tests for EIP-1559 transaction parsing, ERC-20 calldata encoding/parsing, legacy transaction recovery ID handling, swap memo parsing, and address checksums; (2) a typo fix renaming sign_fee_markey_tx to sign_fee_market_tx across the Rust FFI boundary; (3) replacing static with const for several numeric/string constants; (4) refactoring normalize_value to avoid mutating strings in a loop and trimming trailing zeros correctly; (5) adding selector and calldata-length validation in ERC-20 parsing; (6) in the C layer, guarding GetEthContractFromExternal with SdCardInsert() and freeing previously allocated global contract/ERC-20 data before overwriting the pointers. The diff does not show any obvious cryptographic bug or exploit path; it reads as a hardening/review commit.
Changed components
rust/apps/ethereum/src/address.rsrust/apps/ethereum/src/batch_tx_rules.rsrust/apps/ethereum/src/eip1559_transaction.rsrust/apps/ethereum/src/erc20.rsrust/apps/ethereum/src/legacy_transaction.rsrust/apps/ethereum/src/lib.rsrust/apps/ethereum/src/normalizer.rsrust/apps/ethereum/src/structs.rsrust/apps/ethereum/src/swap.rsrust/rust_c/src/ethereum/mod.rssrc/ui/gui_chain/multi/web3/gui_eth.cInspect captured patch +851 / −142
diff --git a/rust/apps/cardano/src/structs.rs b/rust/apps/cardano/src/structs.rs
index e006e76..9eddd34 100644
--- a/rust/apps/cardano/src/structs.rs
+++ b/rust/apps/cardano/src/structs.rs
@@ -1269,7 +1269,7 @@ impl ParsedCardanoTx {
}
}
-static DIVIDER: f64 = 1_000_000f64;
+const DIVIDER: f64 = 1_000_000f64;
fn normalize_coin(value: u64) -> String {
format!("{} ADA", (value as f64).div(DIVIDER))
diff --git a/rust/apps/ethereum/src/address.rs b/rust/apps/ethereum/src/address.rs
index 55a8e3b..ff508f4 100644
--- a/rust/apps/ethereum/src/address.rs
+++ b/rust/apps/ethereum/src/address.rs
@@ -12,6 +12,7 @@ pub fn generate_address(key: PublicKey) -> Result<String> {
checksum_address(&hex::encode(&hash[12..]))
}
+// https://eips.ethereum.org/EIPS/eip-55
pub fn checksum_address(address: &str) -> Result<String> {
let address = address.trim_start_matches("0x").to_lowercase();
let address_hash = hex::encode(keccak256(address.as_bytes()));
@@ -76,4 +77,36 @@ mod tests {
let result = derive_address(hd_path, root_x_pub, root_path).unwrap();
assert_eq!("0x31eA4a0976ceE79AF136B1Cfa914e20E87546156", result);
}
+
+ #[test]
+ fn test_checksum_address() {
+ // Test lowercase address
+ let addr = "0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed";
+ let result = checksum_address(addr).unwrap();
+ assert_eq!("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed", result);
+
+ // Test address without 0x prefix
+ let addr = "5aaeb6053f3e94c9b9a09f33669435e7ef1beaed";
+ let result = checksum_address(addr).unwrap();
+ assert_eq!("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed", result);
+
+ // Test already checksummed address
+ let addr = "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed";
+ let result = checksum_address(addr).unwrap();
+ assert_eq!("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed", result);
+
+ // Test another address
+ let addr = "0xfb6916095ca1df60bb79ce92ce3ea74c37c5d359";
+ let result = checksum_address(addr).unwrap();
+ assert_eq!("0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359", result);
+ }
+
+ #[test]
+ fn test_derive_address_invalid_path() {
+ let root_x_pub = "xpub6BtigCpsVJrCGVhsuMuAshHuQctVUKUeumxP4wkUtypFpXatQ44ZCHwZi6w4Gf5kMN3vpfyGnHo5hLvgjs2NnkewYHSdVHX4oUbR1Xzxc7E";
+ let root_path = "44'/60'/0'";
+ let hd_path = "44'/60'/1'/0/0"; // Different root path
+ let result = derive_address(hd_path, root_x_pub, root_path);
+ assert!(result.is_err());
+ }
}
diff --git a/rust/apps/ethereum/src/batch_tx_rules.rs b/rust/apps/ethereum/src/batch_tx_rules.rs
index 3d35095..1a6ff0e 100644
--- a/rust/apps/ethereum/src/batch_tx_rules.rs
+++ b/rust/apps/ethereum/src/batch_tx_rules.rs
@@ -6,7 +6,6 @@ use alloc::vec::Vec;
use crate::{errors::EthereumError, structs::ParsedEthereumTransaction};
pub fn rule_swap(txs: Vec<ParsedEthereumTransaction>) -> Result<(), EthereumError> {
- //
if txs.is_empty() || txs.len() > 3 {
return Err(EthereumError::InvalidSwapTransaction(format!(
"invalid transaction count: {}",
@@ -41,3 +40,117 @@ pub fn rule_swap(txs: Vec<ParsedEthereumTransaction>) -> Result<(), EthereumErro
Ok(())
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::structs::ParsedEthereumTransaction;
+ use alloc::{string::String, vec};
+
+ extern crate std;
+
+ fn create_test_transaction(input: String) -> ParsedEthereumTransaction {
+ ParsedEthereumTransaction {
+ nonce: 0,
+ chain_id: 1,
+ from: None,
+ to: "0x0000000000000000000000000000000000000000".to_string(),
+ value: "0".to_string(),
+ input,
+ gas_price: None,
+ max_fee_per_gas: None,
+ max_priority_fee_per_gas: None,
+ max_fee: None,
+ max_priority: None,
+ gas_limit: "21000".to_string(),
+ max_txn_fee: "0".to_string(),
+ }
+ }
+
+ #[test]
+ fn test_rule_swap_empty() {
+ let txs = vec![];
+ let result = rule_swap(txs);
+ assert!(result.is_err());
+ assert!(matches!(
+ result.unwrap_err(),
+ EthereumError::InvalidSwapTransaction(_)
+ ));
+ }
+
+ #[test]
+ fn test_rule_swap_too_many() {
+ let txs = vec![
+ create_test_transaction("".to_string()),
+ create_test_transaction("".to_string()),
+ create_test_transaction("".to_string()),
+ create_test_transaction("".to_string()),
+ ];
+ let result = rule_swap(txs);
+ assert!(result.is_err());
+ assert!(matches!(
+ result.unwrap_err(),
+ EthereumError::InvalidSwapTransaction(_)
+ ));
+ }
+
+ #[test]
+ fn test_rule_swap_single() {
+ let txs = vec![create_test_transaction("".to_string())];
+ let result = rule_swap(txs);
+ assert!(result.is_ok());
+ }
+
+ #[test]
+ fn test_rule_swap_two_invalid_first() {
+ // First transaction is not an approval
+ let tx0 = create_test_transaction("a9059cbb00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001".to_string()); // transfer, not approve
+
+ let tx1 = create_test_transaction("a9059cbb00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001".to_string());
+
+ let txs = vec![tx0, tx1];
+ let result = rule_swap(txs);
+ assert!(result.is_err());
+ }
+
+ #[test]
+ fn test_rule_swap_three_valid() {
+ let tx0_input = "095ea7b300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000";
+ assert_eq!(tx0_input.len(), 136, "tx0 input must be 136 characters");
+ let tx0 = create_test_transaction(tx0_input.to_string());
+
+ let tx1_input = "095ea7b300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001";
+ assert_eq!(tx1_input.len(), 136, "tx1 input must be 136 characters");
+ let tx1 = create_test_transaction(tx1_input.to_string());
+
+ let tx2_input = "a9059cbb00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001";
+ assert_eq!(tx2_input.len(), 136, "tx2 input must be 136 characters");
+ let tx2 = create_test_transaction(tx2_input.to_string());
+
+ let txs = vec![tx0, tx1, tx2];
+ let result = rule_swap(txs);
+ match &result {
+ Ok(_) => {}
+ Err(e) => panic!("rule_swap failed: {:?}", e),
+ }
+ assert!(result.is_ok());
+ }
+
+ #[test]
+ fn test_rule_swap_three_invalid_revoke() {
+ // First: invalid revoke (amount != 0)
+ let tx0 = create_test_transaction("095ea7b30000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001".to_string()); // amount = 1, not 0
+
+ let tx1 = create_test_transaction("095ea7b30000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001".to_string());
+
+ let tx2 = create_test_transaction("a9059cbb00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001".to_string());
+
+ let txs = vec![tx0, tx1, tx2];
+ let result = rule_swap(txs);
+ assert!(result.is_err());
+ assert!(matches!(
+ result.unwrap_err(),
+ EthereumError::InvalidSwapTransaction(_)
+ ));
+ }
+}
diff --git a/rust/apps/ethereum/src/eip1559_transaction.rs b/rust/apps/ethereum/src/eip1559_transaction.rs
index 4ccc105..628b13a 100644
--- a/rust/apps/ethereum/src/eip1559_transaction.rs
+++ b/rust/apps/ethereum/src/eip1559_transaction.rs
@@ -77,3 +77,203 @@ impl From<EIP1559Transaction> for ParsedEIP1559Transaction {
}
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::H160;
+ use alloc::vec;
+ use core::str::FromStr;
+
+ extern crate std;
+
+ #[test]
+ fn test_parsed_eip1559_transaction() {
+ let tx = EIP1559Transaction {
+ chain_id: 1,
+ nonce: U256::from(42),
+ max_priority_fee_per_gas: U256::from(2000000000u64), // 2 Gwei
+ max_fee_per_gas: U256::from(100000000000u64), // 100 Gwei
+ gas_limit: U256::from(21000),
+ action: crate::structs::TransactionAction::Call(
+ H160::from_str("0x49aB56B91fc982Fd6Ec1EC7Bb87d74EFA6dA30ab").unwrap(),
+ ),
+ value: U256::from_dec_str("1000000000000000000").unwrap(), // 1 ETH
+ input: vec![],
+ };
+
+ let parsed = ParsedEIP1559Transaction::from(tx);
+ assert_eq!(parsed.chain_id, 1);
+ 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");
+ assert_eq!(parsed.value, "1");
+ }
+
+ #[test]
+ fn test_parsed_eip1559_transaction_fee_calculation() {
+ let tx = EIP1559Transaction {
+ chain_id: 1,
+ nonce: U256::from(0),
+ max_priority_fee_per_gas: U256::from(1000000000u64), // 1 Gwei
+ max_fee_per_gas: U256::from(50000000000u64), // 50 Gwei
+ gas_limit: U256::from(21000),
+ action: crate::structs::TransactionAction::Call(
+ H160::from_str("0x0000000000000000000000000000000000000000").unwrap(),
+ ),
+ value: U256::from(0),
+ input: vec![],
+ };
+
+ let parsed = ParsedEIP1559Transaction::from(tx);
+ // max_fee = max_fee_per_gas * gas_limit = 50 Gwei * 21000 = 1050000000000000 wei = 0.00105 ETH
+ assert_eq!(parsed.max_fee, "0.00105");
+ // max_priority = max_priority_fee_per_gas * gas_limit = 1 Gwei * 21000 = 21000000000000 wei = 0.000021 ETH
+ assert_eq!(parsed.max_priority, "0.000021");
+ assert_eq!(parsed.max_txn_fee, "0.00105");
+ }
+
+ #[test]
+ fn test_parsed_eip1559_transaction_create() {
+ // Test contract creation (TransactionAction::Create)
+ let tx = EIP1559Transaction {
+ chain_id: 1,
+ nonce: U256::from(5),
+ max_priority_fee_per_gas: U256::from(3000000000u64), // 3 Gwei
+ max_fee_per_gas: U256::from(150000000000u64), // 150 Gwei
+ gas_limit: U256::from(500000),
+ action: crate::structs::TransactionAction::Create,
+ value: U256::from_dec_str("5000000000000000000").unwrap(), // 5 ETH
+ input: vec![0x60, 0x80, 0x60, 0x40, 0x52], // Some contract bytecode
+ };
+
+ let parsed = ParsedEIP1559Transaction::from(tx);
+ assert_eq!(parsed.chain_id, 1);
+ 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");
+ assert_eq!(parsed.value, "5");
+ assert_eq!(parsed.input, "6080604052");
+ // For Create action, to should be empty or zero address
+ assert_eq!(parsed.to, "0x");
+ }
+
+ #[test]
+ fn test_parsed_eip1559_transaction_different_chain_id() {
+ // Test with different chain_id
+ let tx = EIP1559Transaction {
+ chain_id: 137, // Polygon
+ nonce: U256::from(10),
+ max_priority_fee_per_gas: U256::from(1000000000u64), // 1 Gwei
+ max_fee_per_gas: U256::from(50000000000u64), // 50 Gwei
+ gas_limit: U256::from(100000),
+ action: crate::structs::TransactionAction::Call(
+ H160::from_str("0x49aB56B91fc982Fd6Ec1EC7Bb87d74EFA6dA30ab").unwrap(),
+ ),
+ value: U256::from_dec_str("2000000000000000000").unwrap(), // 2 ETH
+ input: vec![0xa9, 0x05, 0x9c, 0xbb], // transfer function selector
+ };
+
+ let parsed = ParsedEIP1559Transaction::from(tx);
+ assert_eq!(parsed.chain_id, 137);
+ assert_eq!(parsed.nonce, 10);
+ assert_eq!(parsed.value, "2");
+ assert_eq!(parsed.input, "a9059cbb");
+ }
+
+ #[test]
+ fn test_parsed_eip1559_transaction_zero_value() {
+ // Test with zero value
+ let tx = EIP1559Transaction {
+ chain_id: 1,
+ nonce: U256::from(0),
+ max_priority_fee_per_gas: U256::from(1000000000u64), // 1 Gwei
+ max_fee_per_gas: U256::from(20000000000u64), // 20 Gwei
+ gas_limit: U256::from(21000),
+ action: crate::structs::TransactionAction::Call(
+ H160::from_str("0x49aB56B91fc982Fd6Ec1EC7Bb87d74EFA6dA30ab").unwrap(),
+ ),
+ value: U256::from(0),
+ input: vec![],
+ };
+
+ let parsed = ParsedEIP1559Transaction::from(tx);
+ assert_eq!(parsed.value, "0");
+ assert_eq!(parsed.max_fee, "0.00042"); // 20 Gwei * 21000 = 420000000000000 wei
+ }
+
+ #[test]
+ fn test_parsed_eip1559_transaction_large_gas_limit() {
+ // Test with large gas limit
+ let tx = EIP1559Transaction {
+ chain_id: 1,
+ nonce: U256::from(100),
+ max_priority_fee_per_gas: U256::from(2000000000u64), // 2 Gwei
+ max_fee_per_gas: U256::from(100000000000u64), // 100 Gwei
+ gas_limit: U256::from(1000000), // 1M gas
+ action: crate::structs::TransactionAction::Call(
+ H160::from_str("0x49aB56B91fc982Fd6Ec1EC7Bb87d74EFA6dA30ab").unwrap(),
+ ),
+ value: U256::from(0),
+ input: vec![0x12, 0x34, 0x56],
+ };
+
+ let parsed = ParsedEIP1559Transaction::from(tx);
+ assert_eq!(parsed.gas_limit, "1000000");
+ assert_eq!(parsed.input, "123456");
+ // max_fee = 100 Gwei * 1000000 = 100000000000000000 wei = 0.1 ETH
+ assert_eq!(parsed.max_fee, "0.1");
+ // max_priority = 2 Gwei * 1000000 = 2000000000000000 wei = 0.002 ETH
+ assert_eq!(parsed.max_priority, "0.002");
+ }
+
+ #[test]
+ fn test_parsed_eip1559_transaction_small_fees() {
+ // Test with very small fees
+ let tx = EIP1559Transaction {
+ chain_id: 1,
+ nonce: U256::from(1),
+ max_priority_fee_per_gas: U256::from(1000000u64), // 0.001 Gwei
+ max_fee_per_gas: U256::from(100000000u64), // 0.1 Gwei
+ gas_limit: U256::from(21000),
+ action: crate::structs::TransactionAction::Call(
+ H160::from_str("0x49aB56B91fc982Fd6Ec1EC7Bb87d74EFA6dA30ab").unwrap(),
+ ),
+ value: U256::from(1000000000000000u64), // 0.001 ETH
+ input: vec![],
+ };
+
+ let parsed = ParsedEIP1559Transaction::from(tx);
+ assert_eq!(parsed.max_priority_fee_per_gas, "0.001 Gwei");
+ assert_eq!(parsed.max_fee_per_gas, "0.1 Gwei");
+ assert_eq!(parsed.value, "0.001");
+ }
+
+ #[test]
+ fn test_eip1559_transaction_decode_raw() {
+ let invalid_data = vec![];
+ let result = EIP1559Transaction::decode_raw(&invalid_data);
+ assert!(result.is_err());
+ }
+
+ #[test]
+ fn test_parsed_eip1559_transaction_large_nonce() {
+ let tx = EIP1559Transaction {
+ chain_id: 1,
+ nonce: U256::from(999999u64),
+ max_priority_fee_per_gas: U256::from(1000000000u64),
+ max_fee_per_gas: U256::from(20000000000u64),
+ gas_limit: U256::from(21000),
+ action: crate::structs::TransactionAction::Call(
+ H160::from_str("0x49aB56B91fc982Fd6Ec1EC7Bb87d74EFA6dA30ab").unwrap(),
+ ),
+ value: U256::from(0),
+ input: vec![],
+ };
+
+ let parsed = ParsedEIP1559Transaction::from(tx);
+ assert_eq!(parsed.nonce, 999999);
+ }
+}
diff --git a/rust/apps/ethereum/src/erc20.rs b/rust/apps/ethereum/src/erc20.rs
index 12c5521..270da42 100644
--- a/rust/apps/ethereum/src/erc20.rs
+++ b/rust/apps/ethereum/src/erc20.rs
@@ -16,46 +16,39 @@ pub struct ParsedErc20Approval {
pub value: String,
}
+// ERC20 transfer function selector: keccak256("transfer(address,uint256)")[0:4] = 0xa9059cbb
+// Reference: https://eips.ethereum.org/EIPS/eip-20
+const TRANSFER_SELECTOR: &str = "a9059cbb";
+// ERC20 approve function selector: keccak256("approve(address,uint256)")[0:4] = 0x095ea7b3
+const APPROVE_SELECTOR: &str = "095ea7b3";
+// ABI encoding: selector(8) + address(64) + amount(64) = 136 hex chars (transfer/approve)
+const CALLDATA_LEN: usize = 136;
+const SELECTOR_LEN: usize = 8; // 4 bytes -> 8 hex chars
+const ADDRESS_START: usize = 32; // 32..72: address (right-aligned in 32-byte slot)
+const ADDRESS_END: usize = 72;
+const AMOUNT_START: usize = 72; // 72..136: amount
+
+/// Encode ERC20 transfer function call data
+/// Returns: function_selector(8) + encoded_address(64) + encoded_amount(64) = 136 hex chars
pub fn encode_erc20_transfer_calldata(to: H160, amount: U256) -> String {
- // transfer(address recipient, uint256 amount) function signature is 0xa9059cbb
- let mut calldata = "a9059cbb".to_string();
- calldata.push_str(&format!("{:0>64}", hex::encode(to)));
- // convert value to hex and pad it to 64 bytes
- let amount_hex = format!("{amount:x}");
- let amount_padding = format!("{amount_hex:0>64}");
- calldata.push_str(&amount_padding);
+ let mut calldata = String::with_capacity(CALLDATA_LEN);
+ calldata.push_str(TRANSFER_SELECTOR);
+ // Address: 20 bytes padded to 32 bytes (64 hex chars)
+ calldata.push_str(&format!("{:0>64}", hex::encode(to.as_bytes())));
+ // Amount: 32 bytes (64 hex chars)
+ calldata.push_str(&format!("{amount:0>64x}"));
calldata
}
+
// parse erc20 transfer calldata
pub fn parse_erc20(input: &str, decimal: u32) -> Result<ParsedErc20Transaction, &'static str> {
- if input.len() != 136 {
- return Err("Input must be 136 characters long");
+ validate_calldata_length(input)?;
+ if &input[0..SELECTOR_LEN] != TRANSFER_SELECTOR {
+ return Err("Invalid transfer function selector");
}
- let to = match hex::decode(&input[32..72]) {
- Ok(bytes) => format!("0x{}", hex::encode(bytes)),
- Err(_) => return Err("Failed to decode 'to' address"),
- };
-
- let value_hex = &input[72..];
- let value_biguint = BigUint::parse_bytes(value_hex.as_bytes(), 16)
- .ok_or("Failed to parse 'value' as a big uint")?;
-
- let decimal_biguint = BigUint::from(10u64.pow(decimal));
-
- let value_decimal = value_biguint.clone() / &decimal_biguint;
- let remainder = &value_biguint % &decimal_biguint;
-
- let value = if remainder != BigUint::new(Vec::new()) {
- // If there is a remainder, convert it to a decimal
- let remainder_decimal = remainder.to_string();
- let padded_remainder = format!("{:0>width$}", remainder_decimal, width = decimal as usize);
- format!("{value_decimal}.{padded_remainder}")
- .trim_end_matches('0')
- .to_string()
- } else {
- value_decimal.to_string()
- };
+ let to = decode_address_from_calldata(input)?;
+ let value = parse_amount_from_calldata(input, decimal)?;
Ok(ParsedErc20Transaction { to, value })
}
@@ -64,32 +57,42 @@ pub fn parse_erc20_approval(
input: &str,
decimal: u32,
) -> Result<ParsedErc20Approval, &'static str> {
- //095ea7b30000000000000000000000000000000000001ff3684f28c67538d4d072c2273400000000000000000000000000000000000000000000000000000000006acfc0
- if input.len() != 136 {
- return Err("Input must be 136 characters long");
+ validate_calldata_length(input)?;
+ if &input[0..SELECTOR_LEN] != APPROVE_SELECTOR {
+ return Err("Invalid approve function selector");
}
- let method_id = &input[0..8];
- if method_id != "095ea7b3" {
- return Err("Invalid method id");
+ let spender = decode_address_from_calldata(input)?;
+ let value = parse_amount_from_calldata(input, decimal)?;
+
+ Ok(ParsedErc20Approval { spender, value })
+}
+
+fn validate_calldata_length(input: &str) -> Result<(), &'static str> {
+ if input.len() != CALLDATA_LEN {
+ Err("Input must be 136 characters long")
+ } else {
+ Ok(())
}
+}
- let spender = match hex::decode(&input[32..72]) {
- Ok(bytes) => format!("0x{}", hex::encode(bytes)),
- Err(_) => return Err("Failed to decode 'spender' address"),
- };
+fn decode_address_from_calldata(input: &str) -> Result<String, &'static str> {
+ let address_hex = &input[ADDRESS_START..ADDRESS_END];
+ let address_bytes = hex::decode(address_hex).map_err(|_| "Failed to decode address hex")?;
+ let address = &address_bytes[address_bytes.len().saturating_sub(20)..];
+ Ok(format!("0x{}", hex::encode(address)))
+}
- let value_hex = &input[72..];
+fn parse_amount_from_calldata(input: &str, decimal: u32) -> Result<String, &'static str> {
+ let value_hex = &input[AMOUNT_START..];
let value_biguint = BigUint::parse_bytes(value_hex.as_bytes(), 16)
.ok_or("Failed to parse 'value' as a big uint")?;
let decimal_biguint = BigUint::from(10u64.pow(decimal));
-
- let value_decimal = value_biguint.clone() / &decimal_biguint;
+ let value_decimal = &value_biguint / &decimal_biguint;
let remainder = &value_biguint % &decimal_biguint;
let value = if remainder != BigUint::new(Vec::new()) {
- // If there is a remainder, convert it to a decimal
let remainder_decimal = remainder.to_string();
let padded_remainder = format!("{:0>width$}", remainder_decimal, width = decimal as usize);
format!("{value_decimal}.{padded_remainder}")
@@ -99,12 +102,12 @@ pub fn parse_erc20_approval(
value_decimal.to_string()
};
- Ok(ParsedErc20Approval { spender, value })
+ Ok(value)
}
-
#[cfg(test)]
mod tests {
use super::*;
+ use crate::crypto::keccak256;
use core::str::FromStr;
#[test]
@@ -170,4 +173,44 @@ mod tests {
Err(err) => panic!("Test failed due to error: {}", err),
}
}
+
+ #[test]
+ fn test_transfer_function_selector_deterministic() {
+ let function_signature = "transfer(address,uint256)";
+
+ let hash = keccak256(function_signature.as_bytes());
+
+ let selector = &hash[0..4];
+ let selector_hex = hex::encode(selector);
+
+ assert_eq!(
+ selector_hex, "a9059cbb",
+ "keccak256('transfer(address,uint256)')[0:4] = a9059cbb"
+ );
+ }
+
+ #[test]
+ fn test_parse_erc20_approval() {
+ // Build an example ERC20 approve calldata:
+ // selector (095ea7b3) + spender (padded to 64) + amount (padded to 64)
+ let spender = "5df9b87991262f6ba471f09758cde1c0fc1de734";
+ let amount_hex = "6acfc0"; // 7,000,000 (decimal)
+ let calldata = format!(
+ "095ea7b3{spender_padded}{amount_padded}",
+ spender_padded = format!("{:0>64}", spender),
+ amount_padded = format!("{:0>64}", amount_hex),
+ );
+
+ let decimal = 18;
+ let result = parse_erc20_approval(&calldata, decimal);
+
+ match result {
+ Ok(approval) => {
+ assert_eq!(approval.spender, format!("0x{spender}"));
+ // 7,000,000 / 10^18 = 0.000000000007
+ assert_eq!(approval.value, "0.000000000007");
+ }
+ Err(err) => panic!("Approval parse failed: {}", err),
+ }
+ }
}
diff --git a/rust/apps/ethereum/src/legacy_transaction.rs b/rust/apps/ethereum/src/legacy_transaction.rs
index 4a8c6fe..7530134 100644
--- a/rust/apps/ethereum/src/legacy_transaction.rs
+++ b/rust/apps/ethereum/src/legacy_transaction.rs
@@ -171,23 +171,25 @@ impl LegacyTransaction {
}
pub fn decode_rsv(rlp: &Rlp) -> Result<Option<TransactionSignature>, DecoderError> {
- if rlp.item_count()? == 6 {
- return Ok(None);
- } else if rlp.item_count()? == 9 {
- let v = rlp.val_at(6)?;
- let r = {
- let mut rarr = [0_u8; 32];
- rlp.val_at::<U256>(7)?.to_big_endian(&mut rarr);
- H256::from(rarr)
- };
- let s = {
- let mut sarr = [0_u8; 32];
- rlp.val_at::<U256>(8)?.to_big_endian(&mut sarr);
- H256::from(sarr)
- };
- return Ok(Some(TransactionSignature::new(v, r, s)));
+ let item_count = rlp.item_count()?;
+ match item_count {
+ 6 => Ok(None),
+ 9 => {
+ let v = rlp.val_at(6)?;
+ let r = {
+ let mut rarr = [0_u8; 32];
+ rlp.val_at::<U256>(7)?.to_big_endian(&mut rarr);
+ H256::from(rarr)
+ };
+ let s = {
+ let mut sarr = [0_u8; 32];
+ rlp.val_at::<U256>(8)?.to_big_endian(&mut sarr);
+ H256::from(sarr)
+ };
+ Ok(Some(TransactionSignature::new(v, r, s)))
+ }
+ _ => Err(DecoderError::RlpIncorrectListLen),
}
- Err(DecoderError::RlpIncorrectListLen)
}
pub fn chain_id(&self) -> u64 {
@@ -480,4 +482,96 @@ mod tests {
signed_tx_hash_hex
)
}
+
+ #[test]
+ fn test_transaction_recovery_id_standard() {
+ // Test standard recovery IDs (27, 28)
+ let recovery_id_27 = TransactionRecoveryId(27);
+ assert_eq!(recovery_id_27.standard(), 0);
+
+ let recovery_id_28 = TransactionRecoveryId(28);
+ assert_eq!(recovery_id_28.standard(), 1);
+
+ // Test EIP-155 recovery IDs (> 36)
+ let recovery_id_37 = TransactionRecoveryId(37); // chain_id = 1
+ assert_eq!(recovery_id_37.standard(), 0);
+
+ let recovery_id_38 = TransactionRecoveryId(38); // chain_id = 1
+ assert_eq!(recovery_id_38.standard(), 1);
+
+ let recovery_id_39 = TransactionRecoveryId(39); // chain_id = 2
+ assert_eq!(recovery_id_39.standard(), 0);
+
+ // Test invalid recovery ID (36)
+ let recovery_id_36 = TransactionRecoveryId(36);
+ assert_eq!(recovery_id_36.standard(), 4);
+ }
+
+ #[test]
+ fn test_transaction_recovery_id_chain_id() {
+ // Test non-EIP-155 recovery IDs (27, 28)
+ let recovery_id_27 = TransactionRecoveryId(27);
+ assert_eq!(recovery_id_27.chain_id(), None);
+
+ let recovery_id_28 = TransactionRecoveryId(28);
+ assert_eq!(recovery_id_28.chain_id(), None);
+
+ // Test EIP-155 recovery IDs
+ let recovery_id_37 = TransactionRecoveryId(37); // chain_id = (37-35)/2 = 1
+ assert_eq!(recovery_id_37.chain_id(), Some(1));
+
+ let recovery_id_38 = TransactionRecoveryId(38); // chain_id = (38-35)/2 = 1
+ assert_eq!(recovery_id_38.chain_id(), Some(1));
+
+ let recovery_id_39 = TransactionRecoveryId(39); // chain_id = (39-35)/2 = 2
+ assert_eq!(recovery_id_39.chain_id(), Some(2));
+
+ let recovery_id_41 = TransactionRecoveryId(41); // chain_id = (41-35)/2 = 3
+ assert_eq!(recovery_id_41.chain_id(), Some(3));
+ }
+
+ #[test]
+ fn test_legacy_transaction_chain_id() {
+ // Test unsigned transaction
+ let tx = LegacyTransaction::new(
+ 0,
+ 1000000000,
+ 21000,
+ TransactionAction::Call(
+ H160::from_str("0x0000000000000000000000000000000000000000").unwrap(),
+ ),
+ 0,
+ "".to_string(),
+ );
+ assert_eq!(tx.chain_id(), 1); // Default chain_id for unsigned
+
+ // Test EIP-155 compatible transaction (v > 35)
+ // For signed transactions, s should not be zero
+ let r = H256::from([1u8; 32]); // Non-zero r
+ let mut s_arr = [0u8; 32];
+ s_arr[0] = 1; // Non-zero s
+ let s = H256::from(s_arr);
+ let signature = TransactionSignature::new(37, r, s); // chain_id = (37 - 35) / 2 = 1
+ let tx = tx.set_signature(signature);
+ assert_eq!(tx.chain_id(), 1);
+ assert!(tx.is_eip155_compatible());
+
+ // Test unsigned EIP-155 compatible transaction (s is zero)
+ let r = H256::from([0u8; 32]);
+ let s = H256::zero();
+ let signature = TransactionSignature::new(1, r, s); // v = chain_id for unsigned
+ let tx = LegacyTransaction::new(
+ 0,
+ 1000000000,
+ 21000,
+ TransactionAction::Call(
+ H160::from_str("0x0000000000000000000000000000000000000000").unwrap(),
+ ),
+ 0,
+ "".to_string(),
+ )
+ .set_signature(signature);
+ assert_eq!(tx.chain_id(), 1);
+ assert!(tx.is_eip155_compatible());
+ }
}
diff --git a/rust/apps/ethereum/src/lib.rs b/rust/apps/ethereum/src/lib.rs
index e5a04f5..1ab75c6 100644
--- a/rust/apps/ethereum/src/lib.rs
+++ b/rust/apps/ethereum/src/lib.rs
@@ -55,7 +55,7 @@ pub fn parse_personal_message(
tx_hex: Vec<u8>,
from_key: Option<PublicKey>,
) -> Result<PersonalMessage> {
- let raw_messge = hex::encode(tx_hex.clone());
+ let raw_message = hex::encode(tx_hex.clone());
let utf8_message = match String::from_utf8(tx_hex) {
Ok(utf8_message) => {
if app_utils::is_cjk(&utf8_message) {
@@ -66,7 +66,7 @@ pub fn parse_personal_message(
}
Err(_e) => "".to_string(),
};
- PersonalMessage::from(raw_messge, utf8_message, from_key)
+ PersonalMessage::from(raw_message, utf8_message, from_key)
}
pub fn parse_typed_data_message(tx_hex: Vec<u8>, from_key: Option<PublicKey>) -> Result<TypedData> {
@@ -114,7 +114,7 @@ pub fn sign_legacy_tx_v2(
})
}
-pub fn sign_fee_markey_tx(
+pub fn sign_fee_market_tx(
sign_data: Vec<u8>,
seed: &[u8],
path: &String,
@@ -148,10 +148,7 @@ pub fn sign_personal_message(
Message::from_digest_slice(&hash).map_err(|e| EthereumError::SignFailure(e.to_string()))?;
keystore::algorithms::secp256k1::sign_message_by_seed(seed, path, &message)
.map_err(|e| EthereumError::SignFailure(e.to_string()))
- .map(|(rec_id, rs)| {
- let v = rec_id as u64 + 27;
- EthereumSignature(v, rs)
- })
+ .map(|(rec_id, rs)| EthereumSignature(rec_id as u64 + 27, rs))
}
pub fn sign_typed_data_message(
@@ -172,10 +169,7 @@ pub fn sign_typed_data_message(
keystore::algorithms::secp256k1::sign_message_by_seed(seed, path, &message)
.map_err(|e| EthereumError::SignFailure(e.to_string()))
- .map(|(rec_id, rs)| {
- let v = rec_id as u64 + 27;
- EthereumSignature(v, rs)
- })
+ .map(|(rec_id, rs)| EthereumSignature(rec_id as u64 + 27, rs))
}
#[cfg(test)]
@@ -188,7 +182,7 @@ mod tests {
use crate::alloc::string::ToString;
use crate::eip712::eip712::{Eip712, TypedData as Eip712TypedData};
use crate::{
- parse_fee_market_tx, parse_personal_message, parse_typed_data_message,
+ parse_fee_market_tx, parse_legacy_tx, parse_personal_message, parse_typed_data_message,
sign_personal_message, sign_typed_data_message,
};
@@ -223,6 +217,33 @@ mod tests {
hex::encode(message.serialize()));
}
+ #[test]
+ fn test_parse_legacy_tx() {
+ // Signed legacy transaction (EIP-155 compatible, chain_id=1)
+ // nonce: 33, gas_price: 15198060006, gas_limit: 46000
+ // to: 0xfe2c232adDF66539BFd5d1Bd4B2cc91D358022a2
+ // value: 200000000000000 wei (0.0002 ETH)
+ let sign_data = hex::decode("f86a21850389dffde682b3b094fe2c232addf66539bfd5d1bd4b2cc91d358022a286b5e620f480008026a035df2b615912b8be79a13c9b0a1540ade55434ab68778a49943442a9e6d3141aa00a6e33134ba47c1f1cda59ec3ef62a59d4da6a9d111eb4e447828574c1c94f66").unwrap();
+ let path = "m/44'/60'/0'/0/0".to_string();
+ let seed = hex::decode("5eb00bbddcf069084889a8ab9155568165f5c453ccb85e70811aaed6f6da5fc19a5ac40b389cd370d086206dec8aa6c43daea6690f20ad3d8d48b2d2ce9e38e4").unwrap();
+ let pubkey = get_public_key_by_seed(&seed, &path).unwrap();
+
+ let result = parse_legacy_tx(&sign_data, Some(pubkey)).unwrap();
+
+ assert_eq!(33, result.nonce);
+ assert_eq!(1, result.chain_id);
+ assert_eq!(
+ "0x9858EfFD232B4033E47d90003D41EC34EcaEda94",
+ result.from.unwrap()
+ );
+ assert_eq!("0xfe2c232addf66539bfd5d1bd4b2cc91d358022a2", result.to);
+ assert_eq!("0.0002", result.value);
+ assert_eq!(Some("15.198060006 Gwei".to_string()), result.gas_price);
+ assert_eq!("46000", result.gas_limit);
+ assert_eq!("", result.input);
+ assert_eq!("0.000699110760276", result.max_txn_fee);
+ }
+
#[test]
fn test_parse_tx() {
let sign_data = hex::decode("f902b6011f8405f5e1008503a5fe2f0883026b08943fc91a3afd70395cd496c647d5a6cc9d4b2b7fad872386f26fc10000b902843593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000064996e5f00000000000000000000000000000000000000000000000000000000000000020b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000002386f26fc1000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000002386f26fc10000000000000000000000000000000000000000000000000000f84605ccc515414000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f46b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000c0").unwrap();
diff --git a/rust/apps/ethereum/src/normalizer.rs b/rust/apps/ethereum/src/normalizer.rs
index d964562..c417ef1 100644
--- a/rust/apps/ethereum/src/normalizer.rs
+++ b/rust/apps/ethereum/src/normalizer.rs
@@ -3,8 +3,7 @@ use alloc::string::{String, ToString};
use core::ops::Div;
use ethereum_types::U256;
-static F_DIVIDER: f64 = 1_000_000_000f64;
-static U_DIVIDER: u64 = 1_000_000_000;
+const F_DIVIDER: f64 = 1_000_000_000f64;
pub fn normalize_price(gas: u64) -> String {
format!("{} Gwei", (gas as f64).div(F_DIVIDER))
@@ -19,39 +18,95 @@ pub fn normalize_value(value: U256) -> String {
let padded_value = format!("{value_str:0>18}");
let len = padded_value.len();
- let mut res = if len <= 18 {
- let mut val = padded_value;
- while val.ends_with('0') {
- val.pop();
+ let res = if len <= 18 {
+ let val = padded_value.trim_end_matches('0');
+ if val.is_empty() {
+ "0".to_string()
+ } else {
+ format!("0.{val}")
}
- format!("0.{val}")
} else {
let (int_part, decimal_part) = padded_value.split_at(len - 18);
- let mut decimal = decimal_part.to_string();
- while decimal.ends_with('0') {
- decimal.pop();
+ let decimal = decimal_part.trim_end_matches('0');
+ if decimal.is_empty() {
+ int_part.to_string()
+ } else {
+ format!("{int_part}.{decimal}")
}
- format!("{int_part}.{decimal}")
};
- if res.ends_with('.') {
- res.pop();
- }
res
}
#[cfg(test)]
mod tests {
- use crate::normalizer::normalize_value;
+ use crate::normalizer::{normalize_price, normalize_value};
use ethereum_types::U256;
extern crate std;
- use std::println;
+ #[test]
+ fn test_normalize_value_zero() {
+ let value = U256::from(0u64);
+ let result = normalize_value(value);
+ assert_eq!("0", result);
+ }
+
+ #[test]
+ fn test_normalize_value_small() {
+ let value = U256::from(1u64);
+ let result = normalize_value(value);
+ assert_eq!("0.000000000000000001", result);
+
+ let value = U256::from(1000000000000000u64);
+ let result = normalize_value(value);
+ assert_eq!("0.001", result);
+ }
+
+ #[test]
+ fn test_normalize_value_medium() {
+ let value = U256::from(1000000000000000000u64); // 1 ETH
+ let result = normalize_value(value);
+ assert_eq!("1", result);
+
+ let value = U256::from(1500000000000000000u64); // 1.5 ETH
+ let result = normalize_value(value);
+ assert_eq!("1.5", result);
+ }
#[test]
- fn test() {
- let x = U256::from(000_000_100_000_000_001u64);
- let y = normalize_value(x);
- println!("{y}");
+ fn test_normalize_value_large() {
+ let value = U256::from_dec_str("1000000000000000000000").unwrap(); // 1000 ETH
+ let result = normalize_value(value);
+ assert_eq!("1000", result);
+
+ let value = U256::from_dec_str("1234567890000000000000").unwrap(); // 1234.56789 ETH
+ let result = normalize_value(value);
+ assert_eq!("1234.56789", result);
+ }
+
+ #[test]
+ fn test_normalize_value_trailing_zeros() {
+ let value = U256::from_dec_str("100000000000000000000").unwrap(); // 100 ETH
+ let result = normalize_value(value);
+ assert_eq!("100", result);
+
+ let value = U256::from_dec_str("10000000000000000000").unwrap(); // 10 ETH
+ let result = normalize_value(value);
+ assert_eq!("10", result);
+ }
+
+ #[test]
+ fn test_normalize_price() {
+ let gas = 1000000000u64; // 1 Gwei
+ let result = normalize_price(gas);
+ assert_eq!("1 Gwei", result);
+
+ let gas = 20000000000u64; // 20 Gwei
+ let result = normalize_price(gas);
+ assert_eq!("20 Gwei", result);
+
+ let gas = 500000000u64; // 0.5 Gwei
+ let result = normalize_price(gas);
+ assert_eq!("0.5 Gwei", result);
}
}
diff --git a/rust/apps/ethereum/src/structs.rs b/rust/apps/ethereum/src/structs.rs
index 69f1e6f..67fd326 100644
--- a/rust/apps/ethereum/src/structs.rs
+++ b/rust/apps/ethereum/src/structs.rs
@@ -1,7 +1,7 @@
+use crate::address::generate_address;
use crate::eip1559_transaction::ParsedEIP1559Transaction;
use crate::eip712::eip712::TypedData as Eip712TypedData;
use crate::errors::Result;
-use crate::{address::generate_address, eip712::eip712::Eip712};
use crate::{Bytes, ParsedLegacyTransaction};
use alloc::string::{String, ToString};
use alloc::{format, vec};
diff --git a/rust/apps/ethereum/src/swap.rs b/rust/apps/ethereum/src/swap.rs
index 73a9861..476ecd1 100644
--- a/rust/apps/ethereum/src/swap.rs
+++ b/rust/apps/ethereum/src/swap.rs
@@ -155,3 +155,158 @@ impl SwapkitContractData {
}
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ extern crate std;
+
+ #[test]
+ fn test_swapkit_asset_name_convert_simple() {
+ assert_eq!(
+ swapkit_asset_name_convert("e").unwrap(),
+ ("ETH".to_string(), None)
+ );
+ assert_eq!(
+ swapkit_asset_name_convert("ETH.ETH").unwrap(),
+ ("ETH".to_string(), None)
+ );
+ assert_eq!(
+ swapkit_asset_name_convert("bitcoin").unwrap(),
+ ("BTC".to_string(), None)
+ );
+ assert_eq!(
+ swapkit_asset_name_convert("b").unwrap(),
+ ("BTC".to_string(), None)
+ );
+ assert_eq!(
+ swapkit_asset_name_convert("BTC.BTC").unwrap(),
+ ("BTC".to_string(), None)
+ );
+ assert_eq!(
+ swapkit_asset_name_convert("x").unwrap(),
+ ("XRP".to_string(), None)
+ );
+ assert_eq!(
+ swapkit_asset_name_convert("d").unwrap(),
+ ("DOGE".to_string(), None)
+ );
+ assert_eq!(
+ swapkit_asset_name_convert("DOGE.DOGE").unwrap(),
+ ("DOGE".to_string(), None)
+ );
+ assert_eq!(
+ swapkit_asset_name_convert("s").unwrap(),
+ ("BNB".to_string(), None)
+ );
+ assert_eq!(
+ swapkit_asset_name_convert("BNB.BNB").unwrap(),
+ ("BNB".to_string(), None)
+ );
+ }
+
+ #[test]
+ fn test_swapkit_asset_name_convert_with_contract() {
+ // Test asset with contract address
+ let result =
+ swapkit_asset_name_convert("ETH.USDT-0XDAC17F958D2EE523A2206206994597C13D831EC7")
+ .unwrap();
+ assert_eq!(result.0, "eth.usdt");
+ assert_eq!(
+ result.1,
+ Some("0xdac17f958d2ee523a2206206994597c13d831ec7".to_string())
+ );
+
+ let result =
+ swapkit_asset_name_convert("ETH.USDT-0xdac17f958d2ee523a2206206994597c13d831ec7")
+ .unwrap();
+ assert_eq!(result.0, "eth.usdt");
+ assert_eq!(
+ result.1,
+ Some("0xdac17f958d2ee523a2206206994597c13d831ec7".to_string())
+ );
+ }
+
+ #[test]
+ fn test_swapkit_asset_name_convert_case_insensitive() {
+ // Test case insensitivity
+ assert_eq!(
+ swapkit_asset_name_convert("E").unwrap(),
+ ("ETH".to_string(), None)
+ );
+ assert_eq!(
+ swapkit_asset_name_convert("B").unwrap(),
+ ("BTC".to_string(), None)
+ );
+ assert_eq!(
+ swapkit_asset_name_convert("X").unwrap(),
+ ("XRP".to_string(), None)
+ );
+ }
+
+ #[test]
+ fn test_parse_swapkit_memo_valid() {
+ // Test valid memo format: =:e:0x742636d8FBD2C1dD721Db619b49eaD254385D77d:256699:-_/kns:20/0
+ let memo = "=:e:0x742636d8FBD2C1dD721Db619b49eaD254385D77d:256699:-_/kns:20/0";
+ let result = parse_swapkit_memo(memo);
+ assert!(result.is_ok());
+ let swapkit_memo = result.unwrap();
+ assert_eq!(swapkit_memo.asset, "ETH");
+ assert_eq!(
+ swapkit_memo.receive_address,
+ "0x742636d8FBD2C1dD721Db619b49eaD254385D77d"
+ );
+ assert_eq!(swapkit_memo.swap_out_asset_contract_address, None);
+ }
+
+ #[test]
+ fn test_parse_swapkit_memo_with_contract() {
+ // Test memo with contract address
+ let memo = "=:ETH.USDT-0XDAC17F958D2EE523A2206206994597C13D831EC7:0x742636d8FBD2C1dD721Db619b49eaD254385D77d:662901600:-_/kns:20/0";
+ let result = parse_swapkit_memo(memo);
+ assert!(result.is_ok());
+ let swapkit_memo = result.unwrap();
+ assert_eq!(swapkit_memo.asset, "eth.usdt");
+ assert_eq!(
+ swapkit_memo.receive_address,
+ "0x742636d8FBD2C1dD721Db619b49eaD254385D77d"
+ );
+ assert_eq!(
+ swapkit_memo.swap_out_asset_contract_address,
+ Some("0xdac17f958d2ee523a2206206994597c13d831ec7".to_string())
+ );
+ }
+
+ #[test]
+ fn test_parse_swapkit_memo_invalid_empty() {
+ let memo = "";
+ let result = parse_swapkit_memo(memo);
+ assert!(result.is_err());
+ }
+
+ #[test]
+ fn test_parse_swapkit_memo_invalid_too_short() {
+ let memo = "=:e";
+ let result = parse_swapkit_memo(memo);
+ assert!(result.is_err());
+ }
+
+ #[test]
+ fn test_swapkit_memo_new() {
+ let memo = SwapkitMemo::new(
+ "ETH".to_string(),
+ "0x742636d8FBD2C1dD721Db619b49eaD254385D77d".to_string(),
+ Some("0xDAC17F958D2EE523A2206206994597C13D831EC7".to_string()),
+ );
+ assert_eq!(memo.asset, "ETH");
+ assert_eq!(
+ memo.receive_address,
+ "0x742636d8FBD2C1dD721Db619b49eaD254385D77d"
+ );
+ assert_eq!(
+ memo.swap_out_asset_contract_address,
+ Some("0xDAC17F958D2EE523A2206206994597C13D831EC7".to_string())
+ );
+ }
+}
diff --git a/rust/rust_c/src/common/mod.rs b/rust/rust_c/src/common/mod.rs
index dac8c2b..8e0d1c4 100644
--- a/rust/rust_c/src/common/mod.rs
+++ b/rust/rust_c/src/common/mod.rs
@@ -39,7 +39,7 @@ mod ur_ext;
pub mod utils;
pub mod web_auth;
-pub static KEYSTONE: &str = "keystone";
+pub const KEYSTONE: &str = "keystone";
#[no_mangle]
pub extern "C" fn get_master_fingerprint(seed: PtrBytes, seed_len: u32) -> *mut SimpleResponse<u8> {
diff --git a/rust/rust_c/src/common/ur.rs b/rust/rust_c/src/common/ur.rs
index 4108fed..b964fe5 100644
--- a/rust/rust_c/src/common/ur.rs
+++ b/rust/rust_c/src/common/ur.rs
@@ -82,9 +82,8 @@ use crate::{
impl_response,
};
-#[no_mangle]
-pub static FRAGMENT_MAX_LENGTH_DEFAULT: usize = 200;
-pub static FRAGMENT_UNLIMITED_LENGTH: usize = 11000;
+pub const FRAGMENT_MAX_LENGTH_DEFAULT: usize = 200;
+pub const FRAGMENT_UNLIMITED_LENGTH: usize = 11000;
#[repr(C)]
pub struct UREncodeResult {
diff --git a/rust/rust_c/src/ethereum/mod.rs b/rust/rust_c/src/ethereum/mod.rs
index 7091ab1..f2befec 100644
--- a/rust/rust_c/src/ethereum/mod.rs
+++ b/rust/rust_c/src/ethereum/mod.rs
@@ -171,8 +171,7 @@ fn try_get_eth_public_key(
match eth_sign_request.get_derivation_path().get_path() {
None => Err(RustCError::InvalidHDPath),
Some(path) => {
- let _path = path.clone();
- if let Some(sub_path) = parse_eth_sub_path(_path) {
+ if let Some(sub_path) = parse_eth_sub_path(path.clone()) {
derive_public_key(&xpub, &format!("m/{sub_path}")).map_err(|_e| {
RustCError::UnexpectedError("unable to derive pubkey".to_string())
})
@@ -234,22 +233,23 @@ pub unsafe extern "C" fn eth_parse(
xpub: PtrString,
) -> PtrT<TransactionParseResult<DisplayETH>> {
let crypto_eth = extract_ptr_with_type!(ptr, EthSignRequest);
+ let unsigned_data = crypto_eth.get_sign_data();
let xpub = recover_c_char(xpub);
let pubkey = try_get_eth_public_key(xpub, crypto_eth).ok();
let transaction_type = TransactionType::from(crypto_eth.get_data_type());
match transaction_type {
TransactionType::Legacy => {
- let tx = parse_legacy_tx(&crypto_eth.get_sign_data(), pubkey);
+ let tx = parse_legacy_tx(&unsigned_data, pubkey);
match tx {
Ok(t) => TransactionParseResult::success(DisplayETH::from(t).c_ptr()).c_ptr(),
Err(e) => TransactionParseResult::from(e).c_ptr(),
}
}
TransactionType::TypedTransaction => {
- match crypto_eth.get_sign_data().first() {
+ match unsigned_data.first() {
Some(0x02) => {
//remove envelop
- let payload = &crypto_eth.get_sign_data()[1..];
+ let payload = &unsigned_data[1..];
let tx = parse_fee_market_tx(payload, pubkey);
match tx {
Ok(t) => {
@@ -327,9 +327,7 @@ unsafe fn eth_check_batch_tx(
return Err(e);
}
};
- if ur_mfp == mfp {
- continue;
- } else {
+ if ur_mfp != mfp {
return Err(RustCError::MasterFingerprintMismatch);
}
}
@@ -453,14 +451,13 @@ pub unsafe extern "C" fn eth_sign_batch_tx(
path = format!("m/{path}");
}
+ let sign_data = request.get_sign_data();
let signature = match TransactionType::from(request.get_data_type()) {
TransactionType::Legacy => {
- app_ethereum::sign_legacy_tx(request.get_sign_data().to_vec(), seed, &path)
+ app_ethereum::sign_legacy_tx(sign_data.to_vec(), seed, &path)
}
- TransactionType::TypedTransaction => match request.get_sign_data().first() {
- Some(0x02) => {
- app_ethereum::sign_fee_markey_tx(request.get_sign_data().to_vec(), seed, &path)
- }
+ TransactionType::TypedTransaction => match sign_data.first() {
+ Some(0x02) => app_ethereum::sign_fee_market_tx(sign_data.to_vec(), seed, &path),
Some(x) => {
return UREncodeResult::from(RustCError::UnsupportedTransaction(format!(
"ethereum tx type: {x}"
@@ -550,14 +547,11 @@ pub unsafe extern "C" fn eth_sign_tx_dynamic(
path = format!("m/{path}");
}
+ let sign_data = crypto_eth.get_sign_data();
let signature = match TransactionType::from(crypto_eth.get_data_type()) {
- TransactionType::Legacy => {
- app_ethereum::sign_legacy_tx(crypto_eth.get_sign_data().to_vec(), seed, &path)
- }
- TransactionType::TypedTransaction => match crypto_eth.get_sign_data().first() {
- Some(0x02) => {
- app_ethereum::sign_fee_markey_tx(crypto_eth.get_sign_data().to_vec(), seed, &path)
- }
+ TransactionType::Legacy => app_ethereum::sign_legacy_tx(sign_data.to_vec(), seed, &path),
+ TransactionType::TypedTransaction => match sign_data.first() {
+ Some(0x02) => app_ethereum::sign_fee_market_tx(sign_data.to_vec(), seed, &path),
Some(x) => {
return UREncodeResult::from(RustCError::UnsupportedTransaction(format!(
"ethereum tx type: {x}"
@@ -569,10 +563,10 @@ pub unsafe extern "C" fn eth_sign_tx_dynamic(
}
},
TransactionType::PersonalMessage => {
- app_ethereum::sign_personal_message(crypto_eth.get_sign_data().to_vec(), seed, &path)
+ app_ethereum::sign_personal_message(sign_data.to_vec(), seed, &path)
}
TransactionType::TypedData => {
- app_ethereum::sign_typed_data_message(crypto_eth.get_sign_data().to_vec(), seed, &path)
+ app_ethereum::sign_typed_data_message(sign_data.to_vec(), seed, &path)
}
};
match signature {
@@ -746,13 +740,10 @@ mod tests {
fn test_test() {
let _path = "44'/60'/1'/0/0";
let root_path = "44'/60'/";
- match _path.strip_prefix(root_path) {
- Some(path) => {
- if let Some(index) = path.find('/') {
- println!("{}", &path[index..]);
- }
+ if let Some(path) = _path.strip_prefix(root_path) {
+ if let Some(index) = path.find('/') {
+ println!("{}", &path[index..]);
}
- None => {}
};
}
}
diff --git a/src/ui/gui_chain/gui_chain.c b/src/ui/gui_chain/gui_chain.c
index bae3756..887cd70 100644
--- a/src/ui/gui_chain/gui_chain.c
+++ b/src/ui/gui_chain/gui_chain.c
@@ -193,7 +193,7 @@ UREncodeResult *SignInternal(SignFn sign_func, void *data)
UREncodeResult *encodeResult = NULL;
uint8_t seed[SEED_LEN] = {0};
int ret = 0;
-
+
do {
ret = GetAccountSeed(GetCurrentAccountIndex(), seed, SecretCacheGetPassword());
if (ret != 0) {
diff --git a/src/ui/gui_chain/multi/web3/gui_cosmos.c b/src/ui/gui_chain/multi/web3/gui_cosmos.c
index 160be9a..5ee32b8 100644
--- a/src/ui/gui_chain/multi/web3/gui_cosmos.c
+++ b/src/ui/gui_chain/multi/web3/gui_cosmos.c
@@ -432,7 +432,7 @@ void GetCosmosDetailItemValue(void *indata, void *param, uint32_t maxLen)
if (root == NULL) {
strcpy_s((char *)indata, maxLen, "");
return;
- }
+ }
cJSON* value = cJSON_GetObjectItem(root, indata);
if (value == NULL) {
strcpy_s((char *)indata, maxLen, "");
diff --git a/src/ui/gui_chain/multi/web3/gui_eth.c b/src/ui/gui_chain/multi/web3/gui_eth.c
index 123cd13..e240acc 100644
--- a/src/ui/gui_chain/multi/web3/gui_eth.c
+++ b/src/ui/gui_chain/multi/web3/gui_eth.c
@@ -715,6 +715,7 @@ static bool isErc20Transfer(void *param)
return false;
}
// FIXME: 0xa9059cbb is the method of erc20 transfer
+ // see test_transfer_function_selector_deterministic in erc20.rs for more details
const char *erc20Method = "a9059cbb";
if (strncmp(input, erc20Method, 8) == 0) {
return true;
@@ -1523,7 +1524,9 @@ static void decodeEthContractData(void *parseResult)
if (!GetEthContractFromInternal(contractAddress, result->data->detail->input)) {
char selectorId[9] = {0};
strncpy(selectorId, result->data->detail->input, 8);
- GetEthContractFromExternal(contractAddress, selectorId, result->data->chain_id, result->data->detail->input);
+ if (SdCardInsert()) {
+ GetEthContractFromExternal(contractAddress, selectorId, result->data->chain_id, result->data->detail->input);
+ }
}
}
@@ -1533,12 +1536,11 @@ static void FixRecipientAndValueWhenErc20Contract(const char *inputdata, uint8_t
if (!isErc20Transfer(result->data)) {
return;
}
- PtrT_TransactionParseResult_EthParsedErc20Transaction contractData = eth_parse_erc20((PtrString)inputdata, decimals);
- g_erc20ContractData = contractData;
- // result->data->detail->to = contractData->data->to;
- // result->data->overview->to = contractData->data->to;
- // result->data->detail->value = contractData->data->value;
- // result->data->overview->value = contractData->data->value;
+ if (g_erc20ContractData != NULL) {
+ free_TransactionParseResult_EthParsedErc20Transaction(g_erc20ContractData);
+ g_erc20ContractData = NULL;
+ }
+ g_erc20ContractData = eth_parse_erc20((PtrString)inputdata, decimals);
}
static bool GetEthErc20ContractData(void *parseResult)
@@ -1547,8 +1549,11 @@ static bool GetEthErc20ContractData(void *parseResult)
TransactionParseResult_DisplayETH *result = (TransactionParseResult_DisplayETH *)parseResult;
Response_DisplayContractData *contractData = eth_parse_contract_data(result->data->detail->input, (char *)ethereum_erc20_json);
if (contractData->error_code == 0) {
- g_contractDataExist = true;
+ if (g_contractDataExist && g_contractData != NULL) {
+ free_Response_DisplayContractData(g_contractData);
+ }
g_contractData = contractData;
+ g_contractDataExist = true;
} else {
free_Response_DisplayContractData(contractData);
return false;
Why this scored 35/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.