chore: refactor ton add add more tests
What changed, and why it matters
This commit is a routine code-quality and test-coverage update for the TON (The Open Network) wallet code in the Keystone 3 firmware. It replaces several Rust `.unwrap()` calls with proper error handling, adds many unit tests, and improves how sensitive seed data is cleared from memory after signing. There is no direct evidence in the commit that these changes fix an active exploit; they look like defensive hardening and cleanup.
Treat as a routine hardening/refactoring commit. Reviewers should verify that the new error paths in `structs.rs` propagate correctly to the UI, that `zeroize` is applied consistently to all copies of the seed, and that the added tests pass. No urgent security response is indicated by the diff alone.
Security signals we found
Replaced panicking `.unwrap()` calls in `get_jetton_amount_text` with explicit `Result` error handling
Added `zeroize()` of the normalized mnemonic word vector after seed derivation
Added `seed.zeroize()` in Rust C signing entry points after secret key derivation
Switched C signing code to use `GetCurrentAccountSeedLen()` and check `GetAccountSeed` return value
Added `memset_s` clearing of the local seed buffer in C after TON transaction and proof signing
Removed `#![feature(error_in_core)]` nightly Rust feature
Evidence from the diff
The patch refactors the TON Rust app and its C FFI bindings. Key changes: (1) get_jetton_amount_text now returns Result<String> instead of panicking via .unwrap() on unknown jetton addresses or unparseable amounts. (2) ton_mnemonic_to_entropy and ton_mnemonic_validate accept slices instead of &Vec<String>, and the normalized word vector is zeroized after use in ton_mnemonic_to_master_seed. (3) The Rust C bridge now uses extract_array_mut! and calls seed.zeroize() after deriving the signing key in ton_sign_transaction and ton_sign_proof. (4) The C GUI code now uses GetCurrentAccountSeedLen(), checks the return value of GetAccountSeed, and clears the local seed buffer with memset_s after signing. (5) Large blocks of unit tests were added across jetton, address, mnemonic, transaction, and proof modules. The #![feature(error_in_core)] nightly feature is removed, improving portability.
Changed components
rust/apps/ton/src/jettons.rsrust/apps/ton/src/lib.rsrust/apps/ton/src/mnemonic.rsrust/apps/ton/src/structs.rsrust/apps/ton/src/transaction.rsrust/rust_c/src/ton/mod.rssrc/ui/gui_chain/multi/web3/gui_ton.cInspect captured patch +1534 / −108
diff --git a/rust/apps/ton/src/jettons.rs b/rust/apps/ton/src/jettons.rs
index 10ea7c3..dbe8200 100644
--- a/rust/apps/ton/src/jettons.rs
+++ b/rust/apps/ton/src/jettons.rs
@@ -9,6 +9,9 @@ use lazy_static::lazy_static;
use itertools::Itertools;
+use crate::errors::Result;
+use crate::errors::TonError;
+
pub struct JettonData {
pub contract_address: String,
pub decimal: u8,
@@ -40,12 +43,18 @@ lazy_static! {
];
}
-pub fn get_jetton_amount_text(coins: String, contract_address: String) -> String {
+pub fn get_jetton_amount_text(coins: String, contract_address: String) -> Result<String> {
let target = JETTONS
.iter()
.find_or_first(|v| v.contract_address.eq(&contract_address))
- .unwrap();
- let value = coins.parse::<u128>().unwrap();
+ .ok_or_else(|| {
+ TonError::InvalidTransaction(format!(
+ "Invalid jetton contract address: {contract_address}"
+ ))
+ })?;
+ let value = coins
+ .parse::<u128>()
+ .map_err(|_e| TonError::InvalidTransaction(format!("Invalid jetton amount: {coins}")))?;
let divisor = 10u128.pow(target.decimal as u32);
let integer_part = value / divisor;
@@ -58,21 +67,213 @@ pub fn get_jetton_amount_text(coins: String, contract_address: String) -> String
);
let fractional_str = fractional_str.trim_end_matches('0');
if fractional_str.is_empty() {
- format!("{} {}", integer_part, target.symbol)
+ Ok(format!("{} {}", integer_part, target.symbol))
} else {
- format!("{}.{} {}", integer_part, fractional_str, target.symbol)
+ Ok(format!(
+ "{}.{} {}",
+ integer_part, fractional_str, target.symbol
+ ))
}
}
#[cfg(test)]
mod tests {
use super::*;
+ extern crate std;
+ use alloc::string::ToString;
#[test]
fn test_get_jetton_amount_text() {
let coins = "30110292000".to_string();
let contract_address = "EQA2kCVNwVsil2EM2mB0SkXytxCqQjS4mttjDpnXmwG9T6bO".to_string();
- let result = get_jetton_amount_text(coins, contract_address);
+ let result = get_jetton_amount_text(coins, contract_address).unwrap();
assert_eq!(result, "30.110292 STON");
}
+
+ #[test]
+ fn test_get_jetton_amount_null_unit() {
+ let result = get_jetton_amount_text("100".to_string(), "NULL".to_string()).unwrap();
+ assert_eq!(result, "100 Unit");
+ }
+
+ #[test]
+ fn test_get_jetton_amount_not_token() {
+ // NOT token has 8 decimals
+ let result = get_jetton_amount_text(
+ "100000000".to_string(),
+ "EQAvlWFDxGF2lXm67y4yzC17wYKD9A0guwPkMs1gOsM__NOT".to_string(),
+ )
+ .unwrap();
+ assert_eq!(result, "1 NOT");
+ }
+
+ #[test]
+ fn test_get_jetton_amount_not_token_partial() {
+ // NOT token has 8 decimals - testing fractional amount
+ let result = get_jetton_amount_text(
+ "12345678".to_string(),
+ "EQAvlWFDxGF2lXm67y4yzC17wYKD9A0guwPkMs1gOsM__NOT".to_string(),
+ )
+ .unwrap();
+ assert_eq!(result, "0.12345678 NOT");
+ }
+
+ #[test]
+ fn test_get_jetton_amount_usdt_token() {
+ // USDT has 6 decimals
+ let result = get_jetton_amount_text("1000000".to_string(), "0".to_string()).unwrap();
+ assert_eq!(result, "1 USDT");
+ }
+
+ #[test]
+ fn test_get_jetton_amount_usdt_token_large() {
+ // USDT has 6 decimals - testing large amount
+ let result = get_jetton_amount_text("123456789000".to_string(), "0".to_string()).unwrap();
+ assert_eq!(result, "123456.789 USDT");
+ }
+
+ #[test]
+ fn test_get_jetton_amount_ston_token() {
+ // STON token has 9 decimals
+ let result = get_jetton_amount_text(
+ "1000000000".to_string(),
+ "EQA2kCVNwVsil2EM2mB0SkXytxCqQjS4mttjDpnXmwG9T6bO".to_string(),
+ )
+ .unwrap();
+ assert_eq!(result, "1 STON");
+ }
+
+ #[test]
+ fn test_get_jetton_amount_ston_token_partial() {
+ // STON token has 9 decimals - testing fractional amount
+ let result = get_jetton_amount_text(
+ "987654321".to_string(),
+ "EQA2kCVNwVsil2EM2mB0SkXytxCqQjS4mttjDpnXmwG9T6bO".to_string(),
+ )
+ .unwrap();
+ assert_eq!(result, "0.987654321 STON");
+ }
+
+ #[test]
+ fn test_get_jetton_amount_zero() {
+ // Test zero amount
+ let result = get_jetton_amount_text("0".to_string(), "NULL".to_string()).unwrap();
+ assert_eq!(result, "0 Unit");
+ }
+
+ #[test]
+ fn test_get_jetton_amount_unknown_address_fallback() {
+ // Unknown contract address should fall back to first jetton (NULL/Unit)
+ let result =
+ get_jetton_amount_text("123".to_string(), "UNKNOWN_ADDRESS_12345".to_string()).unwrap();
+ assert_eq!(result, "123 Unit");
+ }
+
+ #[test]
+ fn test_get_jetton_amount_invalid_amount_string() {
+ // Invalid amount string should return error
+ let result = get_jetton_amount_text("not_a_number".to_string(), "NULL".to_string());
+ assert!(result.is_err());
+
+ if let Err(e) = result {
+ assert!(e.to_string().contains("Invalid jetton amount"));
+ }
+ }
+
+ #[test]
+ fn test_get_jetton_amount_empty_amount() {
+ // Empty amount string should return error
+ let result = get_jetton_amount_text("".to_string(), "NULL".to_string());
+ assert!(result.is_err());
+ }
+
+ #[test]
+ fn test_get_jetton_amount_negative_amount() {
+ // Negative amount should return error (u64 can't be negative)
+ let result = get_jetton_amount_text("-100".to_string(), "NULL".to_string());
+ assert!(result.is_err());
+ }
+
+ #[test]
+ fn test_get_jetton_amount_large_number() {
+ // Test with large number (limited by f64 precision)
+ let result = get_jetton_amount_text(
+ "1000000000000".to_string(), // 1 trillion - large but within f64 safe integer range
+ "NULL".to_string(),
+ )
+ .unwrap();
+ assert_eq!(result, "1000000000000 Unit");
+ }
+
+ #[test]
+ fn test_get_jetton_amount_overflow() {
+ // Test with number that exceeds u128::MAX
+ let result = get_jetton_amount_text(
+ "340282366920938463463374607431768211456".to_string(), // u128::MAX + 1
+ "NULL".to_string(),
+ );
+ assert!(result.is_err());
+ }
+
+ #[test]
+ fn test_get_jetton_amount_decimal_precision() {
+ // Test decimal precision for each token type
+ let result = get_jetton_amount_text("1".to_string(), "NULL".to_string()).unwrap();
+ assert_eq!(result, "1 Unit"); // 0 decimals
+
+ let result = get_jetton_amount_text("1".to_string(), "0".to_string()).unwrap();
+ assert_eq!(result, "0.000001 USDT"); // 6 decimals
+
+ let result = get_jetton_amount_text(
+ "1".to_string(),
+ "EQAvlWFDxGF2lXm67y4yzC17wYKD9A0guwPkMs1gOsM__NOT".to_string(),
+ )
+ .unwrap();
+ assert_eq!(result, "0.00000001 NOT"); // 8 decimals
+
+ let result = get_jetton_amount_text(
+ "1".to_string(),
+ "EQA2kCVNwVsil2EM2mB0SkXytxCqQjS4mttjDpnXmwG9T6bO".to_string(),
+ )
+ .unwrap();
+ assert_eq!(result, "0.000000001 STON"); // 9 decimals
+ }
+
+ #[test]
+ fn test_jetton_data_structure() {
+ // Verify the JETTONS static data structure is correctly initialized
+ assert_eq!(JETTONS.len(), 4);
+
+ assert_eq!(JETTONS[0].contract_address, "NULL");
+ assert_eq!(JETTONS[0].decimal, 0);
+ assert_eq!(JETTONS[0].symbol, "Unit");
+
+ assert_eq!(
+ JETTONS[1].contract_address,
+ "EQAvlWFDxGF2lXm67y4yzC17wYKD9A0guwPkMs1gOsM__NOT"
+ );
+ assert_eq!(JETTONS[1].decimal, 8);
+ assert_eq!(JETTONS[1].symbol, "NOT");
+
+ assert_eq!(JETTONS[2].contract_address, "0");
+ assert_eq!(JETTONS[2].decimal, 6);
+ assert_eq!(JETTONS[2].symbol, "USDT");
+
+ assert_eq!(
+ JETTONS[3].contract_address,
+ "EQA2kCVNwVsil2EM2mB0SkXytxCqQjS4mttjDpnXmwG9T6bO"
+ );
+ assert_eq!(JETTONS[3].decimal, 9);
+ assert_eq!(JETTONS[3].symbol, "STON");
+ }
+
+ #[test]
+ fn test_get_jetton_amount_with_whitespace() {
+ // Test that strings with whitespace fail (as they should)
+ let result = get_jetton_amount_text("100 ".to_string(), "NULL".to_string());
+ assert!(result.is_err());
+
+ let result = get_jetton_amount_text(" 100".to_string(), "NULL".to_string());
+ assert!(result.is_err());
+ }
}
diff --git a/rust/apps/ton/src/lib.rs b/rust/apps/ton/src/lib.rs
index aee8f78..106d79d 100644
--- a/rust/apps/ton/src/lib.rs
+++ b/rust/apps/ton/src/lib.rs
@@ -1,5 +1,4 @@
#![no_std]
-#![feature(error_in_core)]
use core::str::FromStr;
@@ -40,6 +39,9 @@ pub fn ton_compare_address_and_public_key(pk: Vec<u8>, address: String) -> bool
#[cfg(test)]
mod tests {
+ use super::*;
+ extern crate std;
+ use alloc::vec;
#[test]
fn test_generate_address() {
@@ -48,4 +50,468 @@ mod tests {
let address = super::ton_public_key_to_address(pk).unwrap();
assert_eq!(address, "UQC4FC01K66rElokeYTPeEnWStITQDxGiK8RhkMXpT88FY5b")
}
+
+ #[test]
+ fn test_generate_address_different_key() {
+ // Test with a different public key
+ let pk = hex::decode("82e594257c8c42f193ecef1f7d61f261e817211e2f8033c3e97de8647ddf7855")
+ .unwrap();
+ let address = ton_public_key_to_address(pk).unwrap();
+ // Verify it returns a valid base64 URL address
+ assert!(!address.is_empty());
+ assert!(address.starts_with("UQ") || address.starts_with("EQ"));
+ }
+
+ #[test]
+ fn test_generate_address_invalid_key_length() {
+ // Test with invalid key length (not 32 bytes)
+ let pk = vec![0u8; 16]; // Only 16 bytes instead of 32
+ let result = ton_public_key_to_address(pk);
+ assert!(result.is_err());
+ }
+
+ #[test]
+ fn test_generate_address_empty_key() {
+ // Test with empty public key
+ let pk = vec![];
+ let result = ton_public_key_to_address(pk);
+ assert!(result.is_err());
+ }
+
+ #[test]
+ fn test_generate_address_oversized_key() {
+ // Test with oversized key (more than 32 bytes)
+ let pk = vec![0u8; 64];
+ let result = ton_public_key_to_address(pk);
+ assert!(result.is_err());
+ }
+
+ #[test]
+ fn test_compare_address_and_public_key_match() {
+ // Test that matching public key and address return true
+ let pk = hex::decode("15556a2d93ab1471eb34e1d6873fc637e6a4b5a9cb2638148c12dc4bac1651f1")
+ .unwrap();
+ let address = "UQC4FC01K66rElokeYTPeEnWStITQDxGiK8RhkMXpT88FY5b".to_string();
+ let result = ton_compare_address_and_public_key(pk, address);
+ assert!(result);
+ }
+
+ #[test]
+ fn test_compare_address_and_public_key_mismatch() {
+ // Test that non-matching public key and address return false
+ let pk = hex::decode("15556a2d93ab1471eb34e1d6873fc637e6a4b5a9cb2638148c12dc4bac1651f1")
+ .unwrap();
+ let address = "UQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJKZ".to_string();
+ let result = ton_compare_address_and_public_key(pk, address);
+ assert!(!result);
+ }
+
+ #[test]
+ fn test_compare_address_and_public_key_invalid_address() {
+ // Test with invalid address format
+ let pk = hex::decode("15556a2d93ab1471eb34e1d6873fc637e6a4b5a9cb2638148c12dc4bac1651f1")
+ .unwrap();
+ let address = "invalid_address_format".to_string();
+ let result = ton_compare_address_and_public_key(pk, address);
+ assert!(!result);
+ }
+
+ #[test]
+ fn test_compare_address_and_public_key_empty_address() {
+ // Test with empty address
+ let pk = hex::decode("15556a2d93ab1471eb34e1d6873fc637e6a4b5a9cb2638148c12dc4bac1651f1")
+ .unwrap();
+ let address = "".to_string();
+ let result = ton_compare_address_and_public_key(pk, address);
+ assert!(!result);
+ }
+
+ #[test]
+ fn test_compare_address_and_public_key_invalid_key() {
+ // Test with invalid public key length
+ let pk = vec![0u8; 16];
+ let address = "UQC4FC01K66rElokeYTPeEnWStITQDxGiK8RhkMXpT88FY5b".to_string();
+ let result = ton_compare_address_and_public_key(pk, address);
+ assert!(!result);
+ }
+
+ #[test]
+ fn test_compare_address_and_public_key_empty_key() {
+ // Test with empty public key
+ let pk = vec![];
+ let address = "UQC4FC01K66rElokeYTPeEnWStITQDxGiK8RhkMXpT88FY5b".to_string();
+ let result = ton_compare_address_and_public_key(pk, address);
+ assert!(!result);
+ }
+
+ #[test]
+ fn test_compare_address_with_different_formats() {
+ // Test that UQ (bounceable) and EQ (non-bounceable) are treated as different addresses
+ let pk = hex::decode("15556a2d93ab1471eb34e1d6873fc637e6a4b5a9cb2638148c12dc4bac1651f1")
+ .unwrap();
+ let generated_address = ton_public_key_to_address(pk.clone()).unwrap();
+ // The generated address should be UQ (bounceable) format
+ assert!(generated_address.starts_with("UQ"));
+
+ // Comparing with the correct UQ format should match
+ assert!(ton_compare_address_and_public_key(
+ pk.clone(),
+ generated_address
+ ));
+
+ // Try with EQ prefix (non-bounceable) - this should not match
+ // as the comparison is strict about the format
+ let eq_address = "EQC4FC01K66rElokeYTPeEnWStITQDxGiK8RhkMXpT88FVuL".to_string();
+ let result = ton_compare_address_and_public_key(pk, eq_address);
+ // Different format flags mean different addresses for comparison purposes
+ assert!(!result);
+ }
+
+ #[test]
+ fn test_generate_address_consistency() {
+ // Test that generating address multiple times gives same result
+ let pk = hex::decode("15556a2d93ab1471eb34e1d6873fc637e6a4b5a9cb2638148c12dc4bac1651f1")
+ .unwrap();
+ let address1 = ton_public_key_to_address(pk.clone()).unwrap();
+ let address2 = ton_public_key_to_address(pk.clone()).unwrap();
+ assert_eq!(address1, address2);
+ }
+
+ #[test]
+ fn test_generate_address_different_keys_different_addresses() {
+ // Test that different keys generate different addresses
+ let pk1 = hex::decode("15556a2d93ab1471eb34e1d6873fc637e6a4b5a9cb2638148c12dc4bac1651f1")
+ .unwrap();
+ let pk2 = hex::decode("82e594257c8c42f193ecef1f7d61f261e817211e2f8033c3e97de8647ddf7855")
+ .unwrap();
+
+ let address1 = ton_public_key_to_address(pk1).unwrap();
+ let address2 = ton_public_key_to_address(pk2).unwrap();
+
+ assert_ne!(address1, address2);
+ }
+
+ #[test]
+ fn test_compare_with_generated_address() {
+ // Integration test: generate address and then compare it
+ let pk = hex::decode("82e594257c8c42f193ecef1f7d61f261e817211e2f8033c3e97de8647ddf7855")
+ .unwrap();
+ let generated_address = ton_public_key_to_address(pk.clone()).unwrap();
+ let result = ton_compare_address_and_public_key(pk, generated_address);
+ assert!(result);
+ }
+
+ #[test]
+ fn test_compare_address_case_sensitivity() {
+ // Addresses are case-sensitive in base64
+ let pk = hex::decode("15556a2d93ab1471eb34e1d6873fc637e6a4b5a9cb2638148c12dc4bac1651f1")
+ .unwrap();
+ // Use lowercase which should be invalid
+ let address = "uqc4fc01k66relokeytpeenwstiitqdxgik8rhkmxpt88fy5b".to_string();
+ let result = ton_compare_address_and_public_key(pk, address);
+ assert!(!result);
+ }
+
+ #[test]
+ fn test_generate_address_all_zeros_key() {
+ // Test with all zeros key (edge case)
+ let pk = vec![0u8; 32];
+ let result = ton_public_key_to_address(pk);
+ // Should succeed as it's a valid 32-byte key
+ assert!(result.is_ok());
+ }
+
+ #[test]
+ fn test_generate_address_all_ones_key() {
+ // Test with all 0xFF key (edge case)
+ let pk = vec![0xFFu8; 32];
+ let result = ton_public_key_to_address(pk);
+ // Should succeed as it's a valid 32-byte key
+ assert!(result.is_ok());
+ }
+
+ #[test]
+ fn test_compare_address_with_whitespace() {
+ // Test that address with whitespace doesn't match
+ let pk = hex::decode("15556a2d93ab1471eb34e1d6873fc637e6a4b5a9cb2638148c12dc4bac1651f1")
+ .unwrap();
+ let address = " UQC4FC01K66rElokeYTPeEnWStITQDxGiK8RhkMXpT88FY5b ".to_string();
+ let result = ton_compare_address_and_public_key(pk, address);
+ assert!(!result);
+ }
+
+ #[test]
+ fn test_address_format_validation() {
+ // Verify generated addresses have correct format
+ let pk = hex::decode("15556a2d93ab1471eb34e1d6873fc637e6a4b5a9cb2638148c12dc4bac1651f1")
+ .unwrap();
+ let address = ton_public_key_to_address(pk).unwrap();
+
+ // Address should start with UQ or EQ
+ assert!(address.starts_with("UQ") || address.starts_with("EQ"));
+ // Address should be base64 characters
+ assert!(address
+ .chars()
+ .all(|c| c.is_alphanumeric() || c == '_' || c == '-'));
+ // Address should have reasonable length (typically 48 characters)
+ assert!(address.len() >= 40 && address.len() <= 50);
+ }
+
+ #[test]
+ fn test_multiple_known_address_pairs() {
+ // Test multiple known public key and address pairs
+ let test_cases = vec![(
+ "15556a2d93ab1471eb34e1d6873fc637e6a4b5a9cb2638148c12dc4bac1651f1",
+ "UQC4FC01K66rElokeYTPeEnWStITQDxGiK8RhkMXpT88FY5b",
+ )];
+
+ for (pk_hex, expected_address) in test_cases {
+ let pk = hex::decode(pk_hex).unwrap();
+ let address = ton_public_key_to_address(pk.clone()).unwrap();
+ assert_eq!(address, expected_address);
+ assert!(ton_compare_address_and_public_key(
+ pk,
+ expected_address.to_string()
+ ));
+ }
+ }
+
+ #[test]
+ fn test_key_length_boundary_31_bytes() {
+ // Test with exactly 31 bytes (one less than required)
+ let pk = vec![0u8; 31];
+ let result = ton_public_key_to_address(pk);
+ assert!(result.is_err());
+ }
+
+ #[test]
+ fn test_key_length_boundary_33_bytes() {
+ // Test with exactly 33 bytes (one more than required)
+ let pk = vec![0u8; 33];
+ let result = ton_public_key_to_address(pk);
+ assert!(result.is_err());
+ }
+
+ #[test]
+ fn test_compare_with_truncated_address() {
+ // Test with truncated address
+ let pk = hex::decode("15556a2d93ab1471eb34e1d6873fc637e6a4b5a9cb2638148c12dc4bac1651f1")
+ .unwrap();
+ let address = "UQC4FC01K66rEloke".to_string(); // Truncated
+ let result = ton_compare_address_and_public_key(pk, address);
+ assert!(!result);
+ }
+
+ #[test]
+ fn test_compare_with_extra_characters() {
+ // Test with address having extra characters at the end
+ let pk = hex::decode("15556a2d93ab1471eb34e1d6873fc637e6a4b5a9cb2638148c12dc4bac1651f1")
+ .unwrap();
+ let address = "UQC4FC01K66rElokeYTPeEnWStITQDxGiK8RhkMXpT88FY5bEXTRA".to_string();
+ let result = ton_compare_address_and_public_key(pk, address);
+ assert!(!result);
+ }
+
+ #[test]
+ fn test_address_with_invalid_base64() {
+ // Test with invalid base64 characters
+ let pk = hex::decode("15556a2d93ab1471eb34e1d6873fc637e6a4b5a9cb2638148c12dc4bac1651f1")
+ .unwrap();
+ let address = "UQC4FC01K66rEloke@#$%^&*()+=FY5b".to_string();
+ let result = ton_compare_address_and_public_key(pk, address);
+ assert!(!result);
+ }
+
+ #[test]
+ fn test_sequential_key_bytes() {
+ // Test with sequential bytes (0x00, 0x01, 0x02, ... 0x1F)
+ let pk: Vec<u8> = (0..32).collect();
+ let result = ton_public_key_to_address(pk);
+ assert!(result.is_ok());
+ assert!(!result.unwrap().is_empty());
+ }
+
+ #[test]
+ fn test_alternating_bit_pattern() {
+ // Test with alternating bit pattern (0xAA repeated)
+ let pk = vec![0xAAu8; 32];
+ let result = ton_public_key_to_address(pk);
+ assert!(result.is_ok());
+ }
+
+ #[test]
+ fn test_alternating_bit_pattern_2() {
+ // Test with alternating bit pattern (0x55 repeated)
+ let pk = vec![0x55u8; 32];
+ let result = ton_public_key_to_address(pk);
+ assert!(result.is_ok());
+ }
+
+ #[test]
+ fn test_compare_same_address_different_keys() {
+ // Ensure different keys don't match the same address
+ let pk1 = hex::decode("15556a2d93ab1471eb34e1d6873fc637e6a4b5a9cb2638148c12dc4bac1651f1")
+ .unwrap();
+ let pk2 = hex::decode("82e594257c8c42f193ecef1f7d61f261e817211e2f8033c3e97de8647ddf7855")
+ .unwrap();
+ let address = "UQC4FC01K66rElokeYTPeEnWStITQDxGiK8RhkMXpT88FY5b".to_string();
+
+ let result1 = ton_compare_address_and_public_key(pk1, address.clone());
+ let result2 = ton_compare_address_and_public_key(pk2, address);
+
+ // Only one should match
+ assert_ne!(result1, result2);
+ }
+
+ #[test]
+ fn test_address_with_only_prefix() {
+ // Test with only address prefix
+ let pk = hex::decode("15556a2d93ab1471eb34e1d6873fc637e6a4b5a9cb2638148c12dc4bac1651f1")
+ .unwrap();
+ let address = "UQ".to_string();
+ let result = ton_compare_address_and_public_key(pk, address);
+ assert!(!result);
+ }
+
+ #[test]
+ fn test_address_with_wrong_prefix() {
+ // Test with wrong prefix (not UQ or EQ)
+ let pk = hex::decode("15556a2d93ab1471eb34e1d6873fc637e6a4b5a9cb2638148c12dc4bac1651f1")
+ .unwrap();
+ let address = "ABC4FC01K66rElokeYTPeEnWStITQDxGiK8RhkMXpT88FY5b".to_string();
+ let result = ton_compare_address_and_public_key(pk, address);
+ assert!(!result);
+ }
+
+ #[test]
+ fn test_high_entropy_key() {
+ // Test with high entropy key (pseudo-random bytes)
+ let pk = hex::decode("a3f4b1c2d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2")
+ .unwrap();
+ let result = ton_public_key_to_address(pk);
+ assert!(result.is_ok());
+ let address = result.unwrap();
+ assert!(address.starts_with("UQ") || address.starts_with("EQ"));
+ }
+
+ #[test]
+ fn test_low_entropy_key() {
+ // Test with low entropy key (mostly zeros with few ones)
+ let mut pk = vec![0u8; 32];
+ pk[0] = 1;
+ pk[31] = 1;
+ let result = ton_public_key_to_address(pk);
+ assert!(result.is_ok());
+ }
+
+ #[test]
+ fn test_middle_bit_pattern_key() {
+ // Test with alternating bytes pattern
+ let pk: Vec<u8> = (0..32)
+ .map(|i| if i % 2 == 0 { 0xFF } else { 0x00 })
+ .collect();
+ let result = ton_public_key_to_address(pk);
+ assert!(result.is_ok());
+ }
+
+ #[test]
+ fn test_compare_address_consistency() {
+ // Test that comparison is consistent across multiple calls
+ let pk = hex::decode("15556a2d93ab1471eb34e1d6873fc637e6a4b5a9cb2638148c12dc4bac1651f1")
+ .unwrap();
+ let address = "UQC4FC01K66rElokeYTPeEnWStITQDxGiK8RhkMXpT88FY5b".to_string();
+
+ for _ in 0..5 {
+ let result = ton_compare_address_and_public_key(pk.clone(), address.clone());
+ assert!(result);
+ }
+ }
+
+ #[test]
+ fn test_numeric_string_as_address() {
+ // Test with numeric string (invalid address)
+ let pk = hex::decode("15556a2d93ab1471eb34e1d6873fc637e6a4b5a9cb2638148c12dc4bac1651f1")
+ .unwrap();
+ let address = "123456789012345678901234567890".to_string();
+ let result = ton_compare_address_and_public_key(pk, address);
+ assert!(!result);
+ }
+
+ #[test]
+ fn test_special_characters_in_address() {
+ // Test with special characters
+ let pk = hex::decode("15556a2d93ab1471eb34e1d6873fc637e6a4b5a9cb2638148c12dc4bac1651f1")
+ .unwrap();
+ let address = "UQC4FC01K66r!@#$%^&*()".to_string();
+ let result = ton_compare_address_and_public_key(pk, address);
+ assert!(!result);
+ }
+
+ #[test]
+ fn test_very_long_address_string() {
+ // Test with excessively long address string
+ let pk = hex::decode("15556a2d93ab1471eb34e1d6873fc637e6a4b5a9cb2638148c12dc4bac1651f1")
+ .unwrap();
+ let address = "UQ".to_string() + &"A".repeat(1000);
+ let result = ton_compare_address_and_public_key(pk, address);
+ assert!(!result);
+ }
+
+ #[test]
+ fn test_address_generation_deterministic() {
+ // Verify that address generation is deterministic
+ let pk = hex::decode("82e594257c8c42f193ecef1f7d61f261e817211e2f8033c3e97de8647ddf7855")
+ .unwrap();
+
+ let mut addresses = Vec::new();
+ for _ in 0..10 {
+ addresses.push(ton_public_key_to_address(pk.clone()).unwrap());
+ }
+
+ // All addresses should be identical
+ let first = &addresses[0];
+ for addr in &addresses {
+ assert_eq!(addr, first);
+ }
+ }
+
+ #[test]
+ fn test_single_bit_difference_keys() {
+ // Test that keys differing by a single bit produce different addresses
+ let pk1 = hex::decode("15556a2d93ab1471eb34e1d6873fc637e6a4b5a9cb2638148c12dc4bac1651f1")
+ .unwrap();
+ let mut pk2 = pk1.clone();
+ pk2[0] ^= 0x01; // Flip the least significant bit
+
+ let addr1 = ton_public_key_to_address(pk1).unwrap();
+ let addr2 = ton_public_key_to_address(pk2).unwrap();
+
+ assert_ne!(addr1, addr2);
+ }
+
+ #[test]
+ fn test_first_byte_variations() {
+ // Test variations in the first byte
+ for byte_val in [0x00, 0x7F, 0x80, 0xFF] {
+ let mut pk =
+ hex::decode("15556a2d93ab1471eb34e1d6873fc637e6a4b5a9cb2638148c12dc4bac1651f1")
+ .unwrap();
+ pk[0] = byte_val;
+ let result = ton_public_key_to_address(pk);
+ assert!(result.is_ok());
+ }
+ }
+
+ #[test]
+ fn test_last_byte_variations() {
+ // Test variations in the last byte
+ for byte_val in [0x00, 0x7F, 0x80, 0xFF] {
+ let mut pk =
+ hex::decode("15556a2d93ab1471eb34e1d6873fc637e6a4b5a9cb2638148c12dc4bac1651f1")
+ .unwrap();
+ pk[31] = byte_val;
+ let result = ton_public_key_to_address(pk);
+ assert!(result.is_ok());
+ }
+ }
}
diff --git a/rust/apps/ton/src/mnemonic.rs b/rust/apps/ton/src/mnemonic.rs
index 13a99cc..2f41cde 100644
--- a/rust/apps/ton/src/mnemonic.rs
+++ b/rust/apps/ton/src/mnemonic.rs
@@ -14,10 +14,7 @@ const PBKDF_ITERATIONS: u32 = 100000;
const TON_MNEMONIC_24_WORDS: usize = 24;
const TON_MNEMONIC_12_WORDS: usize = 12;
-pub fn ton_mnemonic_to_entropy(
- normalized_words: &Vec<String>,
- password: &Option<String>,
-) -> Vec<u8> {
+pub fn ton_mnemonic_to_entropy(normalized_words: &[String], password: &Option<String>) -> Vec<u8> {
let mut binding = Hmac::new(Sha512::new(), normalized_words.join(" ").as_bytes());
if let Some(password) = password {
binding.input(password.as_bytes());
@@ -25,10 +22,7 @@ pub fn ton_mnemonic_to_entropy(
binding.result().code().to_vec()
}
-pub fn ton_mnemonic_validate(
- normalized_words: &Vec<String>,
- password: &Option<String>,
-) -> Result<()> {
+pub fn ton_mnemonic_validate(normalized_words: &[String], password: &Option<String>) -> Result<()> {
if normalized_words.len() != TON_MNEMONIC_24_WORDS
&& normalized_words.len() != TON_MNEMONIC_12_WORDS
{
@@ -92,9 +86,10 @@ pub fn ton_mnemonic_to_master_seed(
if words.len() != TON_MNEMONIC_24_WORDS && words.len() != TON_MNEMONIC_12_WORDS {
return Err(MnemonicError::UnexpectedWordCount(words.len()).into());
}
- let normalized_words: Vec<String> = words.iter().map(|w| w.trim().to_lowercase()).collect();
+ let mut normalized_words: Vec<String> = words.iter().map(|w| w.trim().to_lowercase()).collect();
ton_mnemonic_validate(&normalized_words, &password)?;
let entropy = ton_mnemonic_to_entropy(&normalized_words, &password);
+ normalized_words.zeroize();
Ok(ton_entropy_to_seed(&entropy))
}
@@ -157,7 +152,6 @@ mod tests {
.iter()
.map(|v| v.to_lowercase())
.collect();
- let words_len = words.len();
let result = ton_mnemonic_to_master_seed(words, None);
assert!(result.is_err());
assert_eq!(
@@ -178,11 +172,542 @@ mod tests {
assert!(result.is_err());
assert_eq!(
result.err().unwrap().to_string(),
- format!(
- "Invalid TON Mnemonic, Invalid mnemonic word count (count: {})",
- words_len
- )
+ format!("Invalid TON Mnemonic, Invalid mnemonic word count (count: {words_len})",)
)
}
}
+
+ #[test]
+ fn test_ton_mnemonic_with_password() {
+ let words: Vec<String> = vec![
+ "dose", "ice", "enrich", "trigger", "test", "dove", "century", "still", "betray",
+ "gas", "diet", "dune", "use", "other", "base", "gym", "mad", "law", "immense",
+ "village", "world", "example", "praise", "game",
+ ]
+ .iter()
+ .map(|v| v.to_lowercase())
+ .collect();
+
+ let password = Some("test_password".to_string());
+ let entropy = ton_mnemonic_to_entropy(&words, &password);
+
+ // Verify entropy is generated with password
+ assert_eq!(entropy.len(), 64);
+ assert_ne!(
+ hex::encode(&entropy),
+ "46dcfbce05b1a1b42c535b05f84461f5bb1d62b1429b283fcfed943352c0ecfb29f671d2e8e6885d4dd3a8c85d99f91bbb7a17c73d96c918e6200f49268ea1a3"
+ );
+ }
+
+ #[test]
+ fn test_ton_mnemonic_12_words_valid() {
+ // Test with a valid 12-word mnemonic
+ let words: Vec<String> = [
+ "abandon", "abandon", "abandon", "abandon", "abandon", "abandon", "abandon", "abandon",
+ "abandon", "abandon", "abandon", "about",
+ ]
+ .iter()
+ .map(|v| v.to_lowercase())
+ .collect();
+
+ // Note: This might fail validation due to checksum, but should not fail on word count
+ assert!(words.len() == TON_MNEMONIC_12_WORDS);
+ }
+
+ #[test]
+ fn test_ton_master_seed_to_public_key() {
+ let words: Vec<String> = vec![
+ "dose", "ice", "enrich", "trigger", "test", "dove", "century", "still", "betray",
+ "gas", "diet", "dune", "use", "other", "base", "gym", "mad", "law", "immense",
+ "village", "world", "example", "praise", "game",
+ ]
+ .iter()
+ .map(|v| v.to_lowercase())
+ .collect();
+
+ let master_seed = ton_mnemonic_to_master_seed(words, None).unwrap();
+ let public_key = ton_master_seed_to_public_key(master_seed);
+
+ // Verify public key is 32 bytes
+ assert_eq!(public_key.len(), 32);
+ assert_eq!(
+ hex::encode(public_key),
+ "c04ad1885c127fe863abb00752fa844e6439bb04f264d70de7cea580b32637ab"
+ );
+ }
+
+ #[test]
+ fn test_ton_entropy_to_seed_deterministic() {
+ let entropy = vec![0u8; 64];
+ let seed1 = ton_entropy_to_seed(&entropy);
+ let seed2 = ton_entropy_to_seed(&entropy);
+
+ // Verify the function is deterministic
+ assert_eq!(seed1, seed2);
+ assert_eq!(seed1.len(), 64);
+ }
+
+ #[test]
+ fn test_ton_mnemonic_with_empty_password() {
+ let words: Vec<String> = vec![
+ "dose", "ice", "enrich", "trigger", "test", "dove", "century", "still", "betray",
+ "gas", "diet", "dune", "use", "other", "base", "gym", "mad", "law", "immense",
+ "village", "world", "example", "praise", "game",
+ ]
+ .iter()
+ .map(|v| v.to_lowercase())
+ .collect();
+
+ // Empty password should be treated same as no password
+ let result_no_password = ton_mnemonic_to_master_seed(words.clone(), None).unwrap();
+ let result_empty_password =
+ ton_mnemonic_to_master_seed(words, Some("".to_string())).unwrap();
+
+ assert_eq!(result_no_password, result_empty_password);
+ }
+
+ #[test]
+ fn test_ton_mnemonic_normalization() {
+ let words_lower: Vec<String> = vec![
+ "dose", "ice", "enrich", "trigger", "test", "dove", "century", "still", "betray",
+ "gas", "diet", "dune", "use", "other", "base", "gym", "mad", "law", "immense",
+ "village", "world", "example", "praise", "game",
+ ]
+ .iter()
+ .map(|v| v.to_string())
+ .collect();
+
+ let words_mixed: Vec<String> = vec![
+ "Dose", "ICE", "Enrich", "TRIGGER", "test", "Dove", "CENTURY", "still", "Betray",
+ "GAS", "diet", "DUNE", "use", "Other", "BASE", "gym", "MAD", "law", "Immense",
+ "VILLAGE", "world", "Example", "PRAISE", "game",
+ ]
+ .iter()
+ .map(|v| v.to_string())
+ .collect();
+
+ let seed_lower = ton_mnemonic_to_master_seed(words_lower, None).unwrap();
+ let seed_mixed = ton_mnemonic_to_master_seed(words_mixed, None).unwrap();
+
+ // Should produce same result regardless of case
+ assert_eq!(seed_lower, seed_mixed);
+ }
+
+ #[test]
+ fn test_ton_mnemonic_with_whitespace() {
+ let words_with_spaces: Vec<String> = vec![
+ " dose ", " ice", "enrich ", "trigger", "test", "dove", "century", "still", "betray",
+ "gas", "diet", "dune", "use", "other", "base", "gym", "mad", "law", "immense",
+ "village", "world", "example", "praise", "game",
+ ]
+ .iter()
+ .map(|v| v.to_string())
+ .collect();
+
+ let words_no_spaces: Vec<String> = vec![
+ "dose", "ice", "enrich", "trigger", "test", "dove", "century", "still", "betray",
+ "gas", "diet", "dune", "use", "other", "base", "gym", "mad", "law", "immense",
+ "village", "world", "example", "praise", "game",
+ ]
+ .iter()
+ .map(|v| v.to_string())
+ .collect();
+
+ let seed_with_spaces = ton_mnemonic_to_master_seed(words_with_spaces, None).unwrap();
+ let seed_no_spaces = ton_mnemonic_to_master_seed(words_no_spaces, None).unwrap();
+
+ // Should produce same result after trimming
+ assert_eq!(seed_with_spaces, seed_no_spaces);
+ }
+
+ #[test]
+ fn test_ton_mnemonic_invalid_word_counts() {
+ // Test various invalid word counts
+ let invalid_counts = vec![1, 5, 10, 13, 15, 20, 25, 30];
+
+ for count in invalid_counts {
+ let words: Vec<String> = (0..count).map(|i| format!("word{i}")).collect();
+ let result = ton_mnemonic_to_master_seed(words.clone(), None);
+
+ assert!(result.is_err());
+ let error_msg = result.err().unwrap().to_string();
+ assert!(error_msg.contains(&format!("count: {count}")));
+ }
+ }
+
+ #[test]
+ fn test_ton_keypair_consistency() {
+ let words: Vec<String> = vec![
+ "dose", "ice", "enrich", "trigger", "test", "dove", "century", "still", "betray",
+ "gas", "diet", "dune", "use", "other", "base", "gym", "mad", "law", "immense",
+ "village", "world", "example", "praise", "game",
+ ]
+ .iter()
+ .map(|v| v.to_lowercase())
+ .collect();
+
+ let master_seed = ton_mnemonic_to_master_seed(words, None).unwrap();
+ let (secret_key, public_key) = ton_master_seed_to_keypair(master_seed);
+ let public_key_direct = ton_master_seed_to_public_key(master_seed);
+
+ // Verify both methods produce same public key
+ assert_eq!(public_key, public_key_direct);
+
+ // Verify key sizes
+ assert_eq!(secret_key.len(), 64);
+ assert_eq!(public_key.len(), 32);
+ }
+
+ #[test]
+ fn test_ton_mnemonic_validate_different_passwords() {
+ let words: Vec<String> = vec![
+ "dose", "ice", "enrich", "trigger", "test", "dove", "century", "still", "betray",
+ "gas", "diet", "dune", "use", "other", "base", "gym", "mad", "law", "immense",
+ "village", "world", "example", "praise", "game",
+ ]
+ .iter()
+ .map(|v| v.to_lowercase())
+ .collect();
+
+ // Test validation with no password
+ let result_no_password = ton_mnemonic_validate(&words, &None);
+ assert!(result_no_password.is_ok());
+
+ // Test validation with different passwords - these may fail if mnemonic was not created with password
+ let _result_with_password = ton_mnemonic_validate(&words, &Some("password123".to_string()));
+ // The validation might fail because this specific mnemonic is passwordless
+ }
+
+ #[test]
+ fn test_ton_entropy_to_seed_different_inputs() {
+ let entropy1 = vec![1u8; 64];
+ let entropy2 = vec![2u8; 64];
+
+ let seed1 = ton_entropy_to_seed(&entropy1);
+ let seed2 = ton_entropy_to_seed(&entropy2);
+
+ // Different entropies should produce different seeds
+ assert_ne!(seed1, seed2);
+ }
+
+ #[test]
+ fn test_ton_mnemonic_different_passwords_different_seeds() {
+ let words: Vec<String> = vec![
+ "dose", "ice", "enrich", "trigger", "test", "dove", "century", "still", "betray",
+ "gas", "diet", "dune", "use", "other", "base", "gym", "mad", "law", "immense",
+ "village", "world", "example", "praise", "game",
+ ]
+ .iter()
+ .map(|v| v.to_lowercase())
+ .collect();
+
+ let entropy_no_password = ton_mnemonic_to_entropy(&words, &None);
+ let entropy_password1 = ton_mnemonic_to_entropy(&words, &Some("password1".to_string()));
+ let entropy_password2 = ton_mnemonic_to_entropy(&words, &Some("password2".to_string()));
+
+ // Different passwords should produce different entropies
+ assert_ne!(entropy_no_password, entropy_password1);
+ assert_ne!(entropy_password1, entropy_password2);
+ assert_ne!(entropy_no_password, entropy_password2);
+ }
+
+ #[test]
+ fn test_ton_mnemonic_to_entropy_12_words() {
+ let words: Vec<String> = [
+ "abandon", "abandon", "abandon", "abandon", "abandon", "abandon", "abandon", "abandon",
+ "abandon", "abandon", "abandon", "about",
+ ]
+ .iter()
+ .map(|v| v.to_lowercase())
+ .collect();
+
+ let entropy = ton_mnemonic_to_entropy(&words, &None);
+
+ // Should generate entropy for 12-word mnemonic
+ assert_eq!(entropy.len(), 64);
+
+ let entropy_with_password = ton_mnemonic_to_entropy(&words, &Some("test".to_string()));
+
+ // Different entropy with password
+ assert_ne!(entropy, entropy_with_password);
+ }
+
+ #[test]
+ fn test_ton_mnemonic_with_special_characters_password() {
+ let words: Vec<String> = vec![
+ "dose", "ice", "enrich", "trigger", "test", "dove", "century", "still", "betray",
+ "gas", "diet", "dune", "use", "other", "base", "gym", "mad", "law", "immense",
+ "village", "world", "example", "praise", "game",
+ ]
+ .iter()
+ .map(|v| v.to_lowercase())
+ .collect();
+
+ // Test with various special characters in password
+ let passwords = vec![
+ "p@ssw0rd!",
+ "密码123",
+ "pass word with spaces",
+ "pass\nword\twith\twhitespace",
+ "!@#$%^&*()_+-=[]{}|;:',.<>?/`~",
+ ];
+
+ for password in passwords {
+ let entropy = ton_mnemonic_to_entropy(&words, &Some(password.to_string()));
+ assert_eq!(entropy.len(), 64);
+ }
+ }
+
+ #[test]
+ fn test_ton_master_seed_to_keypair_deterministic() {
+ let words: Vec<String> = vec![
+ "dose", "ice", "enrich", "trigger", "test", "dove", "century", "still", "betray",
+ "gas", "diet", "dune", "use", "other", "base", "gym", "mad", "law", "immense",
+ "village", "world", "example", "praise", "game",
+ ]
+ .iter()
+ .map(|v| v.to_lowercase())
+ .collect();
+
+ let master_seed = ton_mnemonic_to_master_seed(words, None).unwrap();
+
+ // Generate keypair multiple times
+ let (sk1, pk1) = ton_master_seed_to_keypair(master_seed);
+ let (sk2, pk2) = ton_master_seed_to_keypair(master_seed);
+
+ // Should be deterministic
+ assert_eq!(sk1, sk2);
+ assert_eq!(pk1, pk2);
+ }
+
+ #[test]
+ fn test_ton_mnemonic_validate_empty_words() {
+ let words: Vec<String> = vec![];
+ let result = ton_mnemonic_validate(&words, &None);
+
+ assert!(result.is_err());
+ assert_eq!(
+ result.err().unwrap().to_string(),
+ "Invalid TON Mnemonic, Invalid mnemonic word count (count: 0)"
+ );
+ }
+
+ #[test]
+ fn test_ton_mnemonic_validate_24_words() {
+ let words: Vec<String> = vec![
+ "dose", "ice", "enrich", "trigger", "test", "dove", "century", "still", "betray",
+ "gas", "diet", "dune", "use", "other", "base", "gym", "mad", "law", "immense",
+ "village", "world", "example", "praise", "game",
+ ]
+ .iter()
+ .map(|v| v.to_lowercase())
+ .collect();
+
+ // Verify it's 24 words
+ assert_eq!(words.len(), TON_MNEMONIC_24_WORDS);
+
+ // Should validate successfully
+ let result = ton_mnemonic_validate(&words, &None);
+ assert!(result.is_ok());
+ }
+
+ #[test]
+ fn test_ton_entropy_to_seed_output_size() {
+ let entropies = vec![vec![0u8; 64], vec![255u8; 64], (0..64).collect::<Vec<u8>>()];
+
+ for entropy in entropies {
+ let seed = ton_entropy_to_seed(&entropy);
+ // Seed should always be 64 bytes
+ assert_eq!(seed.len(), 64);
+ }
+ }
+
+ #[test]
+ fn test_ton_mnemonic_long_password() {
+ let words: Vec<String> = vec![
+ "dose", "ice", "enrich", "trigger", "test", "dove", "century", "still", "betray",
+ "gas", "diet", "dune", "use", "other", "base", "gym", "mad", "law", "immense",
+ "village", "world", "example", "praise", "game",
+ ]
+ .iter()
+ .map(|v| v.to_lowercase())
+ .collect();
+
+ // Test with very long password
+ let long_password = "a".repeat(1000);
+ let entropy = ton_mnemonic_to_entropy(&words, &Some(long_password));
+
+ assert_eq!(entropy.len(), 64);
+ }
+
+ #[test]
+ fn test_ton_mnemonic_unicode_in_words() {
+ // Test with unicode characters that get normalized
+ let words: Vec<String> = vec![
+ "DOSE", "ICE", "ENRICH", "TRIGGER", "TEST", "DOVE", "CENTURY", "STILL", "BETRAY",
+ "GAS", "DIET", "DUNE", "USE", "OTHER", "BASE", "GYM", "MAD", "LAW", "IMMENSE",
+ "VILLAGE", "WORLD", "EXAMPLE", "PRAISE", "GAME",
+ ]
+ .iter()
+ .map(|v| v.to_string())
+ .collect();
+
+ let result = ton_mnemonic_to_master_seed(words, None);
+ assert!(result.is_ok());
+ }
+
+ #[test]
+ fn test_ton_keypair_from_different_seeds() {
+ let seed1 = [1u8; 64];
+ let seed2 = [2u8; 64];
+
+ let (sk1, pk1) = ton_master_seed_to_keypair(seed1);
+ let (sk2, pk2) = ton_master_seed_to_keypair(seed2);
+
+ // Different seeds should produce different keys
+ assert_ne!(sk1, sk2);
+ assert_ne!(pk1, pk2);
+ }
+
+ #[test]
+ fn test_ton_public_key_from_different_seeds() {
+ let seed1 = [1u8; 64];
+ let seed2 = [2u8; 64];
+
+ let pk1 = ton_master_seed_to_public_key(seed1);
+ let pk2 = ton_master_seed_to_public_key(seed2);
+
+ // Different seeds should produce different public keys
+ assert_ne!(pk1, pk2);
+
+ // Public keys should be 32 bytes
+ assert_eq!(pk1.len(), 32);
+ assert_eq!(pk2.len(), 32);
+ }
+
+ #[test]
+ fn test_ton_mnemonic_case_insensitivity() {
+ let test_cases = vec![
+ ("dose", "DOSE"),
+ ("ice", "Ice"),
+ ("enrich", "ENRICH"),
+ ("trigger", "TrIgGeR"),
+ ];
+
+ for (lower, upper) in test_cases {
+ let words1: Vec<String> = vec![lower.to_string(); 24];
+ let words2: Vec<String> = vec![upper.to_string(); 24];
+
+ let normalized1: Vec<String> = words1.iter().map(|w| w.trim().to_lowercase()).collect();
+ let normalized2: Vec<String> = words2.iter().map(|w| w.trim().to_lowercase()).collect();
+
+ let entropy1 = ton_mnemonic_to_entropy(&normalized1, &None);
+ let entropy2 = ton_mnemonic_to_entropy(&normalized2, &None);
+
+ assert_eq!(entropy1, entropy2);
+ }
+ }
+
+ #[test]
+ fn test_ton_mnemonic_validate_with_invalid_password_check() {
+ let words: Vec<String> = vec![
+ "dose", "ice", "enrich", "trigger", "test", "dove", "century", "still", "betray",
+ "gas", "diet", "dune", "use", "other", "base", "gym", "mad", "law", "immense",
+ "village", "world", "example", "praise", "game",
+ ]
+ .iter()
+ .map(|v| v.to_lowercase())
+ .collect();
+
+ // This mnemonic is passwordless, so validating with a password should fail
+ let result = ton_mnemonic_validate(&words, &Some("wrongpassword".to_string()));
+ assert!(result.is_err());
+ }
+
+ #[test]
+ fn test_ton_mnemonic_boundary_exact_24_words() {
+ // Test exactly 24 words
+ let words: Vec<String> = (0..24).map(|i| format!("word{i}")).collect();
+ assert_eq!(words.len(), TON_MNEMONIC_24_WORDS);
+
+ // Should not fail on word count
+ let result = ton_mnemonic_validate(&words, &None);
+ // May fail on validation but not on count check
+ if result.is_err() {
+ let error_msg = result.err().unwrap().to_string();
+ assert!(!error_msg.contains("Invalid mnemonic word count"));
+ }
+ }
+
+ #[test]
+ fn test_ton_mnemonic_boundary_exact_12_words() {
+ // Test exactly 12 words
+ let words: Vec<String> = (0..12).map(|i| format!("word{i}")).collect();
+ assert_eq!(words.len(), TON_MNEMONIC_12_WORDS);
+
+ // Should not fail on word count
+ let result = ton_mnemonic_validate(&words, &None);
+ // May fail on validation but not on count check
+ if result.is_err() {
+ let error_msg = result.err().unwrap().to_string();
+ assert!(!error_msg.contains("Invalid mnemonic word count"));
+ }
+ }
+
+ #[test]
+ fn test_ton_entropy_consistent_across_calls() {
+ let words: Vec<String> = vec![
+ "dose", "ice", "enrich", "trigger", "test", "dove", "century", "still", "betray",
+ "gas", "diet", "dune", "use", "other", "base", "gym", "mad", "law", "immense",
+ "village", "world", "example", "praise", "game",
+ ]
+ .iter()
+ .map(|v| v.to_lowercase())
+ .collect();
+
+ // Call multiple times to ensure consistency
+ let entropy1 = ton_mnemonic_to_entropy(&words, &None);
+ let entropy2 = ton_mnemonic_to_entropy(&words, &None);
+ let entropy3 = ton_mnemonic_to_entropy(&words, &None);
+
+ assert_eq!(entropy1, entropy2);
+ assert_eq!(entropy2, entropy3);
+ }
+
+ #[test]
+ fn test_ton_master_seed_consistency_with_clone() {
+ let words1: Vec<String> = vec![
+ "dose", "ice", "enrich", "trigger", "test", "dove", "century", "still", "betray",
+ "gas", "diet", "dune", "use", "other", "base", "gym", "mad", "law", "immense",
+ "village", "world", "example", "praise", "game",
+ ]
+ .iter()
+ .map(|v| v.to_lowercase())
+ .collect();
+
+ let words2 = words1.clone();
+
+ let seed1 = ton_mnemonic_to_master_seed(words1, None).unwrap();
+ let seed2 = ton_mnemonic_to_master_seed(words2, None).unwrap();
+
+ assert_eq!(seed1, seed2);
+ }
+
+ #[test]
+ fn test_ton_mnemonic_empty_string_vs_none_password() {
+ let words: Vec<String> = vec![
+ "dose", "ice", "enrich", "trigger", "test", "dove", "century", "still", "betray",
+ "gas", "diet", "dune", "use", "other", "base", "gym", "mad", "law", "immense",
+ "village", "world", "example", "praise", "game",
+ ]
+ .iter()
+ .map(|v| v.to_lowercase())
+ .collect();
+
+ // Validation: empty string password should behave same as None
+ let validate_none = ton_mnemonic_validate(&words, &None);
+ let validate_empty = ton_mnemonic_validate(&words, &Some("".to_string()));
+
+ assert_eq!(validate_none.is_ok(), validate_empty.is_ok());
+ }
}
diff --git a/rust/apps/ton/src/structs.rs b/rust/apps/ton/src/structs.rs
index d60cee8..9f34429 100644
--- a/rust/apps/ton/src/structs.rs
+++ b/rust/apps/ton/src/structs.rs
@@ -81,7 +81,7 @@ impl TryFrom<&SigningMessage> for TonTransaction {
let amount = jettons::get_jetton_amount_text(
jetton_transfer_message.amount.clone(),
to.clone(),
- );
+ )?;
Ok(Self {
to: destination,
amount,
@@ -211,3 +211,247 @@ impl TonProof {
})
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ extern crate std;
+ use alloc::vec;
+ use base64::{engine::general_purpose::STANDARD, Engine};
+ use std::println;
+
+ #[test]
+ fn test_parse_simple_ton_transfer() {
+ // Simple TON transfer without comment
+ let body = "te6cckEBAgEARwABHCmpoxdmOz6lAAAACAADAQBoQgArFnMvHAX9tOjTp4/RDd3vP2Bn8xG+U5MTuKRKUE1NoqHc1lAAAAAAAAAAAAAAAAAAAHBy4G8=";
+ let serial = STANDARD.decode(body).unwrap();
+
+ let tx = TonTransaction::parse_hex(&serial).unwrap();
+
+ assert_eq!(tx.action, "Ton Transfer");
+ assert!(tx.comment.is_none());
+ assert!(tx.data_view.is_none());
+ assert!(tx.contract_data.is_none());
+ assert!(!tx.to.is_empty());
+ assert!(!tx.amount.is_empty());
+ assert!(!tx.raw_data.is_empty());
+ }
+
+ #[test]
+ fn test_parse_ton_transfer_with_comment() {
+ // TON transfer with a long comment
+ let serial = "b5ee9c724102050100019700011c29a9a31766611df6000000140003010166420013587ccf19c39b1ca51c29f0253ac98d03b8e5ccfc64c3ac2f21c59c20ee8b65987a1200000000000000000000000000010201fe000000004b657973746f6e652068617264776172652077616c6c6574206f666665727320756e6265617461626c65207365637572697479207769746820332050434920736563757269747920636869707320746f206d616e61676520426974636f696e20616e64206f746865722063727970746f20617373657473206f66660301fe6c696e652e4b657973746f6e65206f666665727320332077616c6c6574732c207768696368206d65616e7320796f752063616e206d616e616765206d756c7469706c65206163636f756e74732073657061726174656c79206f6e206f6e65206465766963652e4b657973746f6e65206f666665727320332077616c6c6574730400942c207768696368206d65616e7320796f752063616e206d616e616765206d756c7469706c65206163636f756e74732073657061726174656c79206f6e206f6e65206465766963652e0a0ac04eabc7";
+ let serial = hex::decode(serial).unwrap();
+
+ let tx = TonTransaction::parse_hex(&serial).unwrap();
+
+ assert_eq!(tx.action, "Ton Transfer");
+ assert!(tx.comment.is_some());
+ let comment = tx.comment.as_ref().unwrap();
+ assert!(comment.contains("Keystone"));
+ assert!(!tx.to.is_empty());
+ assert!(!tx.amount.is_empty());
+ }
+
+ #[test]
+ fn test_parse_jetton_transfer() {
+ // Jetton transfer (STON)
+ let serial = "b5ee9c7241010301009e00011c29a9a3176656eb410000001000030101686200091c1bd942402db834b5977d2a1313119c3a3800c8e10233fa8eaf36c655ecab202faf0800000000000000000000000000010200a80f8a7ea5546de4ef815e87fb3989680800ac59ccbc7017f6d3a34e9e3f443777bcfd819fcc46f94e4c4ee291294135368b002d48cb0c90c22c52394f297b33990c2f6bbf6c425780862733961fa457f014ec02025f1050ae";
+ let serial = hex::decode(serial).unwrap();
+
+ let tx = TonTransaction::parse_hex(&serial).unwrap();
+
+ assert_eq!(tx.action, "Jetton Transfer");
+ assert!(tx.data_view.is_some());
+ assert!(tx.contract_data.is_some());
+ assert!(!tx.to.is_empty());
+
+ // Contract data should contain Jetton Wallet Address
+ let contract_data = tx.contract_data.as_ref().unwrap();
+ assert!(contract_data.contains("Jetton Wallet Address"));
+ }
+
+ #[test]
+ fn test_parse_transaction_from_boc() {
+ let body = "te6cckEBAgEARwABHCmpoxdmOz6lAAAACAADAQBoQgArFnMvHAX9tOjTp4/RDd3vP2Bn8xG+U5MTuKRKUE1NoqHc1lAAAAAAAAAAAAAAAAAAAHBy4G8=";
+ let serial = STANDARD.decode(body).unwrap();
+ let boc = BagOfCells::parse(&serial).unwrap();
+
+ let tx = TonTransaction::parse(boc).unwrap();
+
+ assert_eq!(tx.action, "Ton Transfer");
+ assert!(!tx.to.is_empty());
+ assert!(!tx.amount.is_empty());
+ }
+
+ #[test]
+ fn test_transaction_to_json() {
+ let body = "te6cckEBAgEARwABHCmpoxdmOz6lAAAACAADAQBoQgArFnMvHAX9tOjTp4/RDd3vP2Bn8xG+U5MTuKRKUE1NoqHc1lAAAAAAAAAAAAAAAAAAAHBy4G8=";
+ let serial = STANDARD.decode(body).unwrap();
+
+ let tx = TonTransaction::parse_hex(&serial).unwrap();
+ let json = tx.to_json().unwrap();
+
+ assert!(json.is_object());
+ assert!(json.get("to").is_some());
+ assert!(json.get("amount").is_some());
+ assert!(json.get("action").is_some());
+ }
+
+ #[test]
+ fn test_parse_invalid_transaction_empty() {
+ // Try to parse empty data
+ let serial = vec![];
+ let result = TonTransaction::parse_hex(&serial);
+
+ assert!(result.is_err());
+ }
+
+ #[test]
+ fn test_parse_invalid_transaction_corrupted() {
+ // Try to parse corrupted data
+ let serial = vec![0x00, 0x01, 0x02, 0x03];
+ let result = TonTransaction::parse_hex(&serial);
+
+ assert!(result.is_err());
+ }
+
+ #[test]
+ fn test_parse_ton_proof() {
+ // Valid TON proof data
+ let serial = hex::decode("746f6e2d70726f6f662d6974656d2d76322f00000000b5232c324308b148e53ca5ecce6430bdaefdb1095e02189cce587e915fc053b015000000746b6170702e746f6e706f6b65722e6f6e6c696e65142b5866000000003735323061653632393534653666666330303030303030303636353765333639").unwrap();
+
+ let proof = TonProof::parse_hex(&serial).unwrap();
+
+ assert_eq!(proof.domain, "tkapp.tonpoker.online");
+ assert!(!proof.address.is_empty());
+ assert!(!proof.payload.is_empty());
+ assert!(!proof.raw_message.is_empty());
+ println!("Proof address: {}", proof.address);
+ println!("Proof domain: {}", proof.domain);
+ println!("Proof payload: {}", proof.payload);
+ }
+
+ #[test]
+ fn test_parse_ton_proof_invalid_too_short() {
+ // Proof data that is too short
+ let serial = vec![0x74, 0x6f, 0x6e]; // "ton"
+ let result = TonProof::parse_hex(&serial);
+
+ assert!(result.is_err());
+ if let Err(TonError::InvalidProof(msg)) = result {
+ assert!(msg.contains("too short"));
+ } else {
+ panic!("Expected InvalidProof error");
+ }
+ }
+
+ #[test]
+ fn test_parse_ton_proof_invalid_utf8() {
+ // Create valid structure but with invalid UTF-8 in domain
+ let mut serial = b"ton-proof-item-v2/".to_vec();
+ serial.extend_from_slice(&[0u8; 4]); // workchain
+ serial.extend_from_slice(&[0u8; 32]); // address hash
+ serial.extend_from_slice(&[5, 0, 0, 0]); // domain length = 5
+ serial.extend_from_slice(&[0xFF, 0xFE, 0xFD, 0xFC, 0xFB]); // invalid UTF-8
+ serial.extend_from_slice(&[0u8; 8]); // timestamp
+
+ let result = TonProof::parse_hex(&serial);
+ // Should fail due to invalid UTF-8 in domain
+ assert!(result.is_err());
+ }
+
+ #[test]
+ fn test_parse_ton_proof_invalid_domain_length() {
+ // Valid header but domain length exceeds actual data
+ let mut serial = b"ton-proof-item-v2/".to_vec();
+ serial.extend_from_slice(&[0u8; 4]); // workchain
+ serial.extend_from_slice(&[0u8; 32]); // address hash
+ serial.extend_from_slice(&[0xFF, 0xFF, 0xFF, 0x7F]); // invalid domain length (very large)
+ serial.extend_from_slice(&[0u8; 10]); // timestamp + small data
+
+ let result = TonProof::parse_hex(&serial);
+ assert!(result.is_err());
+ if let Err(TonError::InvalidProof(msg)) = result {
+ assert!(msg.contains("too short"));
+ } else {
+ panic!("Expected InvalidProof error");
+ }
+ }
+
+ #[test]
+ fn test_ton_transaction_default() {
+ let tx = TonTransaction::default();
+
+ assert_eq!(tx.to, "");
+ assert_eq!(tx.amount, "");
+ assert_eq!(tx.action, "");
+ assert!(tx.comment.is_none());
+ assert!(tx.data_view.is_none());
+ assert_eq!(tx.raw_data, "");
+ assert!(tx.contract_data.is_none());
+ }
+
+ #[test]
+ fn test_ton_proof_default() {
+ let proof = TonProof::default();
+
+ assert_eq!(proof.domain, "");
+ assert_eq!(proof.payload, "");
+ assert_eq!(proof.address, "");
+ assert_eq!(proof.raw_message, "");
+ }
+
+ #[test]
+ fn test_transaction_clone() {
+ let body = "te6cckEBAgEARwABHCmpoxdmOz6lAAAACAADAQBoQgArFnMvHAX9tOjTp4/RDd3vP2Bn8xG+U5MTuKRKUE1NoqHc1lAAAAAAAAAAAAAAAAAAAHBy4G8=";
+ let serial = STANDARD.decode(body).unwrap();
+
+ let tx1 = TonTransaction::parse_hex(&serial).unwrap();
+ let tx2 = tx1.clone();
+
+ assert_eq!(tx1.to, tx2.to);
+ assert_eq!(tx1.amount, tx2.amount);
+ assert_eq!(tx1.action, tx2.action);
+ }
+
+ #[test]
+ fn test_proof_clone() {
+ let serial = hex::decode("746f6e2d70726f6f662d6974656d2d76322f00000000b5232c324308b148e53ca5ecce6430bdaefdb1095e02189cce587e915fc053b015000000746b6170702e746f6e706f6b65722e6f6e6c696e65142b5866000000003735323061653632393534653666666330303030303030303636353765333639").unwrap();
+
+ let proof1 = TonProof::parse_hex(&serial).unwrap();
+ let proof2 = proof1.clone();
+
+ assert_eq!(proof1.domain, proof2.domain);
+ assert_eq!(proof1.payload, proof2.payload);
+ assert_eq!(proof1.address, proof2.address);
+ }
+
+ #[test]
+ fn test_transaction_with_empty_messages() {
+ // This should fail as transaction needs at least one message
+ // We need to craft a BOC with valid structure but empty messages
+ let body = "te6cckEBAgEARwABHCmpoxdmOz6lAAAACAADAQBoQgArFnMvHAX9tOjTp4/RDd3vP2Bn8xG+U5MTuKRKUE1NoqHc1lAAAAAAAAAAAAAAAAAAAHBy4G8=";
+ let serial = STANDARD.decode(body).unwrap();
+
+ // This is a valid transaction, so it should succeed
+ let result = TonTransaction::parse_hex(&serial);
+ assert!(result.is_ok());
+ }
+
+ #[test]
+ fn test_raw_data_contains_hex() {
+ let body = "te6cckEBAgEARwABHCmpoxdmOz6lAAAACAADAQBoQgArFnMvHAX9tOjTp4/RDd3vP2Bn8xG+U5MTuKRKUE1NoqHc1lAAAAAAAAAAAAAAAAAAAHBy4G8=";
+ let serial = STANDARD.decode(body).unwrap();
+
+ let tx = TonTransaction::parse_hex(&serial).unwrap();
+
+ // raw_data should be hex encoded (shortened)
+ assert!(!tx.raw_data.is_empty());
+ // Should start with valid hex characters
+ assert!(tx
+ .raw_data
+ .chars()
+ .all(|c| c.is_ascii_hexdigit() || c == '.'));
+ }
+}
diff --git a/rust/apps/ton/src/transaction.rs b/rust/apps/ton/src/transaction.rs
index ae56f86..3f3fe1a 100644
--- a/rust/apps/ton/src/transaction.rs
+++ b/rust/apps/ton/src/transaction.rs
@@ -45,6 +45,7 @@ pub fn sign_proof(serial: &[u8], sk: [u8; 32]) -> Result<[u8; 64]> {
#[cfg(test)]
mod tests {
extern crate std;
+ use alloc::string::ToString;
use base64::{engine::general_purpose::STANDARD, Engine};
use hex;
use std::println;
@@ -59,16 +60,17 @@ mod tests {
// "tonsign://?pk=j7SUzAOty6C3woetBmEXobZoCf6vJZGoQVomHJc42oU=&body=te6cckEBAwEA7AABHCmpoxdmOZW/AAAABgADAQHTYgAIqFqMWTE1aoxM/MRD/EEluAMqKyKvv/FAn4CTTNIDD6B4KbgAAAAAAAAAAAAAAAAAAA+KfqUACSD7UyTMBDtxsAgA7zuZAqJxsqAciTilI8/iTnGEeq62piAAHtRKd6wOcJwQOThwAwIA1yWThWGAApWA5YrFIkZa+bJ7vYJARri8uevEBP6Td4tUTty6RJsGAh5xAC1IywyQwixSOU8pezOZDC9rv2xCV4CGJzOWH6RX8BTsMAK2ELwgIrsrweR+b2yZuUsWugqtisQzBm6gPg1ubkuzBkk1zw8=";
let body = "te6cckEBAwEA7AABHCmpoxdmOZW/AAAABgADAQHTYgAIqFqMWTE1aoxM/MRD/EEluAMqKyKvv/FAn4CTTNIDD6B4KbgAAAAAAAAAAAAAAAAAAA+KfqUACSD7UyTMBDtxsAgA7zuZAqJxsqAciTilI8/iTnGEeq62piAAHtRKd6wOcJwQOThwAwIA1yWThWGAApWA5YrFIkZa+bJ7vYJARri8uevEBP6Td4tUTty6RJsGAh5xAC1IywyQwixSOU8pezOZDC9rv2xCV4CGJzOWH6RX8BTsMAK2ELwgIrsrweR+b2yZuUsWugqtisQzBm6gPg1ubkuzBkk1zw8=";
let result = STANDARD.decode(body).unwrap();
- let result = BagOfCells::parse(&result).unwrap();
- println!("{result:?}");
- result.single_root().unwrap().parse_fully(|parser| {
- let address = parser.load_address().unwrap();
- println!("{}", parser.remaining_bits());
- println!("{address}");
+ let boc = BagOfCells::parse(&result).unwrap();
+
+ assert_eq!(boc.roots.len(), 1);
+ let root = boc.single_root().unwrap();
+
+ let _ = root.parse_fully(|parser| {
+ let _address = parser.load_address().unwrap();
+ let remaining = parser.remaining_bits();
+ assert_eq!(remaining, 110);
Ok(())
});
- // let result = super::parse_transaction(&serial);
- // assert!(result.is_err());
}
#[test]
@@ -77,14 +79,23 @@ mod tests {
let body = "te6cckEBAgEARwABHCmpoxdmOz6lAAAACAADAQBoQgArFnMvHAX9tOjTp4/RDd3vP2Bn8xG+U5MTuKRKUE1NoqHc1lAAAAAAAAAAAAAAAAAAAHBy4G8=";
let serial = STANDARD.decode(body).unwrap();
let tx = parse_transaction(&serial).unwrap();
- println!("{tx:?}");
+
+ assert_eq!(tx.to, "UQBWLOZeOAv7adGnTx-iG7vefsDP5iN8pyYncUiUoJqbRdx9");
+ assert_eq!(tx.amount, "1 Ton");
+ assert_eq!(tx.action, "Ton Transfer");
+ assert!(tx.comment.is_none());
+ assert!(tx.data_view.is_none());
+ assert!(tx.contract_data.is_none());
+
let tx_json = tx.to_json().unwrap();
- println!("{tx_json}");
+ let tx_json_str = tx_json.to_string();
+ assert!(tx_json_str.contains("\"action\":\"Ton Transfer\""));
+ assert!(tx_json_str.contains("\"amount\":\"1 Ton\""));
+ assert!(tx_json_str.contains("\"to\":\"UQBWLOZeOAv7adGnTx-iG7vefsDP5iN8pyYncUiUoJqbRdx9\""));
}
#[test]
fn test_sign_ton_transaction() {
- //j7SUzAOty6C3woetBmEXobZoCf6vJZGoQVomHJc42oU=
// tonsign://?network=ton&pk=j7SUzAOty6C3woetBmEXobZoCf6vJZGoQVomHJc42oU%3D&body=te6cckEBAgEARwABHCmpoxdmQcAiAAAADQADAQBoQgArFnMvHAX9tOjTp4%2FRDd3vP2Bn8xG%2BU5MTuKRKUE1NoqAvrwgAAAAAAAAAAAAAAAAAAPa2C0o%3D
let body = "te6cckEBAgEARwABHCmpoxdmQcAiAAAADQADAQBoQgArFnMvHAX9tOjTp4%2FRDd3vP2Bn8xG%2BU5MTuKRKUE1NoqAvrwgAAAAAAAAAAAAAAAAAAPa2C0o%3D";
let serial = STANDARD
@@ -109,43 +120,50 @@ mod tests {
let serial = "b5ee9c7241010301009e00011c29a9a3176656eb410000001000030101686200091c1bd942402db834b5977d2a1313119c3a3800c8e10233fa8eaf36c655ecab202faf0800000000000000000000000000010200a80f8a7ea5546de4ef815e87fb3989680800ac59ccbc7017f6d3a34e9e3f443777bcfd819fcc46f94e4c4ee291294135368b002d48cb0c90c22c52394f297b33990c2f6bbf6c425780862733961fa457f014ec02025f1050ae";
let serial = hex::decode(serial).unwrap();
let tx = parse_transaction(&serial).unwrap();
- //true destination UQBWLOZeOAv7adGnTx-iG7vefsDP5iN8pyYncUiUoJqbRdx9
- //transaction to: EQASODeyhIBbcGlrLvpUJiYjOHRwAZHCBGf1HV5tjKvZVsJb
- //contract destination: EQBWLOZeOAv7adGnTx+iG7vefsDP5iN8pyYncUiUoJqbRYG4
- println!("{tx:?}");
- }
- // #[test]
- // fn test_ston_provide_liqudity() {
- // let serial = hex::decode("b5ee9c724102060100016500021e29a9a31766612b0c00000015000303030101d3620008a85a8c5931356a8c4cfcc443fc4125b8032a2b22afbff1409f80934cd2030fa07c8acff8000000000000000000000000000f8a7ea500169076c4a3033231210ff800ef3b9902a271b2a01c8938a523cfe24e71847aaeb6a620001ed44a77ac0e709c103dfd240302004ffcf9e58f8010f72448354d4afbe624e28c182138a9bfb313435d4065ec20fa58cbadd10f39c4034901686200091c1bd942402db834b5977d2a1313119c3a3800c8e10233fa8eaf36c655ecab208f0d1800000000000000000000000000010401ae0f8a7ea5001828589d5aad523079df3800ef3b9902a271b2a01c8938a523cfe24e71847aaeb6a620001ed44a77ac0e709d002d48cb0c90c22c52394f297b33990c2f6bbf6c425780862733961fa457f014ec081c9c380105004ffcf9e58f80022a16a3164c4d5aa3133f3110ff10496e00ca8ac8abeffc5027e024d33480c3e403498878abc8").unwrap();
- // let tx = parse_transaction(&serial).unwrap();
- // //true destination UQBWLOZeOAv7adGnTx-iG7vefsDP5iN8pyYncUiUoJqbRdx9
- // //transaction to: EQASODeyhIBbcGlrLvpUJiYjOHRwAZHCBGf1HV5tjKvZVsJb
- // //contract destination: EQBWLOZeOAv7adGnTx+iG7vefsDP5iN8pyYncUiUoJqbRYG4
- // println!("{:?}", tx);
- // }
+ // true destination UQBWLOZeOAv7adGnTx-iG7vefsDP5iN8pyYncUiUoJqbRdx9
+ assert_eq!(tx.to, "UQBWLOZeOAv7adGnTx-iG7vefsDP5iN8pyYncUiUoJqbRdx9");
+ assert_eq!(tx.amount, "10000000 Unit");
+ assert_eq!(tx.action, "Jetton Transfer");
+ assert!(tx.comment.is_none());
+
+ assert!(tx.data_view.is_some());
+ let data_view = tx.data_view.unwrap();
+ assert!(data_view.contains("\"amount\":\"10000000\""));
+ assert!(data_view
+ .contains("\"destination\":\"UQBWLOZeOAv7adGnTx-iG7vefsDP5iN8pyYncUiUoJqbRdx9\""));
+ assert!(data_view.contains("\"forward_ton_amount\":\"1\""));
+
+ // transaction to: EQASODeyhIBbcGlrLvpUJiYjOHRwAZHCBGf1HV5tjKvZVsJb
+ assert!(tx.contract_data.is_some());
+ let contract_data = tx.contract_data.unwrap();
+ assert!(contract_data.contains("Jetton Wallet Address"));
+ assert!(contract_data.contains("EQASODeyhIBbcGlrLvpUJiYjOHRwAZHCBGf1HV5tjKvZVsJb"));
+ }
#[test]
fn test_parse_ton_transfer_with_comment() {
let serial = "b5ee9c724102050100019700011c29a9a31766611df6000000140003010166420013587ccf19c39b1ca51c29f0253ac98d03b8e5ccfc64c3ac2f21c59c20ee8b65987a1200000000000000000000000000010201fe000000004b657973746f6e652068617264776172652077616c6c6574206f666665727320756e6265617461626c65207365637572697479207769746820332050434920736563757269747920636869707320746f206d616e61676520426974636f696e20616e64206f746865722063727970746f20617373657473206f66660301fe6c696e652e4b657973746f6e65206f666665727320332077616c6c6574732c207768696368206d65616e7320796f752063616e206d616e616765206d756c7469706c65206163636f756e74732073657061726174656c79206f6e206f6e65206465766963652e4b657973746f6e65206f666665727320332077616c6c6574730400942c207768696368206d65616e7320796f752063616e206d616e616765206d756c7469706c65206163636f756e74732073657061726174656c79206f6e206f6e65206465766963652e0a0ac04eabc7";
let serial = hex::decode(serial).unwrap();
let tx = parse_transaction(&serial).unwrap();
- println!("{tx:?}");
+
+ assert_eq!(tx.to, "UQAmsPmeM4c2OUo4U-BKdZMaB3HLmfjJh1heQ4s4Qd0Wy7Nc");
+ assert_eq!(tx.amount, "0.001 Ton");
+ assert_eq!(tx.action, "Ton Transfer");
+ assert!(tx.comment.is_some());
+
+ let comment = tx.comment.unwrap();
+ assert!(comment.contains("Keystone hardware wallet"));
+ assert!(comment.contains("3 PCI security chips"));
+ assert!(comment.contains("Bitcoin and other crypto assets offline"));
+ assert!(comment.contains("3 wallets"));
+ assert!(comment.contains("manage multiple accounts"));
}
#[test]
fn test_sign_ton_proof() {
let serial = hex::decode("746f6e2d70726f6f662d6974656d2d76322f00000000b5232c324308b148e53ca5ecce6430bdaefdb1095e02189cce587e915fc053b015000000746b6170702e746f6e706f6b65722e6f6e6c696e65142b5866000000003735323061653632393534653666666330303030303030303636353765333639").unwrap();
- //b4933a592c18291855b30ea5cc8da7cb20da17936df875f018c6027f2103f6ad
let signature = sign_proof(&serial, [0u8; 32]);
- println!("{}", hex::encode(signature.unwrap()));
-
- // ffff746f6e2d636f6e6e6563745adfd8ce7eeb56a65c82002216e042f69abe0dfb40b13b1096b5a817e8f9e8d7
- // e87aadb24661f2b3b517a4a3b24cda5aef17dc381bd99cc971811c4c096385b3
- // f7dfec305cd324692fcb73ce55700724a86b22e3f74d3f06b6712da2f5cfd1b7cfd4a0b1944311951b4c4829dee97dd2fbef989afbcc5756408daa95e6ad1d02
-
- // ffff746f6e2d636f6e6e6563745adfd8ce7eeb56a65c82002216e042f69abe0dfb40b13b1096b5a817e8f9e8d7
- // e87aadb24661f2b3b517a4a3b24cda5aef17dc381bd99cc971811c4c096385b3
- // f7dfec305cd324692fcb73ce55700724a86b22e3f74d3f06b6712da2f5cfd1b7cfd4a0b1944311951b4c4829dee97dd2fbef989afbcc5756408daa95e6ad1d02
+ assert_eq!(hex::encode(signature.unwrap()), "aad6c3f5236a56e4aa3b66d68504895987c40e34b67fd9f353ef9037eb814c3eab51fecea8394412fdeacbd5d68aa1706f925dd8c607c8ecea42ad9b39572f00");
}
}
diff --git a/rust/rust_c/src/ton/mod.rs b/rust/rust_c/src/ton/mod.rs
index cb54361..07bfee7 100644
--- a/rust/rust_c/src/ton/mod.rs
+++ b/rust/rust_c/src/ton/mod.rs
@@ -8,7 +8,7 @@ use crate::{
ur::{UREncodeResult, FRAGMENT_MAX_LENGTH_DEFAULT},
utils::recover_c_char,
},
- extract_array,
+ extract_array, extract_array_mut,
};
use alloc::{
boxed::Box,
@@ -31,6 +31,8 @@ use {
},
};
+use zeroize::Zeroize;
+
#[repr(C)]
pub struct DisplayTon {
text: PtrString,
@@ -107,19 +109,11 @@ fn get_secret_key(tx: &TonSignRequest, seed: &[u8]) -> Result<[u8; 32], RustCErr
.get_path()
.ok_or(RustCError::InvalidHDPath)?;
match ed25519::slip10_ed25519::get_private_key_by_seed(seed, &path) {
- Ok(_sk) => {
- for i in 0..32 {
- sk[i] = _sk[i]
- }
- }
+ Ok(_sk) => sk.copy_from_slice(&_sk),
Err(e) => return Err(RustCError::UnexpectedError(e.to_string())),
}
}
- None => {
- for i in 0..32 {
- sk[i] = seed[i]
- }
- }
+ None => sk.copy_from_slice(seed),
};
Ok(sk)
}
@@ -131,11 +125,12 @@ pub unsafe extern "C" fn ton_sign_transaction(
seed_len: u32,
) -> PtrT<UREncodeResult> {
let ton_tx = extract_ptr_with_type!(ptr, TonSignRequest);
- let seed = extract_array!(seed, u8, seed_len as usize);
+ let seed = extract_array_mut!(seed, u8, seed_len as usize);
let sk = match get_secret_key(ton_tx, seed) {
Ok(_sk) => _sk,
Err(e) => return UREncodeResult::from(e).c_ptr(),
};
+ seed.zeroize();
let result = app_ton::transaction::sign_transaction(&ton_tx.get_sign_data(), sk);
match result {
Ok(sig) => {
@@ -165,11 +160,12 @@ pub unsafe extern "C" fn ton_sign_proof(
seed_len: u32,
) -> PtrT<UREncodeResult> {
let ton_tx = extract_ptr_with_type!(ptr, TonSignRequest);
- let seed = extract_array!(seed, u8, seed_len as usize);
+ let seed = extract_array_mut!(seed, u8, seed_len as usize);
let sk = match get_secret_key(ton_tx, seed) {
Ok(_sk) => _sk,
Err(e) => return UREncodeResult::from(e).c_ptr(),
};
+ seed.zeroize();
let result = app_ton::transaction::sign_proof(&ton_tx.get_sign_data(), sk);
match result {
Ok(sig) => {
@@ -220,7 +216,7 @@ pub unsafe extern "C" fn ton_entropy_to_seed(
#[no_mangle]
pub unsafe extern "C" fn ton_mnemonic_to_seed(mnemonic: PtrString) -> *mut SimpleResponse<u8> {
let mnemonic = recover_c_char(mnemonic);
- let words: Vec<String> = mnemonic.split(' ').map(|v| v.to_lowercase()).collect();
+ let mut words: Vec<String> = mnemonic.split(' ').map(|v| v.to_lowercase()).collect();
let seed = app_ton::mnemonic::ton_mnemonic_to_master_seed(words, None);
match seed {
Ok(seed) => {
diff --git a/src/ui/gui_chain/multi/web3/gui_ton.c b/src/ui/gui_chain/multi/web3/gui_ton.c
index f1f2463..af20310 100644
--- a/src/ui/gui_chain/multi/web3/gui_ton.c
+++ b/src/ui/gui_chain/multi/web3/gui_ton.c
@@ -48,28 +48,16 @@ UREncodeResult *GuiGetTonSignQrCodeData(void)
SetLockScreen(false);
UREncodeResult *encodeResult;
void *data = g_isMulti ? g_urMultiResult->data : g_urResult->data;
+ uint8_t seed[64];
do {
- MnemonicType type = GetMnemonicType();
- uint8_t seed[64];
- int len = 64;
- switch (type) {
- case MNEMONIC_TYPE_BIP39: {
- len = sizeof(seed);
- break;
-
- }
- case MNEMONIC_TYPE_SLIP39: {
- len = GetCurrentAccountEntropyLen();
- break;
- }
- default:
- break;
- }
- GetAccountSeed(GetCurrentAccountIndex(), seed, SecretCacheGetPassword());
+ int len = GetCurrentAccountSeedLen();
+ int ret = GetAccountSeed(GetCurrentAccountIndex(), seed, SecretCacheGetPassword());
+ CHECK_ERRCODE_BREAK("GetAccountSeed", ret);
encodeResult = ton_sign_transaction(data, seed, len);
- ClearSecretCache();
CHECK_CHAIN_BREAK(encodeResult);
} while (0);
+ memset_s(seed, sizeof(seed), 0, sizeof(seed));
+ ClearSecretCache();
SetLockScreen(enable);
return encodeResult;
}
@@ -80,28 +68,16 @@ UREncodeResult *GuiGetTonProofSignQrCodeData(void)
SetLockScreen(false);
UREncodeResult *encodeResult;
void *data = g_isMulti ? g_urMultiResult->data : g_urResult->data;
+ uint8_t seed[64];
do {
- MnemonicType type = GetMnemonicType();
- uint8_t seed[64];
- int len = 64;
- switch (type) {
- case MNEMONIC_TYPE_BIP39: {
- len = sizeof(seed);
- break;
-
- }
- case MNEMONIC_TYPE_SLIP39: {
- len = GetCurrentAccountEntropyLen();
- break;
- }
- default:
- break;
- }
- GetAccountSeed(GetCurrentAccountIndex(), seed, SecretCacheGetPassword());
+ int len = GetCurrentAccountSeedLen();
+ int ret = GetAccountSeed(GetCurrentAccountIndex(), seed, SecretCacheGetPassword());
+ CHECK_ERRCODE_BREAK("GetAccountSeed", ret);
encodeResult = ton_sign_proof(data, seed, len);
- ClearSecretCache();
CHECK_CHAIN_BREAK(encodeResult);
} while (0);
+ memset_s(seed, sizeof(seed), 0, sizeof(seed));
+ ClearSecretCache();
SetLockScreen(enable);
return encodeResult;
}
Why this scored 33/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.